KubernetesにおけるConfigMapとSecret:イメージから設定を分離する

1. 概要

コンテナイメージにアプリケーションをパッケージ化する場合、database URLやAPI keyなどの値をイメージに直接ハードコードするべきではありません。この記事では、KubernetesのConfigMapとSecretについて詳しく解説します。これらは設定をイメージから分離するためのオブジェクトで、作成方法、環境変数またはvolumeを通じてPodに読み込む方法、およびSpring Bootアプリケーションとの統合方法をカバーしています。この記事を読み終わると、イメージを再構築することなく、動的に設定を読み込むDeploymentを自信を持ってセットアップできるようになるでしょう。

2. ConfigMapとは

ConfigMapはkey-valueペアで設定データを保存するKubernetesのオブジェクトで、暗号化されず、非機密データ(service名、feature flag、propertiesファイルなど)に使用されます。

ConfigMap内のデータはPodとは独立して存在します。イメージを再構築することなくConfigMapを編集でき、Podを再起動するだけで済みます(またはアプリケーションがサポートしている場合はリロードメカニズムを使用できます)。

3. ConfigMapの作成

3.1. YAMLファイルから

apiVersion: v1
kind: ConfigMap
metadata:
  name: order-service-config
data:
  APP_ENV: "production"
  LOG_LEVEL: "INFO"
  application.properties: |
    server.port=8080
    spring.jpa.hibernate.ddl-auto=validate

ここでdataには2つのタイプが含まれています。単純なkey(APP_ENV、LOG_LEVEL)とファイル全体の内容を含むkey(application.properties)です。以下で適用します:

kubectl apply -f order-service-config.yaml

3.2. literalまたは既存ファイルから

kubectl create configmap order-service-config \
  --from-literal=APP_ENV=production \
  --from-file=application.properties=./config/application.properties

このコマンドはYAMLを記述することなく、コマンドラインから直接ConfigMapを作成でき、CI/CDスクリプトで便利です。

4. PodでConfigMapを使用する

4.1. 環境変数として読み込む

apiVersion: v1
kind: Pod
metadata:
  name: order-service
spec:
  containers:
    - name: order-service
      image: nonladev/order-service:1.0
      envFrom:
        - configMapRef:
            name: order-service-config

envFromはConfigMap内のすべてのkeyを同じ名前の環境変数として読み込みます。コンテナはAPP_ENVやLOG_LEVELを通常の環境変数のように読み込みます。

特定のkeyのみを選択したい場合:

      env:
        - name: APP_ENVIRONMENT
          valueFrom:
            configMapKeyRef:
              name: order-service-config
              key: APP_ENV

この方法では、コンテナ内の環境変数の名前(APP_ENVIRONMENT)をConfigMapの元のkey(APP_ENV)と異なるものに変更できます。

4.2. volumeとして読み込む

      volumeMounts:
        - name: config-volume
          mountPath: /app/config
  volumes:
    - name: config-volume
      configMap:
        name: order-service-config

ConfigMap内のそれぞれのkeyは/app/config内のファイルになります(例えば/app/config/application.properties)。この方法は、アプリケーションが個別の環境変数ではなく、設定ファイル全体を読む必要がある場合に適しています。

注意: volumeとしてmountする場合、ConfigMapが変更されると、Kubernetesはファイルを自動的に更新します(kubelet同期周期に応じて数十秒から数分の遅延あり)。環境変数(env/envFrom)の場合、Podは自動的にリロードされず、Podを再起動する必要があります。

5. Secretとは何か、ConfigMapとの違い

SecretはConfigMapと似た構造を持ちますが、機密データ(パスワード、トークン、秘密鍵)用です。主な違いは以下の通りです:

項目 ConfigMap Secret
暗号化 なし Base64エンコード(実際の暗号化ではない)
etcdで保存 プレーンテキスト 保存時の暗号化を有効にできる
kubectl getで表示 値を表示 値を非表示(base64のみ表示)
用途 通常の設定 機密データ

注意: Secretはbase64エンコードのみで、デフォルトでは暗号化されません。namespace内でget secret権限がある人なら誰でもデコードできます。本当のセキュリティのためには、etcdで[保存時の暗号化]を有効にするか、External Secrets Operatorを通じて外部シークレットマネージャー(Vault、AWS Secrets Manager)を統合する必要があります。

6. Secretの作成

6.1. YAMLから(base64を自分でエンコードする必要があります)

echo -n "P@ssw0rd123" | base64
# UEBzc3cwcmQxMjM=
apiVersion: v1
kind: Secret
metadata:
  name: order-service-secret
type: Opaque
data:
  DB_PASSWORD: UEBzc3cwcmQxMjM=

dataフィールドはbase64エンコード済みの値が必要です。自分でエンコードしたくない場合は、stringDataを使用します:

stringData:
  DB_PASSWORD: "P@ssw0rd123"

Kubernetesは保存時に自動的にエンコードします。stringDataを使用すると、manifestを手動で記述する際に読みやすくなります。

6.2. コマンドラインから

kubectl create secret generic order-service-secret \
  --from-literal=DB_PASSWORD=P@ssw0rd123 \
  --from-literal=DB_USERNAME=order_app

7. PodでSecretを使用する

読み込み方法はConfigMapと全く同じです:

      envFrom:
        - secretRef:
            name: order-service-secret

またはvolumeをmount:

      volumeMounts:
        - name: secret-volume
          mountPath: /app/secret
          readOnly: true
  volumes:
    - name: secret-volume
      secret:
        secretName: order-service-secret

Secretをvolumeとしてmountする場合は、常に**readOnly: trueを設定**し、必要に応じてdefaultModeでファイルアクセス権限を制限する必要があります:

      secret:
        secretName: order-service-secret
        defaultMode: 0400

8. Spring Bootとの統合

Spring Bootはrelaxed bindingの規約に従って環境変数を読み込むため、特別なコードを書く必要はありません。変数名を規約に合わせるだけで十分です。

      envFrom:
        - configMapRef:
            name: order-service-config
        - secretRef:
            name: order-service-secret
      env:
        - name: SPRING_DATASOURCE_USERNAME
          valueFrom:
            secretKeyRef:
              name: order-service-secret
              key: DB_USERNAME
        - name: SPRING_DATASOURCE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: order-service-secret
              key: DB_PASSWORD

Spring Bootは自動的にSPRING_DATASOURCE_USERNAMEをspring.datasource.usernameにマップします。コード内では、通常の@ConfigurationPropertiesを使用するだけで済みます:

@ConfigurationProperties(prefix = "spring.datasource")
public record DataSourceProperties(String url, String username, String password) {
}
@Service
public class PaymentService {

    private final DataSourceProperties dataSourceProperties;

    public PaymentService(DataSourceProperties dataSourceProperties) {
        this.dataSourceProperties = dataSourceProperties;
    }

    public String describeConnection() {
        return "Connecting as %s".formatted(dataSourceProperties.username());
    }
}

PaymentServiceはKubernetesについてはまったく意識しません。ConfigMap/Secretが提供する環境変数からSpring Bootが事前にバインドした値を受け取るだけです。

8.1. クラスターに依存しないテスト

@TestPropertySourceを使用してシミュレートされた環境変数を設定してロジックをテストでき、実際のKubernetesは必要ありません:

@SpringBootTest
@TestPropertySource(properties = {
    "spring.datasource.username=order_app",
    "spring.datasource.password=test-pass"
})
class PaymentServiceTest {

    @Autowired
    private PaymentService paymentService;

    @Test
    void shouldDescribeConnectionUsingBoundProperties() {
        assertThat(paymentService.describeConnection())
            .isEqualTo("Connecting as order_app");
    }
}

このテストは、DataSourcePropertiesが正しくバインドされ、PaymentServiceが正しい値を使用することを確認します。実際のクラスター上のConfigMapやSecretから値が来るかどうかに依存しません。

PaymentServiceTest > shouldDescribeConnectionUsingBoundProperties() PASSED

9. Podを再起動しない動的更新

ConfigMap/Secretをvolumeとしてmountする場合、ファイルは自動的に更新されますが、Spring Bootアプリケーションは自動的に再読み込みしません。以下の場合を除きます:

  • Spring Cloud Kubernetes(spring-cloud-starter-kubernetes-client-config)を使用してConfigMapをポーリングし、@RefreshScope beanをリフレッシュします。
  • またはmountされたファイルを再読み込みするファイルウォッチャーを自分で実装します。
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-kubernetes-client-config</artifactId>
</dependency>
spring.cloud.kubernetes.config.enabled=true
spring.cloud.kubernetes.reload.enabled=true
spring.cloud.kubernetes.reload.mode=polling

この設定は、Spring Cloud Kubernetesが定期的にConfigMapをポーリングし、@RefreshScopeでマークされたbeanをリフレッシュする機能を有効にします。手動でPodを再起動する必要はありません。

注意: 継続的なポーリングはAPIサーバーのリソースを消費します。大規模なクラスターの場合は、ポーリングの代わりにspring.cloud.kubernetes.reload.mode=event(watch API)の使用を検討してください。

10. よくあるエラーと使用すべきでないケース

  • ConfigMapにパスワードを置く: ConfigMapは暗号化されていないため、kubectl get configmap -o yamlは値を明確に表示します。機密データにはSecretを常に使用してください。
  • Secret YAMLのdataを書く際にbase64エンコードを忘れる: illegal base64 dataエラーが発生します。自分でエンコードしたくない場合はstringDataを使用してください。
  • ConfigMapを変更してもPodが新しい値を受け取らない: env/envFromを通じた読み込みは自動リロードされないためです。kubectl rollout restart deploymentを実行する必要があります。
  • SecretボリュームをmountするがreadOnly: trueを忘れる: コンテナはファイルを上書きでき、デバッグ時に混乱が生じます。
  • Secretを長期的なシークレット保管庫として使用する: Kubernetes内のSecretはランタイム設定に適していますが、監査ログ、自動ローテーション、または厳密なバージョニングが必要な場合は、専用シークレットマネージャー(Vault、AWS Secrets Manager)の代わりにはなりません。
  • ConfigMap/Secretが大きすぎる: Kubernetesはオブジェクトサイズを約1 MiBに制限しています(etcdの制限による)。大きな設定ファイルが必要な場合は、PersistentVolumeからのvolumeマウントまたはオブジェクトストレージから読み込むinitコンテナの使用を検討してください。

11. 結論

ConfigMapとSecretは、イメージから設定を分離し、設定オブジェクトを変更するだけで、同じイメージを複数の環境(dev、staging、production)にデプロイできるようにします。ConfigMapは公開データ用、Secretは機密データ用ですが、Secretはデフォルトでエンコードのみで、実際には暗号化されていないことを忘れずに。Spring Bootでは、relaxed bindingを活用して、ビジネスロジックコードを変更することなく環境変数を通じて値を読み込むことができます。Podを再起動することなく設定を動的にリロードする必要がある場合は、Spring Cloud Kubernetesを有効にできます。

kubernetesconfigmapsecretspring-bootdevops