冒頭まとめ
CreateContainerConfigError は、Kubernetes がコンテナを起動する前段階、設定を組み立てる段階で失敗したことを示します。イメージの取得は成功しており、コンテナの作成にも到達していません。
重要なのは、この文字列自体には原因が書かれていないことです。実装を見ると、これは分類のための名前で、kubectl get pods の状態欄にはこの名前だけが出ます。実際の原因は、kubectl describe pod のイベントの側に入ります。しかも、そのイベントの理由欄は CreateContainerConfigError ではなく Failed です。実装で理由の定数がそう定義されています。
原因の大半は、環境変数として参照している ConfigMap や Secret が解決できないことです。文言は2種類に分かれます。参照先そのものが無い場合は secret "app-secrets" not found の形、参照先はあるがキーが無い場合は couldn't find key API_KEY in Secret default/app-secrets の形になります。実装でも、この2つは別々の分岐で作られています。
もう1つ、実務で効く性質があります。kubelet は失敗しても作成を繰り返します。したがって、足りない ConfigMap や Secret を後から作れば、Pod を作り直さなくても起動します。実際の報告を見ても、イベントには同じ失敗が数分間で8回といった形で記録されています。
エラーの概要
まず状態欄です。分類名だけが出ます。
NAME READY STATUS RESTARTS AGE
app-6f8d9c7b5-x4k2h 0/1 CreateContainerConfigError 0 64s
原因はイベントにあります。理由欄が Failed である点に注意してください。
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Pulled 3m (x8 over 9m) kubelet Successfully pulled image "app:v1.2"
Warning Failed 3m (x8 over 9m) kubelet Error: secret "app-secrets" not found
コンテナの状態を直接読むこともできます。分類名と文言が対で入っています。
kubectl get pod <Pod名> -o jsonpath='{.status.containerStatuses[0].state.waiting}'
# {"message":"secret \"app-secrets\" not found","reason":"CreateContainerConfigError"}
なお、この状態の Pod ではログが取得できません。kubectl logs は、コンテナが起動待ちである旨を返します。コンテナがまだ作られていないため、ログ自体が存在しないからです。ログを追おうとして行き詰まるのは、このエラーの典型的な入口です。見るべきはイベントです。
まず最初に:イベントの文言を読む
第一に、kubectl describe pod を実行し、理由が Failed のイベントを探します。ここに原因の文言が入っています。
第二に、文言の形を見ます。not found で終わっていれば参照先が存在しません。couldn't find key で始まっていれば、参照先はあるがキーがありません。
第三に、文言に含まれる名前空間を確認します。キー欠落の文言には 名前空間/名前 の形で入るため、意図した名前空間かどうかがその場で分かります。
第四に、x8 over 9m のような繰り返し回数を見ます。増え続けていれば、kubelet が再試行を続けている状態です。参照先を用意すれば、そのまま起動します。
よくある原因と解決手順
原因1:参照先の ConfigMap や Secret が存在しない
最も多い形です。文言は secret "app-secrets" not found や configmap "app-config" not found になります。
公式文書の制約の項に明記があります。ConfigMap は Pod の仕様で参照する前に作成しておく必要があり、存在しない ConfigMap を参照し、かつ参照を任意と指定していない場合、Pod は起動しません。
原因は多くの場合、名前の綴り違いか、名前空間の取り違えです。
# 同じ名前空間に存在するかを確認する
kubectl get secret,configmap -n <名前空間>
# Pod が参照している名前を洗い出す
kubectl get pod <Pod名> -o jsonpath='{range .spec.containers[*]}{.envFrom}{.env}{"\n"}{end}'
配備の道具を使っている場合、作成の順序が原因であることもあります。Deployment が先に作られ、Secret が後から作られる構成では、その間だけこの状態になります。前述のとおり kubelet は再試行するため、Secret が作られた時点で自動的に起動します。慌てて Pod を削除する必要はありません。
原因2:参照先はあるが、キーが無い
文言が couldn't find key <キー> in Secret <名前空間>/<名前> の形になります。ConfigMap の場合も同様です。実装では、参照先の取得に成功したあと、キーの存在を確認する段階で作られます。
公式文書にも、存在しないキーへの参照も同様に Pod の起動を妨げる、と書かれています。
# 実際に入っているキーの一覧を確認する
kubectl get secret <名前> -n <名前空間> -o jsonpath='{.data}' | tr ',' '\n'
kubectl get configmap <名前> -n <名前空間> -o jsonpath='{.data}' | tr ',' '\n'
Before(キー名を推測で書く):
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password # 実際は postgresql-password だった
After(実際のキー名に合わせる):
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: postgresql-password
この形は、外部の配布物を使うときによく起きます。作り手が使うキー名と、こちらが書いたキー名がずれているためです。推測せず、実物の一覧を確認してください。
原因3:任意扱いにすべき参照を必須のままにしている
参照を任意と指定すれば、存在しなくても起動します。公式文書によれば、任意と指定した場合、ConfigMap が存在しなければその環境変数は空になり、存在してもキーが無ければ同じく空になります。
env:
- name: FEATURE_FLAG
valueFrom:
configMapKeyRef:
name: optional-config
key: flag
optional: true # 無ければ空のまま起動する
ただし、無条件に指定するのは危険です。必須の資格情報を任意にすると、値が空のまま起動し、アプリケーションの中でより分かりにくい失敗に変わります。任意にしてよいのは、無くても動作が定義できる項目だけです。
なお、実装を読むと、任意の指定が効くのは参照先が「見つからない」場合に限られます。権限不足など別の理由で取得に失敗した場合は、任意でも失敗します。
原因4:環境変数の設定以外が原因
頻度は下がりますが、設定を組み立てる段階には他の処理も含まれます。実装では、イメージの利用者の解決や、runAsNonRoot の検証、ログ用ディレクトリの作成もこの段階で行われ、いずれの失敗も同じ分類名になります。
したがって、イベントの文言が ConfigMap や Secret に触れていない場合は、そちらを疑ってください。とくに runAsNonRoot を指定しているのにイメージが root で動く設定になっている場合が該当します。
原因5:分類名だけで判断してしまう
対処ではなく、進め方の問題です。この分類名は原因を含まないため、名前だけを検索しても自分の状況には辿り着きません。
# 状態と文言を同時に取り出す(複数コンテナにも対応)
kubectl get pod <Pod名> -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.state.waiting.reason}{"\t"}{.state.waiting.message}{"\n"}{end}'
複数のコンテナを持つ Pod では、どのコンテナで失敗しているかも重要です。補助的なコンテナだけが失敗している場合もあります。
補足:似ているが別のもの
CreateContainerError は別の分類です。実装では定数からして別物で、こちらは設定の組み立てではなく、コンテナの作成そのものに失敗した場合に使われます。名前が似ているため取り違えやすいので、文字列を最後まで読んでください。
イメージが取得できない場合は ImagePullBackOff です(Kubernetes の ImagePullBackOff の記事)。このエラーはイメージの取得に成功したあとの段階なので、両者は排他です。
コンテナが起動したあとで落ちる場合は CrashLoopBackOff です(Kubernetes の CrashLoopBackOff の記事)。設定は組み立てられているため、原因はアプリケーション側にあります。
ContainerCreating のまま進まない場合は、多くがボリュームの割り当て待ちです。イベントの理由が FailedMount になります。
なお、envFrom で ConfigMap 全体を取り込む場合、環境変数名として使えないキーは読み飛ばされ、Pod は起動します。公式文書によれば、この場合はイベントに InvalidVariableNames として記録されます。起動しているのに値が入っていない、という現象はこちらです。
切り分けの順序
kubectl logsではなくkubectl describe podを見る。ログはまだ存在しない。- 理由が
Failedのイベントを探す。原因の文言はそこにある。 - 文言の形を見る。
not foundなら参照先が無い、couldn't find keyならキーが無い。 - 文言に含まれる名前空間を確認する。取り違えていないか。
- 実物の一覧と突き合わせる。推測でキー名を直さない。
- 配備直後なら、順序の問題かもしれない。後から作れば自動的に起動する。
- 文言が ConfigMap や Secret に触れていないなら、
runAsNonRootなどの設定を疑う。 - 複数コンテナなら、どのコンテナで失敗しているかを確認する。
確認コマンド集
# 1. 状態と原因の文言をまとめて取り出す
kubectl get pod <Pod名> -n <名前空間> -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\t"}{.state.waiting.reason}{"\t"}{.state.waiting.message}{"\n"}{end}'
# 2. 原因のイベントだけを見る
kubectl describe pod <Pod名> -n <名前空間> | sed -n '/Events:/,$p'
# 3. 名前空間全体で同じ失敗が出ていないかを確認する
kubectl get events -n <名前空間> --field-selector reason=Failed \
--sort-by=.lastTimestamp | tail -20
# 4. Pod が参照している ConfigMap と Secret を洗い出す
kubectl get pod <Pod名> -n <名前空間> -o json \
| jq -r '.spec.containers[].env[]?.valueFrom | (.configMapKeyRef, .secretKeyRef) | select(.) | "\(.name)\t\(.key)"'
# 5. 実際に存在するキーの一覧を確認する
kubectl get secret <名前> -n <名前空間> -o jsonpath='{.data}' | tr ',' '\n'
kubectl get configmap <名前> -n <名前空間> -o jsonpath='{.data}' | tr ',' '\n'
# 6. 同じ名前空間に参照先が存在するかを確認する
kubectl get secret,configmap -n <名前空間>
# 7. 同じ状態の Pod を一覧化する
kubectl get pods -A --field-selector status.phase=Pending \
-o custom-columns='NS:.metadata.namespace,POD:.metadata.name,REASON:.status.containerStatuses[0].state.waiting.reason'
# 8. 参照先を作ったあとの復帰を確認する
kubectl get pod <Pod名> -n <名前空間> -w
Editor’s Note
このエラーの分かりにくさは、分類名と原因が別の場所にあるという構造に由来します。それを一目で示す記録があります(Error: couldn’t find key postgresql-password in Secret)。
貼られている出力では、kubectl get pod の状態欄に3つの Pod が CreateContainerConfigError と並んでいます。ここまでは、どれも同じに見えます。ところがイベントを見ると、内訳が違いました。ある Pod は Error: couldn't find key postgresql-password in Secret test/my-postgresql、別の Pod は Error: secret "sentry-snuba-env" not found。前者は参照先はあるがキーが違う、後者はそもそも参照先が無いという、対処のまったく異なる2つの問題が、同じ分類名の下に混ざっていたわけです。
イベントの理由欄がどちらも Failed である点も、実装のとおりでした。分類名で検索しても解決しないのは、この構造のためです。
もう1つ、入口でつまずく例もあります(CreateContainerConfigError on multiple pods after install)。報告者が原因を調べようとログを取得したところ、コンテナが起動待ちである旨だけが返ってきています。この段階ではコンテナが存在しないため、当然の応答です。同じ報告のイベントには、失敗が9分間で8回繰り返された記録が残っており、kubelet が諦めずに再試行していることも読み取れます。
つまり、このエラーで最初にやるべきことは決まっています。ログではなくイベントを見る。文言が not found か couldn't find key かを読む。それだけで、作るべきものと直すべきものが分かれます。
免責事項:本記事の内容は、執筆時点の公開情報をもとに作成したものです。ソフトウェアの仕様は予告なく変更されることがあります。最新の情報は各ツールの公式サポートページをご確認ください。本記事の情報を利用した結果生じたいかなる損害についても、著者および運営者は責任を負いかねます。
この記事でエラーは解決しましたか?