TT Lab
はじめる
学ぶ 学習パス コース

CNPE — クラウドネイティブプラットフォームエンジニア

CNPE模擬試験A

TT Labで続きを見る

目標

実際のCNPEと同じ条件で、課題17個を120分以内に解きます。合格ラインは64%で、部分点方式なので、17個のうち11個を通過すれば完了として処理されます。

模擬試験です。ヒントと正解を見ずに、まず最後まで解いてみてください。行き詰まった課題は印を付けて先に進み、残った時間に戻ってくるほうがよいです。採点はいつ押してもかまいませんし、何度押しても結果は変わりません。

なぜ重要なのか

CNPEは専門家レベルで、問われるのは操作方法ではなく判断です。同じルールを、パイプラインで止めるのか、APIサーバーで止めるのか、インシデントが起きたときに制約を消すのか満たすのか、テナントに何を開けて何を閉じておくのかを、決める場面が、課題ごとに1つずつ入っています。ツールは複数出てきますが、どれも深くは問いません。代わりに、どのツールをどこに置くかを問います。

試験環境(実際の試験で確認された事実)

この模擬試験環境で異なる点

このラボのクラスターは、Pod内で動く1人用のクラスターです。kube-apiserverとコントローラーマネージャー、スケジューラーは本物なので、スキーマ検証、アドミッションポリシー、RBACの判定、クォータ、スケジューリング、集約ClusterRole、エビクションAPIは、実際に動作し、実際に拒否します。ただし、コンテナを実際に動かすランタイムがないため、次の点が異なります。

ノードは3台で、それぞれCPU 8コア、メモリ32Giで、ゾーンはzone-0、zone-1、zone-2と、1台ずつ異なります。

ステップ

GitOps and Continuous Delivery

  1. /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、そして任意の有効なダイジェストです。
  2. /root/exam/portalにkustomizeディレクトリを作ってください。Deploymentportalは、レプリカ2、コンテナ名app、イメージはダイジェスト固定で、envFromでConfigMapportal-configを丸ごと読み込みます。そのConfigMapは、ファイルとして置かず、configMapGeneratorで作り、LOG_LEVEL=infoとFEATURE_FLAGS=betaの2項目を入れ、名前のハッシュ接尾辞をオフにしないでください。レンダリング結果で、Deploymentが参照する名前と、生成されたConfigMapの名前が、同じである必要があります。
  3. /root/exam/gitops/appproject.yamlに、Argo CD AppProjectplatformを書いてください。ネームスペースはargocdです。sourceReposには、実際のリポジトリのアドレスを書き、*を使わないでください。destinationsは、サーバーとネームスペースの両方を固定し、ネームスペースに*を使わないでください。clusterResourceWhitelistは空のままにし、namespaceResourceBlacklistには、コアグループのResourceQuotaとLimitRangeを入れてください。
  4. /root/exam/promoteをgitリポジトリにしてください。envs/staging/deployment.yamlとenvs/prod/deployment.yamlの2つのファイルがあり、どちらもDeploymentledgerで、コンテナ名はapp、イメージはダイジェストで固定します。最初のコミットでは、2つのダイジェストが互いに異なります。そのあと、ステージングのダイジェストを本番に上げるプロモーションのコミット1つを、さらに積んでください。そのコミットで、ステージングのファイルは変わっていてはならず、コミットメッセージには、上げたダイジェストが含まれている必要があり、作業ツリーはクリーンでなければなりません。

Platform APIs and Self-Service Capabilities

  1. /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だけを持ち、非推奨の表示をして、非推奨の警告文を自分で書いてください。スキーマにないフィールドは、サーバーが拒否する必要があります。
  2. /root/exam/api/env-defaults.yamlに、Kyverno ClusterPolicyenv-defaultsを書いてください。Environmentリソースにだけ適用されるミューテーション(mutate)ルールで、ラベルplatform.labhub.io/ownerをspec.ownerの値で、アノテーションplatform.labhub.io/requested-tierをspec.tierの値で埋めます。値を固定して書かず、リクエストから読み取って埋める必要があります。
  3. /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をバインドしてください。どのルールにも*を使わないでください。
  4. /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

  1. /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が、どちらも通過する必要があります。
  2. ネームスペースplatform-systemを作り、/root/exam/ops/servicemonitor.yamlにServiceMonitorplatform-apiを書いて適用してください。対象のセレクターはapp: platform-api、namespaceSelectorはplatform-systemです。エンドポイントは1つで、ポート名はmetrics、収集間隔は30s、収集のタイムアウトは間隔より短い必要があります。metricRelabelingsには、__name__を見て特定のメトリクスをdropするルールと、高カーディナリティのラベルをlabeldropで落とすルールが、それぞれ1つ以上ある必要があります。
  3. ネームスペース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がいくつ以下でなければならないかを、ミリコアの整数で書きます。単位の文字は付けないでください。
  4. クォータを上げずに、searchのPod4つがすべてRunningになるようにしてください。レプリカ数とメモリのrequestはそのままにして、CPUのrequestだけを、課題11で計算した値に下げます。以前のrequest値を使うPodが残っていてはいけません。

Platform Architecture and Infrastructure

  1. ノードちょうど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である必要があります。
  2. PriorityClassplatform-critical(値100000以上)とtenant-batch(値1000以下、preemptionPolicy: Never)を作ってください。どちらもglobalDefaultはfalseです。tenant-amberにResourceQuotaamber-priorityをかけて、platform-criticalの優先度を使うPodを、そのネームスペースでは、そもそも作れないようにしてください。そして、課題13のportalが、platform-criticalを使うようにしてください。
  3. platform-systemにPodDisruptionBudgetportalを作ってください。セレクターはportalのPodを選び、minAvailableは2で、maxUnavailableは使いません。メンテナンスのときに、一度に1台だけ空にできる必要があります。

Security and Policy Enforcement

  1. ネームスペースtenant-tealを作り、Pod Security Admissionのenforce、audit、warnをすべてrestrictedにしてかけてください。ただし、3つのモードのバージョンラベルは、latestではなくv1.31に固定してください。その中に、Deploymentcheckoutをレプリカ2で作ってください。コンテナ名はapp、イメージはダイジェスト固定で、Podはrestrictedを通過する必要があります。Pod2つが実際にRunningである必要があります。
  2. 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では通過する必要があります。

参考

チャートが誤った値を自分で拒否するように

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がなければ、監査だけを残して通過させます。