AWS Certified Security Specialty を取得しました

AWS が提供している認定資格、AWS Certified Security Specialty を取得しました。
忘れないうちに受験準備や参照したリソースについて書き残しておきたいと思います。

試験概要

AWS Certified Security Specialty は AWS の認定資格の中でも Specialty (専門知識) に分類される資格のひとつです。
AWS のセキュリティ関連サービスの設計・実装・運用・トラブルシュートなどに関する知識を問われる内容となっています。

aws.amazon.com

  • 試験時間:170 分
  • 出題形式:選択肢式・多肢選択式
  • 受験料:$300
  • 試験官との応対言語:日本語を選択可
  • 試験問題の言語:日本語を選択可
  • 合格ライン 750/1000

おすすめの学習リソース

試験ガイド

今回の受験に最も役立ったのが公式の試験ガイドです。
この資格では IAM、セキュリティグループ、KMS、などどのプロジェクトでも使われるような基本的なサービスをはじめ、Control Tower, Security Hub, Amazon Macie など幅広く AWS のセキュリティに関する知識が問われます。
どのような観点でサービスの選定や設計を問われるのか、試験ガイドにかなり詳しく書かれていました。
そのため、各タスクステートメントの「次のスキル」に列挙されている内容の実装がイメージできるか考え、理解の足りていなさそうな項目に関して Blackbelt セミナーの資料や公式ドキュメントを参照する、あるいは実際に該当サービスを使うことで効率的に学習を進めることができました。

https://d1.awsstatic.com/ja_JP/training-and-certification/docs-security-spec/AWS-Certified-Security-Specialty_Exam-Guide.pdfd1.awsstatic.com

Blackbelt セミナー資料 Amazon S3 セキュリティ編

AWS が提供しているこちらの Blackbelt セミナー資料では、Amazon S3 のセキュリティ観点の主な機能紹介やベストプラクティスがまとまっています。
近年の主要アップデートの紹介だけでなく、IAM ポリシーとバケットポリシーの関係性の解説など、おさえておくべき基本概念も載っており、知識の再整理に役立ちました。
S3 Storage Lens や Access Analyzer for S3 などの概要も紹介されていますので、近年追加された機能を使ったことのない方は一読しておくと良いのではないでしょうか。

https://pages.awscloud.com/rs/112-TZM-766/images/AWS-Black-Belt_2023_Amazon_S3_Security_0131_v1.pdfpages.awscloud.com

受験方法

今回はテストセンターで受験しました。
いくつか受験の流れのなかで気になった点は こちら の記事で書いていますので割愛しますが、結論だけ再掲しておきます:

  • テストセンター受験の場合は、2023 年 3 月現在も日本語の身分証明書 2 点で受験可能
  • 受験直後に確認することができた暫定合否が 2023 年からは表示されなくなり、受験後 5 営業日以内に試験結果が通知される仕様に変更

ちなみに、今回の私の場合は受験後 6 時間ほどで合格通知が届きました。

受験時の所感

IAM や Cloud Trail などの基本的なサービスはもちろんのこと、マルチアカウント構成でよく使われる Organizations や AWS SSO などもかなりの問題数に登場している印象でした。
Amazon Detective などの比較的新しいサービスも試験範囲に含まれていますし、従来からあるセキュリティ系サービスについても最新のアップデートを踏まえた回答が求められます。
仕様や設計に関する知識を問う問題はもちろんのこと、トラブルシュートや、オンプレミスからの移行計画など、運用中の環境に関する問題も多く見受けられました。
また、EKS を使ったコンテナワークロードにおけるアクセス制御など、EC2 以外のターゲットが題材となるセキュリティ設計のシナリオも試験範囲に含まれていますので、ターゲットリソースに応じた設計の選択肢の違いなどを抑えておくと良いのではないでしょうか。

おわりに

AWS Certified Security Specialty を取得しました。
受験準備を通じて、AWS のセキュリティの知識を再整理することができました。

Artifact Registry で Immutable tags を利用する

Artifact Registry で Immutable tags の設定がリリースされました(記事執筆時点ではプレビュー)。
Artifact Registry の Docker リポジトリで Immutable tags を有効化することにより、当該リポジトリでは一度付与したタグを変更・削除することができなくなります。
一度付与したタグを変更・削除できなくなることで、誤って新しいイメージに古いタグを付与してタグを上書きしてしまったり、latest タグを本番利用して意図せずコンテナイメージが更新されてしまう、などのトラブルを予防することができます。

cloud.google.com

Immutable tags の設定はリポジトリ毎に行えるため、同じプロジェクト内の Artifact Registry の Docker リポジトリであっても、有効化するリポジトリを個別に選択することが可能です。
新規リポジトリの作成時に適用することも、既存リポジトリに後から適用することも可能です。

実際にどのような挙動になるのか、環境を構築して試してみた様子をご紹介します。

検証

まずはプロジェクトの Artifact Registry のページに移動し、サンプルの Docker リポジトリを作成します。

すでにコンソールからも設定できるようになっていますが、今回は挙動の違いを説明するため最初は Immutable tags を設定せずにリポジトリを作成します。

無事に作成されました。
それでは Cloud Shell をローカルの開発環境に見立てて、コンテナイメージの push や pull, それに伴うタグの変化を見ていきましょう。

Immutable tags の有効化されていないリポジトリ

まずは適当なコンテナイメージを作成します。
Dockerfile は次のとおりとしました。

$ cat Dockerfile
FROM scratch
LABEL version="1.0"

ビルドしてリポジトリに push します。
リポジトリにアクセスする前に、Artifact Registry に対するリクエストの認証 も行っておきましょう。

$ gcloud auth configure-docker us-central1-docker.pkg.dev
WARNING: Your config file at [/home/*******/.docker/config.json] contains these credential helper entries:

{
  "credHelpers": {
    "gcr.io": "gcloud",
    "us.gcr.io": "gcloud",
    "eu.gcr.io": "gcloud",
    "asia.gcr.io": "gcloud",
    "staging-k8s.gcr.io": "gcloud",
    "marketplace.gcr.io": "gcloud",
    "us-central1-docker.pkg.dev": "gcloud"
  }
}
Adding credentials for: us-central1-docker.pkg.dev
gcloud credential helpers already registered correctly.

$ docker build .
[+] Building 0.0s (3/3) FINISHED
 => [internal] load .dockerignore  0.0s
 => => transferring context: 2B  0.0s
 => [internal] load build definition from Dockerfile  0.0s
 => => transferring dockerfile: 70B  0.0s
 => exporting to image  0.0s
 => => writing image sha256:f81ebf003bbfd311ca96466afcea450c2eb5f1a99c6cff9d2f2eb6e28423244d  0.0s
 
$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED   SIZE
<none>       <none>    f81ebf003bbf   N/A       0B

$ docker tag f81ebf003bbf us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app

$ docker tag f81ebf003bbf us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app:1.0

$ docker images
REPOSITORY                                                                      TAG       IMAGE ID       CREATED   SIZE
us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app   1.0       f81ebf003bbf   N/A       0B
us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app   latest    f81ebf003bbf   N/A       0B

$ docker push us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app
Using default tag: latest
The push refers to repository [us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app]
latest: digest: sha256:ac784f1fc90581fdf4a3c799a0511267e7a885396671fc87a58fec3e5683d970 size: 313

$ docker push us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app:1.0
The push refers to repository [us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app]
1.0: digest: sha256:ac784f1fc90581fdf4a3c799a0511267e7a885396671fc87a58fec3e5683d970 size: 313

コンソールからも、Artifact Registry のリポジトリにイメージが push されたことが確認できます。
この時点で、sample-app のコンテナイメージ(イメージ ID f81ebf003bbf)には 1.0latest のタグが付与されています。

次に、更新版のコンテナイメージをビルドしてリポジトリに push します。
ここではバージョンを 1.1 としました。

$ cat Dockerfile
FROM scratch
LABEL version="1.1"

$ docker build .
[+] Building 0.0s (3/3) FINISHED
 => [internal] load .dockerignore  0.0s
 => => transferring context: 2B  0.0s
 => [internal] load build definition from Dockerfile  0.0s
 => => transferring dockerfile: 70B  0.0s
 => exporting to image  0.0s
 => => writing image sha256:b7fe6fa49cf475863c3f799d32167a28cc3a25a1b192640e43643b660fa8dda1  0.0s
 
$ docker images
REPOSITORY                                                                      TAG       IMAGE ID       CREATED   SIZE
us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app   1.0       f81ebf003bbf   N/A       0B
us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app   latest    f81ebf003bbf   N/A       0B
<none>                                                                          <none>    b7fe6fa49cf4   N/A       0B

$ docker tag b7fe6fa49cf4 us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app

$ docker tag b7fe6fa49cf4 us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app:1.1

$ docker images
REPOSITORY                                                                      TAG       IMAGE ID       CREATED   SIZE
us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app   1.0       f81ebf003bbf   N/A       0B
us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app   1.1       b7fe6fa49cf4   N/A       0B
us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app   latest    b7fe6fa49cf4   N/A       0B

$ docker push us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app
Using default tag: latest
The push refers to repository [us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app]
latest: digest: sha256:0270d6f43f79a1501be08239e133f760e000ff2cc9a31fdd0e60679fb56e61d4 size: 313

$ docker push us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app:1.1
The push refers to repository [us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app]
1.1: digest: sha256:0270d6f43f79a1501be08239e133f760e000ff2cc9a31fdd0e60679fb56e61d4 size: 313

コンソールからも、Artifact Registry のリポジトリにイメージが push されたことが確認できます。
latest のタグを付与し直したため、sample-app の最新のコンテナイメージ(イメージ ID b7fe6fa49cf4)に 1.1latest のタグが付与され、古いコンテナイメージ(イメージ ID f81ebf003bbf)からは latest タグが削除されています(レジストリ内のすべてのタグにユニーク制約があるため)。

Immutable tags の有効化されたリポジトリ

それでは、リポジトリの設定を変更して Immutable tags を有効化してみましょう。
今回はコンソールから変更します。

次に、更新版のコンテナイメージをビルドしてリポジトリに push します。
ここではバージョンを 1.2 としました。

$ cat Dockerfile
FROM scratch
LABEL version="1.2"

$ docker build .
[+] Building 0.0s (3/3) FINISHED
 => [internal] load build definition from Dockerfile  0.0s
 => => transferring dockerfile: 70B  0.0s
 => [internal] load .dockerignore  0.0s
 => => transferring context: 2B  0.0s
 => exporting to image  0.0s
 => => writing image sha256:b65e3f9f75d82ff6c5bf3faf7b66bdd9347d8876bc364d9ac52c686b46608b4d  0.0s
 
$ docker images
REPOSITORY                                                                      TAG       IMAGE ID       CREATED   SIZE
us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app   1.0       f81ebf003bbf   N/A       0B
us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app   1.1       b7fe6fa49cf4   N/A       0B
us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app   latest    b7fe6fa49cf4   N/A       0B
<none>                                                                          <none>    b65e3f9f75d8   N/A       0B

$ docker tag b65e3f9f75d8 us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app

$ docker tag b65e3f9f75d8 us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app:1.2

$ docker images
REPOSITORY                                                                      TAG       IMAGE ID       CREATED   SIZE
us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app   1.0       f81ebf003bbf   N/A       0B
us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app   1.1       b7fe6fa49cf4   N/A       0B
us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app   1.2       b65e3f9f75d8   N/A       0B
us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app   latest    b65e3f9f75d8   N/A       0B

$ docker push us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app
Using default tag: latest
The push refers to repository [us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app]
manifest invalid: cannot update tag latest. The repository has enabled tag immutability

$ docker push us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app:1.2
The push refers to repository [us-central1-docker.pkg.dev/titanium-atlas-382304/sample-repository/sample-app]
1.2: digest: sha256:ab999980dde7c3fd1208685ef5fd32b844ccd888cbbf4d4c75b9b146adc4333a size: 313

コンソールからも、Artifact Registry のリポジトリにイメージが push されたことが確認できます。
latest のタグを付与し直しましたが、Immutable tags が有効化されているためイメージの push 時にエラーが表示され、push に失敗していることがわかります(manifest invalid: cannot update tag latest. The repository has enabled tag immutability のエラーメッセージ)。
sample-app の最新のコンテナイメージ(イメージ ID b65e3f9f75d8)には 1.2 のタグが付与されましたが、latest タグは古いコンテナイメージ(イメージ ID b7fe6fa49cf4)に付与されたままとなります。

ちなみに、タグの削除が行えないことで、該当するコンテナイメージを削除する、あるいはパッケージごと削除するといった操作も行えなくなります。
タグを付与したイメージを削除する必要がある場合は、Immutable tags を一度無効化する必要があります。
gcr-cleaner などのツールを使ってリポジトリのハウスキーピングを行っている場合は注意が必要です。

タグの削除操作の失敗

イメージの削除操作の失敗

利用時の注意点

リポジトリ作成後も Immutable tags の有効・無効は切り替えられるため、セキュリティ対策としては Immutable tags の有効化と併せて、Artifact Registry のリポジトリ設定の変更ができる権限を付与するユーザを限定しておくことをおすすめします。
誰でもリポジトリの設定変更ができてしまうと、Immutable tags を無効化して悪意のあるコンテナイメージに正規のタグを付与して Immutable tags を再度有効化する、あるいは意図せず設定を変更してしまいいつの間にか Immutable tags が無効化されている、といった事態を招きかねないためです。
設定変更の操作は Audit logs に残るため、不正行為を行ったユーザを後から特定することは可能ですが、監査システムとしてリポジトリの設定変更を追跡していない場合は見逃したり、発覚が遅れる場合があるため、予防的な対策として IAM の最小権限の原則は守っておくと良いでしょう。

また、上記検証でも見たように、Immutable tags を有効化すると、それまでに付与されていた latest タグは Immutable tags をもう一度無効化しない限り削除できません。
latest タグのように付与先のイメージをローテーションするようなタグを利用していたリポジトリで、後から Immutable tags を有効化する場合は、誤った利用を防ぐため事前にそうしたタグを削除してから有効化すると良いでしょう。

雑感

Immutable tags は単体の機能としても有用ですが、今後 Binary Authorization を有効化した環境でタグによるイメージ指定に使えるようになるとより便利になるのではと思いました。
現状、Binary Authorization を有効化した環境では、デプロイするコンテナイメージをダイジェストで指定する必要がありますが、これはタグ指定では署名したイメージの特定を保証できないためと思われます。
信頼済みのイメージだけを GKE クラスタに展開できる安全性という点では魅力的な Binary Authorization ですが、ダイジェストでのイメージの指定は人間にとっての可読性が劣る面もあります。
そのため、今回の Artifact Registry のアップデートのように、タグの Immutability が保証されているリポジトリを対象として、タグ指定でも Binary Authorization が使えるようになるとより使い勝手が良くなりそうです。

また、組織ポリシーとして Immutable tags の有効化を必須とするようなポリシーがデフォルトで提供されれば、組織内でのリポジトリ設定の統制もより簡単になりそうです。

まとめ

Artifact Registry に Immutable tags の機能がリリースされました。
タグの付与・更新に際して故意・過失を問わず起こる可能性のあったトラブルを未然に防ぐ手段として有用です。
タグの Immutability が保証されているリポジトリを使うことで、チームの習慣付けとして「小さな変更もこまめにバージョニングする」、「latest タグの利用を抑制する」といったことにも自然とつながるため、今後機会があれば活用していきたいと思います。

AWS Certified Advanced Networking Specialty を取得しました

AWS が提供している認定資格、AWS Certified Advanced Networking Specialty を取得しました。
忘れないうちに受験準備や参照したリソースについて書き残しておきたいと思います。

試験概要

AWS Certified Advanced Networking Specialty は AWS の認定資格の中でも Specialty (専門知識) に分類される資格のひとつです。
AWS のネットワーク系サービスの設計・実装・運用・トラブルシュートなどに関する知識を問われる内容となっています。

aws.amazon.com

  • 試験時間:170 分
  • 出題形式:選択肢式・多肢選択式
  • 受験料:$300
  • 試験官との応対言語:日本語を選択可
  • 試験問題の言語:日本語を選択可
  • 合格ライン 750/1000

おすすめの学習リソース

試験ガイド

今回の受験に最も役立ったのが公式の試験ガイドです。
この資格では Direct Connect, Transit Gateway, AWS WAF など幅広く AWS のネットワークに関する知識が問われますが、どのような観点でサービスの選定や設計を問われるのか、試験ガイドにかなり詳しく書かれていました。
そのため、各タスクステートメントの「次のスキル」に列挙されている内容の実装がイメージできるか考え、理解の足りていなさそうな項目に関して Blackbelt セミナーの資料や公式ドキュメントを参照する、あるいは実際に該当サービスを使うことで効率的に学習を進めることができました。

https://d1.awsstatic.com/ja_JP/training-and-certification/docs-advnetworking-spec/AWS-Certified-Advanced-Networking-Specialty_Exam-Guide.pdfd1.awsstatic.com

受験方法

今回はテストセンターで受験しました(久しぶりにテストセンターで資格試験を受験しました)。
いくつか受験の流れのなかで気になった点を記載しておきます。

受験者氏名の記載

試験申し込みにあたり、最新の 受験ポリシー のドキュメントを参照したところ、以下のような記載がありました。

AWS 認定アカウントではローマ字アルファベット (英語) のみ使用できます。

ポリシードキュメントにもあるとおり、Pearson VUE で受験する際は身分を証明する ID が 2 つ(プライマリ ID が 2 つか、プライマリ ID 1 つとセカンダリ ID 1 つ)必要になります。
以前のポリシーの記載がどうだったか覚えていませんが、この記載によればテストセンターの場合でもローマ字アルファベット表記の ID が 2 つ必要になりそうに見えます。
が、実際には試験会場でローマ字アルファベット表記の ID 2 つが必須といった指摘は受けず、日本語表記の身分証明書(運転免許証と健康保険証)で本人確認を受けることができました。

ちなみに、同じ Pearson VUE の試験でも、聞くところによるとオンライン試験の場合はローマ字アルファベット表記の ID が必須となっているようです。

試験結果の通知

以前の AWS 認定試験では問題に回答しアンケートを入力すると、直後に暫定の合否が表示される仕様になっていました。
どうやら今年(2023 年)からほとんどの AWS 試験で仕様が変わったらしく、受験直後には合否が表示されず、後日(5 営業日以内)に追って通知される、という通知形式になっていました。

ちなみに、今回の私の場合は受験後 6 時間ほどで合否通知が届きました。

受験時の所感

ハイブリッド構成や、マルチリージョン・マルチ VPC な構成でよく使われる Direct Connect, Transit Gateway, Route 53 などはかなりの問題数に登場している印象でした。
仕様や設計に関する知識を問う問題はもちろんのこと、トラブルシュートや、オンプレミスからの移行計画など、運用中の環境に関する問題も多く見受けられました。
また、EKS を使ったコンテナワークロードへのロードバランスなど、EC2 以外のターゲットが題材となるネットワーク設計のシナリオも試験範囲に含まれていますので、ターゲットリソースに応じた設計の選択肢の違いなどを抑えておくと良いのではないでしょうか。

おわりに

AWS Certified Advanced Networking Specialty を取得しました。
受験準備を通じて、AWS のネットワーク関連の知識を再整理することができました。

Amazon Linux 2023 の EC2 インスタンス起動時間

先日、Amazon Linux 2023 が GA になりました。
今回は、Amazon Linux 2023 の AMI を使った EC2 インスタンスの起動時間がどの程度高速化されているのかを見ていきます。

https://aws.amazon.com/jp/linux/amazon-linux-2023/

Amazon Linux 2 と Amazon Linux 2023 の違い

Amazon Linux 2023 の登場に際して最もよく言及されている変更点のひとつといえば、アップストリームプロジェクトの変更ではないかと思います。
これまでに提供されていた Amazon Linux では、CentOS (Amazon LinuxCentOS 6 、Amazon Linux 2 は CentOS 7 ) がベースになっているとされてきました。
一方、Amazon Linux 2023 (以下、AL2023) は Fedora をアップストリームとしていることが明言されています。

そして、アップストリームプロジェクトの変更だけに留まらず、AL2023 では以下のように様々な変更が行われていることが公式ドキュメントで紹介されています。

  • SELinux のデフォルト有効化(従来はデフォルト無効)
  • ライフサイクルポリシーの導入(2年毎のメジャーバージョンリリース、5年サポート)
  • 最適化による起動時間の短縮

etc.

https://docs.aws.amazon.com/linux/al2023/ug/compare-with-al2.html

インスタンス起動時間の計測

上記のドキュメントでも紹介されているとおり、AL2023 では EC2 アーキテクチャへの最適化によってインスタンスの起動時間が従来より改善されているようです。

AL2023 は起動時間を最適化し、インスタンスの起動から顧客のワークロードの実行までの時間を短縮します

https://docs.aws.amazon.com/linux/al2023/ug/compare-with-al2.html

詳細は こちら にも記載されています。

今回は、実際にいくつかのインスタンスタイプで AL2023 の AMI を使って EC2 インスタンスを起動するときの所要時間を計測し、Amazon Linux 2 (以下、AL2) と比較してどの程度起動時間が短くなっているかを見ていきたいと思います。

計測条件

インスタンスの起動にかかる時間は、インスタンスの CPU やメモリ、CPU アーキテクチャなどによって違いが出るのではと考え、以下の AMI とインスタンスタイプの組み合わせで全 6 パターンを検証しました。

AMI

  • Amazon Linux 2023 AMI 2023.0.20230315.0 x86_64 HVM kernel-6.1 (ami-02f3f602d23f1659d)
  • Amazon Linux 2 Kernel 5.10 AMI 2.0.20230307.0 x86_64 HVM gp2 (ami-005f9685cb30f234b)

インスタンスタイプ

  • t2.micro -> 検証用途、汎用
  • c6i.large -> コンピューティング最適化、Intel 製 CPU の標準的なインスタンスタイプ
  • m6a.large -> メモリ最適化、AMD 製 CPU の標準的なインスタンスタイプ

計測にあたり、インスタンスの起動にかかった時間の開始と終了は以下の定義としました。
こちらを参考にさせて頂き、ユーザーデータの実行可能になっている=ユーザアプリケーションの処理が開始できるようになっていると見立て、このタイミングでインスタンスの起動が完了したとみなします。
起動開始の指標とした LaunchTime はミリ秒以下の情報が省略されているため、少々荒い計測ではありますが秒の精度で比較したいと思います。

なお、起動時間にはある程度ばらつきが出ると予想されるため、各 AMI x インスタンスタイプの組み合わせで 10 回ずつ試行し、その平均値を取ることとしました。

ユーザーデータの内容は以下のとおりとしました。

#!/bin/bash
date >> /tmp/start_time

インスタンスの起動は awscli から行いました。
コマンドの一例を以下に示します。

$ aws ec2 run-instances --image-id ami-02f3f602d23f1659d --count 10 --instance-type c61.large --key-name ******* --subnet-id subnet-******* --security-group-ids sg-******* --user-data file://userdata.txt

検証結果

検証結果は以下のとおりとなりました。

Amazon Linux 2 Kernel 5.10 AMI 2.0.20230307.0 x86_64 HVM gp2

  • t2.micro ... 48 秒
  • c6i.large ... 21 秒
  • m6a.large ... 20 秒

Amazon Linux 2023 AMI 2023.0.20230315.0 x86_64 HVM kernel-6.1

  • t2.micro ... 36 秒
  • c6i.large ... 13 秒
  • m6a.large ... 14 秒

最も起動時間の短い組み合わせは AL2023 x c6i.large の 13 秒でした。
最も起動時間の長い組み合わせは AL2023 x t2.micro の 48 秒でした。
すべてのインスタンスタイプにおいて、AL2023 は AL2 比で 25 %以上起動時間が短縮される結果となりました。

ちなみに、Graviton 2 インスタンスにはより最適化されているという記載を見かけたため、以下の組み合わせも追加で計測してみました。

Amazon Linux 2023 AMI 2023.0.20230315.0 arm64 HVM kernel-6.1 (ami-05fab674de2157a80)

  • c7g.large ... 13 秒

考察

すべてのインスタンスタイプで AL2 よりも AL2023 のほうが起動時間が早く、AL2023 の最適化の効果が伺える結果となりました。
急速なリクエスト増などでスピーディなオートスケールが求められるようなケースではこの差が効いてくる可能性もあるのではないでしょうか。

ちなみに今回は large のインスタンスサイズを中心に検証しましたが、t2.micro を使った計測では c6i.large や m6a.large と比べて起動までに 2〜3 倍程度長い時間がかかったため、マシンリソースの多少によって起動時間が変わってくる可能性は考えられそうです。
(ある程度のところで頭打ちもあると思うので、8xlarge や 16xlarge などのより大きいインスタンスサイズを使ったからといってより早くなるという単純な話ではないと思いますが)

一方、同じ AMI を使った c6i.large と m6a.large のインスタンス起動時間にはほぼ差がないように見受けられるため、AL2023 の AMI を利用する際にインスタンスファミリーや CPU アーキテクチャによって起動時間の顕著な違いは無さそうです。
Graviton 2 インスタンスではより最適化されているという触れ込みでしたので c7g.large についても追加で検証しましたが、今回の簡易な計測では顕著な差は見られませんでした。
どちらにしても仮想マシンの起動が十数秒で行えている時点で十分早いとは思います。

以上より、Amazon Linux 2023 (AL2023) の EC2 インスタンスの起動にかかる時間は、large サイズであればインスタンスファミリーや CPU アーキテクチャに関わらず 10〜20 秒程度と思っておいても良さそうです。

まとめ

Amazon Linux 2023 の AMI を使った EC2 インスタンスの起動は、従来の Amazon Linux 2 を用いる場合よりも高速化されていることが確認できました。
あくまで個人による簡易な検証結果ですが、何かのお役に立てば幸いです。

VPC Service Controls の境界内で Cloud Scheduler を使う

少し前のリリースに今更気付いたのですが、VPC Service Controls が Cloud Scheduler のサポートを GA したようです。

cloud.google.com

VPC Service Controls が新たにサービスのサポートを GA したときは VPC Service Controls のリリースノートに掲載されることが多いのですが、今回に関しては掲載されなかったようです。
(滅多に更新されない Cloud Scheduler のリリースノートでのみアナウンスされていました)

cloud.google.com

リリースノートには掲載されていなかったものの、VPC Service Controls のサポートしているプロダクトの一覧には登場していますね。

cloud.google.com

何ができるようになったか

従来は、VPC Service Controls (以下、VPC SC) の境界内で Cron 式ベースのスケジュールされたジョブを実行したい場合は、GKE の CronJob や GCE を利用する必要がありました。
今回のリリースによって、Cloud Scheduler の境界内での利用がサポートされたため、Cron 式ベースのジョブ実行をより簡単にトリガできるようになりました。

このリリースに伴い、VPC SC の境界内での Cloud Scheduler の利用方法に関する公式ドキュメントの新規ページも登場しています。

cloud.google.com

利用時の注意点として、Cloud Scheduler を境界内で利用する場合、構成は Pub/Sub ターゲットのみがサポート対象となっています。
HTTP や App Engine(明記されていませんが恐らく Eventarc も含む)といったその他のターゲット構成は利用できません。
試しに HTTP ターゲットで設定を試みたところ、エラーで弾かれることが確認できました。

検証

では実際に VPC SC の境界内で Cloud Scheduler による定期的なジョブ実行を試してみましょう。
今回作る環境のイメージ図は以下のとおりです。

サンプルのアプリケーションは Cloud Run で実行する構成としました。
余談ですが、VPC SC の境界内で Cloud Run を使うときはイメージレジストリのサービスも同じ境界内に入れておく必要があります。
今回はレジストリサービスに Artifact Registry を利用します。

$ gcloud projects describe perceptive-lamp-******
createTime: '2023-02-19T13:18:20.934Z'
lifecycleState: ACTIVE
name: perceptive-lamp-******
parent:
  id: ************
  type: organization
projectId: perceptive-lamp-******
projectNumber: ************

$ gcloud access-context-manager policies list --organization=************
NAME: ************
ORGANIZATION: ************
SCOPES:
TITLE: default policy
ETAG: ************

$ gcloud access-context-manager perimeters list --policy=************
NAME: sample_perimeter
TITLE: sample-perimeter

$ gcloud access-context-manager perimeters describe sample_perimeter --policy=************
name: accessPolicies/************/servicePerimeters/sample_perimeter
status:
  ingressPolicies:
  - ingressFrom:
      identities:
      - user:************
      sources:
      - accessLevel: '*'
    ingressTo:
      operations:
      - serviceName: '*'
      resources:
      - '*'
  resources:
  - projects/************
  restrictedServices:
  - cloudscheduler.googleapis.com
  - run.googleapis.com
  - artifactregistry.googleapis.com
  vpcAccessibleServices: {}
title: sample-perimeter

$ gcloud artifacts docker images list us-central1-docker.pkg.dev/perceptive-lamp-******/sample-repository
Listing items under project perceptive-lamp-******, location us-central1, repository sample-repository.
IMAGE: us-central1-docker.pkg.dev/perceptive-lamp-******/sample-repository/hello
DIGEST: sha256:2e70803db...
CREATE_TIME: 2023-02-22T13:17:20
UPDATE_TIME: 2023-02-22T13:17:20

$ gcloud pubsub subscriptions list
---
ackDeadlineSeconds: 10
expirationPolicy:
  ttl: 2678400s
messageRetentionDuration: 604800s
name: projects/perceptive-lamp-******/subscriptions/sample-subscription
pushConfig:
  attributes:
    x-goog-version: v1
  oidcToken:
    serviceAccountEmail: scheduled-app-invoker@perceptive-lamp-******.iam.gserviceaccount.com
  pushEndpoint: https://sample-app-zkh7f********.a.run.app
state: ACTIVE
topic: projects/perceptive-lamp-******/topics/sample-topic

$ gcloud run services list
✔
SERVICE: sample-app
REGION: us-central1
URL: https://sample-app-zkh7f********.a.run.app
LAST DEPLOYED BY: ************
LAST DEPLOYED AT: 2023-02-22T13:34:22.319722Z

Pub/Sub から Cloud Run を呼び出すセットアップができました。
これを定期実行するために Cloud Scheduler のジョブを作成し、一定時間待ちます。

$ gcloud scheduler jobs list --location us-central1
ID: sample-scheduler
LOCATION: us-central1
SCHEDULE (TZ): 0 */6 * * * (Asia/Tokyo)
TARGET_TYPE: Pub/Sub
STATE: ENABLED

$ gcloud scheduler jobs describe sample-scheduler --location us-central1
lastAttemptTime: '2023-02-24T09:00:00.600638Z'
name: projects/perceptive-lamp-******/locations/us-central1/jobs/sample-scheduler
pubsubTarget:
  data: ************
  topicName: projects/perceptive-lamp-******/topics/sample-topic
retryConfig:
  maxBackoffDuration: 3600s
  maxDoublings: 5
  maxRetryDuration: 0s
  minBackoffDuration: 5s
schedule: 0 */6 * * *
scheduleTime: '2023-02-24T15:00:00.782322Z'
state: ENABLED
status: {}
timeZone: Asia/Tokyo
userUpdateTime: '2023-02-22T14:18:37Z'

結果を Cloud Logging のログエクスプローラから確認してみましょう。

無事に定期実行できていることが確認できました。

まとめ

VPC Service Controls が Cloud Scheduler をサポートしたことで、境界内でも Cron 式ベースの定期実行が手軽に構成できるようになりました。
実機でも確認しましたが、境界内では現状 Pub/Sub ターゲットの構成でしか利用できない点は認識しておく必要がありますね。

Managed ASM の新機能 Service Mesh Cloud Gateway とは

以下のリリースノートのとおり、Managed Anthos Service Mesh (以下、Managed ASM) に Service Mesh Cloud Gateway という新しい機能がリリースされました。
早速試してみたいと思います。

cloud.google.com

概要

従来、Istio Ingress Gateway を Cloud Load Balancing とつなぐには、Ingress Gateway の Service リソースに cloud.google.com/neg: '{"ingress": true}' などのアノテーションを付与することで NEG の作成と同時に Ingress Controller に Cloud Load Balancing の各種リソースを自動生成させる(あるいは Standalone NEG を作成し手動で Backend Service に指定する)ことで、Backend Service と NEG を接続する必要がありました。
今回リリースされた Service Mesh Cloud Gateway では、昨年 GA となった Kubernetes Gateway API を使って、Cloud Load Balancing と Anthos Service Mesh Ingress Gateway をひとまとめで作成できるようになりました。
現状はプレビューリリースとなっており、以下のように複数の制約があることに注意しましょう。

  • Rapid Channel の Managed ASM を利用する v1.24 以降の GKE クラスタのみで利用可能
  • Classic External L7 LB のみ作成可能
  • Autopilot では利用不可 など

公式ドキュメントは以下にあります。

cloud.google.com

Service Mesh Cloud Gateway を展開してみる

ここからは、こちら の公式ドキュメントの手順を追って Service Mesh Cloud Gateway を展開してゆきたいと思います。

まずは以下のように In-cluster Control Plane の GKE クラスタを用意しました。

$ gcloud container clusters describe sample-cluster --region=us-central1
currentMasterVersion: 1.25.5-gke.1500
currentNodeVersion: 1.25.5-gke.1500
location: us-central1
locations:
- us-central1-a
- us-central1-f
- us-central1-c
name: sample-cluster
networkConfig:
  gatewayApiConfig:
    channel: CHANNEL_STANDARD
  ...
releaseChannel:
  channel: RAPID
resourceLabels:
  mesh_id: proj-***********
status: RUNNING
zone: us-central1
...

次に、GatewayClass リソースを展開します。
GatewayClass リソースは、Kubernetes Gateway API を使ってロードバランサを作成するためのテンプレートを定義するクラスタスコープのリソースです。
今回は、Service Mesh Cloud Gateway がサポートしている asm-l7-gxlb の GatewayClassを利用します(Service Mesh Cloud Gateway としては現状 Classic External L7 LB のみのサポートとなっていますが、こちらの一覧 に記載のとおり、GKE で利用可能な GatewayClass としては他の Cloud Load Balancing のためのテンプレートも用意されています)。

GatewayClass リソースの主な役割は、この後に展開する Gateway リソースをどの GKE Gateway Controller に制御させるかを決定することにあります。
GatewayClass は Gateway を作成する際の実装を抽象化する役割を担うため、Gateway は自身の利用する GatewayClass を指定し、GatewayClass は自身のテンプレートに沿ったインスタンス化を行ってくれる Gateway Controller を指定し、指定されたコントローラが Ingress Gateway や Cloud Load Balancing のセットアップを行う、という仕組みになっています。

ちなみに、コントローラの実装も進化しており、GKE Gateway Controller は(以前から提供されていた GKE Ingress Controller とは異なり)Google がホストしているため、クラスタやプロジェクトに依存せず利用できるようになっています。

$ cat l7-gateway-class.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: GatewayClass
metadata:
  name: asm-l7-gxlb
spec:
  controllerName: mesh.cloud.google.com/gateway

$ kubectl apply -f l7-gateway-class.yaml
gatewayclass.gateway.networking.k8s.io/asm-l7-gxlb created

$ kubectl get gatewayclasses.gateway.networking.k8s.io
NAME                             CONTROLLER                      ACCEPTED   AGE
asm-l7-gxlb                      mesh.cloud.google.com/gateway   Unknown    17h
gke-l7-global-external-managed   networking.gke.io/gateway       True       18h
gke-l7-gxlb                      networking.gke.io/gateway       True       18h
gke-l7-rilb                      networking.gke.io/gateway       True       18h
istio                            istio.io/gateway-controller     True       13h

ACCEPTED が Unknown になっているのがちょっと気になりますが先に進みましょう。※

準備ができたので Gateway リソースを展開していきます。

$ kubectl get namespace istio-ingress
NAME            STATUS   AGE
istio-ingress   Active   17h

$ cat gateway.yaml
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
  name: servicemesh-cloud-gw
  namespace: istio-ingress
  labels:
    istio-injection: enabled
spec:
  gatewayClassName: asm-l7-gxlb
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    allowedRoutes:
      namespaces:
        from: All

$ kubectl apply -f gateway.yaml
gateway.gateway.networking.k8s.io/servicemesh-cloud-gw created

$ kubectl get gateways.gateway.networking.k8s.io -n istio-ingress
NAME                                CLASS         ADDRESS                                                                READY   AGE
asm-gw-gke-servicemesh-cloud-gw     gke-l7-gxlb   34.111.216.187                                                         True    17h
asm-gw-istio-servicemesh-cloud-gw   istio         asm-gw-istio-servicemesh-cloud-gw.istio-ingress.svc.cluster.local:80   True    17h
servicemesh-cloud-gw                asm-l7-gxlb                                                                                  17h

Gateway リソースが展開されると、Gateway Controller は asm-gw-istio-servicemesh-cloud-gw という名前で Ingress Gateway の Deployment を展開します。
加えて、この Ingress Gateway の NEG をバックエンドに設定した External L7 LB を自動作成します。
具体的には servicemesh-cloud-gw(GatewayClass: asm-l7-gxlb)が asm-gw-gke-servicemesh-cloud-gw(GatewayClass: gke-l7-gxlb)と asm-gw-istio-servicemesh-cloud-gw(GatewayClass: istio)を作成することで、asm-gw-gke-servicemesh-cloud-gw に基づいて L7 LB を作成し、asm-gw-istio-servicemesh-cloud-gw に基づいて Ingress Gateway を作成しているようです。

最後のコマンド kubectl get gateways.gateway.networking.k8s.io ... の実行結果の Gateway asm-gw-gke-servicemesh-cloud-gw の ADDRESS に表示されているのが、最終的に LB に付与された外部 IP アドレスですね。

では Service Mesh Cloud Gateway が展開できたので、リクエストを処理するサンプルアプリケーションも展開したいと思います。
手順の詳細は こちら の公式ドキュメントに載っていますので併せてご確認ください。

$ kubectl label namespace default istio-injection=enabled istio.io/rev- --overwrite
label "istio.io/rev" not found.
namespace/default labeled

$ cat whereami.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: whereami-v1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: whereami-v1
  template:
    metadata:
      labels:
        app: whereami-v1
    spec:
      containers:
      - name: whereami
        image: gcr.io/google-samples/whereami:v1.2.19
        ports:
          - containerPort: 8080
        env:
        - name: METADATA
          value: "whereami-v1"
---
apiVersion: v1
kind: Service
metadata:
  name: whereami-v1
spec:
  selector:
    app: whereami-v1
  ports:
  - port: 8080
    targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: whereami-v2
spec:
  replicas: 2
  selector:
    matchLabels:
      app: whereami-v2
  template:
    metadata:
      labels:
        app: whereami-v2
    spec:
      containers:
      - name: whereami
        image: gcr.io/google-samples/whereami:v1.2.19
        ports:
          - containerPort: 8080
        env:
        - name: METADATA
          value: "whereami-v2"
---
apiVersion: v1
kind: Service
metadata:
  name: whereami-v2
spec:
  selector:
    app: whereami-v2
  ports:
  - port: 8080
    targetPort: 8080

$ kubectl apply -f whereami.yaml
deployment.apps/whereami-v1 created
service/whereami-v1 created
deployment.apps/whereami-v2 created
service/whereami-v2 created

$ kubectl get pods
NAME                           READY   STATUS    RESTARTS   AGE
whereami-v1-768bf6449c-c87sz   2/2     Running   0          18h
whereami-v1-768bf6449c-gql2x   2/2     Running   0          18h
whereami-v2-7cf74dd8fc-bnbf5   2/2     Running   0          18h
whereami-v2-7cf74dd8fc-fvg74   2/2     Running   0          18h

$ cat http-route.yaml
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
  name: where-route
spec:
 parentRefs:
 - kind: Gateway
   name: servicemesh-cloud-gw
   namespace: istio-ingress
 hostnames:
 - "where.example.com"
 rules:
 - matches:
   - headers:
     - name: version
       value: v2
   backendRefs:
   - name: whereami-v2
     port: 8080
 - backendRefs:
   - name: whereami-v1
     port: 8080

$ kubectl apply -f http-route.yaml
httproute.gateway.networking.k8s.io/where-route created

$ VIP=$(kubectl get gateways.gateway.networking.k8s.io asm-gw-gke-servicemesh-cloud-gw -o=jsonpath="{.status.addresses[0].value}" -n istio-ingress)

$ curl ${VIP} -H "host: where.example.com"
{
  "cluster_name": "sample-cluster",
  "gce_instance_id": "*******************",
  "gce_service_account": "*******************.svc.id.goog",
  "host_header": "where.example.com",
  "metadata": "whereami-v1",
  "pod_name": "whereami-v1-768bf6449c-c87sz",
  "pod_name_emoji": "*****",
  "project_id": "*******************",
  "timestamp": "2023-01-22T06:10:08",
  "zone": "us-central1-f"
}

$ curl ${VIP} -H "host: where.example.com" -H "version: v2"
{
  "cluster_name": "sample-cluster",
  "gce_instance_id": "*******************",
  "gce_service_account": "*******************.svc.id.goog",
  "host_header": "where.example.com",
  "metadata": "whereami-v2",
  "pod_name": "whereami-v2-7cf74dd8fc-fvg74",
  "pod_name_emoji": "*****",
  "project_id": "*******************",
  "timestamp": "2023-01-22T06:10:19",
  "zone": "us-central1-c"
}

上記ではまず、v1 と v2 の 2 つの API を提供するサンプルアプリケーション whereami を展開しました。
その後、先ほどクラスタに展開した Gateway servicemesh-cloud-gw を参照する HTTPRoute リソースを展開しました。
展開した HTTPRoute リソースは、ヘッダの有無でルーティング先を変える HTTPRouteMatch のルールを持っており、これに従ってそれぞれの API にアクセスできることが curl によるテストの結果からわかります。

なお、HTTPRoute は Istio の VirtualService に似ていますが、VirtualService がすべてのプロトコルをひとつのリソース内で構成していたのに対し、Gateway API では HTTPRouteTCPRoute のようにプロトコル毎に別々のリソースを利用する必要があります。

※ GatewayClass のステータスに関する補足

今回展開した GatewayClass asm-l7-gxlb の ACCEPTED のステータスは、展開から一定時間が経過した後も Unknown のままとなっていました。
Gateway API の Status を定義している GEP-1364 をざっと見たかぎりでは、ACCEPTED はコントローラによるバリデーションを通じて対象のリソースが正常に受け入れ可能と判定されると True になる、という定義のようです。
が、この GEP-1364 が取り込まれた Gateway API v0.6.0 は昨年末にリリースされたばかりという状況であるため、Service Mesh Cloud Gateway としては未対応という可能性はありそうです。

Service Mesh Cloud Gateway 特有の注意点

さて、ここまではほぼ公式ドキュメントに沿った流れでしたが、以降では Service Mesh Cloud Gateway の設定を色々と試していくなかで得られた知見をひとつご紹介したいと思います。

Kubernetes Gateway API のいち実装である Service Mesh Cloud Gateway は、基本的にはオープンソースGateway API の仕様に沿った挙動となっています。
が、現状の Service Mesh Cloud Gateway には、Gateway API の仕様に準拠していない挙動が一部含まれており、公式ドキュメントでは以下のように記載されています。

Route matching logic does not follow Gateway API specifications and matches in the order of the HTTPRoute instead.
Configure external HTTP(S) Load Balancing for managed Anthos Service Mesh  |  Google Cloud

直訳すると「ルートマッチングロジックは Gateway API の仕様に従わず、代わりに HTTPRoute の順序でマッチングする」という感じになるかと思いますが、この但し書きの意味を実際の挙動から確認してみましょう。

$ curl ${VIP} -H "host: where.example.com"
{
  "cluster_name": "sample-cluster",
  "gce_instance_id": "*******************",
  "gce_service_account": "*******************.svc.id.goog",
  "host_header": "where.example.com",
  "metadata": "whereami-v1",
  "pod_name": "whereami-v1-768bf6449c-c87sz",
  "pod_name_emoji": "*****",
  "project_id": "*******************",
kind: Gateway
  "timestamp": "2023-01-22T06:47:02",
  "zone": "us-central1-f"
}

$ cat gateway.yaml                                                                             
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
  name: servicemesh-cloud-gw
  namespace: istio-ingress
  labels:
    istio-injection: enabled
spec:
  gatewayClassName: asm-l7-gxlb
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    allowedRoutes:
      namespaces:
        from: All

$ curl ${VIP} -H "host: where.example.com"
{
  "cluster_name": "sample-cluster",
  "gce_instance_id": "*******************",
  "gce_service_account": "*******************.svc.id.goog",
  "host_header": "where.example.com",
  "metadata": "whereami-v1",
  "pod_name": "whereami-v1-768bf6449c-gql2x",
  "pod_name_emoji": "*****",
  "project_id": "*******************",
  "timestamp": "2023-01-22T06:47:08",
  "zone": "us-central1-a"
}

$ vim gateway.yaml

$ cat gateway.yaml 
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
  name: servicemesh-cloud-gw
  namespace: istio-ingress
  labels:
    istio-injection: enabled
spec:
  gatewayClassName: asm-l7-gxlb
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    hostname: "where.example.com"
    allowedRoutes:
      namespaces:
        from: All

$ kubectl apply -f gateway.yaml
gateway.gateway.networking.k8s.io/servicemesh-cloud-gw configured

$ curl ${VIP} -H "host: where.example.com"
<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>502 Server Error</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Server Error</h1>
<h2>The server encountered a temporary error and could not complete your request.<p>Please try again in 30 seconds.</h2>
<h2></h2>
</body></html>

先ほどの Gateway servicemesh-cloud-gw の Listener に hostname を追加したところ、サンプルアプリケーションにルーティングされなくなってしまいました。
Listener にも hostname が付与されている場合の挙動を Gateway API ではどのように定義しているのか、HTTPRoute のリファレンスを確認してみましょう。

A Listener with test.example.com as the hostname matches HTTPRoutes that have either not specified any hostnames, or have specified at least one of test.example.com or *.example.com.
API specification - Kubernetes Gateway API

Gateway が受けたリクエストを処理する HTTPRoute の候補は複数存在する可能性があるため、どの Gateway が受けたリクエストをどの HTTPRoute に従ってルーティングするかを判断する必要がある、という事がわかります。
リクエストの HTTP ホストヘッダは Gateway に定義された Listener の hostname ないし HTTPRoute の hostname と照合されますが、Listener と HTTPRoute の両方に hostname が定義されている場合は、両方の指定したホスト名の条件に合致した HTTPRoute にリクエストを処理させる、という仕様になっているようです。
そして、仕様どおりであれば、Gateway の Listener に HTTPRoute と同じホスト名 'where.example.com' を追加しても Listener と HTTPRoute のマッチングには問題ないはずです(Google Cloud の公式ドキュメントでは、この Listener と HTTPRoute を結びつけるロジックのことをルートマッチングロジックと言っているようですね)。 しかし、ワイルドカードを含むいくつかのパターンを試した限りでは、Service Mesh Cloud Gateway の Listener に hostname を追加すると HTTPRoute とのマッチングが行えなくなっていました。

仕様と挙動の比較からの推測にはなりますが、「ルートマッチングロジックは Gateway API の仕様に従わず、代わりに HTTPRoute の順序でマッチングする」という説明の意味は、「現状の Service Mesh Cloud Gateway は、HTTPRoute の hostname にのみに従ってリクエストを処理する HTTPRoute を選択するため、Listener にホスト名を指定すると期待した動作にならない(Gateway API の仕様とは異なる挙動になる)」ということのようです。

実際の利用シーンでは Gateway の Listener にワイルドカードを使ったホスト名を指定しておき、HTTPRoute で特定のホスト名ごとに詳細な(例えば API バージョン毎の)ルーティングを行う、といった使い方も想定されるかと思いますが、現状は制約があると考えておくと良いでしょう。
なお、公式ドキュメントに記載のとおり、この挙動はいずれ Gateway API の仕様に沿うよう修正される見込みとのことなので、 Service Mesh Cloud Gateway が GA されるまでには Gateway API の仕様に準拠するのではないかと期待されます。

まとめ

Managed ASM の新機能 Service Mesh Cloud Gateway を試しました。
現状はプレビューゆえに複数の制約があり、利用シーンは限られていますが、今後より簡単に Istio メッシュと Google Cloud の橋渡し部分を設定してゆくことができるようになってゆきそうです。
ルートマッチングロジックの挙動については、あくまでプレビューリリースにおける一時的なものなので将来的には気にする必要がなくなるかと思いますが、私個人にとっては検証の過程で Service Mesh Cloud Gateway の実装や Gateway API の仕様の理解を深める良い機会になりました。

Managed Anthos Service Mesh の Proxy Injection の仕組み

(Qiita Advent Calendar - GCP(Google Cloud Platform) Advent Calendar 2022 の 5 日目の記事です)

Google Cloud が提供している Managed Anthos Service Mesh(以下、Managed ASM)は、Istio をベースとしたサービスメッシュのマネージドサービスです。
最近は、Managed Data Plane という Istio-proxy の自動アップグレードオプションも登場してきています。

今回は、Managed ASM や Managed Data Plane がどのようにバージョンをコントロールしているのか、GKE に展開されているカスタムリソースから分かる情報や、環境を構築してみて得られた知見をいくつかご紹介します。
当記事内の情報は公式ドキュメントに仕様として明記されているものではなく、あくまで実機のカスタムリソースの定義などからいちユーザが推測したものとなりますのでご承知おきください。
そもそもマネージドであるがゆえ、できる限りユーザがその挙動を知らなくても良いようにしてくれているものではあるのですが、裏で動いている仕組みを推測・理解しておくことでトラブルシューティングなどの際に役立つこともあるかと思います。

GKE Autopilot + Managed ASM + Managed Data Plane による Kubernetes, Istio 環境をセットアップし、アプリケーションの Pod 起動時に ASM の Data Plane として Istio-proxy が自動インジェクションされるところまでを 前回記事 でご紹介していますので、ご興味のある方はこちらもご参照ください。

おさらい

前回記事の最後では、Istio-proxy の自動インジェクションを Namespace へのラベル付与で有効化し、サンプルアプリケーションとして httpd の Pod を起動した結果、ASM の Data Plane である Istio-proxy がサイドカーとして Pod に挿入されていることを確認しました。
以下のとおり、コンテナイメージの情報を取得してみると、インジェクションされた Istio-proxy のバージョンは 1.14.5-asm.3 だとわかります。

$ kubectl get pod httpd -o jsonpath='{.spec.containers[*].image}' | tr ' ' '\n'
httpd
gcr.io/gke-release/asm/proxyv2:1.14.5-asm.3

Istio-proxy のバージョンの決定

Managed ASM の場合、公式ドキュメントによれば、Istio-proxy のバージョンは ASM のリリースチャネルに沿って決定されるとあります。
Managed ASM のリリースチャネルは、GKE のリリースチャネルとは独立していますが、似たようなコンセプトで Rapid, Regular, Stable の 3 種類のチャネルが提供されています。

cloud.google.com

記事執筆時点では、Regular Channel のバージョンは ASM 1.14 となっていました。
なお、ASM のセマンティックバージョニング部分の表記は基本的に Istio のバージョンと一致しています。
Istio のバージョンに -asm**サフィックスを付与したものが ASM の完全なバージョン表記となります。

上記リンクで紹介したように、各リリースチャネルに対応した ASM のバージョンはドキュメントから確認することができますが、挿入されるはずのバージョンを実機から確認したいというケースもあるでしょう。
各チャネルに対応する現在のバージョンは、以下のコマンドを実行することで実機から確認することができます。

$ kubectl get dataplanecontrols.mesh.cloud.google.com -n istio-system
NAME                       REVISION             VERSION        TARGETBASISPOINTS   STATUS   AGE
asm-managed-9kxsf          asm-managed          1.14.5-asm.6   10000               Ready    3d1h
asm-managed-rapid-7zclm    asm-managed-rapid    1.15.3-asm.1   10000               Ready    3d1h
asm-managed-stable-9wss4   asm-managed-stable   1.13.9-asm.2   10000               Ready    3d1h

$ kubectl describe dataplanecontrols.mesh.cloud.google.com asm-managed-9kxsf
Name:         asm-managed-9kxsf
Namespace:
Labels:       <none>
Annotations:  <none>
API Version:  mesh.cloud.google.com/v1alpha1
Kind:         DataPlaneControl
Metadata:
  Creation Timestamp:  2022-11-26T12:29:07Z
  Generate Name:       asm-managed-
  Generation:          1
  Managed Fields:
    API Version:  mesh.cloud.google.com/v1alpha1
    Fields Type:  FieldsV1
    fieldsV1:
      f:metadata:
        f:generateName:
      f:spec:
        .:
        f:proxyTargetBasisPoints:
        f:proxyVersion:
        f:revision:
    Manager:      Google-GKEHub-Controllers-Servicemesh
    Operation:    Update
    Time:         2022-11-26T12:29:07Z
    API Version:  mesh.cloud.google.com/v1alpha1
    Fields Type:  FieldsV1
    fieldsV1:
      f:status:
        .:
        f:observedGeneration:
        f:proxyMetrics:
        f:proxyTargetBasisPoints:
        f:state:
    Manager:         mdp
    Operation:       Update
    Subresource:     status
    Time:            2022-11-26T12:32:15Z
  Resource Version:  28883
  UID:               ed9323f2-a420-4bc3-976a-13c70fd0d906
Spec:
  Proxy Target Basis Points:  10000
  Proxy Version:              1.14.5-asm.6
  Revision:                   asm-managed
Status:
  Observed Generation:  1
  Proxy Metrics:
  Proxy Target Basis Points:  10000
  State:                      Ready
Events:                       <none>

dataplanecontrols は、ASM のために Google が提供しているカスタムリソースです。
metadata.managedFields を見る限り、各リリースチャネルの ASM バージョンは Google-GKEHub-Controllers-Servicemesh というサービスが更新しているようですね。
他にも、mdp(Managed Data Plane の略でしょうか)というサービスがこのリソースを共同所有していることがわかります。

デフォルトでは、Regular Channel を示す asm-managed のバージョンが適用されるため、1.14.5 の istio-proxy が挿入されたことがわかります。
ASM 固有のリビジョンまで細かく見ていくと、httpd に挿入されている Istio-proxy のバージョンが 1.14.5-asm.3 であるのに対し、dataplanecontrols の asm-managed は 1.14.5-asm.6 となっています。
httpd の Pod をデプロイしてから asm-managed のリビジョンに更新が入ったのかとも考えましたが、新たに別の Pod を展開しても 1.14.5-asm.3 の Istio-proxy が挿入されました。

$ kubectl get dataplanecontrols.mesh.cloud.google.com -n istio-system
NAME                       REVISION             VERSION        TARGETBASISPOINTS   STATUS   AGE
asm-managed-9kxsf          asm-managed          1.14.5-asm.6   10000               Ready    3d1h
asm-managed-rapid-7zclm    asm-managed-rapid    1.15.3-asm.1   10000               Ready    3d1h
asm-managed-stable-9wss4   asm-managed-stable   1.13.9-asm.2   10000               Ready    3d1h

$ kubectl run httpd-alt --image httpd
Warning: Autopilot set default resource requests on Pod default/httpd-alt for container httpd-alt, as resource requests were not specified, and adjusted resource requests to meet requirements. See http://g.co/gke/autopilot-defaults and http://g.co/gke/autopilot-resources.
pod/httpd-alt created

$ kubectl get pod
NAME        READY   STATUS    RESTARTS   AGE
httpd       2/2     Running   0          12h
httpd-alt   2/2     Running   0          2m27s

$ kubectl get pod httpd-alt -o jsonpath='{.spec.containers[*].image}' | tr ' ' '\n'
httpd
gcr.io/gke-release/asm/proxyv2:1.14.5-asm.3

インジェクション後にマネージドコントロールプレーンのバージョンが上がった場合は、「マネージド コントロール プレーンがアップグレードされてから 1〜2 週間後に完了します」と公式ドキュメントに記載があるため、一時的にバージョンが一致しない期間ができるとは想定していました。
一方で、新規にインジェクションする場合は基本的に dataplanecontrols の Proxy Version が適用されるものと想像していたため、これは意外な結果となりました。

上記の結果からすると、どうやら dataplanecontrols のリソースに定義されているバージョンは、実際に自動でインジェクションされる Istio-proxy のバージョンとは完全には連動していないようです。
あるいは、ASM 固有のリビジョンでは更新がかからず、Istio のマイナーバージョンやパッチバージョンに変更が発生した場合にのみ更新がかかる、という条件になっているのかもしれません。

cloud.google.com

ASM における Proxy Injection の実装先

では、実際に Istio-procy をインジェクションする際のバージョンを決定する仕組みはどこに実装されているのでしょうか。
自動インジェクションのメカニズムは Mutating Webhook によって実装されているため、Webhook の定義を確認してみましょう。
Webhook の名前からして、istio-revision-tag-default というリソースがそれらしいと推測できます。

$ kubectl get mutatingwebhookconfigurations.admissionregistration.k8s.io
NAME                                                                  WEBHOOKS   AGE
admissionwebhookcontroller.config.common-webhooks.networking.gke.io   1          3d2h
filestorecsi-mutation-webhook.storage.k8s.io                          1          3d2h
gke-vpa-webhook-config                                                1          3d2h
istio-revision-tag-default                                            4          3d1h
istiod-asm-managed                                                    2          3d1h
mutate-scheduler-profile.config.common-webhooks.networking.gke.io     1          3d2h
neg-annotation.config.common-webhooks.networking.gke.io               1          3d2h
pod-ready.config.common-webhooks.networking.gke.io                    1          3d2h
sasecret-redacter.config.common-webhooks.networking.gke.io            1          3d2h
workload-defaulter.config.common-webhooks.networking.gke.io           1          3d2h

$ kubectl describe mutatingwebhookconfigurations.admissionregistration.k8s.io istio-revision-tag-default
Name:         istio-revision-tag-default
Namespace:
Labels:       app=sidecar-injector
              istio.io/owned-by=mesh.googleapis.com
              istio.io/rev=asm-managed
              istio.io/tag=default
Annotations:  <none>
API Version:  admissionregistration.k8s.io/v1
Kind:         MutatingWebhookConfiguration
Metadata:
  Creation Timestamp:  2022-11-26T12:31:43Z
  Generation:          1
  Managed Fields:
    API Version:  admissionregistration.k8s.io/v1
    Fields Type:  FieldsV1
    fieldsV1:
        ... (略) ...
    Manager:         Google-GKEHub-Controllers-Servicemesh
    Operation:       Update
    Time:            2022-11-26T12:31:43Z
  Resource Version:  28506
  UID:               ae84eef6-0371-4375-8979-2c268a513feb
Webhooks:
  Name:            rev.namespace.sidecar-injector.istio.io
  ... (略) ...
  Name:            rev.object.sidecar-injector.istio.io
  ... (略) ...
  Admission Review Versions:
    v1beta1
    v1
  Client Config:
    URL:           https://asm-autopilot-cluster-asm-managedtiwu6ybzqbglr-n354hllh2a-uc.a.run.app:443/inject/ISTIO_META_CLOUDRUN_ADDR/asm-autopilot-cluster-asm-managedtiwu6ybzqbglr-n354hllh2a-uc.a.run.app:443
  Failure Policy:  Fail
  Match Policy:    Equivalent
  Name:            namespace.sidecar-injector.istio.io
  Namespace Selector:
    Match Expressions:
      Key:       istio-injection
      Operator:  In
      Values:
        enabled
  Object Selector:
    Match Expressions:
      Key:       sidecar.istio.io/inject
      Operator:  NotIn
      Values:
        false
  Reinvocation Policy:  Never
  Rules:
    API Groups:

    API Versions:
      v1
    Operations:
      CREATE
    Resources:
      pods
    Scope:          *
  Side Effects:     None
  Timeout Seconds:  10
  ... (略) ...
  Name:            object.sidecar-injector.istio.io
  ... (略) ...
Events:             <none>

(今回はリビジョン指定なしの istio-injection ラベルを Namespace に付与して自動インジェクションを利用しているため、kubectl describe の結果は対応する Webhook の namespace.sidecar-injector.istio.io だけを抜粋しています)

各 webhook の Client Config の URL を見るに、Managed ASM の Istio-proxy の自動インジェクションは Cloud Run で実装されていることがわかります。
したがって、ASM の自動インジェクションはクラスタに展開された CRD と Webhook によって、Cloud Run を呼び出すことで Istio-proxy のバージョンを決定し、Pod にサイドカーとしてインジェクションしている、といった実装になっているものと考えられます。
dataplanecontrols の ASM のバージョン定義が更新されると、恐らく Cloud Run 側にも反映され、いずれ更新後のバージョンの Istio-proxy がインジェクションされるようになると思われますが、どうやら dataplanecontrols の更新の Cloud Run への反映はリアルタイムでは行われない実装になっていると思われます。

自動インジェクションの実装からわかる Managed ASM 利用時の注意点

上記の調査結果からわかる、ユーザが気をつけるべき点としては、自動インジェクション発生時に Cloud Run のコールドスタートの影響を受ける可能性がある、ということが挙げられます。
Webhook の呼び出し先の Cloud Run は一定時間呼び出しがなければインスタンスの数がゼロにスケールインすることができるため、下記のように自動インジェクションが失敗することで Pod の起動に失敗する可能性があります。
CI / CD パイプラインなど自動化の仕組みで Pod を展開する場合には、この事象が発生する可能性を考慮してリカバリの仕組みを備えておく必要がありそうです。

$ kubectl run httpd --image httpd
Error from server (InternalError): Internal error occurred: failed calling webhook "namespace.sidecar-injector.istio.io": failed to call webhook: Post "https://asm-autopilot-cluster-asm-managedtiwu6ybzqbglr-n354hllh2a-uc.a.run.app:443/inject/ISTIO_META_CLOUDRUN_ADDR/asm-autopilot-cluster-asm-managedtiwu6ybzqbglr-n354hllh2a-uc.a.run.app:443?timeout=10s": context deadline exceeded

なお、最近の Cloud Run には、startup CPU boost や Always on CPU など、コールドスタートの影響を低減あるいは解消するためのアップデートも出てきていますので、今後は無視できるレベルになる可能性も期待できそうですね。

GKE Autopilot 利用時の追加の注意点

今回の検証には GKE Autopilot を利用しました。
GKE Autopilot では、ゼロにスケールした(クラスタに割り当てられたノードが存在しない)状態でも Pod の作成リクエストが成功するため、リクエストを受け付けてから実際にノードが割り当てられるまでの時間が一定程度かかった場合、結果的に Pod のスケジューリング先がないものとして失敗してしまうケースがあります。
Managed ASM と併せて GKE Autopilot を利用する場合はこちらも注意が必要ですね。

$ kubectl run httpd --image httpd
Warning: Autopilot set default resource requests on Pod default/httpd for container httpd, as resource requests were not specified, and adjusted resource requests to meet requirements. See http://g.co/gke/autopilot-defaults and http://g.co/gke/autopilot-resources.
pod/httpd created

$ kubectl get po -w
NAME    READY   STATUS     RESTARTS   AGE
httpd   0/2     Init:0/1   0          11s
httpd   0/2     Init:0/1   0          25s
httpd   0/2     Init:Error   0          33s
httpd   0/2     Init:0/1     1 (6s ago)   35s
httpd   0/2     Terminating   1 (6s ago)   35s

$ kubectl get po
No resources found in default namespace.

まとめ

前回記事の補完的な位置付けで、Managed ASM の自動インジェクションの仕組みを調べました。
クラスタに展開されたカスタムリソースや Webhook の定義から仕組みを推測し、実機の調査を通じて、利用時にユーザが注意すべきいくつかのポイントをご紹介しました。
普段ユーザが考慮すべき設計・運用の要素を低減してくれるという点でとても便利なマネージドサービスですが、その仕様を実機の設定や挙動から把握しておくことで、より上手に使っていくことができるのではと思います。