本記事には広告(アフィリエイトリンク)が含まれます。

KubernetesPVC+StatefulSetでDB永続化【CKAD第9回】

広告
広告

第9回スコープ・学習目標・今ここマップ

動作確認バージョン: kind v0.31.0 / kindest/node:v1.35.0 / kubectl v1.35.0 (Kustomize v5.7.1) / postgres:18.3 / fanclub-backend:0.1.0 (eclipse-temurin:25-jre + Payara Micro 7.2026.4) / busybox:1.36 / Docker CE 29.4.3 / containerd 2.2.3 / AlmaLinux 10.1(kernel 6.12.0-124.55.3.el10_1)(2026-05-10 時点・k8s-ops 実機検証済・SP_vol1-pre-15 起点)

本回は Kubernetes 実践教科書 第1巻(CKAD 対応・全 19 回)の第9回です。第3部「アプリリソース」の第3回として、PV / PVC / StorageClass による永続ストレージ・StatefulSet の 3 つの安定(Pod 名・DNS 名・ストレージ)・volumeClaimTemplates による Pod 専用 PVC の自動生成・Headless Service との連携・PostgreSQL 18 の StatefulSet 運用 を扱います。

CKAD D1(Application Design and Build・20 %)の中核「永続ボリュームとエフェメラルボリュームの活用」「適切なワークロードリソースの選択と使用(StatefulSet)」を網羅し、D5 補完として Headless Service を本格実装します。

第8回からの継承状態確認(SP_vol1-pre-15 状態):

項目状態出典
kind クラスタkind-control-plane Ready(v1.35.0・170m old)Lead 実機観察
fanclub-backend Poddefault ns で 1/1 Running(IP 10.244.0.10・86m・Burstable QoS)Lead 実機観察
fanclub-backend ServiceClusterIP 10.96.150.60:80 稼働中Lead 実機観察
PV / PVC / StatefulSet未作成(ep9 で新規作成する)Lead 実機観察
StorageClassstandard(rancher.io/local-path・WaitForFirstConsumer・default)Lead 実機観察
metrics-serverkube-system ns で 1/1 Runningep6 H2-9 で導入済
~/fanclub-manifests/ディレクトリ作成済(ep7 で初作成・ep8 で Service YAML 追加済)ep7 / ep8 完了済

今ここマップ(第1巻 19 回中の現在位置):

第1部 コンテナとDocker
    第1回: コンテナ技術概念 + Docker環境準備  [完了]
    第2回: Docker基本操作  [完了]
    第3回: Dockerfile + マルチステージビルド + JDK 25/Payara Micro  [完了]
    第4回: コンテナレジストリ + イメージタグ戦略 + Trivy スキャン  [完了]

第2部 Kubernetes基礎
    第5回: K8s全体像 + kind で軽量K8s  [完了]
    第6回: kubectl基本操作 + Observability・Debug  [完了]

第3部 アプリリソース
    第7回: Pod + Multi-containerパターン  [完了]
    第8回: Service とネットワーキング  [完了]
  ★ 第9回: ストレージ(PVC + StatefulSet)+ PostgreSQL DB追加  ← 今ここ
    第10回: ConfigMap + Secret + ServiceAccount 基礎
    第11回: Job + CronJob + DaemonSet

第4部 ワークロード戦略(第12〜14回)
第5部 セキュリティ基礎(第15〜16回)
第6部 パッケージ管理 + HTTPS公開(第17〜19回)

第9回を終えると、以下を習得した状態になります。

  • PV / PVC / StorageClass の関係と動的プロビジョニングの仕組みを説明できる
  • StatefulSet の「3 つの安定」(Pod 名・DNS 名・ストレージ)を Deployment との違いを含めて説明できる
  • volumeClaimTemplates を記述した StatefulSet YAML を作成して PostgreSQL 18 を起動できる
  • Headless Service(clusterIP: None)と StatefulSet を組み合わせた構成を実装できる
  • StatefulSet Pod を削除・再起動しても PVC にデータが残ることを実機で確認できる

模擬アプリ進捗(第9回):第8回までで fanclub-backend Pod と ClusterIP Service の 2 層構成が動いていますが、DB は未接続の状態です。本回では PostgreSQL 18 を StatefulSet + PVC で起動し、Backend を DB 接続版の環境変数を持つ形で再 apply することで、Backend + Service + DB の 3 層構成を完成させます。

DB 接続の疎通は (a) Init Container wait-for-db の完了・(b) Pod への env 注入確認・(c) psql 直接実行 の 3 点で証明します。

第9回完了後の模擬アプリ状態:fanclub-backend Pod(DB 接続版・再 apply 済)+ fanclub-backend Service(ClusterIP・ep8 から継続)+ fanclub-db StatefulSet(fanclub-db-0 Pod Running)+ fanclub-db-headless Service + fanclub-db ClusterIP Service + postgres-data-fanclub-db-0 PVC(Bound)+ members テーブル初期化済。第10回で DB 接続情報を ConfigMap / Secret 化します。

なぜ永続ストレージが必要か — Pod は使い捨てという現実

第7回・第8回で扱ってきた fanclub-backend Pod は、状態を持たない(ステートレス)アプリです。Pod が削除されて再作成されても、新しい Pod が同じイメージを起動すれば、外から見た振る舞いは変わりません。Backend 自身が記憶しなければならないデータが何もないからです。

一方、本回で追加する PostgreSQL は状態を持つ(ステートフル)アプリです。テーブル定義・登録された会員レコード・トランザクションログ・統計情報など、Pod のライフサイクルとは独立して保持されなければならないデータがあります。Pod が削除された瞬間にデータが消えてしまえば、データベースとして成立しません。

Pod の標準ボリュームでは何が起きるか

第7回で扱った Pod の spec.volumes には、Pod のライフサイクルに紐づく短命なボリューム(エフェメラルボリューム)がいくつか存在します。代表的な emptyDir を例に取ると、Pod が起動するときに空ディレクトリが用意され、Pod 内のコンテナで共有でき、Pod が削除されると同時にその中身も完全に消えます

仮にこれを PostgreSQL のデータディレクトリに使った場合、何が起きるかを考えてみます。次は「もし PostgreSQL に emptyDir を使ってしまった場合」のアンチパターン例です。このマニフェストは本番でも学習でも採用してはいけません

apiVersion: v1
kind: Pod
metadata:
  name: bad-postgres-emptydir
spec:
  containers:
    - name: postgres
      image: postgres:18
      env:
        - name: POSTGRES_USER
          value: "appuser"
        - name: POSTGRES_PASSWORD
          value: "apppassword"
        - name: POSTGRES_DB
          value: "fanclubdb"
      volumeMounts:
        - name: postgres-data
          mountPath: /var/lib/postgresql/data
  volumes:
    - name: postgres-data
      emptyDir: {}

このマニフェストでは emptyDir/var/lib/postgresql/data にマウントしているため、PostgreSQL の起動時にはデータが書き込まれます。会員レコードを INSERT すれば、その瞬間はテーブルにデータが存在します。

しかし Pod を kubectl delete pod bad-postgres-emptydir で削除すると、emptyDir のディレクトリも一緒に消えます。再び kubectl apply で同名 Pod を作成しても、新しい emptyDir は空のディレクトリであり、PostgreSQL は「初期化されていないデータディレクトリ」と判断して initdb をやり直します。INSERT したデータは復元できません。

Deployment + 永続ストレージという発想の限界

「永続ストレージを Pod ではなく Deployment に紐づければよいのでは」と考えた読者もいるかもしれません。しかし Deployment はステートレスなレプリカ集合を管理するワークロードリソースであり、複数 Pod に対して個別の PVC を割り当てる仕組みを持ちません

すべての Pod が同じ PVC を共有しようとすると、PostgreSQL のように「データディレクトリは単一プロセスが排他的に書き込む」前提のアプリは動作しません。

さらに Deployment は Pod を再作成するたびに新しいランダムなサフィックスを付けた Pod 名を使います(例: fanclub-db-7d5f9c4b6-x2k8h)。Pod 名が変わると、ログ収集・監視・Pod ごとの外部識別といった「個体を追跡する運用」が成立しなくなります。

データベース運用では「どのレプリカが Primary でどれが Replica か」を Pod 単位で識別できることが重要であり、ランダム名はその識別性を損ないます。

これらの問題を解決するために Kubernetes は 2 つの仕組みを提供しています。

  • PV / PVC / StorageClass:Pod のライフサイクルから独立した永続ストレージリソース。Pod が削除されても PVC は残り、再作成された Pod が同じ PVC を再マウントできる
  • StatefulSet:Pod ごとに固定された名前・DNS 名・専用 PVC を保証するステートフルワークロードリソース

本回ではこの 2 つを組み合わせて PostgreSQL 18 を運用します。次のセクションでまず PV / PVC / StorageClass の三者関係を整理します。

PV / PVC / StorageClass の関係 — 動的プロビジョニングの仕組み

Kubernetes の永続ストレージは、3 つのリソースの役割分担によって成り立っています。

リソース立場役割
PersistentVolume (PV)クラスタ管理者の視点実際のストレージ実体(hostPath / NFS / EBS / Ceph 等)への参照を表すクラスタリソース
PersistentVolumeClaim (PVC)アプリ開発者の視点「容量・アクセスモード・StorageClass」を指定した「ストレージ要求」を表す namespace リソース
StorageClass (SC)クラスタ管理者の視点PV を動的に作るレシピ(プロビジョナ・パラメータ・ReclaimPolicy・VolumeBindingMode)

PVC は PV を「請求」するリソースです。PVC を作成すると、Kubernetes は条件に合う既存 PV を探して紐づけ(Bound)、見つからない場合は StorageClass のプロビジョナに依頼して PV を新規作成します。後者の流れを動的プロビジョニングと呼びます。

Static プロビジョニングと Dynamic プロビジョニング

PV を作る方法には 2 通りあります。

方式誰が PV を作るか使い所
Static Provisioningクラスタ管理者が手動で PV YAML を apply する既存の共有ストレージ(NFS / iSCSI 等)を K8s に取り込む場合
Dynamic ProvisioningStorageClass のプロビジョナが PVC apply に応じて自動生成するクラウド環境(EBS / Persistent Disk)・kind 学習環境

本シリーズの kind 環境では Dynamic Provisioning を使います。kind 標準同梱の standard StorageClass が PVC を見つけ次第、rancher.io/local-path プロビジョナにホスト上のディレクトリを切り出させて PV を生成する仕組みです。

kind の standard StorageClass を確認する

第6回で導入した kind クラスタには、最初から standard という StorageClass が default として登録されています。実機で確認します。

実行コマンド:

$ kubectl get storageclass

実行結果:

NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
standard (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  170m

各カラムの意味を整理します。

  • PROVISIONER: rancher.io/local-path:kind が同梱する local-path-provisioner。Pod がスケジュールされたノードのホスト上に /var/local-path-provisioner/ 以下のディレクトリを切り出して PV にする
  • RECLAIMPOLICY: Delete:PVC を削除すると PV と背後のディレクトリも削除される。学習環境向けの設定
  • VOLUMEBINDINGMODE: WaitForFirstConsumer:PVC を apply しても Pod がスケジュールされるまで PV を作らない遅延バインディングモード
  • ALLOWVOLUMEEXPANSION: false:PVC のサイズを後から拡張できない(kind 環境の制約)

WaitForFirstConsumer がなぜ存在するか

VolumeBindingMode には ImmediateWaitForFirstConsumer の 2 値があります。Immediate は PVC を apply した瞬間に PV を作って Bound しますが、WaitForFirstConsumer「PVC を使う Pod がどのノードにスケジュールされるか確定するまで PV 作成を遅延させる」挙動を取ります。

なぜこのような遅延が必要かというと、ローカルストレージや AZ(アベイラビリティゾーン)に紐づいたストレージでは「PV が存在するノードと Pod がスケジュールされるノードが一致していなければならない」からです。

Pod がどのノードに置かれるか分からない段階で PV を先に作ると、後から Pod スケジューラが「PV のあるノードには配置できない」と判断してデッドロックになる事故があります。WaitForFirstConsumer はスケジューラの判断を先に走らせ、確定したノードに PV を作ることで整合性を保つ仕組みです。

kind の local-path はホスト(Control Plane Node の Docker コンテナ)上の hostPath ベースであり、この「ノード密接結合」型ストレージの典型例です。本回の演習①では PVC を apply した直後の STATUSPending になっていることを実際に観察します。

アクセスモードの概念(詳細は H2-10 で整理)

PVC にはアクセスモードspec.accessModes)を必ず指定します。同じ PV を「何台のノードから・どのような読み書き権限で」使えるかを表す重要なフィールドです。

  • ReadWriteOnce (RWO):単一ノードで読み書き可能。PostgreSQL の StatefulSet に向く
  • ReadOnlyMany (ROX):複数ノードから読み取り専用
  • ReadWriteMany (RWX):複数ノードから同時読み書き可能。NFS や Longhorn などの分散ストレージで提供
  • ReadWriteOncePod (RWOP):単一 Pod のみで読み書き可能(v1.29 GA)

本回で作成する PVC は ReadWriteOnce を指定します。PostgreSQL は単一プロセスがデータディレクトリを排他的に書き込む前提のため、複数ノードから同時書き込みできてしまうと壊れます。各モードの詳細と本番ストレージとの対応表は H2-10 でまとめます。

動的プロビジョニングの仕組みを示す縦フロー図。フローの起点である PVC(postgres-data-fanclub-db-0)から storageClassName を参照して StorageClass standard へ、次に Pod スケジュール後に PV が動的プロビジョニングされ、最後に StatefulSet Pod fanclub-db-0 が volumeMounts でマウントする。PVC と PV は側面の双方向矢印で対等に Bound する関係として強調され、Pod 削除後も PVC が残りデータが復元されることを下部の注記が補足している。

StatefulSet の概念 — Deployment との違いと 3 つの「安定」

StatefulSet はステートフルワークロード向けのコントローラです。Deployment が「Pod の集合を雑に並列稼働させる」のに対し、StatefulSet は「Pod の個体性を保ちつつ順序立てて運用する」設計です。CKAD 試験では「DB / メッセージキュー / 分散ストレージなど状態を持つアプリには StatefulSet を使う」という選択判断が頻出します。

StatefulSet の 3 つの「安定」

StatefulSet の核は次の 3 点です。これらをまとめて「StatefulSet の 3 つの安定」と呼びます。

  • 安定した Pod 名<StatefulSet 名>-<序数> の形式で連番が割り当てられる(例: fanclub-db-0fanclub-db-1)。Pod が削除されても、StatefulSet コントローラは同じ名前で Pod を再作成する
  • 安定した DNS 名:Headless Service と組み合わせると <Pod 名>.<Headless Service 名>.<namespace>.svc.cluster.local という Pod 個別の FQDN が DNS で解決可能になる(例: fanclub-db-0.fanclub-db-headless.default.svc.cluster.local
  • 安定した永続ストレージvolumeClaimTemplates によって Pod ごとに専用の PVC が自動作成され、Pod が削除されても PVC は残る。同名 Pod が再作成されると同じ PVC が再マウントされる

Deployment と StatefulSet の比較

両者の違いを表にまとめます。

特性DeploymentStatefulSet
Pod 名ランダムサフィックス(fanclub-backend-7d5f9c4b6-x2k8h連番固定(fanclub-db-0fanclub-db-1
Pod 再作成後の名前新しいランダムサフィックス同じ連番名(fanclub-db-0 → 削除 → fanclub-db-0 として再作成)
起動順序並列(順序保証なし)序数順(-0 が Ready になるまで -1 は作らない)
永続ストレージPod 個別 PVC は持てない(共有 PVC か emptyDir)volumeClaimTemplates で Pod ごと専用 PVC が自動生成
DNS 名Service の ClusterIP のみHeadless Service による Pod 個別 FQDN
適切な用途ステートレスアプリ(Web / API サーバー)ステートフルアプリ(DB / メッセージキュー / 分散 KV)

spec.serviceName と spec.podManagementPolicy

StatefulSet には Deployment にない 2 つの重要フィールドがあります。

  • spec.serviceName:Headless Service の名前を指定する必須フィールド。本回では fanclub-db-headless を指定する。これにより各 Pod の安定 DNS 名が DNS サーバ(CoreDNS)で解決可能になる
  • spec.podManagementPolicyOrderedReady(デフォルト)と Parallel の 2 値。OrderedReady は Pod を -0 から順に Ready まで待ってから次へ進める。Parallel は並列に作成する。本回は replicas=1 のため挙動は変わらないが、概念として把握する

なぜ replicas: 1 なのか

本回の StatefulSet は replicas: 1 で作成します。「StatefulSet なら複数レプリカを冗長化するのでは」と疑問を持つ読者もいるかもしれませんが、PostgreSQL のような RDBMS は単純にレプリカを増やしても水平スケールできません

  • PVC のアクセスモードが ReadWriteOnce のため、複数 Pod が同じ PVC を同時マウントできない(StatefulSet の volumeClaimTemplates は Pod ごとに別 PVC を作るため、各 Pod は独立したデータディレクトリになる)
  • 各 Pod が独立したデータディレクトリを持つと、それぞれ別の DB として動作してしまい、データの整合性が取れない
  • 本格的な PostgreSQL の高可用性構成には Streaming Replication(Primary / Replica)や論理レプリケーション、または CloudNativePG / Patroni のようなオペレータが必要

本シリーズでは第3巻第3回で CloudNativePG を扱う計画です。第1巻スコープではシングルレプリカで「StatefulSet がデータを永続化する仕組み」の理解に集中します。

Headless Service — StatefulSet の安定 DNS 名を実現する

第8回で概念を先行紹介した Headless Service を、本回で本格的に実装します。Headless Service は spec.clusterIP: None を指定した特殊な Service で、ClusterIP(仮想 IP)を持たず、DNS が直接 Pod の実 IP を返します。StatefulSet と組み合わせることで、Pod 個別の FQDN が解決可能になります。

DNS 名の構造

StatefulSet + Headless Service の組み合わせで、各 Pod は次の形式の FQDN を持ちます。

<Pod 名>.<Headless Service 名>.<namespace>.svc.cluster.local

本回では具体的に次の名前になります。

fanclub-db-0.fanclub-db-headless.default.svc.cluster.local

この FQDN は Pod が再作成されても変わらないため、PostgreSQL のレプリケーション設定や Operator から「Primary は -0・Replica は -1」のように Pod 個体を名指しできるようになります。Deployment の Pod 名はランダムなので、同じ運用は成立しません。

Headless Service と通常 ClusterIP Service の役割分担

本回ではあえてHeadless Service と ClusterIP Service を 2 つ同時に作ります。役割が異なるためです。

Servicespec.clusterIP役割誰が使うか
fanclub-db-headlessNoneStatefulSet Pod の安定 DNS 名解決のためStatefulSet コントローラ・将来のレプリカ間通信
fanclub-db自動採番(10.96.x.x)Backend → DB の接続先(負荷分散・接続抽象化)fanclub-backend Pod の DB_HOST 環境変数

Backend からの DB 接続は通常の ClusterIP Service(fanclub-db)に向けます。Headless Service を Backend が直接使う必要はありません。

Headless Service は「StatefulSet 内部の安定識別」という別の文脈で使うものです。本シリーズの app-spec では DB_HOST: fanclub-db として ClusterIP Service 名を使う設計としています。

Headless Service 完全 YAML

本回で ~/fanclub-manifests/fanclub-db-headless-service.yaml として作成する Headless Service の完全 YAML です。

apiVersion: v1
kind: Service
metadata:
  name: fanclub-db-headless
  namespace: default
  labels:
    app: fanclub-db
spec:
  clusterIP: None
  selector:
    app: fanclub-db
  ports:
    - name: postgres
      port: 5432
      targetPort: 5432
      protocol: TCP

設計ポイントを整理します。

  • spec.clusterIP: None:これが Headless Service の決定的な目印。kube-proxy は転送ルールを作らず、CoreDNS は Pod の実 IP を直接返す
  • spec.selector.app: fanclub-db:StatefulSet の Pod テンプレートのラベルと一致させる。一致しないと Endpoints が空になり、DNS 解決が失敗する
  • ports[].port: 5432:PostgreSQL のデフォルト Listen ポート。Headless Service の場合、port は SRV レコード生成のためのメタデータとして使われる

DB ClusterIP Service 完全 YAML

続いて Backend からの接続先となる通常の ClusterIP Service を ~/fanclub-manifests/fanclub-db-service.yaml として作成します。

apiVersion: v1
kind: Service
metadata:
  name: fanclub-db
  namespace: default
  labels:
    app: fanclub-db
spec:
  type: ClusterIP
  selector:
    app: fanclub-db
  ports:
    - name: postgres
      port: 5432
      targetPort: 5432
      protocol: TCP

第8回で扱った fanclub-backend Service と同じ構造です。違いは selector が app: fanclub-db になっている点と、port / targetPort が PostgreSQL の 5432 になっている点だけです。Backend の DB_HOST はこの Service 名 fanclub-db を指します。

第9回完了時点の fanclub-api 3 層構成完成図。kubectl/curl からのリクエストが Backend Service fanclub-backend(ClusterIP)を経由して Backend Pod へ転送され、Backend Pod が DB_HOST=fanclub-db で DB Service 群に接続する。DB Service 群は ClusterIP 型の fanclub-db と Headless 型の fanclub-db-headless の複合パネルで構成され、その先の StatefulSet Pod fanclub-db-0(PostgreSQL 18)内には PVC と PV のサブパネルがネストされている。Pod を削除しても PVC が残りデータが復元されることを下部の注記が示している。

volumeClaimTemplates — 各 Pod に固有の PVC を自動作成する

StatefulSet 固有の機能の中でも、もっとも重要なのが spec.volumeClaimTemplates です。これは「PVC のひな型」を StatefulSet 自体に組み込む仕組みで、Pod が作成されるたびに対応する PVC を自動生成します。

PVC 名の命名規則

volumeClaimTemplates から生成される PVC の名前は次の規則に従います。

<volumeClaimTemplates[].metadata.name>-<Pod 名>

本回の StatefulSet では volumeClaimTemplates[].metadata.name: postgres-datafanclub-db-0 が組み合わさって、生成される PVC は postgres-data-fanclub-db-0 になります。仮に replicas を 3 にすれば postgres-data-fanclub-db-0 / -1 / -2 の 3 つの PVC が独立して作られます。

StatefulSet 削除後も PVC が残る設計意図

volumeClaimTemplates から自動生成された PVC は、StatefulSet を kubectl delete してもデフォルトでは自動削除されません。これは「データを誤って消さない」という強い設計意図によるものです。

StatefulSet の YAML 定義をミスして再作成したいとき、運用者が誤って kubectl delete statefulset を実行してしまっても、PVC は残ります。新しい StatefulSet を同じ名前で apply すれば、既存の PVC を再マウントしてデータが復活します。「Pod は使い捨て、PVC はデータの番人」という役割分担が貫かれています。

もし PVC を含めて完全に削除したい場合は、明示的に kubectl delete pvc <pvc-name> を実行します。あるいは spec.persistentVolumeClaimRetentionPolicy フィールドで挙動を変えることもできますが(v1.27 GA)、本回ではデフォルト挙動(PVC 残存)を前提に説明します。

volumes と volumeClaimTemplates の違い

第7回までで扱ってきた spec.volumes と本回の spec.volumeClaimTemplates は別物です。混同しないよう整理します。

フィールド場所性質用途
spec.volumesPod テンプレート内Pod ライフサイクル一致(emptyDir / configMap / secret 等)すべての Pod が同じ ConfigMap を共有する場面
spec.volumeClaimTemplatesStatefulSet 直下Pod ごとに専用 PVC を自動生成(永続)Pod ごとに独立したデータディレクトリが必要な場面

本回の StatefulSet では両方を併用します。volumeClaimTemplatespostgres-data(PostgreSQL のデータディレクトリ)を、spec.volumesinit-sql(初期化 SQL を含む ConfigMap)をマウントします。

PostgreSQL の PGDATA サブディレクトリパターン

PostgreSQL 公式イメージは環境変数 PGDATA でデータディレクトリの場所を制御します。デフォルトは /var/lib/postgresql/data ですが、PVC をこのパスに直接マウントすると初期化時に問題が起きることがあります。これは現場で頻繁に遭遇するトラブルで、後の H2-11(現場ヒヤリハット)で詳しく扱いますが、ここで結論を先に示しておきます。

  • PVC のマウント先:/var/lib/postgresql/data(ディレクトリ全体を PVC でバックアップ)
  • PGDATA 環境変数:/var/lib/postgresql/data/pgdata(サブディレクトリを明示)

PostgreSQL は PGDATA が指すサブディレクトリに initdb を実行します。PVC マウントポイント直下ではなく一段下げることで、所有権・パーミッション関連の事故を回避できます。これは PostgreSQL を K8s で動かすときの定石です。

init.sql を ConfigMap で注入する

PostgreSQL 公式イメージは /docker-entrypoint-initdb.d/ 配下の .sql.sh ファイルを初回起動時のみ実行します。本回では members テーブルを作る SQL を ConfigMap に格納し、StatefulSet の volumes/docker-entrypoint-initdb.d/ にマウントします。

ここで重要な制約は「初期化 SQL は PGDATA が空のときのみ実行される」点です。Pod が再起動して既存の PVC を再マウントした場合、PGDATA/pgdata/ にデータが既に存在するため、ConfigMap の SQL は再実行されません。

これは事故防止のための仕様で、Pod 再起動するたびに CREATE TABLE が走って既存データが上書きされるような事態を防いでいます。本回の演習③で実際にこの冪等性を確認します。

本回で ~/fanclub-manifests/fanclub-db-init-configmap.yaml として作成する InitDB 用 ConfigMap の完全 YAML です。ConfigMap 自体の解説は第10回で本格的に行うため、本回では「初期化 SQL を ConfigMap でマウントする」という仕組みの利用にとどめます。

apiVersion: v1
kind: ConfigMap
metadata:
  name: fanclub-db-init
  namespace: default
  labels:
    app: fanclub-db
data:
  init.sql: |
    CREATE TABLE IF NOT EXISTS members (
        id         SERIAL PRIMARY KEY,
        name       VARCHAR(100) NOT NULL,
        email      VARCHAR(255) NOT NULL UNIQUE,
        plan       VARCHAR(50)  NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );

CREATE TABLE IF NOT EXISTS としている点に注目してください。冪等性を高めるため、もし何らかの事情でテーブルが先に存在していてもエラーにならない記述にしています。app-spec.md §6 で定義した members テーブルのカラム構成(id / name / email / plan / created_at)と完全一致させています。

StatefulSet 完全 YAML

ここまでの設計を集約した StatefulSet の完全 YAML です。本回で ~/fanclub-manifests/fanclub-db-statefulset.yaml として作成します。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: fanclub-db
  namespace: default
  labels:
    app: fanclub-db
spec:
  serviceName: "fanclub-db-headless"
  replicas: 1
  selector:
    matchLabels:
      app: fanclub-db
  template:
    metadata:
      labels:
        app: fanclub-db
    spec:
      containers:
        - name: fanclub-db
          image: postgres:18
          imagePullPolicy: IfNotPresent
          ports:
            - name: postgres
              containerPort: 5432
              protocol: TCP
          env:
            - name: POSTGRES_USER
              value: "appuser"
            - name: POSTGRES_PASSWORD
              value: "apppassword"
            - name: POSTGRES_DB
              value: "fanclubdb"
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          volumeMounts:
            - name: postgres-data
              mountPath: /var/lib/postgresql/data
            - name: init-sql
              mountPath: /docker-entrypoint-initdb.d
      volumes:
        - name: init-sql
          configMap:
            name: fanclub-db-init
  volumeClaimTemplates:
    - metadata:
        name: postgres-data
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: standard
        resources:
          requests:
            storage: 1Gi

このマニフェストの設計ポイントを整理します。

  • apiVersion: apps/v1:StatefulSet は apps/v1 グループ。apiVersion: v1 ではない点に注意
  • spec.serviceName: "fanclub-db-headless":Headless Service 名と完全一致させる必須フィールド。一致しないと Pod の DNS 名が解決できない
  • spec.replicas: 1:単一レプリカ。本回スコープでは複数レプリカは扱わない
  • spec.selector.matchLabels.app: fanclub-db:Pod テンプレートの labels と完全一致させる。Service の selector と同じラベルを使うことで、Service が Pod を発見できる
  • image: postgres:18:PostgreSQL 公式イメージ。app-spec.md §3 で固定したバージョン
  • imagePullPolicy: IfNotPresent:kind ノード上に既に存在するイメージは再 pull しない。初回 apply 時に alma-proxy 経由で pull される
  • env.PGDATA: /var/lib/postgresql/data/pgdata:PVC マウントポイント直下ではなくサブディレクトリを指定。ヒヤリハット 1 の予防策
  • volumeMounts が 2 つpostgres-data(永続データ)と init-sql(初期化 SQL ConfigMap)
  • volumeClaimTemplates[].spec.storageClassName: standard:kind の default StorageClass を明示。storageClassName を省略するとクラスタの default が使われるが、明示する方が他環境への移植性が高い
  • resources.requests.storage: 1Gi:学習環境向けの控えめなサイズ。PostgreSQL 18 の初期データ + members テーブルの動作確認には十分

YAML の各フィールドのスキーマは kubectl explain statefulset.speckubectl explain statefulset.spec.volumeClaimTemplates で確認できます。CKAD 試験中も kubectl explain は利用可能です。

やってみよう①:PostgreSQL StatefulSet を起動して PVC 動的プロビジョニングを観察する

演習①では Headless Service・ClusterIP Service・InitDB ConfigMap・StatefulSet の 4 マニフェストを作成して apply し、PVC が Pending から Bound に遷移する動的プロビジョニングを実機で観察します。所要時間の目安は約 25 分です。

Step 1:前提状態を確認する

第8回完了時点(fanclub-backend Pod + Service あり / DB 系リソースなし)であることを確認します。

実行コマンド:

$ kubectl get pods,svc,pvc,statefulset -n default

実行結果:

NAME                  READY   STATUS    RESTARTS   AGE
pod/fanclub-backend   1/1     Running   0          86m

NAME                        TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/fanclub-backend     ClusterIP   10.96.150.60    <none>        80/TCP    86m
service/kubernetes          ClusterIP   10.96.0.1       <none>        443/TCP   6h7m

No resources found in default namespace.

No resources found in default namespace.

fanclub-backend Pod が Running、fanclub-backend Service が ClusterIP で稼働中、PVC・StatefulSet はまだ存在しない、という状態であれば前提が整っています。

Step 2:InitDB 用 ConfigMap を作成する

members テーブルを作成する SQL を含む ConfigMap を作成します。

実行コマンド:

$ cat > ~/fanclub-manifests/fanclub-db-init-configmap.yaml <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
  name: fanclub-db-init
  namespace: default
  labels:
    app: fanclub-db
data:
  init.sql: |
    CREATE TABLE IF NOT EXISTS members (
        id         SERIAL PRIMARY KEY,
        name       VARCHAR(100) NOT NULL,
        email      VARCHAR(255) NOT NULL UNIQUE,
        plan       VARCHAR(50)  NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
EOF

実行コマンド:

$ kubectl apply -f ~/fanclub-manifests/fanclub-db-init-configmap.yaml

実行結果:

configmap/fanclub-db-init created

Step 3:Headless Service と ClusterIP Service を作成する

StatefulSet apply の前に Headless Service を先に作るのが定石です。StatefulSet が spec.serviceName で参照する先が存在していないと、DNS 解決の準備が整わないためです。

実行コマンド:

$ cat > ~/fanclub-manifests/fanclub-db-headless-service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: fanclub-db-headless
  namespace: default
  labels:
    app: fanclub-db
spec:
  clusterIP: None
  selector:
    app: fanclub-db
  ports:
    - name: postgres
      port: 5432
      targetPort: 5432
      protocol: TCP
EOF

実行コマンド:

$ cat > ~/fanclub-manifests/fanclub-db-service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: fanclub-db
  namespace: default
  labels:
    app: fanclub-db
spec:
  type: ClusterIP
  selector:
    app: fanclub-db
  ports:
    - name: postgres
      port: 5432
      targetPort: 5432
      protocol: TCP
EOF

実行コマンド:

$ kubectl apply -f ~/fanclub-manifests/fanclub-db-headless-service.yaml
$ kubectl apply -f ~/fanclub-manifests/fanclub-db-service.yaml

実行結果:

service/fanclub-db-headless created
service/fanclub-db created

この時点では Pod が存在しないため、両 Service の Endpoints は空です。kubectl get svcfanclub-db-headlessCLUSTER-IPNone になっていることを確認します。

実行コマンド:

$ kubectl get svc

実行結果:

NAME                  TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
fanclub-backend       ClusterIP   10.96.150.60    <none>        80/TCP     3h32m
fanclub-db            ClusterIP   10.96.131.167   <none>        5432/TCP   5s
fanclub-db-headless   ClusterIP   None            <none>        5432/TCP   5s
kubernetes            ClusterIP   10.96.0.1       <none>        443/TCP    6h7m

Step 4:StatefulSet を apply する

StatefulSet 本体を apply します。volumeClaimTemplates から PVC が自動生成され、続いて Pod がスケジュールされ、最後に動的プロビジョニングで PV が作成される流れを次のステップで観察します。

実行コマンド:

$ cat > ~/fanclub-manifests/fanclub-db-statefulset.yaml <<'EOF'
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: fanclub-db
  namespace: default
  labels:
    app: fanclub-db
spec:
  serviceName: "fanclub-db-headless"
  replicas: 1
  selector:
    matchLabels:
      app: fanclub-db
  template:
    metadata:
      labels:
        app: fanclub-db
    spec:
      containers:
        - name: fanclub-db
          image: postgres:18
          imagePullPolicy: IfNotPresent
          ports:
            - name: postgres
              containerPort: 5432
              protocol: TCP
          env:
            - name: POSTGRES_USER
              value: "appuser"
            - name: POSTGRES_PASSWORD
              value: "apppassword"
            - name: POSTGRES_DB
              value: "fanclubdb"
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          volumeMounts:
            - name: postgres-data
              mountPath: /var/lib/postgresql/data
            - name: init-sql
              mountPath: /docker-entrypoint-initdb.d
      volumes:
        - name: init-sql
          configMap:
            name: fanclub-db-init
  volumeClaimTemplates:
    - metadata:
        name: postgres-data
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: standard
        resources:
          requests:
            storage: 1Gi
EOF

実行コマンド:

$ kubectl apply -f ~/fanclub-manifests/fanclub-db-statefulset.yaml

実行結果:

statefulset.apps/fanclub-db created

Step 5:PVC が Pending → Bound に遷移する様子を観察する

StatefulSet apply 直後の PVC の状態を観察します。kubectl get pvc -w-w は watch モード)で状態変化を継続表示します。

実行コマンド:

$ kubectl get pvc -w

実行結果(PVC 状態遷移):

NAME                         STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
postgres-data-fanclub-db-0   Pending                                      standard       0s
postgres-data-fanclub-db-0   Pending                                      standard       1s
postgres-data-fanclub-db-0   Bound     pvc-8cba891c-8d9b-4483-88c6-213d38565178   1Gi   RWO   standard   11s

確認できたら Ctrl+C で watch を抜けます。最終状態を改めて確認します。

実行コマンド:

$ kubectl get pvc

実行結果:

NAME                         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS
postgres-data-fanclub-db-0   Bound    pvc-8cba891c-8d9b-4483-88c6-213d38565178   1Gi        RWO            standard

PVC 名は postgres-data-fanclub-db-0 です。volumeClaimTemplates[].metadata.namepostgres-data)と Pod 名(fanclub-db-0)の連結で生成されたものです。

Step 6:自動生成された PV を確認する

local-path-provisioner が動的に作成した PV を確認します。

実行コマンド:

$ kubectl get pv

実行結果:

NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                STORAGECLASS
pvc-8cba891c-8d9b-4483-88c6-213d38565178   1Gi        RWO            Delete           Bound    default/postgres-data-fanclub-db-0   standard

PV 名は UUID 由来でランダムです。CLAIM カラムに default/postgres-data-fanclub-db-0 が表示されていれば、PV と PVC が正しく Bound されています。

PVC の詳細情報も確認します。

実行コマンド:

$ kubectl describe pvc postgres-data-fanclub-db-0

実行結果(抜粋):

Name:          postgres-data-fanclub-db-0
Namespace:     default
StorageClass:  standard
Status:        Bound
Volume:        pvc-8cba891c-8d9b-4483-88c6-213d38565178
Capacity:      1Gi
Access Modes:  RWO
Used By:       fanclub-db-0
Events:
  Type    Reason                 Age   From                   Message
  ----    ------                 ----  ----                   -------
  Normal  WaitForFirstConsumer   20s   persistentvolume-controller  waiting for first consumer to be created before binding
  Normal  Provisioning           11s   rancher.io/local-path  External provisioner is provisioning volume for claim "default/postgres-data-fanclub-db-0"
  Normal  ProvisioningSucceeded  10s   rancher.io/local-path  Successfully provisioned volume pvc-8cba891c-8d9b-4483-88c6-213d38565178

Events セクションには waiting for first consumer to be created before bindingSuccessfully provisioned volume pvc-... といったメッセージが時系列で残ります。これが WaitForFirstConsumer の挙動の動かぬ証拠です。

Step 7:PostgreSQL 起動ログを確認する

fanclub-db-0 Pod が Running になっているか確認し、PostgreSQL の起動ログと initdb の実行ログを確認します。

実行コマンド:

$ kubectl get pods -l app=fanclub-db

実行結果:

NAME           READY   STATUS    RESTARTS   AGE
fanclub-db-0   1/1     Running   0          20s

実行コマンド:

$ kubectl logs fanclub-db-0 --tail=30

実行結果(抜粋):

2026-05-10 05:37:15.204 UTC [56] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2026-05-10 05:37:15.219 UTC [56] LOG:  database system is ready to accept connections
 done
server started
CREATE DATABASE


/usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/init.sql
CREATE TABLE


2026-05-10 05:37:15.413 UTC [56] LOG:  received fast shutdown request
2026-05-10 05:37:15.636 UTC [1] LOG:  starting PostgreSQL 18.3 (Debian 18.3-1.pgdg13+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
2026-05-10 05:37:15.637 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2026-05-10 05:37:15.642 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2026-05-10 05:37:15.655 UTC [1] LOG:  database system is ready to accept connections

注目するログメッセージは次の 3 点です。

  • PostgreSQL Database directory appears to contain a database; Skipping initialization または Performing initial database creation:PGDATA が空かどうかの判定結果
  • /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/init.sql:ConfigMap でマウントした初期化 SQL の実行
  • database system is ready to accept connections:PostgreSQL が接続を受け付ける状態になった合図

Step 8:psql で members テーブルが作られているか確認する

fanclub-db-0 Pod に kubectl exec で入り、psql でテーブル一覧を表示します。

実行コマンド:

$ kubectl exec fanclub-db-0 -- psql -U appuser -d fanclubdb -c "\dt"

実行結果:

           List of tables
 Schema |  Name   | Type  |  Owner
--------+---------+-------+---------
 public | members | table | appuser
(1 row)

カラム構造も確認します。

実行コマンド:

$ kubectl exec fanclub-db-0 -- psql -U appuser -d fanclubdb -c "\d members"

実行結果:

                                        Table "public.members"
   Column   |            Type             | Collation | Nullable |               Default
------------+-----------------------------+-----------+----------+-------------------------------------
 id         | integer                     |           | not null | nextval('members_id_seq'::regclass)
 name       | character varying(100)      |           | not null |
 email      | character varying(255)      |           | not null |
 plan       | character varying(50)       |           | not null |
 created_at | timestamp without time zone |           |          | CURRENT_TIMESTAMP
Indexes:
    "members_pkey" PRIMARY KEY, btree (id)
    "members_email_key" UNIQUE CONSTRAINT, btree (email)

members テーブルが public スキーマに存在し、Owner が appuser であれば、ConfigMap の init.sql が initdb 直後に実行されたことを意味します。

Step 9:Headless Service の DNS 名を解決してみる

StatefulSet + Headless Service の核となる仕組みである Pod 個別 FQDN が解決できるか、busybox:1.36 の使い捨て Pod で nslookup を実行して確認します。

実行コマンド:

$ kubectl run dns-check \
    --image=busybox:1.36 \
    --rm -it \
    --restart=Never \
    -- nslookup fanclub-db-0.fanclub-db-headless.default.svc.cluster.local

実行結果:

Server:		10.96.0.10
Address:	10.96.0.10:53

Name:	fanclub-db-0.fanclub-db-headless.default.svc.cluster.local
Address: 10.244.0.18

pod "dns-check" deleted

FQDN fanclub-db-0.fanclub-db-headless.default.svc.cluster.local が Pod の実 IP(10.244.0.X)に解決されています。これが StatefulSet の「安定 DNS 名」です。Pod を削除しても再作成された fanclub-db-0 に対して同じ FQDN が向き直ります。

演習①完了:PostgreSQL StatefulSet を起動し、PVC の動的プロビジョニング(Pending → Bound)を観察し、members テーブルの初期化と Headless Service の DNS 解決まで確認しました。

やってみよう②:Backend Pod を再 apply して DB 接続を確認する

演習②では既存の fanclub-backend Pod を削除し、DB 接続用の環境変数(DB_HOST 等)を追加した新マニフェストで再 apply します。DB 接続の疎通は (a) Init Container wait-for-db の完了・(b) Pod への env 注入確認・(c) psql 直接 SELECT の 3 点で確認します。所要時間の目安は約 15 分です。

注記:fanclub-backend:0.1.0 の REST endpoint について

fanclub-backend:0.1.0 は第3回で構築した最小 Payara Micro 構成のイメージです。src/main/java には AppConfig.javaHealthChecks.java のみが存在し、MembersResource.java/api/members CRUD endpoint)は未実装です。

このため curl http://fanclub-backend/api/members を実行しても HTTP 404(Payara Error report HTML)が返ります。

本回の主目的は「PVC + StatefulSet による永続ストレージ」の理解であり、REST endpoint の Java 実装は本シリーズのスコープ外です。DB 接続の疎通確認は psql を使って PostgreSQL に直接アクセスすることで行います。/api/members 等の REST endpoint 実装は読者の応用課題、または別シリーズで扱う内容です。

Step 1:既存の fanclub-backend Pod を削除する

第7回で作成した fanclub-backend Pod は単独 Pod であり、env セクションに DB 接続情報を持ちません。env を追加するには Pod を一度削除して新マニフェストで再作成する必要があります(Pod の env は不変フィールドのため、稼働中 Pod に対する kubectl apply での env 追加はできません)。

実行コマンド:

$ kubectl delete pod fanclub-backend

実行結果:

pod "fanclub-backend" deleted from default namespace

第8回で作成した fanclub-backend Service(ClusterIP 10.96.150.60)はそのまま残しておきます。Service は selector でラベルマッチングしているため、新しい Pod が同じラベル app: fanclub-backend で起動すれば自動的に Endpoints に登録されます。

Step 2:DB 接続 env と Init Container を追加した新 Pod YAML を作成する

今回は DB 接続情報の env と、PostgreSQL が起動するまで待機する Init Container(wait-for-db)を追加したマニフェストを新規ファイルとして作成します。env セクションに DB 接続情報を追加するのが本回の差分です。

実行コマンド:

$ cat > ~/fanclub-manifests/fanclub-backend-with-db-pod.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: fanclub-backend
  namespace: default
  labels:
    app: fanclub-backend
spec:
  initContainers:
    - name: wait-for-db
      image: busybox:1.36
      command:
        - sh
        - -c
        - "until nc -z fanclub-db 5432; do echo 'waiting for DB...'; sleep 2; done"
  containers:
    - name: fanclub-backend
      image: fanclub-backend:0.1.0
      imagePullPolicy: IfNotPresent
      ports:
        - containerPort: 8080
      env:
        - name: DB_HOST
          value: "fanclub-db"
        - name: DB_PORT
          value: "5432"
        - name: DB_NAME
          value: "fanclubdb"
        - name: DB_USER
          value: "appuser"
        - name: DB_PASSWORD
          value: "apppassword"
        - name: JAVA_OPTS
          value: "-XX:MaxRAMPercentage=75.0"
      resources:
        requests:
          memory: "256Mi"
          cpu: "250m"
        limits:
          memory: "512Mi"
          cpu: "1000m"
EOF

第7回からの差分は 2 点です。(1) initContainerswait-for-db を追加。nc -z fanclub-db 5432 で ClusterIP Service 経由の TCP 疎通が確立するまでループします。(2) env セクションに DB_HOST から DB_PASSWORD までの 5 つの環境変数が追加。

DB_HOST: "fanclub-db" は Step 3(演習①)で作成した ClusterIP Service 名と一致しています。CoreDNS が fanclub-db.default.svc.cluster.local に解決し、Service 経由で fanclub-db-0 Pod の 5432 番ポートに転送されます。

本回時点では DB の認証情報を Pod YAML に平文で書いている点に違和感を覚える読者もいるかもしれません。これは「動く状態を作ってからセキュアにする」という段階的学習のための意図的な選択です。問題点と解消方法は H2-12(ep10 への橋渡し)で扱います。

Step 3:再 apply して Pod を起動する

実行コマンド:

$ kubectl apply -f ~/fanclub-manifests/fanclub-backend-with-db-pod.yaml

実行結果:

pod/fanclub-backend created

Init Container(wait-for-db)の実行完了 → メインコンテナ起動 → Payara Micro 起動完了までを待ちます。第7回で確認した通り、Payara Micro の起動には約 10 秒かかります。

実行コマンド:

$ kubectl get pod fanclub-backend -w

実行結果(ライフサイクル遷移):

NAME              READY   STATUS     RESTARTS   AGE
fanclub-backend   0/1     Init:0/1   0          0s
fanclub-backend   0/1     Init:0/1   0          2s
fanclub-backend   1/1     Running    0          10s

READY が 1/1 になったら Ctrl+C で watch を抜けます。

Step 4:Pod に注入された環境変数を確認する

新しい fanclub-backend Pod に DB 接続情報が正しく注入されているか確認します。

実行コマンド:

$ kubectl exec fanclub-backend -- printenv | grep -E "^DB_|^JAVA_OPTS"

実行結果:

JAVA_OPTS=-XX:MaxRAMPercentage=75.0
DB_HOST=fanclub-db
DB_PORT=5432
DB_NAME=fanclubdb
DB_USER=appuser
DB_PASSWORD=apppassword

5 つの DB 接続環境変数すべてが Pod に注入されていることが確認できました。DB_PASSWORDkubectl exec で平文で見えてしまっています。これも H2-12 で扱う論点です。

Step 5:Backend Pod のログで Payara Micro 起動完了を確認する

Init Container wait-for-db が完了し、メインコンテナが起動していることをログで確認します。

wait-for-db の完了は「fanclub-db ClusterIP Service(TCP 5432)への TCP 疎通が確立した」ことを意味します。つまり DB Service 経由で fanclub-db-0 Pod の PostgreSQL にネットワーク到達できている証拠です。

実行コマンド:

$ kubectl logs fanclub-backend --tail=20

実行結果(抜粋):

[2026-05-10T05:39:43.829+0000] [INFO] Initializing Soteria 4.0.1.payara-p1 for context ''
[2026-05-10T05:39:44.017+0000] [INFO] Loading application [app] at [/]
[2026-05-10T05:39:44.025+0000] [INFO] Instance Configuration:
        Host: fanclub-backend
        Http Port(s): 8080
        Instance Name: Careful-Sardine
        Deployed: app (war) at /
[2026-05-10T05:39:44.029+0000] [INFO] Payara Micro 7.2026.4 (build 7) ready in 10,916 (ms)

Payara Micro 7.2026.4 ready in 10,916 (ms) が表示されていれば、アプリケーションが正常に起動しています。

Step 6:psql で members テーブルを直接 SELECT して DB 疎通を確認する

DB 接続の疎通確認として、fanclub-db-0 Pod に kubectl exec で psql を実行します。members テーブルに直接アクセスして、ConfigMap の init.sql が正しく反映されている状態を確認します。

実行コマンド:

$ kubectl exec fanclub-db-0 -- psql -U appuser -d fanclubdb -c "SELECT id, name, email, plan, created_at FROM members ORDER BY id;"

実行結果:

 id | name | email | plan | created_at
----+------+-------+------+------------
(0 rows)

members テーブルに行がない(0 rows)ことが確認できました。PostgreSQL が正常稼働し、テーブル SELECT が可能な状態です。Backend Pod が Running になった事実・Init Container wait-for-db の完了・env 注入確認・この psql SELECT の 4 点を総合して「DB 接続の疎通が成立している」と判断します。

演習②完了:fanclub-backend Pod を DB 接続版(Init Container wait-for-db + DB env)で再 apply し、Backend が DB Service 経由で PostgreSQL に TCP 接続できること・psql で members テーブルにアクセスできることを確認しました。3 層構成(Backend Pod + Backend Service + DB StatefulSet + DB Service)が完成しました。

やってみよう③:psql で INSERT → StatefulSet 削除 → 再 apply → データ残存確認

演習③は StatefulSet + PVC の核となる挙動を実機で確認します。psql でデータを INSERT してから StatefulSet を削除し、PVC が残存することを確認した後、再 apply で同じ PVC が再マウントされてデータが保持されることを実証します。所要時間の目安は約 10 分です。

Step 1:psql で会員データを INSERT する

fanclub-db-0 Pod に直接 psql で会員データを 2 件 INSERT します。

実行コマンド:

$ kubectl exec fanclub-db-0 -- psql -U appuser -d fanclubdb -c "INSERT INTO members (name, email, plan) VALUES ('山田太郎', 'yamada@example.com', 'premium'), ('鈴木花子', 'suzuki@example.com', 'standard') RETURNING id, name, plan;"

実行結果:

 id |   name   |   plan
----+----------+----------
  1 | 山田太郎 | premium
  2 | 鈴木花子 | standard
(2 rows)

INSERT 0 2

id=1・id=2 が自動採番され、INSERT が成功しました。SELECT で内容を確認します。

実行コマンド:

$ kubectl exec fanclub-db-0 -- psql -U appuser -d fanclubdb -c "SELECT id, name, email, plan, created_at FROM members ORDER BY id;"

実行結果:

 id |   name   |       email        |   plan   |         created_at
----+----------+--------------------+----------+----------------------------
  1 | 山田太郎 | yamada@example.com | premium  | 2026-05-10 05:42:17.132549
  2 | 鈴木花子 | suzuki@example.com | standard | 2026-05-10 05:42:17.132549
(2 rows)

2 件のレコードが確認できました。created_at の値(2026-05-10 05:42:17.132549)を記録しておきます。後で PVC 再マウント後に値が完全一致することを確認します。

Step 2:StatefulSet を削除する(PVC は残る)

kubectl delete statefulset fanclub-db を実行します。StatefulSet と Pod は削除されますが、volumeClaimTemplates から自動生成された PVC は残ります。

実行コマンド:

$ kubectl delete statefulset fanclub-db

実行結果:

statefulset.apps "fanclub-db" deleted from default namespace

Step 3:PVC が残存していることを確認する

StatefulSet と Pod が消えた後の状態を確認します。

実行コマンド:

$ kubectl get pods,pvc,statefulset

実行結果:

NAME                  READY   STATUS    RESTARTS   AGE
pod/fanclub-backend   1/1     Running   0          2m56s

NAME                                               STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS
persistentvolumeclaim/postgres-data-fanclub-db-0   Bound    pvc-8cba891c-8d9b-4483-88c6-213d38565178   1Gi        RWO            standard

StatefulSet と fanclub-db-0 Pod は消えましたが、postgres-data-fanclub-db-0 PVC は Bound 状態のまま残存しています。PVC は StatefulSet を削除しても自動削除されません。これが「データの番人」としての設計意図です。

Step 4:StatefulSet を再 apply する

同じ YAML で StatefulSet を再作成します。StatefulSet コントローラが既存の PVC(postgres-data-fanclub-db-0)を発見し、再マウントします。

実行コマンド:

$ kubectl apply -f ~/fanclub-manifests/fanclub-db-statefulset.yaml

実行結果:

statefulset.apps/fanclub-db created

実行コマンド(10 秒後に状態確認):

$ kubectl get pods,statefulset -l app=fanclub-db

実行結果:

NAME           READY   STATUS    RESTARTS   AGE
fanclub-db-0   1/1     Running   0          10s

NAME                        READY   AGE
statefulset.apps/fanclub-db 1/1     10s

Step 5:データが残っていることを psql で確認する(永続性の核心)

再作成された fanclub-db-0 Pod に psql で接続し、Step 1 で INSERT したデータが残っているか確認します。

実行コマンド:

$ kubectl exec fanclub-db-0 -- psql -U appuser -d fanclubdb -c "SELECT id, name, email, plan, created_at FROM members ORDER BY id;"

実行結果:

 id |   name   |       email        |   plan   |         created_at
----+----------+--------------------+----------+----------------------------
  1 | 山田太郎 | yamada@example.com | premium  | 2026-05-10 05:42:17.132549
  2 | 鈴木花子 | suzuki@example.com | standard | 2026-05-10 05:42:17.132549
(2 rows)

2 件のデータが完全に残っています。created_at の値(2026-05-10 05:42:17.132549)が Step 1 と一致していることを確認してください。StatefulSet と Pod が削除・再作成されても、PVC に保存されたデータはマイクロ秒レベルで保持されています。これが StatefulSet + PVC を DB に使う根本的な理由です。

補足:init.sql は再実行されない

StatefulSet を再 apply して fanclub-db-0 Pod が再作成されたとき、ConfigMap でマウントしている /docker-entrypoint-initdb.d/init.sql再実行されません。PostgreSQL 公式イメージの entrypoint スクリプトは、PGDATA が空の場合のみ初期化処理を走らせるためです。

再起動した PostgreSQL のログを確認すると、初期化スキップのメッセージが出ています。

実行コマンド:

$ kubectl logs fanclub-db-0 --tail=5

実行結果(抜粋):

PostgreSQL Database directory appears to contain a database; Skipping initialization
2026-05-10 05:47:32.154 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2026-05-10 05:47:32.159 UTC [1] LOG:  database system is ready to accept connections

Skipping initialization の一行が、PGDATA に既存データがあると判断されたことを示しています。この冪等性のおかげで「Pod 再起動するたびに CREATE TABLE が走って既存データが破壊される」事故が防がれています。

演習③完了:psql で INSERT → StatefulSet 削除 → PVC 残存確認 → 再 apply → psql SELECT でデータ完全残存を確認しました。PVC が Pod のライフサイクルから独立してデータを保持する StatefulSet の核となる挙動を実証できました。

アクセスモード / Reclaim Policy / StorageClass のまとめ

本回で軽く触れたアクセスモード・Reclaim Policy・StorageClass の選択論点を整理します。CKAD 試験で「どのアクセスモードを選ぶべきか」「ReclaimPolicy の違いは何か」が問われる頻度は高く、概念整理が試験対策の中心になります。

アクセスモード比較表

アクセスモード略称意味用途例対応ストレージ例
ReadWriteOnceRWO単一ノードで読み書き可能PostgreSQL / MySQL(StatefulSet)EBS / GCE PD / Azure Disk / kind の local-path
ReadOnlyManyROX複数ノードから読み取り専用静的コンテンツ配信NFS(読み取り専用マウント)
ReadWriteManyRWX複数ノードから同時読み書き可能共有ファイルシステム / アップロード受け皿NFS / Longhorn / Ceph / EFS
ReadWriteOncePodRWOP単一 Pod のみで読み書き可能(v1.29 GA)厳格な排他制御が必要なケースCSI ドライバが対応しているもの

注意点として、PVC で RWX を要求しても、StorageClass が対応していなければ Bound できません。kind の local-pathRWO のみサポートします。本番では Longhorn や NFS で RWX をサポートする StorageClass を別途用意します。

また、複数 Pod が同じ PVC を共有する設計はファイルシステム整合性の確保が複雑になります。RDBMS のように「単一プロセスが排他的に書き込む」前提のアプリは、レプリカ間で個別の PVC を持つ(StatefulSet の volumeClaimTemplates 方式)方が安全です。

ReclaimPolicy の選択

ポリシーPVC 削除時の PV 挙動推奨環境
DeletePV と背後のストレージ実体(クラウドディスク等)も削除される開発・テスト・kind 環境(学習目的)
RetainPV は Released 状態で残り、管理者が手動で対処する本番環境(誤削除からの保護)

kind の standard StorageClass は Delete がデフォルトです。これは学習環境としては「片付けが楽」という長所がある一方、本番に同じ感覚で持ち込むと事故の元になります。

本番運用警告:本シリーズの kind 環境に慣れた状態で本番クラスタに移行する際は、StorageClass の ReclaimPolicy を必ず確認してください。クラウド環境のデフォルト StorageClass が Delete のままの場合、運用者が誤って PVC を削除すると、背後の EBS / Persistent Disk も同時に消えます。

スナップショットの復元が必要な事態を避けるため、本番では Retain を選ぶか、Delete を許容する代わりに別途バックアップ機構(Velero 等)を整備します。

StorageClass の使い分け(kind と本番の違い)

環境StorageClassプロビジョナ特徴
kind(本シリーズ第1巻)standardrancher.io/local-pathシングルノード hostPath ベース・本番不可
kubeadm 自前構築(本シリーズ第2巻)Longhorn の longhorndriver.longhorn.io分散ストレージ・RWX 対応・スナップショット可
AWS EKSgp3 / io2ebs.csi.aws.comEBS ボリューム・AZ 内 RWO
GCP GKEstandard-rwo / premium-rwopd.csi.storage.gke.ioPersistent Disk・ゾーン RWO

YAML の storageClassName はクラスタごとに別の値になります。マニフェストを別環境にそのまま持ち込むと PVC が Bound できません。本シリーズ第2巻第12回で Longhorn を導入し、本番想定の StorageClass を扱います。

AllowVolumeExpansion の有無

StorageClass の allowVolumeExpansion: true が設定されていれば、PVC の spec.resources.requests.storage を後から拡張できます。kind の standardfalse のため、本回で 1Gi として作成した PVC のサイズ変更は不可です。

本番環境(EBS / Longhorn)では true が標準で、データを保持したまま拡張できます。

現場ヒヤリハット — PGDATA 未指定で PostgreSQL が起動しない / volumeClaimTemplates 不変フィールド変更失敗

本回扱った技術は本番でも頻繁に遭遇するトラブルが集中する領域です。代表的な 2 つを紹介します。いずれも筆者および現場で実際に起きた失敗事例をもとにしています。

ヒヤリハット 1:PGDATA サブディレクトリ未指定で PostgreSQL が CrashLoopBackOff

状況:チームメンバーが「PostgreSQL を K8s で動かす」タスクで、PVC を /var/lib/postgresql/data にマウントする StatefulSet を書きました。PGDATA 環境変数は設定していません(PostgreSQL のデフォルトを使う想定)。

Apply すると Pod が CrashLoopBackOff 状態に陥り、数分後にチャットに「PostgreSQL が立ち上がりません」と SOS が飛んできました。

確認したログ

$ kubectl logs fanclub-db-0
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

initdb: error: directory "/var/lib/postgresql/data" exists but is not empty
initdb: hint: If you want to create a new database system, either remove or empty
the directory "/var/lib/postgresql/data" or run initdb with an argument other
than "/var/lib/postgresql/data".

根本原因:local-path-provisioner が PV のディレクトリを作る際、ノード上のホストパスにマウント前提の lost+found やロストファイルが残っている、あるいはマウント時のメタデータディレクトリが既に存在しているため、PostgreSQL の initdb が「データディレクトリが空ではない」と誤認します。クラウドの EBS でも同様で、新規ボリュームでもファイルシステム作成時に lost+found が予約されているケースで同じ症状が出ます。

解決策PGDATA 環境変数を PVC マウントポイントのサブディレクトリに設定します。PostgreSQL は PGDATA が指す場所に initdb を実行するため、サブディレクトリが「空」であれば成功します。

env:
  - name: PGDATA
    value: /var/lib/postgresql/data/pgdata
volumeMounts:
  - name: postgres-data
    mountPath: /var/lib/postgresql/data

これで PostgreSQL は /var/lib/postgresql/data/pgdata/ 以下に initdb を実行します。PVC マウントポイント直下にあった lost+found 等は initdb の対象外になり、無視されます。本シリーズの StatefulSet YAML(H2-6)でこのパターンを採用しているのは、このヒヤリハットを未然に防ぐためです。

本番運用警告:PostgreSQL の StatefulSet を K8s で構築するときは、PGDATA をサブディレクトリに設定することを定石として身につけてください。PostgreSQL 公式 Docker Hub のドキュメントにも、ボリュームマウントを使う場合の推奨設定として記載があります。

MySQL 公式イメージにも類似の問題があり、こちらは MYSQL_DATABASE--datadir オプションで対応します。

ヒヤリハット 2:volumeClaimTemplates の変更で Forbidden エラー

状況:稼働中の StatefulSet(PostgreSQL)の PVC サイズ 1Gi が手狭になったため、volumeClaimTemplates[].spec.resources.requests.storage5Gi に変更して kubectl apply したところ、エラーで弾かれました。

確認したエラー

$ kubectl apply -f ~/fanclub-manifests/fanclub-db-statefulset.yaml
The StatefulSet "fanclub-db" is invalid:
spec: Forbidden: updates to statefulset spec for fields other than 'replicas',
'template', 'updateStrategy', 'persistentVolumeClaimRetentionPolicy' and
'minReadySeconds' are forbidden

根本原因:StatefulSet の spec.volumeClaimTemplates は不変フィールド(Immutable Field)です。エラーメッセージに列挙されている「変更可能なフィールド」(replicastemplateupdateStrategypersistentVolumeClaimRetentionPolicyminReadySeconds)以外は、稼働中の StatefulSet に対して変更できません。

解決策:StatefulSet を --cascade=orphan オプション付きで削除し、Pod と PVC を残したまま StatefulSet 本体だけ削除します。その後、新しい volumeClaimTemplates を持つ StatefulSet を再 apply すると、既存の PVC を新 StatefulSet が引き取ります。

実行コマンド(StatefulSet のみ削除・Pod と PVC は残す):

$ kubectl delete statefulset fanclub-db --cascade=orphan

実行コマンド(新しい volumeClaimTemplates で StatefulSet を再作成):

$ kubectl apply -f ~/fanclub-manifests/fanclub-db-statefulset.yaml

cascade オプションの動作整理kubectl delete statefulset の cascade オプションは挙動が紛らわしいので整理します。

オプションStatefulSet 本体PodPVC
--cascade=foreground(デフォルト)削除削除(StatefulSet が削除される過程で連鎖削除)残る(StatefulSet の設計)
--cascade=orphan削除残る(孤立 Pod として継続稼働)残る
--cascade=background削除削除(バックグラウンド処理)残る

--cascade=orphan の主な使い所は「Pod を稼働させたまま StatefulSet の制御を一時的に切り離す」場面です。Pod は孤立 Pod として動き続け、PVC も残るため、データを止めずに StatefulSet 定義のみ作り直したいときに有効です。

今回のように volumeClaimTemplates を変更したいケースでは、孤立した Pod を別途 kubectl delete pod fanclub-db-0 で停止してから新 StatefulSet を apply すれば、新 Pod が既存 PVC を再マウントして起動します。

本番運用警告:実際の本番運用では、PVC のサイズ変更には別の手段が望ましいです。StorageClass の allowVolumeExpansion: true が設定されていれば、StatefulSet を削除せずに PVC の resources.requests.storage を直接 patch して拡張できます。

kubectl edit pvc postgres-data-fanclub-db-0kubectl patch pvcstorage 値を増やすだけで、データを保持したまま拡張可能です。volumeClaimTemplates 経由の変更は新規 PVC(例: replicas を増やしたとき)にしか効かないため、既存 PVC のサイズはこの方法では変わらない点も注意が必要です。

kind の standard StorageClass は allowVolumeExpansion: false のためこの手段は使えません。本番では拡張可能な StorageClass を選ぶことが定石です。

第10回への橋渡し — DB 接続情報を ConfigMap / Secret で外部化する伏線

本回の fanclub-backend Pod YAML を見直すと、env セクションが次のように平文で書かれています。

env:
  - name: DB_HOST
    value: "fanclub-db"
  - name: DB_PORT
    value: "5432"
  - name: DB_NAME
    value: "fanclubdb"
  - name: DB_USER
    value: "appuser"
  - name: DB_PASSWORD
    value: "apppassword"

動く状態を作るうえでは問題ありませんが、本番運用には看過できない問題が 2 つあります。

問題 1:認証情報がソースコード管理に漏れる

YAML を Git にコミットすると、DB_PASSWORD: "apppassword" がそのまま履歴に残ります。後から削除しても git log から復元可能です。リポジトリが Public になった瞬間に、Backend → DB の認証情報が世界中に公開されることを意味します。これはセキュリティインシデントとして重大です。

問題 2:kubectl 実行権限を持つユーザー全員が認証情報を見られる

kubectl describe pod fanclub-backend を実行すると、env セクションの値が平文で表示されます。Pod の閲覧権限を持つすべてのユーザーが DB パスワードにアクセスできてしまいます。本番では権限分離が崩壊します。

本番運用警告:「Pod の env に機密情報を平文で書く」設計は、本番では採用しません。短期検証や学習目的でやむを得ず使う場合も、その YAML が Git やチャットに流出しないよう厳重に管理する必要があります。

本番では Secret か外部の Secret 管理ツール(External Secrets Operator + AWS Secrets Manager / HashiCorp Vault 等)を使うのが定石です。

第10回での解決方針

第10回では、本回で平文 env として埋め込んだ DB 接続情報を ConfigMap と Secret に分離します。

  • ConfigMap(非機密情報):DB_HOST / DB_PORT / DB_NAME / JAVA_OPTS
  • Secret(機密情報):DB_USER / DB_PASSWORD

あわせて ServiceAccount の基礎(default SA・automountServiceAccountToken: false による Pod から API Server へのトークン非マウント設定)も第10回で扱います。本回で完成した 3 層構成(Backend + Service + DB)に、第10回では「セキュアな設定管理」のレイヤーを重ねていきます。

このように「動く状態を作ってからセキュアにする」段階的アプローチは、実務でも有用な思考の進め方です。最終形を一発で書こうとして手が止まるよりも、まず最小限で動かし、問題を特定してから直す方が、結局早く本番に近づけます。

理解度チェック・第9回まとめ・次回予告・シリーズ一覧

理解度チェック(○×形式・9 問)

問 1:StatefulSet の Pod を kubectl delete pod で削除すると、StatefulSet コントローラが同名の Pod を自動再作成する。

問 2:StatefulSet の spec.volumeClaimTemplates を使うと、各 Pod に固有の PVC が自動作成される。

問 3:StatefulSet を kubectl delete statefulset で削除すると、volumeClaimTemplates から作成された PVC も同時に自動削除される。

問 4:kind の standard StorageClass は WaitForFirstConsumer モードのため、PVC を apply しても Pod がスケジュールされるまで PV が作成されない。

問 5:StatefulSet には Headless Service(clusterIP: None)を組み合わせる必要がある。

問 6:PostgreSQL の StatefulSet で PGDATA 環境変数を設定せず PVC を /var/lib/postgresql/data に直接マウントすると、initdb 時に「ディレクトリが空でない」エラーが出ることがある。

問 7:Deployment と StatefulSet は、どちらもデータベースの永続化に同じく適したワークロードリソースである。

問 8:アクセスモード ReadWriteOnce(RWO)では、複数ノードからの同時読み書きが可能である。

問 9:StatefulSet の spec.volumeClaimTemplates フィールドは、作成後に kubectl apply で変更できる。

解答

解答解説
問 1StatefulSet コントローラは declared replicas を満たすため、削除された Pod を同名で再作成する。Deployment と同様の reconciliation の仕組み
問 2volumeClaimTemplates[].metadata.name と Pod 名の連結(例: postgres-data-fanclub-db-0)で命名された PVC が Pod ごとに自動生成される
問 3×StatefulSet を削除しても volumeClaimTemplates 由来の PVC は残る。データ保護のための設計意図。完全に削除したい場合は kubectl delete pvc を別途実行するか、spec.persistentVolumeClaimRetentionPolicy を設定する
問 4kind の standard SC は WaitForFirstConsumerkubectl get pvc で apply 直後は Pending、Pod スケジュール後に Bound に遷移する。本回演習①で実機確認した挙動
問 5厳密には Headless Service なしでも StatefulSet 自体は動作するが、Pod 個別の安定 DNS 名(fanclub-db-0.fanclub-db-headless.default.svc.cluster.local)は得られない。CKAD 試験文脈では「StatefulSet には Headless Service」が定石
問 6PVC マウントポイント直下に lost+found 等が残っているケースで initdb が失敗する。回避策は PGDATA: /var/lib/postgresql/data/pgdata でサブディレクトリを指定する
問 7×Deployment はステートレスアプリ向け。Pod ごとに独立した PVC を持てず、Pod 名がランダムサフィックスで再作成のたびに変わるため、データベースには StatefulSet が適切
問 8×RWO は単一ノード内の読み書き。複数ノードから同時読み書きするには RWX が必要。RWX をサポートする StorageClass(NFS / Longhorn 等)を使う必要がある
問 9×volumeClaimTemplates は不変フィールド。変更すると Forbidden エラー。--cascade=orphan で StatefulSet のみ削除して再作成する手順を取る

第9回まとめ

第9回では以下を実施しました。

  • Pod は使い捨てのためデータが消える前提で設計される。emptyDir や Deployment の標準ボリュームではデータベースの永続化要件を満たせない。PV / PVC / StorageClass は Pod ライフサイクルからストレージを独立させる仕組みであり、kind 環境では rancher.io/local-path プロビジョナの動的プロビジョニングを使う
  • StatefulSet は「安定した Pod 名・DNS 名・ストレージ」の 3 つを保証する DB 向けワークロードリソース。spec.serviceName で Headless Service を指定し、volumeClaimTemplates で Pod ごとに専用 PVC を生成する。本回 PostgreSQL 18 を fanclub-db-0 として起動し、postgres-data-fanclub-db-0 PVC が Bound する一連の流れを実機確認した
  • Headless Service(clusterIP: None)は StatefulSet の Pod 個別 FQDN(fanclub-db-0.fanclub-db-headless.default.svc.cluster.local)を実現する。Backend からの DB 接続は別の通常 ClusterIP Service(fanclub-db)を使う役割分担とした。fanclub-backend:0.1.0 は ep3 構築時点の最小 Payara Micro 構成であり /api/members CRUD endpoint は未実装のため、DB 接続の疎通確認は Init Container wait-for-db の完了・env 注入確認・psql 直接 SELECT の 3 点で行った
  • 演習③では psql で members テーブルに 2 件 INSERT した後、StatefulSet を削除・再 apply したところ、PVC(postgres-data-fanclub-db-0)が残存し、再作成された Pod が同じ PVC を再マウントして created_at 値まで一致するデータが保持されることを確認した。StatefulSet を削除しても PVC は自動削除されない設計が「データの番人」として機能している
  • PostgreSQL の StatefulSet では PGDATA をサブディレクトリ(/var/lib/postgresql/data/pgdata)に設定するのがトラブル回避の定石。volumeClaimTemplates は不変フィールドのため、変更したい場合は --cascade=orphan で StatefulSet のみ削除して再作成する。本番では allowVolumeExpansion: true の StorageClass を選び、PVC patch でサイズ拡張する

次回予告

第10回 ConfigMap + Secret + ServiceAccount 基礎では、本回で平文 env として埋め込んだ DB 接続情報を ConfigMap と Secret に分離します。DB_HOST / DB_PORT / DB_NAME / JAVA_OPTS を ConfigMap に、DB_USER / DB_PASSWORD を Secret に移行し、envFrom で Pod に注入する設計を扱います。

あわせて ServiceAccount の基礎(default SA の挙動・automountServiceAccountToken: false による API Server トークンの非マウント設定)も学びます。

本回完成した 3 層構成(Backend + Service + DB)に、第10回ではセキュアな設定管理層を追加します。CKAD ドメイン D4(Application Environment, Configuration and Security・25 %)の中核を網羅する重要回です。

シリーズ一覧

第1部:コンテナと Docker

第2部:Kubernetes 基礎

第3部:アプリリソース

第4部:ワークロード戦略

第5部:セキュリティ基礎

第6部:パッケージ管理 + HTTPS 公開

広告
kubernetes
スポンサーリンク