調整ループを手で一周回す
目標
調整ループが1周する間に何が起きるかを、自分で作ってみます。望む状態を所有マークとともに適用し、揺れるフィールドを取り除き、無視することにしたフィールドを除いて比較し、削除の候補を選び出し、空の結果を拒否するところまで進みます。
なぜ重要なのか
gitops-manifestラボでは、手でドリフトを作って手で元に戻しました。ここで質問を1つだけすると、GitOpsの最後の部品が出てきます。その適用を誰が実行するのか、という質問です。人が実行するなら、それはよく整理されたデプロイスクリプトであって、GitOpsではありません。
コントローラーに代わりに実行させるには、判断の基準がコードとしてある必要があります。何を揺れる値と見なすか、どのフィールドを他者が所有するように置くか、何をこのアプリの所有として印を付けるか、そして結果が空だったときにどうするかです。selfHealは最悪の場合に手動の変更が消えることで、pruneは最悪の場合にデータが消えることなので、危険の度合いが違います。その判断をスクリプトに書いてみると、スイッチの名前の裏に何があるのかがはっきりします。
環境
このPodにはArgo CDコントローラーがありません。Applicationオブジェクトは作れますが、ひとりでにSyncedに変わることはありません。そのため、このラボは宣言を書いておいて、その宣言どおりに動くツールを自分で作る方式で進めます。フィールドの所有権とサーバーサイドの適用、スケジューリングの判定は、kwokが立ち上げた本物のapiserverが処理するので、その部分は本物です。作業ディレクトリは/root/gitops-shで、その下のk8s/・desired/・bin/・out/を使います。
ステップ
Application shopにスイッチと無視するフィールドを宣言します。- 宣言した状態を、所有マークとともにサーバーサイドで適用します。
bin/normalize.shで、揺れるフィールドを取り除きます。bin/drift.shで、無視するフィールドを除いて比較します。- ドリフトを作って、片方だけを元に戻したあと、
out/selfheal.txtに書きます。 bin/prune.shで、削除の候補を選び出します。bin/sync.shが、空の結果を拒否するようにします。- Syncedなのに正常ではない状態を作って、
out/status.txtにまとめます。
参考
- ステップ2で
spec.replicasを宣言しないでください。無視することにしたフィールドを宣言すると、所有権がこちらに移ってきて、毎回衝突します。 - ステップ5で
--force-conflictsを外すと、イメージが元に戻りません。失敗ではなく、フィールドの所有権が違うからなので、メッセージを読んでみてください。 - ステップ7の拒否は、何も消さずに止まる必要があります。採点ツールが、その間にオブジェクトが消えていないかを確認します。
- スクリプト3つには、実行権限を与えてください。
ループをどう回すかを宣言する
/root/gitops-sh/k8s/application.yamlにargocdネームスペースのApplicationshopを書いて適用してください。syncPolicy.automatedにprune: true・selfHeal: true・allowEmpty: falseを、syncOptionsにPruneLast=trueとServerSideApply=trueを置き、そしてignoreDifferencesでapps/Deploymentの/spec/replicasを無視するようにしてください。対象のネームスペースはsh-labです。
このPodにはArgo CDコントローラーがないので、このオブジェクトはひとりでにSyncedにはなりません。その代わり、ループをどう回すかを書いておく場所として使います。あとのステップで作るスクリプトが、ここに書かれたとおりに動けばよいのです。allowEmptyはデフォルトがfalseですが、このラボでは意図を明らかにするために明示します。ignoreDifferencesは、HPAのように他のコントローラーが正当に所有するフィールドを、元に戻さないようにする仕組みです。
望む状態を所有マークとともに適用する
/root/gitops-sh/desired/にdeployment.yaml(名前orders)とconfigmap.yaml(名前orders-config)を作ってください。どちらもネームスペースはsh-labで、argocd.argoproj.io/tracking-idアノテーションがshop:で始まる必要があります。Deploymentでは、spec.replicasを書かないでください。そのあと--server-side --field-manager=argocd-controllerでディレクトリをまるごと適用してください。
無視することにしたフィールドは、そもそも宣言しないのが定石です。宣言しておくと、そのフィールドの所有権がこちらに移り、あとでHPAや人が変えた値と毎回衝突します。tracking-idは、Argo CDが自分の所有を示すアノテーションで、<앱>:<그룹>/<종류>:<네임스페이스>/<이름>の形です(プレースホルダーは、順にアプリ、グループ、種類、ネームスペース、名前です)。所有マークがないと、ステップ6の削除候補の計算で、そのオブジェクトが見えません。適用したあと、kubectl get deploy orders -o yaml --show-managed-fieldsで、誰がどのフィールドを所有しているかを見てください。
揺れるフィールドを取り除く前処理を作る
/root/gitops-sh/bin/normalize.sh <매니페스트>を作ってください(プレースホルダーはマニフェストです)。metadataのresourceVersion・uid・generation・creationTimestamp・managedFieldsと、トップレベルのstatusを取り除いたYAMLを、標準出力に出力します。人が宣言したもの(kind・名前・ネームスペース・アノテーション・spec)は、そのまま残す必要があります。
Kubernetesが自分で埋めて、常に変わる値をそのまま比較すると、何も変えていなくても毎回差があると報告されます。そうすると、調整ループが永遠に止まりません。反対に、消しすぎると本当の差も見えなくなるので、何を残すかは、何を消すかと同じくらい重要です。python3のyamlモジュールで読んで、yaml.safe_dump_allで出力すれば済みます。採点ツールが、フィクスチャ1つでこのスクリプトを実際に動かしてみます。
無視することにしたフィールドを除いて比較する
/root/gitops-sh/bin/drift.sh <선언파일> <실제파일>を作ってください(プレースホルダーは、順に宣言ファイルと実際のファイルです)。正規化したあと、apps/Deploymentのspec.replicasを除いて比較し、同じならSYNCEDを出力して0で、違えばOUTOFSYNCを出力して1で終了します。
比較は、宣言したものが実際にそのまま入っているかを見る方向です。実際の側には、Kubernetesが埋めたフィールドがたくさん余計にありますが、それまで差として数えると、何も通らなくなります。そのため、宣言側のキーを1つずつたどりながら、実際の側に同じ値があるかを確認する方式が楽です。リストは、長さと順序まで合わせてみてください。採点ツールが、3組のペアでこのスクリプトを実際に動かしてみます。
ドリフトを作って片方だけを元に戻す
kubectl scaleでordersのreplicasを5に上げ、kubectl set imageでイメージのタグを別の値に変えてください。そのあと、宣言ディレクトリを--server-side --field-manager=argocd-controller --force-conflictsでもう一度適用し、結果を/root/gitops-sh/out/selfheal.txtに5行で書いてください。DRIFT_FIELD、IGNORED、LIVE_REPLICAS、IMAGE_RESTORED、CONFLICT_RESOLUTIONです。
ここで2つのことが同時に見えます。宣言していないspec.replicasは所有者が違うのでそのまま残り、宣言したイメージは元に戻ります。--force-conflictsを外すと、イメージも元に戻りません。kubectl set imageが、そのフィールドの所有権を持っていったからです。実際のArgo CDも、ServerSideApply=trueで同期するとき、同じ方式で所有権を取り戻します。LIVE_REPLICASは、クラスターから読んで書いてください。
削除の候補を所有マークで選び出す
宣言ファイルなしで、sh-labにConfigMapを1つ作って、argocd.argoproj.io/tracking-idを付けてください。そのあと/root/gitops-sh/bin/prune.shを作り、このアプリが所有しているものとして印が付いていて、宣言ディレクトリにはないオブジェクトごとに、PRUNE=<종류>/<이름>を1行ずつ出力させてください(プレースホルダーは、順に種類と名前です)。実際には消しません。
判定の基準は3つです。宣言ディレクトリを読んで、あるべき一覧を作り、クラスターでこのアプリが所有しているオブジェクトを探し、2つ目にはあって1つ目にはないものを選びます。所有マークのないオブジェクトは、いつ作られたものでも対象ではありません。kube-root-ca.crtのように、クラスターが自分で作ったものまで拾ってはいけません。採点ツールが、同じ計算を自分で行って、あなたの出力と照合します。
レンダリング結果が空なら止まる
/root/gitops-sh/bin/sync.shを作ってください。環境変数DESIRED_DIR(デフォルトは宣言ディレクトリ)を読んで、マニフェストが1つもなければREFUSEDが入ったメッセージを出力して、0以外のコードで終了します。あれば--server-side --field-manager=argocd-controller --force-conflictsで適用して、APPLIED=<개수>を出力したあと、0で終了します(プレースホルダーは個数です)。
source.pathを空のディレクトリに誤って変えたコミット1つで、あるべき一覧が0個になり、そのアプリが管理していたすべてのオブジェクトが削除の対象になります。レビュアーが1文字の打ち間違いを見逃すだけで十分です。そのため、空の結果そのものを拒否するのが、最も直接的な防御です。拒否するときは、何も消さずに止まる必要があります。採点ツールは、空のディレクトリで1回、正常なディレクトリで1回動かして、その間にオブジェクトが消えていないかも確認します。
Syncedなのに正常ではない状態を作る
/root/gitops-sh/desired/broken.yamlに、スケジュールできないnodeSelectorを持つDeploymentbrokenを宣言して(所有マークを含む)、sync.shで適用してください。そのあと/root/gitops-sh/out/status.txtに5行でまとめます。SYNC、HEALTH、REASON、PRUNE、ALLOW_EMPTYです。
Sync状態はリポジトリと同じかどうか、Health状態は正しく動いているかなので、別々の軸です。そのため、リポジトリが指示したとおりにデプロイされたのにPodが起動できない、という組み合わせが、正常な状態として出てきます。実務では、イメージのタグを間違えて書いた場合が、まさにこの状態であり、2つの軸を混ぜて読むと、障害の原因を見当違いの場所で探すことになります。クラスターにないラベルをnodeSelectorに書けば、同じ状態を作れます。PRUNEとALLOW_EMPTYは、ステップ1で宣言した値をそのまま書いてください。