Certified Kubernetes Application Developer (CKAD) #17 Volumes: emptyDir, PVC, projected, ephemeral

コンテナのファイルシステムは、コンテナが死ぬ瞬間に一緒に消えます。クラッシュで再起動されても、ローリングアップデートで新しいイメージが上がっても、その中に書いたファイルは残りません。では、ログを少しの間まとめておいたり、データベースファイルを永続的に保管したり、コンテナ 2 つが同じディレクトリを共有しなければならないときは、データをどこに置けばよいのでしょうか。今回の記事は、その問いに答える Kubernetes volume の種類を実技の観点から整理します。

K8s 実務トラックのストレージ編 で PV・PVC の概念を一度扱ったのなら、ここでは CKAD 試験で実際に書くことになるマニフェストの形に集中します。覚えるべきはフィールド名ではなく、どの状況でどの volume を選ぶかという判断です。

volume が解く問題 #

Kubernetes の volume は、Pod のコンテナにディレクトリを付けてあげる抽象化です。volume の種類によって、そのディレクトリの実体はノードの一時ディスクであることも、ネットワークストレージであることも、メモリであることもあります。CKAD によく登場する種類は 4 つに絞られます。コンテナ間の一時共有用の emptyDir、永続データ用の PersistentVolumeClaim、複数の設定ソースをまとめる projected、そして PVC を Pod の中でその場で定義する generic ephemeral です。

volume は spec.volumes で定義し、各コンテナの volumeMounts でマウントパスを指定します。定義とマウントが分離されているという点が核心です。1 つの volume を複数のコンテナがそれぞれのパスにマウントでき、これがコンテナ間共有の基盤になります。

emptyDir: Pod の寿命の間の一時領域 #

emptyDir は、Pod がノードにスケジュールされるとき空のディレクトリとして生成され、Pod がノードから削除されるとき一緒に削除される volume です。コンテナがクラッシュで再起動されても emptyDir の内容は維持されます。Pod 自体が消えるときだけ消えるので、コンテナ 1 回の寿命よりは長く、Pod 1 回の寿命とは同じです。

最もよくある用途は、同じ Pod の中の 2 つのコンテナがファイルをやり取りする通路です。たとえば書き込み側のコンテナがファイルを書き、サーバーコンテナがそのファイルを読む sidecar 構成で、emptyDir を両側にマウントすればよいです。

medium: Memory オプションを与えると、ディスクではなく tmpfs (メモリ) にディレクトリを作ります。速度が速くディスクに痕跡を残しませんが、メモリを消費し Pod のメモリ使用量に含まれます。キャッシュや機密性のある一時ファイルに使われます。

apiVersion: v1
kind: Pod
metadata:
  name: shared-emptydir
spec:
  containers:
    - name: writer
      image: busybox
      command: ["sh", "-c", "while true; do date >> /data/out.log; sleep 5; done"]
      volumeMounts:
        - name: scratch
          mountPath: /data
    - name: reader
      image: busybox
      command: ["sh", "-c", "tail -f /data/out.log"]
      volumeMounts:
        - name: scratch
          mountPath: /data
  volumes:
    - name: scratch
      emptyDir: {}

writerreader が同じ scratch volume をそれぞれ /data にマウントするので、writer が書いたログを reader がそのまま読みます。メモリベースに変えるには emptyDir: {}emptyDir: { medium: Memory } に置き換えます。

hostPath: ノードディスクの直接マウント (試験ではまれ) #

hostPath はノードのファイルシステムパスを Pod に直接付けます。ノードの特定のディレクトリやデバイスにアクセスする必要があるシステムレベルの作業に使われますが、その Pod がそのノードに乗っているときだけデータが意味を持ちます。Pod が別のノードに移ると、そこの hostPath は空です。ノード依存とセキュリティリスクのため、アプリデータの保存には推奨されず、CKAD の実技でも直接書くことはまれです。概念とリスクだけ知っておけば十分です。

PV と PVC: 永続データの標準 #

データベースファイルのように、Pod が消えても生き残らなければならないデータは PersistentVolume (PV) と PersistentVolumeClaim (PVC) で扱います。PV はクラスターが持つ実際のストレージの一片で、PVC は「これだけの容量をこういうアクセス方式でくれ」というユーザーの要求です。Pod は PV を直接指さず、PVC を通してストレージを消費します。この間接レイヤーのおかげで、アプリマニフェストはバックエンドストレージの実装を知らなくて済みます。

accessModes #

PVC が要求するアクセス方式は 3 つが核心です。

モード略語意味
ReadWriteOnceRWO単一ノードでの読み書きマウント
ReadOnlyManyROX複数ノードでの読み取り専用マウント
ReadWriteManyRWX複数ノードでの読み書きマウント

RWO はノード単位の制限なので、同じノードの複数の Pod は同時にマウントできます。RWX は NFS のような共有ファイルシステムが裏付けしてはじめて可能で、すべてのストレージが対応するわけではありません。

StorageClass と動的プロビジョニング #

かつては管理者が PV をあらかじめ作っておき、PVC がその PV にバインドされるのを待ちました。今は StorageClass を指定すると、PVC を生成する瞬間にクラスターが PV を自動で作って付けます。これが動的プロビジョニングです。PVC の storageClassName にクラス名を書けばよく、空にしておくとクラスターのデフォルト StorageClass が使われます。

PVC と PV のバインドは一対一です。PVC が要求した容量・accessModes・StorageClass の条件を満たす PV が選ばれて一度結ばれると、その PV は別の PVC が使えません。PVC が Pending 状態に留まるなら、条件に合う PV がないか動的プロビジョニングが動いていないということなので、kubectl describe pvc でイベントをまず確認します。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: standard
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: db
spec:
  containers:
    - name: db
      image: busybox
      command: ["sh", "-c", "echo hello > /var/lib/data/seed; sleep 3600"]
      volumeMounts:
        - name: store
          mountPath: /var/lib/data
  volumes:
    - name: store
      persistentVolumeClaim:
        claimName: data-pvc

Pod の volume で persistentVolumeClaim.claimName で PVC を指すのがマウントのすべてです。名前が PVC 名と正確に一致しなければならず、異なると Pod がストレージを見つけられず起動できません。

projected volume: 複数の設定を 1 つのディレクトリに #

projected volume は、secret・configMap・serviceAccountToken・downwardAPI のような異なるソースを 1 つのディレクトリに結合 します。コンテナから見れば、出どころが何であれ 1 つのパスの下にファイルが集まっているので、設定を 1 か所から読めます。証明書と設定値とトークンを同じマウント地点にまとめなければならないときに便利です。

各ソースは sources リストの項目として入り、項目ごとにどのキーをどのファイル名で公開するかを指定します。

downwardAPI: Pod 自身の情報をファイル・env に #

downwardAPI は、Pod が自分自身のメタデータを読めるようにする通路です。コンテナは、自分がどの namespace にいるのか、Pod 名が何か、どの label が付いているのかを、イメージにハードコードしなくても知ることができます。公開方式は 2 つで、env 変数として注入するか、ファイルとしてマウントします。

env として使うときは fieldRefmetadata.namemetadata.namespacestatus.podIP のようなフィールドを指します。コンテナのリソース要求・制限の値は resourceFieldRef で公開します。ファイルとして使うときは projected や downwardAPI volume の items にフィールドパスとファイルパスを書きます。label や annotation のようにキーが複数ある情報は、env よりファイル方式が自然です。

apiVersion: v1
kind: Pod
metadata:
  name: projected-demo
  labels:
    app: demo
    tier: web
spec:
  containers:
    - name: app
      image: busybox
      command: ["sh", "-c", "ls -l /etc/podinfo; cat /etc/podinfo/labels; sleep 3600"]
      env:
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
      volumeMounts:
        - name: bundle
          mountPath: /etc/podinfo
          readOnly: true
  volumes:
    - name: bundle
      projected:
        sources:
          - configMap:
              name: app-config
          - secret:
              name: app-secret
          - downwardAPI:
              items:
                - path: labels
                  fieldRef:
                    fieldPath: metadata.labels
                - path: name
                  fieldRef:
                    fieldPath: metadata.name

この Pod は、configMap と secret のキーたち、そして自分の label と名前をすべて /etc/podinfo という 1 つのディレクトリの下のファイルとして見ます。同時に POD_NAMESPACE env で namespace も受け取ります。projected と downwardAPI を一度に見せる形なので、2 つの概念の結合をこの例で身につけておくとよいです。

generic ephemeral volume: 一行まとめ #

generic ephemeral volume は、Pod 定義の中に PVC テンプレートを直接書き、Pod と同じ寿命を持つ永続ボリュームをその場でプロビジョニングする方式です。Pod が削除されると一緒に片付けられるので、StorageClass の動的プロビジョニングを使いながらも別途 PVC オブジェクトをあらかじめ作る必要がないときに使われます。CKAD での比重は低いので、こういう選択肢があるという程度だけ覚えておけば十分です。

試験ポイント #

  • emptyDir は Pod の寿命と同じです。 コンテナの再起動には耐えますが、Pod の削除時に消えます。コンテナ間の共有が必要なら、同じ volume を両側の volumeMounts に付けます。
  • medium: Memory は tmpfs です。 速いですがメモリを使い、Pod のメモリに計上されます。
  • PVC のマウントは persistentVolumeClaim.claimName です。 名前が PVC と正確に一致しなければなりません。PVC が Pending なら、describe でイベントから見ます。
  • accessModes の略語を覚えます。 RWO は単一ノード、ROX は複数ノードの読み取り、RWX は複数ノードの読み書きです。
  • projected は sources リストです。 1 つのディレクトリに configMap・secret・downwardAPI・serviceAccountToken を集めます。
  • downwardAPI は fieldRef と resourceFieldRef です。 メタデータは fieldRef、リソースの値は resourceFieldRef で公開します。
  • dry-run で PVC と ConfigMap の骨格を抜きつつ、projected と downwardAPI は generator がないので、ドキュメントや kubectl explain pod.spec.volumes.projected でフィールドパスを確認します。

まとめ #

この記事で押さえたこと:

  • コンテナファイルシステムは揮発する という前提から、データを置く場所を volume で分けて選びました。
  • emptyDir。 Pod の寿命の間の一時領域であり、コンテナ間の共有通路。medium: Memory で tmpfs に切り替え
  • hostPath。 ノードディスクの直接マウント。ノード依存とセキュリティリスクでアプリデータには不向き
  • PV/PVC。 PVC でストレージを要求し StorageClass で動的プロビジョニング。accessModes (RWO・ROX・RWX) と一対一バインド
  • projected。 複数の設定ソースを 1 つのディレクトリに結合
  • downwardAPI。 Pod 自身のメタデータ・リソースをファイルや env で公開
  • generic ephemeral。 Pod の寿命に縛られたその場の PVC プロビジョニング

次へ: Services #

ここまでは Pod の内側、つまりコンテナがデータをどう扱うかを見てきました。これから Pod の外側、トラフィックが Pod へどう入ってくるかへ進みます。

#18 Services: ClusterIP、NodePort、LoadBalancer、ExternalName では、Pod の IP が頻繁に変わる環境で安定したアクセス点を作る Service の 4 つのタイプ、selector と label で Pod をまとめる方式、kubectl expose で Service を素早く抜く方法、そして試験によく出る「この Service がなぜ Pod へトラフィックを送れないのか」というタイプまで、実際に作ってみながら整理します。

X