Skip to content
Published on

Kubernetesでデータベース運用完全ガイド:StatefulSet、Operator、バックアップ/リカバリ戦略

Authors

TL;DR

  • StatefulSet: DB等のステートフルアプリケーション向けK8sワークロード(安定したネットワークID + 永続ストレージ)
  • DB Operator: CloudNativePG(PostgreSQL)、Percona Operator(MySQL/MongoDB)で運用自動化
  • ストレージ: PV/PVC/StorageClassで永続データ管理、CSIドライバー活用
  • 高可用性: Primary-Replica構成、自動フェイルオーバー、PodDisruptionBudget
  • バックアップ/リカバリ: pgBackRest、Velero、PITR(Point-in-Time Recovery)戦略
  • モニタリング: PMM、pg_exporter + Prometheus + Grafanaダッシュボード

目次

  1. K8sでDBを運用すべきか?
  2. StatefulSet詳細解説
  3. ストレージ戦略
  4. Headless ServiceとDNS
  5. Database Operator
  6. 高可用性(HA)
  7. バックアップとリカバリ
  8. モニタリング
  9. パフォーマンスチューニング
  10. セキュリティ
  11. VMからK8sへの移行
  12. プロダクションチェックリスト
  13. 実践クイズ
  14. 参考資料

1. K8sでDBを運用すべきか?

1.1 メリットとデメリット

K8s DB運用の判断マトリックス:

運用すべき場合:                         運用すべきでない場合:
+ マルチクラウド/ハイブリッド環境        - マネージドDBサービスが利用可能
+ インフラの一貫性が重要                 - DBAリソース不足
+ GitOps/IaCパイプライン統合            - 超大規模な単一DBインスタンス
+ 開発/テスト環境の自動化               - 極めて低いレイテンシ要件
+ コスト最適化が必須                     - K8s経験不足
+ データ主権規制                         - シンプルなアーキテクチャで十分
基準K8s DB運用マネージドサービス(RDS/CloudSQL)
初期セットアップ複雑度高い低い
運用自動化Operatorで可能標準提供
コスト効率的(リソース共有)プレミアム価格
マルチクラウド容易ベンダーロックイン
カスタマイズ完全な自由制限あり
バックアップ/リカバリ手動構成が必要標準提供
スケーラビリティ手動/半自動自動スケーリング

1.2 どのDBがK8sに適しているか?

K8s適合度ランキング:

非常に適合:
  - PostgreSQL(CloudNativePGエコシステムが優秀)
  - MongoDB(ReplicaSet構造がK8sと自然にマッチ)
  - Redis(Sentinel/Clusterモード)

適合:
  - MySQL(Percona/Oracle Operatorを使用)
  - Elasticsearch(ECK Operator)
  - Cassandra(K8ssandra Operator)

注意が必要:
  - Oracle DB(ライセンス、複雑性)
  - SQL Server(Windowsコンテナの制限)
  - 大規模な単一インスタンスDB

2. StatefulSet詳細解説

2.1 StatefulSet vs Deployment

Deployment:                    StatefulSet:
- Pod名: ランダム (abc-xyz)     - Pod名: 順番 (db-0, db-1, db-2)
- 並列作成/削除                 - 順番に作成/削除 (0 -> 1 -> 2)
- 共有ボリュームまたはなし       - Pod毎に専用PVC(自動作成)
- 交換可能なPod                - 固有IDを持つPod
- ステートレスアプリに適合       - ステートフルアプリ(DB)に適合

2.2 StatefulSet YAMLの例

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: database
spec:
  serviceName: postgres-headless  # Headless Service名
  replicas: 3
  podManagementPolicy: OrderedReady  # 順番に作成(デフォルト)
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1  # K8s 1.24+
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      terminationGracePeriodSeconds: 120  # DB停止に十分な時間
      securityContext:
        fsGroup: 999       # postgresグループ
        runAsUser: 999     # postgresユーザー
      containers:
        - name: postgres
          image: postgres:16-alpine
          ports:
            - containerPort: 5432
              name: postgresql
          env:
            - name: POSTGRES_DB
              value: myapp
            - name: POSTGRES_USER
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: username
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          resources:
            requests:
              cpu: "500m"
              memory: "1Gi"
            limits:
              cpu: "2"
              memory: "4Gi"
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
          livenessProbe:
            exec:
              command:
                - pg_isready
                - -U
                - postgres
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            exec:
              command:
                - pg_isready
                - -U
                - postgres
            initialDelaySeconds: 5
            periodSeconds: 5
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 100Gi

2.3 PodManagementPolicy

# OrderedReady(デフォルト): 順番に作成/削除
# Pod 0 Ready -> Pod 1作成 -> Pod 1 Ready -> Pod 2作成
podManagementPolicy: OrderedReady

# Parallel: 全Pod同時作成/削除
# 初期ブートストラップに注意(DBは通常OrderedReadyを使用)
podManagementPolicy: Parallel

2.4 UpdateStrategy

updateStrategy:
  type: RollingUpdate
  rollingUpdate:
    # Partition: この値以上のordinalを持つPodのみ更新
    # カナリアデプロイに活用(Pod 2のみ先に更新)
    partition: 2

# OnDelete: 手動でPod削除時のみ更新
# DBアップグレード時のきめ細かな制御が可能
updateStrategy:
  type: OnDelete

3. ストレージ戦略

3.1 PV / PVC / StorageClass

# StorageClass定義(AWS EBS gp3)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "5000"
  throughput: "250"    # MB/s
  encrypted: "true"
reclaimPolicy: Retain  # DBデータは必ずRetain!
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true  # オンラインボリューム拡張を許可
# StorageClass定義(GCP PD SSD)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: pd.csi.storage.gke.io
parameters:
  type: pd-ssd
  replication-type: regional-pd  # リージョナルPD(高可用性)
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

3.2 ローカルストレージ vs クラウドボリューム

パフォーマンス比較:

Local NVMe SSD:
  - ランダム読み取り: 500K+ IOPS
  - レイテンシ: 0.1ms未満
  - 欠点: Pod移動不可、ノード障害時データ損失リスク

Cloud EBS gp3:
  - ベースライン: 3,000 IOPS / 125 MB/s
  - 最大: 16,000 IOPS / 1,000 MB/s
  - 利点: Pod移動可能、スナップショットサポート

Cloud EBS io2:
  - 最大: 64,000 IOPS
  - 99.999%の耐久性
  - コストは高いがミッションクリティカルなDBに適合

3.3 ボリューム拡張

# PVCサイズ拡張(StorageClassにallowVolumeExpansion: trueが必要)
kubectl patch pvc data-postgres-0 -n database \
  -p '{"spec": {"resources": {"requests": {"storage": "200Gi"}}}}'

# 拡張ステータス確認
kubectl get pvc data-postgres-0 -n database -o yaml | grep -A 5 status

4. Headless ServiceとDNS

4.1 Headless Service定義

apiVersion: v1
kind: Service
metadata:
  name: postgres-headless
  namespace: database
spec:
  clusterIP: None  # Headless Serviceの核心
  selector:
    app: postgres
  ports:
    - port: 5432
      targetPort: 5432
      name: postgresql

4.2 DNSルール

StatefulSetの各Podは予測可能なDNS名を持ちます。

DNSパターン:
  pod-name.service-name.namespace.svc.cluster.local

:
  postgres-0.postgres-headless.database.svc.cluster.local
  postgres-1.postgres-headless.database.svc.cluster.local
  postgres-2.postgres-headless.database.svc.cluster.local
# 読み取り/書き込み分離のための追加Service
---
apiVersion: v1
kind: Service
metadata:
  name: postgres-primary
  namespace: database
spec:
  selector:
    app: postgres
    role: primary
  ports:
    - port: 5432
      targetPort: 5432
---
apiVersion: v1
kind: Service
metadata:
  name: postgres-replica
  namespace: database
spec:
  selector:
    app: postgres
    role: replica
  ports:
    - port: 5432
      targetPort: 5432

5. Database Operator

5.1 なぜOperatorが必要なのか?

DB運用は単純なデプロイを超えて、複雑なDay-2運用が必要です。

Operatorが自動化するタスク:

1. クラスター初期化(Primary + Replica構成)
2. 自動フェイルオーバー(Primary障害時にReplica昇格)
3. バックアップ/リカバリ(スケジューリング、PITR4. ローリングアップグレード(ゼロダウンタイム)
5. 水平スケーリング(Replica追加/削除)
6. モニタリング統合
7. 証明書管理(TLS8. 設定変更(再起動なし)

5.2 PostgreSQL - CloudNativePG(CNPG)

# CloudNativePGインストール
kubectl apply --server-side -f \
  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.24/releases/cnpg-1.24.1.yaml
# CloudNativePGクラスター定義
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: myapp-db
  namespace: database
spec:
  instances: 3
  imageName: ghcr.io/cloudnative-pg/postgresql:16.4

  postgresql:
    parameters:
      max_connections: "200"
      shared_buffers: "1GB"
      effective_cache_size: "3GB"
      work_mem: "16MB"
      maintenance_work_mem: "256MB"
      wal_buffers: "16MB"
      random_page_cost: "1.1"
      effective_io_concurrency: "200"
      max_wal_size: "2GB"
      checkpoint_completion_target: "0.9"

  bootstrap:
    initdb:
      database: myapp
      owner: app_user
      secret:
        name: myapp-db-credentials

  storage:
    size: 100Gi
    storageClass: fast-ssd

  resources:
    requests:
      memory: "2Gi"
      cpu: "1"
    limits:
      memory: "4Gi"
      cpu: "2"

  backup:
    barmanObjectStore:
      destinationPath: "s3://my-backup-bucket/cnpg/"
      s3Credentials:
        accessKeyId:
          name: aws-creds
          key: ACCESS_KEY_ID
        secretAccessKey:
          name: aws-creds
          key: SECRET_ACCESS_KEY
      wal:
        compression: gzip
      data:
        compression: gzip
    retentionPolicy: "30d"

  monitoring:
    enablePodMonitor: true

5.3 MySQL - Percona XtraDB Cluster Operator

# Percona Operatorインストール
kubectl apply -f https://raw.githubusercontent.com/percona/percona-xtradb-cluster-operator/v1.15.0/deploy/bundle.yaml
# Percona XtraDB Cluster定義
apiVersion: pxc.percona.com/v1
kind: PerconaXtraDBCluster
metadata:
  name: myapp-mysql
  namespace: database
spec:
  crVersion: "1.15.0"
  secretsName: myapp-mysql-secrets

  pxc:
    size: 3
    image: percona/percona-xtradb-cluster:8.0.36
    resources:
      requests:
        memory: 2G
        cpu: "1"
      limits:
        memory: 4G
        cpu: "2"
    volumeSpec:
      persistentVolumeClaim:
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 100Gi
    affinity:
      antiAffinityTopologyKey: "kubernetes.io/hostname"

  haproxy:
    enabled: true
    size: 3
    image: percona/haproxy:2.8.5
    resources:
      requests:
        memory: 512M
        cpu: "500m"

  backup:
    image: percona/percona-xtradb-cluster-operator:1.15.0-pxc8.0-backup
    storages:
      s3-backup:
        type: s3
        s3:
          bucket: my-backup-bucket
          credentialsSecret: aws-creds
          region: ap-northeast-2
    schedule:
      - name: daily-backup
        schedule: "0 3 * * *"
        keep: 7
        storageName: s3-backup

5.4 MongoDB - Community Operator

# MongoDB Community Operatorインストール
kubectl apply -f https://raw.githubusercontent.com/mongodb/mongodb-kubernetes-operator/master/config/crd/bases/mongodbcommunity.mongodb.com_mongodbcommunity.yaml
kubectl apply -k https://github.com/mongodb/mongodb-kubernetes-operator/config/rbac/
kubectl create -f https://raw.githubusercontent.com/mongodb/mongodb-kubernetes-operator/master/config/manager/manager.yaml
# MongoDB ReplicaSet定義
apiVersion: mongodbcommunity.mongodb.com/v1
kind: MongoDBCommunity
metadata:
  name: myapp-mongodb
  namespace: database
spec:
  members: 3
  type: ReplicaSet
  version: "7.0.14"

  security:
    authentication:
      modes: ["SCRAM"]

  users:
    - name: app-user
      db: admin
      passwordSecretRef:
        name: mongodb-password
      roles:
        - name: readWrite
          db: myapp
        - name: clusterAdmin
          db: admin
      scramCredentialsSecretName: app-user-scram

  statefulSet:
    spec:
      template:
        spec:
          containers:
            - name: mongod
              resources:
                requests:
                  cpu: "1"
                  memory: 2Gi
                limits:
                  cpu: "2"
                  memory: 4Gi
      volumeClaimTemplates:
        - metadata:
            name: data-volume
          spec:
            storageClassName: fast-ssd
            resources:
              requests:
                storage: 100Gi

5.5 Operator比較

特性CloudNativePGPercona XtraDBMongoDB Community
DBPostgreSQLMySQLMongoDB
ライセンスApache 2.0Apache 2.0SSPL + Apache
HA方式Streaming ReplicationGalera ClusterReplicaSet
自動フェイルオーバーサポートサポートサポート
バックアップBarman/S3xtrabackup/S3mongodump連携
モニタリングPodMonitorPMM統合基本メトリクス
成熟度非常に高い高い中程度

6. 高可用性(HA)

6.1 Primary-Replica構成

PostgreSQL HAアーキテクチャ(CloudNativePG):

  [CNPG Operator]
       |
       v
  [Primary Pod]  ----Streaming Replication---->  [Replica Pod 1]
       |                                          [Replica Pod 2]
       |
  [Headless Service]
       |
  postgres-primary(書き込み)----> Primaryのみにルーティング
  postgres-replica(読み取り)----> Replicaのみにルーティング

6.2 自動フェイルオーバー

フェイルオーバーシナリオ:

1. Primary Podの障害発生
2. Operatorが検出(liveness probe失敗)
3. 最新のLSNを持つReplicaを選択
4. 選択されたReplicaをPrimaryに昇格
5. 残りのReplicaが新しいPrimaryに追従
6. Serviceエンドポイントを更新
7. 障害Podを再作成し新しいReplicaとして合流

全プロセス: 通常30秒以内

6.3 PodDisruptionBudget

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: postgres-pdb
  namespace: database
spec:
  minAvailable: 2  # 最低2つのPodを維持
  selector:
    matchLabels:
      app: postgres

6.4 ノード障害への備え

# Pod Topology Spread Constraints
spec:
  template:
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: postgres
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: postgres

7. バックアップとリカバリ

7.1 バックアップの種類

論理バックアップ(pg_dump/mysqldump):
  + 移植性が高い(異なるバージョン/プラットフォームへ復元可能)
  + 個別テーブル/スキーマのバックアップが可能
  - 大規模DBでは遅い
  - 復元時にインデックス再構築が必要

物理バックアップ(pgBackRest/xtrabackup):
  + 大規模DBで高速
  + 増分バックアップサポート
  + PITR(Point-in-Time Recovery)可能
  - 同じメジャーバージョンでのみ復元
  - クラスター全体の単位でバックアップ

7.2 pgBackRest(PostgreSQL)

# CloudNativePGのバックアップ設定
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: myapp-db
spec:
  backup:
    barmanObjectStore:
      destinationPath: "s3://my-backup-bucket/cnpg/myapp-db/"
      s3Credentials:
        accessKeyId:
          name: aws-creds
          key: ACCESS_KEY_ID
        secretAccessKey:
          name: aws-creds
          key: SECRET_ACCESS_KEY
      wal:
        compression: gzip
        maxParallel: 4
      data:
        compression: gzip
        immediateCheckpoint: true
    retentionPolicy: "30d"
---
# スケジュールバックアップ
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
  name: myapp-db-daily
spec:
  schedule: "0 3 * * *"
  cluster:
    name: myapp-db
  backupOwnerReference: self
  method: barmanObjectStore

7.3 PITR(Point-in-Time Recovery)

# PITRで特定時点にリカバリ
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: myapp-db-recovered
spec:
  instances: 3

  bootstrap:
    recovery:
      source: myapp-db-backup
      recoveryTarget:
        targetTime: "2026-03-24T10:30:00Z"  # リカバリ時点

  externalClusters:
    - name: myapp-db-backup
      barmanObjectStore:
        destinationPath: "s3://my-backup-bucket/cnpg/myapp-db/"
        s3Credentials:
          accessKeyId:
            name: aws-creds
            key: ACCESS_KEY_ID
          secretAccessKey:
            name: aws-creds
            key: SECRET_ACCESS_KEY

7.4 Veleroを活用したフルバックアップ

# Veleroインストール
velero install \
  --provider aws \
  --bucket my-velero-bucket \
  --secret-file ./credentials-velero \
  --plugins velero/velero-plugin-for-aws:v1.10.0

# Namespace単位のバックアップ
velero backup create database-backup \
  --include-namespaces database \
  --snapshot-volumes=true \
  --volume-snapshot-locations default

# リストア
velero restore create --from-backup database-backup

8. モニタリング

8.1 Prometheus + Grafana

# PostgreSQL Exporter(pg_exporter)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-exporter
  namespace: database
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres-exporter
  template:
    metadata:
      labels:
        app: postgres-exporter
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9187"
    spec:
      containers:
        - name: exporter
          image: prometheuscommunity/postgres-exporter:0.15.0
          ports:
            - containerPort: 9187
          env:
            - name: DATA_SOURCE_URI
              value: "postgres-primary.database.svc:5432/myapp?sslmode=disable"
            - name: DATA_SOURCE_USER
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: username
            - name: DATA_SOURCE_PASS
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password

8.2 主要モニタリング指標

DBモニタリングの重要メトリクス:

パフォーマンス:
  - クエリ数/秒(QPS  - クエリレイテンシ(p50, p95, p99)
  - アクティブコネクション数
  - キャッシュヒット率(Buffer Cache Hit Ratio)

レプリケーション:
  - レプリケーション遅延
  - WAL受信遅延
  - Replicaステータス

ストレージ:
  - ディスク使用量
  - IOPS / スループット
  - WALサイズ

リソース:
  - CPU使用率
  - メモリ使用量
  - Pod再起動回数

運用:
  - Dead tuples比率
  - Vacuum実行状態
  - ロック待ち数

8.3 Percona Monitoring and Management(PMM)

# PMM Serverインストール
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pmm-server
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pmm-server
  template:
    spec:
      containers:
        - name: pmm-server
          image: percona/pmm-server:2
          ports:
            - containerPort: 443
          volumeMounts:
            - name: pmm-data
              mountPath: /srv
      volumes:
        - name: pmm-data
          persistentVolumeClaim:
            claimName: pmm-data

9. パフォーマンスチューニング

9.1 リソースRequests/Limits

# DB Podリソース設定ガイド
resources:
  requests:
    # CPU: 保証される最低CPU
    # DBはCPU競合に敏感なので余裕を持って
    cpu: "2"
    # Memory: shared_buffers + work_mem * max_connections + OS
    memory: "4Gi"
  limits:
    # CPU limitは設定しないか余裕を持たせる
    #(スロットリングがクエリ遅延を引き起こす)
    cpu: "4"
    # Memory limitはOOM Kill防止のためrequestより余裕を持って
    memory: "8Gi"

9.2 AffinityとAnti-Affinity

# DB Podスケジューリング最適化
spec:
  template:
    spec:
      # DB専用ノードにのみスケジューリング
      nodeSelector:
        node-type: database

      # またはTolerationで専用ノード
      tolerations:
        - key: "dedicated"
          operator: "Equal"
          value: "database"
          effect: "NoSchedule"

      # Replicaを異なるノード/ゾーンに分散
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - postgres
              topologyKey: "kubernetes.io/hostname"

9.3 カーネルパラメータチューニング

# initContainerでカーネルパラメータ設定
spec:
  template:
    spec:
      initContainers:
        - name: sysctl-tuning
          image: busybox:1.36
          securityContext:
            privileged: true
          command:
            - sh
            - -c
            - |
              sysctl -w vm.swappiness=1
              sysctl -w vm.dirty_background_ratio=5
              sysctl -w vm.dirty_ratio=10
              sysctl -w vm.overcommit_memory=2
              sysctl -w net.core.somaxconn=65535
              sysctl -w net.ipv4.tcp_max_syn_backlog=65535

10. セキュリティ

10.1 NetworkPolicy

# DB Podへのネットワークアクセス制限
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgres-network-policy
  namespace: database
spec:
  podSelector:
    matchLabels:
      app: postgres
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              name: application
          podSelector:
            matchLabels:
              app: backend
        - podSelector:
            matchLabels:
              app: postgres  # Pod間レプリケーションを許可
      ports:
        - port: 5432
          protocol: TCP
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - port: 5432
          protocol: TCP
    - to:  # DNSを許可
        - namespaceSelector: {}
      ports:
        - port: 53
          protocol: UDP
        - port: 53
          protocol: TCP

10.2 Secrets管理

# External Secrets Operatorの使用
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: postgres-secret
  namespace: database
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: postgres-secret
  data:
    - secretKey: username
      remoteRef:
        key: prod/database/postgres
        property: username
    - secretKey: password
      remoteRef:
        key: prod/database/postgres
        property: password

10.3 TLS設定

# cert-managerでDB証明書を発行
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: postgres-tls
  namespace: database
spec:
  secretName: postgres-tls-secret
  duration: 8760h  # 1年
  renewBefore: 720h  # 期限切れ30日前に更新
  issuerRef:
    name: internal-ca
    kind: ClusterIssuer
  dnsNames:
    - postgres-primary.database.svc.cluster.local
    - postgres-headless.database.svc.cluster.local
    - "*.postgres-headless.database.svc.cluster.local"

11. VMからK8sへの移行

11.1 移行フェーズ

Phase 1: 準備
  1. K8sクラスターにDB Operatorをインストール
  2. StorageClass、NetworkPolicyを構成
  3. ターゲットDBクラスターを作成(空の状態)

Phase 2: データ移行
  方法A - 論理レプリケーション(最小ダウンタイム):
    1. VM DBで論理レプリケーション(Logical Replication)を設定
    2. K8s DBをSubscriberとして設定
    3. 初期同期 + 変更分ストリーミング
    4. アプリケーション切り替え(短時間のダウンタイム)

  方法B - pg_dump/restore:
    1. VM DBバックアップ(pg_dump)
    2. K8s DBにリストア(pg_restore)
    3. ダウンタイム中に切り替え

Phase 3: 切り替え
  1. アプリケーションのDB接続文字列を変更
  2. DNS変更またはServiceエンドポイント更新
  3.VM DBを読み取り専用に切り替え(ロールバック備え)

Phase 4: クリーンアップ
  1. 検証完了後、旧VM DBを削除
  2. モニタリング/アラートを更新

11.2 論理レプリケーション設定

-- VM DB(Publisher)設定
-- postgresql.conf
-- wal_level = logical
-- max_replication_slots = 10

CREATE PUBLICATION myapp_pub FOR ALL TABLES;

-- K8s DB(Subscriber)設定
CREATE SUBSCRIPTION myapp_sub
  CONNECTION 'host=vm-db.example.com port=5432 dbname=myapp user=repl_user password=secret'
  PUBLICATION myapp_pub;

12. プロダクションチェックリスト

K8s DBプロダクションチェックリスト:

ストレージ:
  [ ] StorageClassにreclaimPolicy: Retainを設定
  [ ] ボリュームサイズに20%以上の空き容量
  [ ] allowVolumeExpansion: trueを確認
  [ ] IOPS/スループットがワークロードに適合しているか確認

高可用性:
  [ ] 最低3インスタンス(1 Primary + 2 Replica)
  [ ] PodDisruptionBudgetを設定
  [ ] Pod Anti-Affinity(異なるノード/ゾーンに分散)
  [ ] 自動フェイルオーバーテスト完了

バックアップ:
  [ ] 自動バックアップスケジュール設定(日1回以上)
  [ ] WALアーカイブ有効化(PITR対応)
  [ ] バックアップリストアテスト完了
  [ ] 保持ポリシー設定(30日以上)

セキュリティ:
  [ ] NetworkPolicyでアクセス制限
  [ ] SecretsはExternal Secrets Operatorで管理
  [ ] TLS暗号化を有効化
  [ ] DBユーザー権限を最小化(最小権限の原則)

モニタリング:
  [ ] Prometheus + Grafanaダッシュボード構成
  [ ] アラートルール設定(レプリケーション遅延、ディスク使用率、コネクション数)
  [ ] ログ収集(Loki/EFK  [ ] クエリパフォーマンスモニタリング

パフォーマンス:
  [ ] リソースrequests/limitsを適切に設定
  [ ] DBパラメータチューニング(shared_buffers、work_memなど)
  [ ] カーネルパラメータ最適化
  [ ] 専用ノードを使用(Taint/Toleration)

13. 実践クイズ

Q1: StatefulSetとDeploymentの核心的な違いは何ですか?

回答:

StatefulSetは以下を保証します:

  1. 安定したネットワークID: 各Podに順番の名前が付与されます(例:db-0、db-1、db-2)。Podが再作成されても同じ名前を維持します。
  2. 永続ストレージバインディング: volumeClaimTemplatesにより各Podに専用のPVCが自動作成されます。Podが削除/再作成されても同じPVCに再接続されます。
  3. 順番通りの作成/削除: Pod 0がReady状態になった後にPod 1が作成されます(OrderedReadyポリシー)。

一方、DeploymentはランダムなPod名、共有ボリューム、並列作成を使用するため、ステートレスアプリに適しています。

Q2: DBストレージのreclaimPolicyをRetainに設定すべき理由は?

回答:

reclaimPolicyがDelete(デフォルト)の場合、PVCが削除される際にPVと実際のストレージ(EBSボリュームなど)も一緒に削除されます。これは以下の状況でデータ損失を引き起こします:

  • 誤ってStatefulSetやPVCを削除
  • Namespace削除
  • Helm uninstall

Retainに設定すると、PVCが削除されてもPVと実際のストレージが維持され、データリカバリが可能になります。プロダクションDBでは必ずRetainを使用すべきです。

Q3: CloudNativePGの自動フェイルオーバーはどのように動作しますか?

回答:

  1. CNPG Operatorが全インスタンスの状態を継続的にモニタリングします。
  2. Primary Podのliveness probeが失敗すると、Operatorが障害を検出します。
  3. Operatorは全ReplicaのWAL LSN(Log Sequence Number)を比較し、最新のデータを持つReplicaを選択します。
  4. 選択されたReplicaがPrimaryに昇格され、pg_promoteが実行されます。
  5. 残りのReplicaが新しいPrimaryに追従するように再設定されます。
  6. Serviceエンドポイントが新しいPrimaryを指すように自動更新されます。
  7. 障害Podは再作成され、新しいReplicaとして合流します。

全プロセスは通常30秒以内に完了します。

Q4: K8sでDB PodにCPU limitを設定しないことが推奨される理由は?

回答:

CPU limitが設定されると、KubernetesはCFS(Completely Fair Scheduler)スロットリングを適用します。DBが瞬間的に高いCPUを必要とする場合(例:複雑なクエリ、VACUUM)、スロットリングが発生するとクエリレイテンシが大幅に増加します。

代わりに以下の戦略を推奨します:

  • CPU requestのみ設定して保証されたCPUを確保
  • DB専用ノードを使用(Taint/Toleration)して他のワークロードとのリソース競合を防止
  • ノードレベルでCPUリソースが十分であることを確認

Memory limitはOOM Killを防止するために設定すべきですが、requestよりも十分に余裕を持たせます。

Q5: VMからK8sにDBを移行する際の最小ダウンタイム戦略は?

回答:

論理レプリケーション(Logical Replication)を活用します:

  1. VM DBでwal_level = logicalを設定し、Publicationを作成します。
  2. K8s DBでSubscriptionを作成し、VM DBに接続します。
  3. 初期データ同期が自動的に進行します。
  4. 同期完了後、変更分がリアルタイムでストリーミングされます。
  5. アプリケーションを一時停止(数秒~数分)し、レプリケーション遅延が0であることを確認します。
  6. アプリケーションのDB接続文字列をK8s DBに変更します。
  7. アプリケーションを再起動します。

この方法でダウンタイムを数秒~数分に最小化できます。


14. 参考資料

  1. CloudNativePG Documentation
  2. Percona Operator for MySQL Documentation
  3. MongoDB Kubernetes Operator
  4. Kubernetes StatefulSet Documentation
  5. Kubernetes Persistent Volumes
  6. Velero - Backup and Restore
  7. External Secrets Operator
  8. Percona Monitoring and Management (PMM)
  9. PostgreSQL Kubernetes Best Practices
  10. Zalando Postgres Operator
  11. CrunchyData PGO
  12. K8ssandra - Cassandra on Kubernetes
  13. Data on Kubernetes Community
  14. CNCF Storage Landscape