設定をイメージの外へ出すと何が変わるのか
一言でいうと
ConfigMap・Secretは「設定の保管庫」ではなく、同じイメージを複数の環境で再利用するための注入ポイントであり、securityContext・resourcesは「オプション」ではなく、そのコンテナがノード上で何をでき、どれだけ使えるかを定めた契約書です。
なぜ必要なのか
設定をイメージに焼き込むと、開発・ステージング・本番ごとに別のイメージを作る必要があります。すると「ステージングで通ったイメージ」と「本番に上がったイメージ」が別物になり、テストの意味が失われます。そこで、不変のアーティファクト(イメージ)と可変の設定(ConfigMap/Secret)、この2つに分けます。
注入する方法は2通りあります。環境変数はプロセスの開始時に1回だけ読み込まれ、その後は決して変わりません。ボリュームマウントはファイルとして入り、ConfigMapを修正するとkubeletがファイルの内容を更新してくれます(ただしsubPathでマウントした項目は更新されません)。アプリが設定を再読み込みできるならボリューム、開始時に1回だけ読むなら環境変数が楽です。
resourcesには2つの顔があります。requestsはスケジューラーが見る値です。ノードにその分の余裕がないと、Podを載せません。limitsはkubelet/ランタイムが強制する値です。CPUはスロットリングされ、メモリは超えるとOOMKilledになります。この2つの関係がQoSクラスを決め、ノードが圧迫されたときにどれが先に追い出されるかを決めます。
どう動くのか
設定注入の選択肢を表にまとめると、次のようになります。
| 方法 | フィールド | 特徴 |
|---|---|---|
| キー1つだけを環境変数に | env[].valueFrom.configMapKeyRef |
名前を変えて入れられます |
| すべてを環境変数に | envFrom[].configMapRef |
キー名がそのまま変数名になります |
| ファイルとして | volumes[].configMap + volumeMounts |
更新が反映されます。defaultModeで権限を指定します |
| ファイル1つだけを既存のディレクトリに | volumeMounts[].subPath |
ディレクトリを覆いません。更新されません |
optional: trueを指定すると、参照先がなくてもPodが起動します。なければデフォルト値で動くアプリに使います。immutable: trueを指定したConfigMap/Secretは、変更できない代わりにkubeletが変更の監視をしなくなるため、大規模クラスターでAPIサーバーの負荷が減ります。
Secretは暗号化ではなくbase64エンコードにすぎません。etcdにはほぼ平文のまま入ります。実際の保護は3つで行います。RBACで読める主体を絞り、apiserverに--encryption-provider-configを設定して保存時に暗号化し、Podに必要なものだけをマウントします。
securityContextにはPodレベルとコンテナレベルがそれぞれあります。Podレベル(spec.securityContext)のrunAsUser・runAsNonRoot・fsGroupはすべてのコンテナにデフォルトで適用され、コンテナレベル(spec.containers[].securityContext)のcapabilities・readOnlyRootFilesystem・allowPrivilegeEscalationはそのコンテナにだけ適用され、Podの値をオーバーライドします。fsGroupがPodレベルにしかない理由は、ボリュームがPod単位のリソースだからです。
QoSは自動的に決まる読み取り専用の値です。
- すべてのコンテナで
requests == limits(CPU・メモリの両方) → Guaranteed - 1つでもrequestsまたはlimitsがあり、上の条件を満たさない → Burstable
- 何もない → BestEffort(ノードの圧迫時に最初にエビクション)
LimitRangeとResourceQuotaはペアです。LimitRangeは個々のコンテナのデフォルト値・最小値・最大値を決め、ResourceQuotaはネームスペース全体の合計を制限します。クォータが設定されたネームスペースでは、requests/limitsを明示していないPodはそもそも拒否されますが、LimitRangeがデフォルト値を補ってくれれば通ります。そのため、両方を一緒に設定します。
現場での姿
ホームラボのNASをKubernetesのボリュームとして接続したときの出来事です。NFSマウントを確認するためにprivileged: trueのPodを起動したところ、次のように失敗しました。
mount.nfs: Operation not permitted for 10.0.0.109:/volume1/k8s on /mnt/t
mount: permission denied (are you root?)
権限を最大にしたのに、それでも拒否されました。原因はhostNetwork: trueが抜けていたことでした。NFSマウントはrpcbindと通信し、1024未満の予約ポートを送信元ポートとして使いますが、Podのネットワークネームスペースの中ではその動作が制約を受けます。privilegedはcapabilityを与えますが、ネットワークネームスペースの問題は解決できません。securityContextで解決できる問題と、Podのspecの別のフィールドで解決する問題は違うということを、高い代償を払って学んだ事例です。
同じクラスターのGPU側では、反対方向の教訓がありました。Podにresources.limits: {nvidia.com/gpu: 1}の1行を書いただけで、スケジューラーがGPUのあるノードを選び、ランタイムがデバイスを入れてくれました。requestsがスケジューリングの言語であることが、そのまま見えます。ところが、ここで新しい問題が生まれました。ワーカー4台のGPUはそれぞれ24GB(3090)、32GB(5090)、8GB(4070 Laptop)×2ですが、Kubernetesから見るとすべて「GPU 1個」です。32GBが必要な学習が8GBのノートPC用GPUに載る事態が、実際に起きました。結局、gpu.homelab/tier=xlarge|large|smallのような意味に基づくラベルを自分で付け、nodeSelectorで選ばせるようにしました。リソースの要求は量(quantity)だけを表現でき、質(quality)は表現できません。この限界を、ラベルで埋めたのです。
次のラボですること
ckad-configネームスペースでConfigMapをリテラル・ファイルから作成し、envFrom・configMapKeyRef・ボリュームマウント・defaultMode・subPath・optional・immutableをすべて手で指定します。続いてckad-secureネームスペースでServiceAccountの指定、Pod/コンテナレベルのsecurityContext、resources、LimitRange、ResourceQuotaを作成し、QoSクラスが実際に何に決まるかを確認します。