CNPE模擬試験A
目標
実際のCNPEと同じ条件で、課題17個を120分以内に解きます。合格ラインは64%で、部分点方式なので、17個のうち11個を通過すれば完了として処理されます。
模擬試験です。ヒントと正解を見ずに、まず最後まで解いてみてください。行き詰まった課題は印を付けて先に進み、残った時間に戻ってくるほうがよいです。採点はいつ押してもかまいませんし、何度押しても結果は変わりません。
なぜ重要なのか
CNPEは専門家レベルで、問われるのは操作方法ではなく判断です。同じルールを、パイプラインで止めるのか、APIサーバーで止めるのか、インシデントが起きたときに制約を消すのか満たすのか、テナントに何を開けて何を閉じておくのかを、決める場面が、課題ごとに1つずつ入っています。ツールは複数出てきますが、どれも深くは問いません。代わりに、どのツールをどこに置くかを問います。
試験環境(実際の試験で確認された事実)
- 見られるドキュメントは、
kubernetes.io/docs、kubernetes.io/blog、そして課題ごとに別に与えられるQuick Referenceのリンクです。そのリンクは許可リストに追加されます。 - リモートデスクトップ内で、ターミナルとWebインターフェースを一緒に使います。課題によっては、ブラウザーでコンソールを開くほうが速いことがあります。
- 試験に出る可能性のあるツールは、Argo、Crossplane、Flagger、Flux、Gatekeeper、Grafana、Istio、Jaeger、Kyverno、Linkerd、OPA、OpenCost、OpenTelemetry、Prometheus、Tektonです。
- 公式カリキュラムは、見慣れないツールが出ても、試験中に提供されるドキュメントを見て扱えなければならないと明記しています。そのため、ツールの一覧をすべて暗記するよりも、初めて見るCRDのスキーマを
kubectl explainで読み解く手のほうが重要です。 - ターミナルのコピーは
Ctrl+Shift+C、貼り付けはCtrl+Shift+Vです。
この模擬試験環境で異なる点
このラボのクラスターは、Pod内で動く1人用のクラスターです。kube-apiserverとコントローラーマネージャー、スケジューラーは本物なので、スキーマ検証、アドミッションポリシー、RBACの判定、クォータ、スケジューリング、集約ClusterRole、エビクションAPIは、実際に動作し、実際に拒否します。ただし、コンテナを実際に動かすランタイムがないため、次の点が異なります。
- Argo CDとArgo Rolloutsは、CRDだけが登録されていて、コントローラーがありません。課題3のAppProjectは、ファイルとしてのみ評価します。
- ワークロードの
exec、logs、port-forwardは使えません。Podは起動しますが、中でプロセスが動くわけではありません。 - Prometheusサーバーがありません。課題9と課題10は、
promtoolとCRDで評価します。実際の収集は起こりません。
ノードは3台で、それぞれCPU 8コア、メモリ32Giで、ゾーンはzone-0、zone-1、zone-2と、1台ずつ異なります。
ステップ
GitOps and Continuous Delivery
/root/exam/chartにHelmチャートを作ってください。チャート名はpaved-appで、テンプレートはDeploymentが1つで、その名前はpaved-appです。コンテナ名はapp、レプリカ数は.Values.replicas、イメージは.Values.image.repositoryと.Values.image.digestを@でつないだものです。values.schema.jsonで、replicasは2以上10以下の整数、tierはbronze、silver、goldのいずれか、image.digestはsha256:の後ろに16進数64桁が続く文字列だけを許可し、3つの値をすべて必須にしてください。デフォルト値は、それぞれ3、silver、そして任意の有効なダイジェストです。/root/exam/portalにkustomizeディレクトリを作ってください。Deploymentportalは、レプリカ2、コンテナ名app、イメージはダイジェスト固定で、envFromでConfigMapportal-configを丸ごと読み込みます。そのConfigMapは、ファイルとして置かず、configMapGeneratorで作り、LOG_LEVEL=infoとFEATURE_FLAGS=betaの2項目を入れ、名前のハッシュ接尾辞をオフにしないでください。レンダリング結果で、Deploymentが参照する名前と、生成されたConfigMapの名前が、同じである必要があります。/root/exam/gitops/appproject.yamlに、Argo CD AppProjectplatformを書いてください。ネームスペースはargocdです。sourceReposには、実際のリポジトリのアドレスを書き、*を使わないでください。destinationsは、サーバーとネームスペースの両方を固定し、ネームスペースに*を使わないでください。clusterResourceWhitelistは空のままにし、namespaceResourceBlacklistには、コアグループのResourceQuotaとLimitRangeを入れてください。/root/exam/promoteをgitリポジトリにしてください。envs/staging/deployment.yamlとenvs/prod/deployment.yamlの2つのファイルがあり、どちらもDeploymentledgerで、コンテナ名はapp、イメージはダイジェストで固定します。最初のコミットでは、2つのダイジェストが互いに異なります。そのあと、ステージングのダイジェストを本番に上げるプロモーションのコミット1つを、さらに積んでください。そのコミットで、ステージングのファイルは変わっていてはならず、コミットメッセージには、上げたダイジェストが含まれている必要があり、作業ツリーはクリーンでなければなりません。
Platform APIs and Self-Service Capabilities
/root/exam/api/environment-crd.yamlにCRDを書いてapplyしてください。グループはplatform.labhub.io、種類はEnvironment、複数形はenvironments、短縮名はenv、スコープはNamespacedです。バージョンは2つです。v1はservedであり保存バージョンで、spec.ownerは必須の文字列、spec.tierはbronze、silver、goldのいずれかでデフォルト値がbronze、spec.retentionDaysは1以上90以下の整数でデフォルト値が7です。v1alpha1はservedですが保存バージョンではなく、spec.ownerだけを持ち、非推奨の表示をして、非推奨の警告文を自分で書いてください。スキーマにないフィールドは、サーバーが拒否する必要があります。/root/exam/api/env-defaults.yamlに、Kyverno ClusterPolicyenv-defaultsを書いてください。Environmentリソースにだけ適用されるミューテーション(mutate)ルールで、ラベルplatform.labhub.io/ownerをspec.ownerの値で、アノテーションplatform.labhub.io/requested-tierをspec.tierの値で埋めます。値を固定して書かず、リクエストから読み取って埋める必要があります。/root/exam/api/env-rbac.yamlに集約の権限を書いてapplyしてください。ClusterRoleplatform-env-authorは、独自のルールを持たず、ラベルplatform.labhub.io/aggregate-to-env-author: "true"が付いたClusterRoleを集めます。ClusterRoleplatform-env-author-baseにそのラベルを付け、Environmentに対してget、list、watch、create、update、patchを許可しますが、deleteは与えないでください。ネームスペースtenant-amberとServiceAccountenv-authorを作り、RoleBindingenv-authorで、その集約ClusterRoleをバインドしてください。どのルールにも*を使わないでください。/root/exam/api/env-status-rbac.yamlに、specとstatusの権限を分けてください。tenant-amberに、ServiceAccountenv-controller、Roleenv-controller、RoleBindingenv-controllerを作ります。このアカウントは、Environmentを読み、environments/statusをupdate、patchできますが、Environment自体をcreate、update、patch、deleteすることはできません。逆に、課題7のenv-authorは、environments/statusをupdateできない必要があります。
Observability and Operations
/root/exam/ops/platform-rules.yamlにPrometheusルールを書いてください。グループ名はplatform-apiで、レコーディングルールplatform:request_error:ratio5mは、割り算と5分の区間を使い、アラートルールPlatformApiErrorBudgetBurnには、for、labels.severity、annotations.summary、annotations.runbook_urlがある必要があります。そして、/root/exam/ops/platform-rules-test.yamlに、ルールのユニットテストを書いてください。rule_filesには、相対名platform-rules.yamlだけを書き、alert_rule_testを2つ以上置いて、1つはアラートが鳴る時刻を、もう1つは鳴らない時刻(exp_alerts: [])を、確認する必要があります。promtool check rulesとpromtool test rulesが、どちらも通過する必要があります。- ネームスペース
platform-systemを作り、/root/exam/ops/servicemonitor.yamlにServiceMonitorplatform-apiを書いて適用してください。対象のセレクターはapp: platform-api、namespaceSelectorはplatform-systemです。エンドポイントは1つで、ポート名はmetrics、収集間隔は30s、収集のタイムアウトは間隔より短い必要があります。metricRelabelingsには、__name__を見て特定のメトリクスをdropするルールと、高カーディナリティのラベルをlabeldropで落とすルールが、それぞれ1つ以上ある必要があります。 - ネームスペース
tenant-ochreを作ってラベルplatform.labhub.io/tenant: ochreを付け、ResourceQuotaochre-quotaで、requests.cpuを2、requests.memoryを4Giに制限してください。その中に、Deploymentsearchを、レプリカ4で作ってください。コンテナ名はapp、イメージはダイジェスト固定、requestはCPU 700mとメモリ512Miです。この時点では、Podはすべては起動できません。そのあと、/root/exam/ops/triage.txtに、quota_cpu_hard、quota_memory_hard、desired_replicas、max_cpu_per_podの4行を、키=값の形式(プレースホルダーはキーと値です)で書いてください。CPUはミリコアの整数で、メモリはMiの整数で、最後の値は、このクォータの中でレプリカがすべて起動するには、Pod1つのCPU requestがいくつ以下でなければならないかを、ミリコアの整数で書きます。単位の文字は付けないでください。 - クォータを上げずに、
searchのPod4つがすべてRunningになるようにしてください。レプリカ数とメモリのrequestはそのままにして、CPUのrequestだけを、課題11で計算した値に下げます。以前のrequest値を使うPodが残っていてはいけません。
Platform Architecture and Infrastructure
- ノードちょうど2台に、ラベル
platform.labhub.io/pool=platformとテイントplatform.labhub.io/pool=platform:NoScheduleを、一緒に付けてください。そして、platform-systemにDeploymentportalを、レプリカ3で作ってください。コンテナ名はapp、イメージはダイジェスト固定、requestはCPU 100mとメモリ128Miです。このワークロードは、そのプールだけを選び、そのテイントを許容し、topology.kubernetes.io/zoneを基準に、maxSkew: 1とwhenUnsatisfiable: DoNotScheduleで分散する必要があります。Pod3つがすべて、プールのノード上でRunningである必要があります。 - PriorityClass
platform-critical(値100000以上)とtenant-batch(値1000以下、preemptionPolicy: Never)を作ってください。どちらもglobalDefaultはfalseです。tenant-amberにResourceQuotaamber-priorityをかけて、platform-criticalの優先度を使うPodを、そのネームスペースでは、そもそも作れないようにしてください。そして、課題13のportalが、platform-criticalを使うようにしてください。 platform-systemにPodDisruptionBudgetportalを作ってください。セレクターはportalのPodを選び、minAvailableは2で、maxUnavailableは使いません。メンテナンスのときに、一度に1台だけ空にできる必要があります。
Security and Policy Enforcement
- ネームスペース
tenant-tealを作り、Pod Security Admissionのenforce、audit、warnをすべてrestrictedにしてかけてください。ただし、3つのモードのバージョンラベルは、latestではなくv1.31に固定してください。その中に、Deploymentcheckoutをレプリカ2で作ってください。コンテナ名はapp、イメージはダイジェスト固定で、Podはrestrictedを通過する必要があります。Pod2つが実際にRunningである必要があります。 - 1つのルールを、2か所で執行してください。ルールは「Deploymentにはラベル
platform.labhub.io/ownerがなければならない」です。/root/exam/security/owner-policy.yamlに、Kyverno ClusterPolicyrequire-ownerをEnforceで書いてください。パイプラインがkyverno applyで実行するゲートです。/root/exam/security/owner-vap.yamlには、ValidatingAdmissionPolicyrequire-ownerと、同じ名前のバインディングを書いて、applyしてください。validationActionsはDenyで、適用範囲は、ラベルplatform.labhub.io/gate=ownerが付いたネームスペースだけです。ネームスペースdeliveryを作って、そのラベルを付けてください。ラベルのないDeploymentは、deliveryでは拒否され、defaultでは通過する必要があります。
参考
- サーバーが実際に何をするかを見るときは、
kubectl apply --dry-run=serverを使ってください。アドミッションのチェーンをそのまま通しながら、クラスターには何も残しません。-o jsonを付けると、デフォルト値が埋められた結果まで見られます。 - サブリソースの権限は、
kubectl auth can-i update environments.platform.labhub.io --subresource=status처럼--subresourceで尋ねる必要があります。リソース名の後ろにスラッシュを付けて書くと、的外れな判定が出ます。 - CRDを適用した直後は、
kubectl wait --for=condition=Established crd/...で、登録が終わるのを待ってください。 - クォータがいっぱいの状態でローリングアップデートをかけると、新しいPodを作る場所がなく、デプロイが止まってしまいます。以前のPodを先に空にしなければ、新しいPodは入りません。
- よくある間違いは3つです。
labeldropルールにsourceLabelsを書いてはいけません。RoleBindingなしでClusterRoleだけを作っても、何の権限も生まれません。そして、集約ClusterRoleのrulesは、人が埋める場所ではなく、コントローラーが埋める場所です。
チャートが誤った値を自分で拒否するように
helmは、チャートの中にvalues.schema.jsonがあれば、templateとinstallの前に値を検証し、合わなければレンダリングそのものを拒否します。JSONスキーマのminimum、maximum、enum、pattern、requiredを使ってください。スキーマが効いているかは、helm template ... --set replicas=1のように、わざと外れた値を入れて確認します。
設定が変わったらデプロイも変わるように
configMapGeneratorは、内容のハッシュを名前の後ろに付け、同じkustomizationの中でその名前を参照している場所を、自動的に書き換えます。Deploymentには、接尾辞のない元の名前を書いておけばよいです。disableNameSuffixHashをオンにすると、このつながりが切れて、設定だけを変えたときにPodが再起動しません。
プロジェクトが何を許可するかを固定する
AppProjectは、Applicationが何をどこにデプロイできるかを決める塀です。sourceReposとdestinationsにアスタリスクを残しておくと、塀がないのと同じです。clusterResourceWhitelistを空にしておくと、クラスタースコープのリソースをそもそも作れず、namespaceResourceBlacklistは、ネームスペースの中でも触れさせない種類を書く場所です。
プロモーションを1つのコミットとして残す
GitOpsでは、デプロイはコミットです。そのため、何がいつ上がったかはログに残る必要があり、元に戻すのはrevertでなければなりません。ステージングで検証したダイジェストを、そのまま本番に移しますが、ステージングのファイルには触れないでください。すべて終えたら、git statusが空である必要があります。コミットしていない変更は、デプロイされたことにはなりません。
スキーマが契約になるように
構造スキーマ(structural schema)にdefaultを書くとサーバーが値を埋めてくれ、書かれていないフィールドは、サーバーが拒否します。2つの性質がそろって初めて、スキーマが契約になります。非推奨は、versions項目のdeprecatedとdeprecationWarningで表示し、その文言は、実際にkubectlの画面にWarningとして表示されます。確認は、kubectl apply --dry-run=server -o jsonで行ってください。
リクエストに足りないものをプラットフォームが埋める
セルフサービスは、ユーザーが書くべきものが少ないほど良いです。ユーザーがすでに書いた値から導き出せるものは、プラットフォームが埋めるべきです。Kyvernoのmutateルールで、{{ request.object... }}を使って、リクエスト本文を読めます。値を固定して書くと、採点ツールが別のリクエストで尋ねたときに明らかになります。確認は、kyverno apply <ポリシー> --resource <ファイル> -o <ディレクトリ>で行ってください。
権限を断片に分けて集める
集約ClusterRoleは、自分のルールを書きません。ラベルセレクターだけを書いておくと、コントローラーが、そのラベルが付いた別のClusterRoleのルールを集めて、埋め込みます。そのため、あとで権限を増やすときに、このロールを直さず、断片を1つ追加で付ければよいです。集約が実際に行われたかは、kubectl get clusterrole <名前> -o jsonpath='{.rules}'で確認してください。空なら、ラベルがずれています。
書く人と埋める人を分ける
statusはコントローラーが埋める場所で、specはユーザーが書く場所です。RBACでは、この2つは別のリソース名なので、environmentsとenvironments/statusを別々に書きます。確認するときは、リソース名の後ろにスラッシュを付けず、--subresource=statusを使ってください。スラッシュで書くと、kubectlが的外れに解釈して、反対の結果を出します。
アラートルールをテストとして固定する
promtool test rulesは、偽の時系列を入れて、ルールを実際に評価します。input_seriesの値は、「0+3x30」のように、開始値、増加幅、繰り返し回数で書きます。鳴る時刻だけを確認すると、常に鳴るルールも通過してしまうため、鳴ってはいけない時刻をexp_alerts: []で一緒に書いてください。rule_filesは、テストファイルがあるディレクトリを基準に探します。
何を測らないかを決める
観測のコストは、収集した時系列の数で決まり、その数はラベルの組み合わせで爆発します。そのため、収集の設定には、何を測るかと同じくらい、何を捨てるかも入っている必要があります。dropは、sourceLabelsで選んだ値がregexに合えば、そのメトリクスを丸ごと捨て、labeldropは、regexに合うラベル名を消します。labeldropにsourceLabelsを書いてはいけません。
クォータの枯渇を再現して数字で書く
クォータが止めても、Deploymentはエラーを出しません。代わりにReplicaSetがReplicaFailure条件を付け、Podはそもそも作られません。kubectl describe rsやkubectl get rs -o yamlで、その条件を見てください。分類は、感覚ではなく割り算です。クォータの上限を、望むレプリカ数で割ると、Pod1つが使える最大値が出ます。
クォータを上げずに復旧する
requestを下げるだけでは終わりません。クォータがいっぱいの状態では、新しいPodを作る場所がなく、ローリングアップデートが途中で止まります。以前のrequest値を使うPodが残っている限り、場所は空きません。レプリカを0に下げてからもう一度上げるか、以前のPodを先に空にしてください。確認は、Podの数ではなく、Podごとのrequest値で行ってください。
プールを分けてワークロードを分散させる
ラベルは選ばせ、テイントは押し出します。2つを一緒にかけて初めて、そのプールが専用になります。ラベルだけをかけると、誰でも入ってこられ、テイントだけをかけると、入る方法がありません。topologySpreadConstraintsのlabelSelectorは、Podのラベルを指す必要があり、DoNotScheduleにしておかなければ、実際に強制されません。確認は、Podがどのノードに載っているかで行ってください。
優先度を使える人を決める
PriorityClassはクラスタースコープなので、作っておけば、誰でも名前で持ってきて使えます。そのため、優先度そのものではなく、その優先度を使う権利を、別に止める必要があります。ResourceQuotaのscopeSelectorにPriorityClassスコープをかけ、上限を0にすると、そのネームスペースでは、そのレベルのPodをそもそも作れません。preemptionPolicy: Neverは、自分が押し出されないという意味ではなく、他を押し出さないという意味です。
メンテナンス中にも残っている数を決める
PodDisruptionBudgetは、スケジューラーではなく、エビクションAPIが見ます。kubectl drainとノードのアップグレードが、その経路を通ります。レプリカ3でminAvailable 2をかけると、一度に1台だけ空にできます。コントローラーが計算した結果は、kubectl get pdbのALLOWED DISRUPTIONS列に出て、この値が0ならメンテナンスがそもそも止まり、レプリカ数と同じなら、何も守っていないことになります。
テナントのネームスペースにベースラインをかける
Pod Security Admissionは、Deploymentを止めるのではなく、そのDeploymentが作るPodを止めます。Deploymentは作られたのにPodがないなら、kubectl describe rsで理由を見てください。restrictedは、Podレベルに、runAsNonRootとseccompProfileを、コンテナレベルに、allowPrivilegeEscalation: falseとcapabilities.drop [ALL]を要求します。バージョンラベルをlatestにしておくと、クラスターをアップグレードするときに、基準が静かに厳しくなります。
同じルールを2か所で執行する
パイプラインのゲートは、素早いフィードバックをくれますが、パイプラインを通らない変更は止められず、アドミッションポリシーは、何を通って入ってきても止めますが、開発者はデプロイを押してみて初めてわかります。そのため、同じルールを2か所に置きます。VAPの適用範囲は、バインディングが決め、matchResourcesのnamespaceSelectorで絞れます。validationActionsにDenyがなければ、監査だけを残して通過させます。