Managed Anthos Service Mesh を Private Cluster で利用する

Google Cloud でマイクロサービスを実現する際、サービスメッシュが必要であれば Anthos Service Mesh (以下、ASM) はその有力な選択肢のひとつです。
今回は、Managed ASM を GKE の Private Cluster で利用する際の注意点についてご紹介したいと思います。

Managed Anthos Service Mesh

Managed ASM は、Istio サービスメッシュの Control Plane に Google が管理するマネージド Istiod を用いる、ASM の利用方法のひとつです。
従来から提供されてきた In-cluster Cotrol Plane の ASM と比べて、Istiod の運用や管理の必要がなくなります。
最近 ASM は Anthos API を有効化しなくても利用できるようになったため、Managed ASM も Standalone ASM として利用可能です。

関連する Google Cloud の中の人のブログは こちら

Managed ASM の概要は以下の図のようになります。
図の左下部の Managed Istiod の部分がユーザのクラスタに展開されない点が In-cluster との大きな違いです。

f:id:polar3130:20211026132503p:plain

ASM のインストールには従来 install_asm というインストール補助スクリプトが提供されてきましたが、後継として asmcli というコマンドラインツールが最近 GA になりました。
今回はインストールに asmcli を利用します。

Private Cluster

Private Cluster (限定公開クラスタとも呼ばれます) のクラスタエンドポイントへのアクセスは以下の 3 通りの構成方法 があります。

  • Private Endpoint のみ (Public Endpoint が無効)
  • Public Endpoint が有効、かつ Master Authorized Networks が無効
  • Public Endpoint が有効、かつ Master Authorized Networks が有効

Master Authorized Networks (マスター承認済みネットワークとも呼ばれます) は Public Endpoint を有効化した場合に利用できる、エンドポイントへのアクセス元を限定するための機能です。
Master Authorized Networks を設定することで、外部 IP アドレスとノードおよび Pod 以外の内部 IP アドレスからクラスタエンドポイントへの (クライアントからの) アクセスを限定することができます。
以下の図は Public Endpoint と Master Authorized Networks を有効化した Private Cluster を示しています。

f:id:polar3130:20211026132738p:plain

上記の 3 つの構成方法のうち、Managed ASM で利用可能な Private Cluster の選択肢は Public Endpoint が有効な構成に限られます。

パブリック エンドポイント アクセスを備えた GKE 限定公開クラスタ。Master Authorized Network (MAN) が有効かどうかは関係ありません ... (中略) ... Google が管理するコントロール プレーンでは、パブリック エンドポイント アクセスを有効にする必要があります。これは、パブリック エンドポイントに外部からアクセスできることを意味するわけではありません。

引用元:https://cloud.google.com/service-mesh/docs/supported-features-mcp#environments

Private Cluster における Managed ASM のセットアップと注意点

以降では、Managed ASM を Private Cluster で利用するためのセットアップの流れを示しながら、注意すべきポイントをいくつかご紹介します。

検証環境のバージョン

以降の動作検証には下記バージョンの GKE, ASM を利用しました。

  • GKE : 1.21.4-gke.2300 (cos_containerd)
  • Managed ASM : Regular Channel (1.10.4-asm.14)
  • asmcli : 1.11.2-asm.17+config2

クラスタエンドポイントとアクセス経路

まず、Public Endpoint と Master Authorized Networks を有効にした Private Cluster を用意しました。
クラスタが Managed ASM を利用できる状態にセットアップするためには、asmcli を実行するインスタンス/端末がどこかに必要です。
今回は asmcli の挙動を説明する目的から、Private Cluster と同じ VPC 上に立ち上げた GCE を Bastion サーバとして利用します。
また、設定の簡略化のため、ノードと同じ内部 IP アドレスレンジ内にインスタンスを作成しています。
(Public Endpoint が有効なクラスタであっても、クライアントアクセス元が外部 IP アドレスになるとは限りません。プロジェクトのセキュリティ要件などによりますが、Public Endpoint は別の目的で有効化するものの、ASM のセットアップは同一 VPC 内から行いたい、というケースはあると思います)

$ gcloud container clusters describe temp-cluster --region us-central1 
addonsConfig:
  dnsCacheConfig: {}
  gcePersistentDiskCsiDriverConfig:
    enabled: true
  horizontalPodAutoscaling: {}
  httpLoadBalancing: {}
  kubernetesDashboard:
    disabled: true
  networkPolicyConfig: {}
authenticatorGroupsConfig: {}
autoscaling:
  autoscalingProfile: BALANCED
clusterIpv4Cidr: 10.60.0.0/14
createTime: '2021-10-25T09:18:51+00:00'
currentMasterVersion: 1.21.4-gke.2300
currentNodeCount: 4
currentNodeVersion: 1.21.4-gke.2300
databaseEncryption:
  state: DECRYPTED
defaultMaxPodsConstraint:
  maxPodsPerNode: '110'
endpoint: ***************
id: ec9882ed***************
initialClusterVersion: 1.21.4-gke.2300
instanceGroupUrls:
  - https://www.googleapis.com/compute/v1/projects/***************/zones/us-central1-a/instanceGroupManagers/***************
  - https://www.googleapis.com/compute/v1/projects/***************/zones/us-central1-b/instanceGroupManagers/***************
  - https://www.googleapis.com/compute/v1/projects/***************/zones/us-central1-c/instanceGroupManagers/***************
  - https://www.googleapis.com/compute/v1/projects/***************/zones/us-central1-f/instanceGroupManagers/***************
ipAllocationPolicy:
  clusterIpv4Cidr: 10.60.0.0/14
  clusterIpv4CidrBlock: 10.60.0.0/14
  clusterSecondaryRangeName: gke-temp-cluster-pods-ec9882ed
  servicesIpv4Cidr: 10.64.0.0/20
  servicesIpv4CidrBlock: 10.64.0.0/20
  servicesSecondaryRangeName: gke-temp-cluster-services-ec9882ed
  useIpAliases: true
labelFingerprint: ***************
legacyAbac: {}
location: us-central1
locations:
- us-central1-a
- us-central1-b
- us-central1-c
- us-central1-f
loggingConfig:
  componentConfig:
    enableComponents:
    - SYSTEM_COMPONENTS
    - WORKLOADS
loggingService: logging.googleapis.com/kubernetes
maintenancePolicy:
  resourceVersion: e3b0c442
masterAuth:
  clusterCaCertificate: ***************
masterAuthorizedNetworksConfig:
  enabled: true
monitoringConfig:
  componentConfig:
    enableComponents:
    - SYSTEM_COMPONENTS
monitoringService: monitoring.googleapis.com/kubernetes
name: temp-cluster
network: default
networkConfig:
  datapathProvider: LEGACY_DATAPATH
  defaultSnatStatus: {}
  network: projects/***************/global/networks/default
  subnetwork: projects/***************/regions/us-central1/subnetworks/default
networkPolicy:
  enabled: true
  provider: CALICO
nodeConfig:
  diskSizeGb: 100
  diskType: pd-standard
  imageType: COS_CONTAINERD
  machineType: e2-standard-2
  metadata:
    disable-legacy-endpoints: 'true'
  oauthScopes:
  - https://www.googleapis.com/auth/devstorage.read_only
  - https://www.googleapis.com/auth/logging.write
  - https://www.googleapis.com/auth/monitoring
  - https://www.googleapis.com/auth/servicecontrol
  - https://www.googleapis.com/auth/service.management.readonly
  - https://www.googleapis.com/auth/trace.append
  serviceAccount: default
  shieldedInstanceConfig:
    enableIntegrityMonitoring: true
  workloadMetadataConfig:
    mode: GKE_METADATA
nodePools:
- autoscaling: {}
  config:
    diskSizeGb: 100
    diskType: pd-standard
    imageType: COS_CONTAINERD
    machineType: e2-standard-2
    metadata:
      disable-legacy-endpoints: 'true'
    oauthScopes:
    - https://www.googleapis.com/auth/devstorage.read_only
    - https://www.googleapis.com/auth/logging.write
    - https://www.googleapis.com/auth/monitoring
    - https://www.googleapis.com/auth/servicecontrol
    - https://www.googleapis.com/auth/service.management.readonly
    - https://www.googleapis.com/auth/trace.append
    serviceAccount: default
    shieldedInstanceConfig:
      enableIntegrityMonitoring: true
    workloadMetadataConfig:
      mode: GKE_METADATA
  initialNodeCount: 1
  instanceGroupUrls:
  - https://www.googleapis.com/compute/v1/projects/***************/zones/us-central1-a/instanceGroupManagers/***************
  - https://www.googleapis.com/compute/v1/projects/***************/zones/us-central1-b/instanceGroupManagers/***************
  - https://www.googleapis.com/compute/v1/projects/***************/zones/us-central1-c/instanceGroupManagers/***************
  - https://www.googleapis.com/compute/v1/projects/***************/zones/us-central1-f/instanceGroupManagers/***************
  locations:
  - us-central1-a
  - us-central1-b
  - us-central1-c
  - us-central1-f
  management:
    autoRepair: true
  maxPodsConstraint:
    maxPodsPerNode: '110'
  name: default-pool
  networkConfig:
    podIpv4CidrBlock: 10.60.0.0/14
    podRange: gke-temp-cluster-pods-ec9882ed
  podIpv4CidrSize: 24
  selfLink: https://container.googleapis.com/v1/projects/***************/locations/us-central1/clusters/temp-cluster/nodePools/default-pool
  status: RUNNING
  upgradeSettings:
    maxSurge: 1
  version: 1.21.4-gke.2300
notificationConfig:
  pubsub: {}
privateClusterConfig:
  enablePrivateNodes: true
  masterIpv4CidrBlock: 192.168.1.0/28
  peeringName: gke-***************-peer
  privateEndpoint: 192.168.1.2
  publicEndpoint: ***************
releaseChannel: {}
selfLink: https://container.googleapis.com/v1/projects/***************/locations/us-central1/clusters/temp-cluster
servicesIpv4Cidr: 10.64.0.0/20
shieldedNodes:
  enabled: true
status: RUNNING
subnetwork: default
workloadIdentityConfig:
  workloadPool: ***************.svc.id.goog
zone: us-central1

今のところ、このクラスタは Master Authorized Networks を有効化したものの、何のネットワークも登録していない (許可していない) 状態です。
したがって、Public Endpoint は有効化されているものの、Public Endpoint へのクライアントアクセスは一切許可していません。
一方、Private Endpoint はクラスタの Control Plane と VPC Peering しているノードのネットワーク上から引き続きアクセス可能な状態です (Bastion サーバから Private Endpoint に対してはネットワークの到達性が確保されているものとします) 。
ここで、クラスタへのクレデンシャルを取得し、kubectl を投げてみるとどうなるでしょうか。

$ gcloud container clusters get-credentials temp-cluster
Fetching cluster endpoint and auth data.
kubeconfig entry generated for temp-cluster.

$ kubectl get nodes
(応答なし)

タイムアウトしました。
これは、対象のクラスタが Public Endpoint の有効な Private Cluster である場合に、gcloud container clusters get-credentials コマンドが kubeconfig に書き込むエントリのクラスタエンドポイントは Public Endpoint となるためです。

ドキュメントには下記のように記載されています。

パブリック IP アドレスを持つマシンは、パブリック IP アドレスが承認済みネットワークのリストに含まれている場合にのみ、kubectl を使用してパブリック エンドポイントと通信できます。これには、Google Cloud の外のマシンと外部 IP アドレスを持つ Google Cloud VM が含まれます。

引用元:https://cloud.google.com/kubernetes-engine/docs/concepts/private-cluster-concept#overview

実際に kubeconfig を確認してみると、以下のようになっています。

$ cat ./.kube/config
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: ***************
    server: https://(クラスタの Public Endpoint の IP アドレス)
  name: gke_***************_us-central1_temp-cluster
contexts:
- context:
    cluster: gke_***************_us-central1_temp-cluster
    user: gke_***************_us-central1_temp-cluster
  name: gke_***************_us-central1_temp-cluster
current-context: gke_***************_us-central1_temp-cluster
kind: Config
preferences: {}
users:
- name: gke_***************_us-central1_temp-cluster
  user:
    auth-provider:
      config:
        access-token: ***************
        cmd-args: config config-helper --format=json
        cmd-path: /usr/lib/google-cloud-sdk/bin/gcloud
        expiry: "2021-10-26T01:34:39Z"
        expiry-key: '{.credential.token_expiry}'
        token-key: '{.credential.access_token}'
      name: gcp

したがって、Private Endpoint 経由でクラスタにアクセスする場合は、kubeconfig のエントリを Private Cluster の内部 IP アドレスを使用したものに書き換える必要があります。
これは gcloud container clusters get-credentials に internal-ip オプションを付与することで実現できます。

$ gcloud container clusters get-credentials --internal-ip temp-cluster
Fetching cluster endpoint and auth data.
kubeconfig entry generated for temp-cluster.

$ cat ./.kube/config
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: ***************
    server: https://192.168.1.2
  name: gke_***************_us-central1_temp-cluster
contexts:
- context:
    cluster: gke_***************_us-central1_temp-cluster
    user: gke_***************_us-central1_temp-cluster
  name: gke_***************_us-central1_temp-cluster
current-context: gke_***************_us-central1_temp-cluster
kind: Config
preferences: {}
users:
- name: gke_***************_us-central1_temp-cluster
  user:
    auth-provider:
      config:
        cmd-args: config config-helper --format=json
        cmd-path: /usr/lib/google-cloud-sdk/bin/gcloud
        expiry-key: '{.credential.token_expiry}'
        token-key: '{.credential.access_token}'
      name: gcp

$ kubectl get nodes
NAME                                          STATUS   ROLES    AGE   VERSION
gke-temp-cluster-default-pool-1d81231b-tsck   Ready    <none>   15h   v1.21.4-gke.2300
gke-temp-cluster-default-pool-b5dc41dd-xb6n   Ready    <none>   15h   v1.21.4-gke.2300
gke-temp-cluster-default-pool-ccc654f7-b0qj   Ready    <none>   15h   v1.21.4-gke.2300
gke-temp-cluster-default-pool-d4cae408-2sfz   Ready    <none>   15h   v1.21.4-gke.2300

これで VPC 内の Bastion からクラスタエンドポイントに対して kubectl でアクセスすることができました。

asmcli によるセットアップ

ではいよいよ asmcli install コマンドで Managed ASM を構成してみます。

$ ./asmcli install \
>         -p *************** \
>         -l us-central1 \
>         -n temp-cluster \
>         --managed \
>         --output_dir temp-cluster \
>         --enable-all
asmcli: Setting up necessary files...
asmcli: Using /root/temp-cluster/asm_kubeconfig as the kubeconfig...
asmcli: Checking installation tool dependencies...
asmcli: Fetching/writing GCP credentials to kubeconfig file...
asmcli: [WARNING]: nc not found, skipping k8s connection verification
asmcli: [WARNING]: (Installation will continue normally.)
asmcli: Getting account information...
asmcli: Verifying cluster registration.
asmcli: Enabling required APIs...
asmcli: Enabling the service mesh feature...
asmcli: Verifying cluster registration.
asmcli: Registering the cluster as temp-cluster...
asmcli: [WARNING]: Failed, retrying...(1 of 2)
asmcli: [WARNING]: Failed, retrying...(2 of 2)
asmcli: [WARNING]: Command 'run_command gcloud container hub memberships register temp-cluster --project=*************** --enable-workload-identity --gke-uri=https://container.googleapis.com/v1/projects/***************/locations/us-central1/clusters/temp-cluster' failed.

Verifying cluster registration. の途中で asmcli によるセットアップが中断されてしまいました。
この様子から、先程の kubeconfig の設定とアクセス経路の関係に鑑み、「asmcli コマンドもまた、クラスタの Public Endpoint にアクセスしようとしているのではないか」という仮説が立てられます。
この答え合わせをするには asmcli install コマンドに verbose オプションを付与します。

$ ./asmcli install \
>         -p *************** \
>         -l us-central1 \
>         -n temp-cluster \
>         --managed \
>         --verbose \
>         --output_dir temp-cluster \
>         --enable-all
asmcli: Setting up necessary files...
asmcli: Using /root/temp-cluster/asm_kubeconfig as the kubeconfig...
asmcli: Checking installation tool dependencies...
asmcli: Fetching/writing GCP credentials to kubeconfig file...
asmcli: Running: '/usr/bin/gcloud container clusters get-credentials temp-cluster --project=*************** --zone=us-central1'
asmcli: -------------
Fetching cluster endpoint and auth data.
kubeconfig entry generated for temp-cluster.
asmcli: Running: '/usr/bin/kubectl --kubeconfig /root/temp-cluster/asm_kubeconfig config current-context'
asmcli: -------------
asmcli: [WARNING]: nc not found, skipping k8s connection verification
asmcli: [WARNING]: (Installation will continue normally.)
asmcli: Getting account information...
asmcli: Running: '/usr/bin/gcloud auth list --project=*************** --filter=status:ACTIVE --format=value(account)'
asmcli: -------------
asmcli: Running: '/usr/bin/gcloud config get-value auth/impersonate_service_account'
asmcli: -------------
(unset)
asmcli: Running: '/usr/bin/gcloud container clusters list --project=*************** --filter=name = temp-cluster AND location = us-central1 --format=value(name)'
asmcli: -------------
asmcli: Running: '/root/temp-cluster/kpt version'
asmcli: -------------
asmcli: Verifying cluster registration.
asmcli: Enabling required APIs...
asmcli: Running: '/usr/bin/gcloud services enable --project=*************** container.googleapis.com monitoring.googleapis.com logging.googleapis.com cloudtrace.googleapis.com meshconfig.googleapis.com iamcredentials.googleapis.com gkeconnect.googleapis.com gkehub.googleapis.com cloudresourcemanager.googleapis.com stackdriver.googleapis.com meshca.googleapis.com'
asmcli: -------------
Operation "operations/acf.p2-***************-9134cd80-9677-479a-90ab-f315e7df61f5" finished successfully.
asmcli: Running: '/usr/bin/gcloud container clusters describe --project=*************** --region us-central1 temp-cluster --format=json'
asmcli: -------------
asmcli: Running: '/usr/bin/gcloud container clusters describe --project=*************** --region us-central1 temp-cluster --format=json'
asmcli: -------------
asmcli: Enabling the service mesh feature...
asmcli: Running: 'gcloud beta container hub mesh enable --project=***************'
asmcli: -------------
asmcli: Running: '/usr/bin/gcloud beta container hub mesh enable --project=***************'
asmcli: -------------
Service Mesh Feature for project [***************] is already enabled
asmcli: Verifying cluster registration.
asmcli: Running: '/usr/bin/gcloud container clusters describe temp-cluster --zone=us-central1 --project=*************** --format=value(selfLink, network)'
asmcli: -------------
asmcli: Running: '/usr/bin/gcloud container hub memberships list --format=value(name) --project ***************'
asmcli: -------------
asmcli: Running: '/usr/bin/gcloud container hub memberships list --format=value(name) --project ***************'
asmcli: -------------
asmcli: Registering the cluster as temp-cluster...
asmcli: Running: 'gcloud container hub memberships register temp-cluster --project=*************** --enable-workload-identity --gke-uri=https://container.googleapis.com/v1/projects/***************/locations/us-central1/clusters/temp-cluster'
asmcli: -------------
asmcli: Running: '/usr/bin/gcloud container hub memberships register temp-cluster --project=*************** --enable-workload-identity --gke-uri=https://container.googleapis.com/v1/projects/***************/locations/us-central1/clusters/temp-cluster'
asmcli: -------------
kubeconfig entry generated for temp-cluster.
ERROR: (gcloud.container.hub.memberships.register) Failed to check if the user is a cluster-admin: W1026 01:27:41.354630   27359 transport.go:288] Unable to cancel request for *genericclioptions.CommandHeaderRoundTripper
Unable to connect to the server: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)

asmcli: [WARNING]: Failed, retrying...(1 of 2)
asmcli: Running: 'gcloud container hub memberships register temp-cluster --project=*************** --enable-workload-identity --gke-uri=https://container.googleapis.com/v1/projects/***************/locations/us-central1/clusters/temp-cluster'
asmcli: -------------
asmcli: Running: '/usr/bin/gcloud container hub memberships register temp-cluster --project=*************** --enable-workload-identity --gke-uri=https://container.googleapis.com/v1/projects/***************/locations/us-central1/clusters/temp-cluster'
asmcli: -------------
kubeconfig entry generated for temp-cluster.
ERROR: (gcloud.container.hub.memberships.register) Failed to check if the user is a cluster-admin: W1026 01:28:04.744751   27413 transport.go:288] Unable to cancel request for *genericclioptions.CommandHeaderRoundTripper
Unable to connect to the server: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)

asmcli: [WARNING]: Failed, retrying...(2 of 2)
asmcli: [WARNING]: Command 'run_command gcloud container hub memberships register temp-cluster --project=*************** --enable-workload-identity --gke-uri=https://container.googleapis.com/v1/projects/***************/locations/us-central1/clusters/temp-cluster' failed.

asmcli install コマンドは、その処理中に temp-cluster/asm_kubeconfig という kubeconfig を別途作成し、このファイルにクラスタの Public Endpoint が宛先となるエントリを作成していることがわかります。
gcloud コマンドで作成した kubeconfig のエントリは引き続き内部 IP アドレスを向いていますが、asmcli がクラスタのエンドポイントを選択する際には新しい kubeconfig を作成するため、asmcli は Public Endpoint に向けて通信を試み、Master Authorized Networks で許可されていないためにタイムアウトを起こしてしまう、というわけです。
このとき、asm_kubeconfig は以下のようになっていました。

$ cat /root/temp-cluster/asm_kubeconfig 
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: ***************
    server: https://(クラスタの Public Endpoint の IP アドレス)
  name: gke_***************_us-central1_temp-cluster
contexts:
- context:
    cluster: gke_***************_us-central1_temp-cluster
    user: gke_***************_us-central1_temp-cluster
  name: gke_***************_us-central1_temp-cluster
current-context: gke_***************_us-central1_temp-cluster
kind: Config
preferences: {}
users:
- name: gke_***************_us-central1_temp-cluster
  user:
    auth-provider:
      config:
        access-token: ***************
        cmd-args: config config-helper --format=json
        cmd-path: /usr/lib/google-cloud-sdk/bin/gcloud
        expiry: "2021-10-26T02:16:55Z"
        expiry-key: '{.credential.token_expiry}'
        token-key: '{.credential.access_token}'
      name: gcp

この問題は、asmcli install コマンド実行時に --kc | --kubeconfig オプションを付与し、gcloud コマンドで作成した内部 IP アドレス宛のエントリが含まれる kubeconfig を指定することで解決できます。
--kc | --kubeconfig オプションは本記事の執筆時点 (2021/10/26) では asmcli リファレンスのドキュメントに記載されていないコマンドオプションですが、実際にインストールした asmcli のコマンドオプションの一覧を表示させるとその存在を確認できます。

$ ./asmcli
asmcli 1.11.2-asm.17+config2
usage: asmcli [SUBCOMMAND] [OPTION]...

Set up, validate, and install ASM in a Google Cloud environment.
Use -h|--help with -v|--verbose to show detailed descriptions.

SUBCOMMANDS:
  install
  validate
  print-config
  create-mesh

OPTIONS:
  -l|--cluster_location  <LOCATION>
  -n|--cluster_name      <NAME>
  -p|--project_id        <ID>
  --kc|--kubeconfig      <KUBECONFIG_FILE>
  --ctx|--context        <CONTEXT>
  --fleet_id             <FLEET ID>
  --network_id           <NETWORK ID>
  -c|--ca                <CA>

  -o|--option            <FILE NAME>
  -s|--service_account   <ACCOUNT>
  -k|--key_file          <FILE PATH>
  -D|--output_dir        <DIR PATH>
  --co|--custom_overlay  <FILE NAME>

  --ca_cert              <FILE PATH>
  --ca_key               <FILE PATH>
  --root_cert            <FILE PATH>
  --cert_chain           <FILE PATH>
  --ca_pool              <CA POOL>
  -r|--revision_name     <REVISION NAME>
  --platform             <PLATFORM>
  --channel              <CHANNEL>

FLAGS:
  -e|--enable_all
     --enable_cluster_roles
     --enable_cluster_labels
     --enable_gcp_apis
     --enable_gcp_iam_roles
     --enable_gcp_components
     --enable_registration
     --enable_namespace_creation

     --managed

     --print_config
     --disable_canonical_service
  -v|--verbose
     --dry_run
     --only_validate
     --only_enable
  -h|--help
  --version

では kubeconfig オプションを付与して asmcli install コマンドを実行してみましょう。

$ ./asmcli install \
        --managed \
        --kubeconfig /root/.kube/config \
        --output_dir temp-cluster \
        --enable-all
asmcli: Reading cluster information for gke_***************_us-central1_temp-cluster
asmcli: Setting up necessary files...
asmcli: Using /root/.kube/config as the kubeconfig...
asmcli: Checking installation tool dependencies...
asmcli: [WARNING]: nc not found, skipping k8s connection verification
asmcli: [WARNING]: (Installation will continue normally.)
asmcli: Getting account information...
asmcli: Verifying cluster registration.
asmcli: Enabling required APIs...
asmcli: Enabling the service mesh feature...
asmcli: Verifying cluster registration.
asmcli: Registering the cluster as temp-cluster...
asmcli: [WARNING]: Failed, retrying...(1 of 2)
asmcli: [WARNING]: Failed, retrying...(2 of 2)
asmcli: [WARNING]: Command 'run_command gcloud container hub memberships register temp-cluster --project=*************** --enable-workload-identity --gke-uri=https://container.googleapis.com/v1/projects/***************/locations/us-central1/clusters/temp-cluster' failed.

kubeconfig は指定した内部 IP アドレス向けのエントリを持つものを参照したようですが、依然として asmcli install コマンドは失敗しています。
ここで、外形的な確認方法にはなりますが asmcli install コマンド実行中に tshark でパケットを拾ってみると、以下のような結果が得られました。

$ tshark -i ens4 -f "host (クラスタの Public Endpoint の IP アドレス)"
Capturing on 'ens4'
    1 0.000000000  10.128.0.17 → (クラスタの Public Endpoint の IP アドレス) TCP 74 48032 → 443 [SYN] Seq=0 Win=65320 Len=0 MSS=1420 SACK_PERM=1 TSval=2903931182 TSecr=0 WS=128
    2 1.007583122  10.128.0.17 → (クラスタの Public Endpoint の IP アドレス) TCP 74 [TCP Retransmission] 48032 → 443 [SYN] Seq=0 Win=65320 Len=0 MSS=1420 SACK_PERM=1 TSval=2903932189 TSecr=0 WS=128
    3 3.023571340  10.128.0.17 → (クラスタの Public Endpoint の IP アドレス) TCP 74 [TCP Retransmission] 48032 → 443 [SYN] Seq=0 Win=65320 Len=0 MSS=1420 SACK_PERM=1 TSval=2903934205 TSecr=0 WS=128
    4 7.247576382  10.128.0.17 → (クラスタの Public Endpoint の IP アドレス) TCP 74 [TCP Retransmission] 48032 → 443 [SYN] Seq=0 Win=65320 Len=0 MSS=1420 SACK_PERM=1 TSval=2903938428 TSecr=0 WS=128
    5 15.439576255  10.128.0.17 → (クラスタの Public Endpoint の IP アドレス) TCP 74 [TCP Retransmission] 48032 → 443 [SYN] Seq=0 Win=65320 Len=0 MSS=1420 SACK_PERM=1 TSval=2903946619 TSecr=0 WS=128
    6 23.381462419  10.128.0.17 → (クラスタの Public Endpoint の IP アドレス) TCP 74 48056 → 443 [SYN] Seq=0 Win=65320 Len=0 MSS=1420 SACK_PERM=1 TSval=2903954560 TSecr=0 WS=128
    7 24.399586137  10.128.0.17 → (クラスタの Public Endpoint の IP アドレス) TCP 74 [TCP Retransmission] 48056 → 443 [SYN] Seq=0 Win=65320 Len=0 MSS=1420 SACK_PERM=1 TSval=2903955578 TSecr=0 WS=128
    8 26.415605468  10.128.0.17 → (クラスタの Public Endpoint の IP アドレス) TCP 74 [TCP Retransmission] 48056 → 443 [SYN] Seq=0 Win=65320 Len=0 MSS=1420 SACK_PERM=1 TSval=2903957593 TSecr=0 WS=128
    9 30.543617550  10.128.0.17 → (クラスタの Public Endpoint の IP アドレス) TCP 74 [TCP Retransmission] 48056 → 443 [SYN] Seq=0 Win=65320 Len=0 MSS=1420 SACK_PERM=1 TSval=2903961721 TSecr=0 WS=128
   10 38.735588521  10.128.0.17 → (クラスタの Public Endpoint の IP アドレス) TCP 74 [TCP Retransmission] 48056 → 443 [SYN] Seq=0 Win=65320 Len=0 MSS=1420 SACK_PERM=1 TSval=2903969911 TSecr=0 WS=128
^C10 packets captured

Registering the cluster as temp-cluster. の処理中に、Public Endpoint へのアクセスが行われており、これによって asmcli がタイムアウトしていることがわかります。
この原因は先程とは別のところにあり、verbose オプションを付けて実行するとわかりますが Registering the cluster as temp-cluster. の処理中に asmcli は内部で gcloud container hub memberships register コマンドを実行しており、このときクラスタへのアクセスに Public Endpoint を用いていることがわかります。

... (略) ...
asmcli: -------------
asmcli: Registering the cluster as temp-cluster...
asmcli: Running: 'gcloud container hub memberships register temp-cluster --project=*************** --enable-workload-identity --gke-uri=https://container.googleapis.com/v1/projects/***************/locations/us-central1/clusters/temp-cluster'
asmcli: -------------
asmcli: Running: '/usr/bin/gcloud container hub memberships register temp-cluster --project=*************** --enable-workload-identity --gke-uri=https://container.googleapis.com/v1/projects/***************/locations/us-central1/clusters/temp-cluster'
asmcli: -------------
kubeconfig entry generated for temp-cluster.
ERROR: (gcloud.container.hub.memberships.register) Failed to check if the user is a cluster-admin: W1026 02:15:40.839850   32642 transport.go:288] Unable to cancel request for *genericclioptions.CommandHeaderRoundTripper
Unable to connect to the server: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)
asmcli: [WARNING]: Failed, retrying...(1 of 2)
... (略) ...

私が確認した限りでは、asmcli が内部で実行している gcloud container hub memberships register コマンドの引数をオプション等で変更することはできないように見受けられました。
対象が Public Endpoint の有効な Private Cluster である場合、asmcli は実行元インスタンス/端末がクラスタノードと同一の VPC 上にあったとしても Public Endpoint 経由でアクセスしようとする処理が含まれていることから、現状「Private Cluster に Private Endpoint 経由で Managed ASM をセットアップする」ことはできなさそうです。

なお、クラスタの Master Authorized Networks で Bastion の外部 IP アドレスを許可すれば、asmcli install コマンドの実行は問題なく成功します。

$ ./asmcli install \
        --managed \
        --kubeconfig /root/.kube/config \
        --output_dir temp-cluster \
        --enable-all
asmcli: Reading cluster information for gke_***************_us-central1_temp-cluster
asmcli: Setting up necessary files...
asmcli: Using /root/.kube/config as the kubeconfig...
asmcli: Checking installation tool dependencies...
asmcli: [WARNING]: nc not found, skipping k8s connection verification
asmcli: [WARNING]: (Installation will continue normally.)
asmcli: Getting account information...
asmcli: Verifying cluster registration.
asmcli: Enabling required APIs...
asmcli: Enabling the service mesh feature...
asmcli: Verifying cluster registration.
asmcli: Registering the cluster as temp-cluster...
asmcli: Checking for project ***************...
asmcli: Reading labels for us-central1/temp-cluster...
asmcli: Adding labels to us-central1/temp-cluster...
asmcli: Querying for core/account...
asmcli: Binding ***************@*************** to cluster admin role...
clusterrolebinding.rbac.authorization.k8s.io/***************-cluster-admin-binding created
asmcli: Creating istio-system namespace...
namespace/istio-system created
asmcli: Binding user:***************@*************** to required IAM roles...
asmcli: Initializing meshconfig managed API...
asmcli: Configuring kpt package...
asm/
set 16 field(s) of setter "gcloud.container.cluster" to value "temp-cluster"
asm/
set 20 field(s) of setter "gcloud.core.project" to value "***************"
asm/
set 2 field(s) of setter "gcloud.project.projectNumber" to value "***************"
asm/
set 16 field(s) of setter "gcloud.compute.location" to value "us-central1"
asm/
set 1 field(s) of setter "gcloud.compute.network" to value "***************-default"
asm/
set 3 field(s) of setter "gcloud.project.environProjectNumber" to value "***************"
asm/
set 2 field(s) of setter "anthos.servicemesh.rev" to value "asm-managed"
asm/
set 11 field(s) of setter "anthos.servicemesh.tag" to value "1.11.2-asm.17"
asm/
set 3 field(s) of setter "anthos.servicemesh.trustDomain" to value "***************.svc.id.goog"
asm/
set 1 field(s) of setter "anthos.servicemesh.tokenAudiences" to value "istio-ca,***************.svc.id.goog"
asm/
set 1 field(s) of setter "anthos.servicemesh.spiffeBundleEndpoints" to value "***************.svc.id.goog|https://storage.googleapis.com/mesh-ca-resources/spiffe_bundle.json"
asm/
set 2 field(s) of setter "anthos.servicemesh.idp-url" to value "https://container.googleapis.com/v1/projects/***************/locations/us-central1/clusters/temp-cluster"
asm/
set 3 field(s) of setter "anthos.servicemesh.trustDomainAliases" to value "***************.svc.id.goog"
namespace/istio-system labeled
asmcli: Provisioning control plane...
{}
asmcli: Using the following managed revision for validating webhook: asm-managed-rapid
asm/
set 1 field(s) of setter "anthos.servicemesh.controlplane.validation-url" to value "https://asm-temp-cluster-asm-managed-rapid-***************-uc.a.run.app:443/validate"
asm/
set 1 field(s) of setter "anthos.servicemesh.managed-controlplane.cloudrun-addr" to value "asm-temp-cluster-asm-managed-rapid-***************-uc.a.run.app:443"
asmcli: Configuring ASM managed control plane revision CRD...
asmcli: Configuring base installation for managed control plane...
asmcli: Configuring ASM managed control plane validating webhook config...
asmcli: Configuring CNI...
asmcli: Applying asm/control-plane-revision/crd.yaml...
customresourcedefinition.apiextensions.k8s.io/controlplanerevisions.mesh.cloud.google.com configured
asmcli: Applying istio-1.11.2-asm.17/manifests/charts/base/files/gen-istio-cluster.yaml...
customresourcedefinition.apiextensions.k8s.io/destinationrules.networking.istio.io configured
customresourcedefinition.apiextensions.k8s.io/envoyfilters.networking.istio.io configured
customresourcedefinition.apiextensions.k8s.io/gateways.networking.istio.io configured
customresourcedefinition.apiextensions.k8s.io/serviceentries.networking.istio.io configured
customresourcedefinition.apiextensions.k8s.io/sidecars.networking.istio.io configured
customresourcedefinition.apiextensions.k8s.io/virtualservices.networking.istio.io configured
customresourcedefinition.apiextensions.k8s.io/workloadentries.networking.istio.io configured
customresourcedefinition.apiextensions.k8s.io/workloadgroups.networking.istio.io configured
customresourcedefinition.apiextensions.k8s.io/authorizationpolicies.security.istio.io configured
customresourcedefinition.apiextensions.k8s.io/peerauthentications.security.istio.io configured
customresourcedefinition.apiextensions.k8s.io/requestauthentications.security.istio.io configured
customresourcedefinition.apiextensions.k8s.io/telemetries.telemetry.istio.io configured
customresourcedefinition.apiextensions.k8s.io/dataplanecontrols.mesh.cloud.google.com configured
customresourcedefinition.apiextensions.k8s.io/istiooperators.install.istio.io configured
serviceaccount/istio-reader-service-account configured
serviceaccount/istiod-service-account configured
clusterrole.rbac.authorization.k8s.io/istiod-istio-system configured
clusterrole.rbac.authorization.k8s.io/istio-reader-istio-system configured
clusterrolebinding.rbac.authorization.k8s.io/istio-reader-istio-system configured
clusterrolebinding.rbac.authorization.k8s.io/istiod-istio-system configured
role.rbac.authorization.k8s.io/istiod-istio-system configured
rolebinding.rbac.authorization.k8s.io/istiod-istio-system configured
asmcli: Applying asm/istio/options/managed-control-plane-webhooks.yaml...
validatingwebhookconfiguration.admissionregistration.k8s.io/istiod-istio-system-mcp created
asmcli: Applying mcp_configmap.yaml...
configmap/asm-options created
asmcli: Applying asm/istio/options/cni-managed.yaml...
customresourcedefinition.apiextensions.k8s.io/dataplanecontrols.mesh.cloud.google.com configured
serviceaccount/istio-cni created
clusterrole.rbac.authorization.k8s.io/istio-cni created
clusterrole.rbac.authorization.k8s.io/istio-cni-repair-role created
clusterrolebinding.rbac.authorization.k8s.io/istio-cni created
clusterrolebinding.rbac.authorization.k8s.io/istio-cni-repair-rolebinding created
clusterrole.rbac.authorization.k8s.io/mdp-controller created
clusterrolebinding.rbac.authorization.k8s.io/mdp-controller created
configmap/istio-cni-config created
daemonset.apps/istio-cni-node created
asmcli: Configuring ASM managed control plane revision CR for channels...
asmcli: Installing ASM Control Plane Revision CR with asm-managed-rapid channel in istio-system namespace...
controlplanerevision.mesh.cloud.google.com/asm-managed-rapid created
asmcli: Installing ASM Control Plane Revision CR with asm-managed channel in istio-system namespace...
controlplanerevision.mesh.cloud.google.com/asm-managed created
asmcli: Installing ASM CanonicalService controller in asm-system namespace...
namespace/asm-system created
customresourcedefinition.apiextensions.k8s.io/canonicalservices.anthos.cloud.google.com configured
role.rbac.authorization.k8s.io/canonical-service-leader-election-role created
clusterrole.rbac.authorization.k8s.io/canonical-service-manager-role created
clusterrole.rbac.authorization.k8s.io/canonical-service-metrics-reader created
serviceaccount/canonical-service-account created
rolebinding.rbac.authorization.k8s.io/canonical-service-leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/canonical-service-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/canonical-service-proxy-rolebinding created
service/canonical-service-controller-manager-metrics-service created
deployment.apps/canonical-service-controller-manager created
asmcli: Waiting for deployment...
deployment.apps/canonical-service-controller-manager condition met
asmcli: ...done!
asmcli: 
asmcli: *****************************
1.11.2-asm.17
asmcli: *****************************
asmcli: The ASM control plane installation is now complete.
asmcli: To enable automatic sidecar injection on a namespace, you can use the following command:
asmcli: kubectl label namespace <NAMESPACE> istio-injection- istio.io/rev=asm-managed --overwrite
asmcli: If you use 'istioctl install' afterwards to modify this installation, you will need
asmcli: to specify the option '--set revision=asm-managed' to target this control plane
asmcli: instead of installing a new one.
asmcli: To finish the installation, enable Istio sidecar injection and restart your workloads.
asmcli: For more information, see:
asmcli: https://cloud.google.com/service-mesh/docs/proxy-injection
asmcli: The ASM package used for installation can be found at:
asmcli: /root/temp-cluster/asm
asmcli: The version of istioctl that matches the installation can be found at:
asmcli: /root/temp-cluster/istio-1.11.2-asm.17/bin/istioctl
asmcli: A symlink to the istioctl binary can be found at:
asmcli: /root/temp-cluster/istioctl
asmcli: *****************************
asmcli: Successfully installed ASM.

追加の確認として、(Managed ASM の場合はサポート外ですが) Public Endpoint が無効な Private Cluster に asmcli で In-cluster Control Plane の ASM をインストールした際は問題なく Private Endpoint からクラスタにアクセスし、ASM のインストールに成功していました。
したがって、この事象は Public Endpoint の有効な Private Cluster に限られそうです。

アプリケーションのデプロイ

Managed ASM が利用できるようになったので、Release channel を設定して ASM を利用してみます。
ここでは default namespace で Regular channel を利用し、nginx を起動してみます。

# kubectl get namespaces --show-labels
NAME              STATUS   AGE     LABELS
asm-system        Active   5m34s   control-plane=controller-manager,kubernetes.io/metadata.name=asm-system
default           Active   17h     kubernetes.io/metadata.name=default
gke-connect       Active   7m28s   hub.gke.io/project=***************,kubernetes.io/metadata.name=gke-connect,version=20211015-01-00
istio-system      Active   7m14s   kubernetes.io/metadata.name=istio-system,topology.istio.io/network=***************-default
kube-node-lease   Active   17h     kubernetes.io/metadata.name=kube-node-lease
kube-public       Active   17h     kubernetes.io/metadata.name=kube-public
kube-system       Active   17h     kubernetes.io/metadata.name=kube-system

# kubectl label namespace default istio-injection- istio.io/rev=asm-managed --overwrite
label "istio-injection" not found.
namespace/default labeled

# kubectl get namespaces --show-labels
NAME              STATUS   AGE     LABELS
asm-system        Active   5m49s   control-plane=controller-manager,kubernetes.io/metadata.name=asm-system
default           Active   17h     istio.io/rev=asm-managed,kubernetes.io/metadata.name=default
gke-connect       Active   7m43s   hub.gke.io/project=***************,kubernetes.io/metadata.name=gke-connect,version=20211015-01-00
istio-system      Active   7m29s   kubernetes.io/metadata.name=istio-system,topology.istio.io/network=***************-default
kube-node-lease   Active   17h     kubernetes.io/metadata.name=kube-node-lease
kube-public       Active   17h     kubernetes.io/metadata.name=kube-public
kube-system       Active   17h     kubernetes.io/metadata.name=kube-system

# kubectl run nginx --namespace default --image nginx
pod/nginx created

# kubectl get pods --namespace default -o jsonpath="{.items[*].spec.containers[*]['name', 'image']}"
nginx         istio-proxy 
nginx         gcr.io/gke-release/asm/proxyv2:1.10.4-asm.14
(見やすさのため改行しています)

バージョン 1.10.4-asm.14 の Istio-proxy がインジェクションされました。

ドキュメントでは こちら に各 Release channel で利用される ASM のバージョンが記載されていますが、本記事の執筆時点 (2021/10/26) では実機とドキュメントに乖離がありました。
ドキュメントでは Regular channel が 1.10.4-asm.9 となっていますが、実際に Regular channel を利用してインジェクションされた Istio-proxy は上記のとおり 1.10.4-asm.14 となっています。
今回は紹介しませんでしたが、Managed ASM を利用する場合は Data Plane の自動更新を行う Managed Data Plane も併せて利用できるため、アップグレード運用をマネージドにするなら気にする必要がないといえばそうなのですが、現在の ASM バージョンを確認したい場合は実機を参照したほうが確実と言えそうです。

このとき、Istio-proxy は Google の管理する Managed Istiod と通信をしていますが、Managed ASM の Managed Istiod から Istio-proxy への通信はクライアントアクセスではないため、Master Authorized Networks を有効にする場合でも明示的に許可する必要はありません (送信元がわからないため設定のしようもないはずです) 。
Private Cluster のドキュメントには下記のような記載があり、Managed Istiod も同様にユーザには見えないかたちで Public Endpoint を利用しているものと思われます。

パブリック エンドポイントへのアクセスを無効にしている場合でも、Google は、定期メンテナンスやコントロール プレーンの自動アップグレードなどのクラスタ管理目的で、コントロール プレーンのパブリック エンドポイントにアクセスできます。

引用元:https://cloud.google.com/kubernetes-engine/docs/concepts/private-cluster-concept#overview

ドキュメントとの整合性

前述のように、Private Cluster における Managed ASM のサポートされる環境のドキュメントには下記のような記載がありました。

パブリック エンドポイント アクセスを備えた GKE 限定公開クラスタ。Master Authorized Network (MAN) が有効かどうかは関係ありません

引用元:https://cloud.google.com/kubernetes-engine/docs/concepts/private-cluster-concept#overview

ドキュメントには Master Authorized Networks が有効かどうかは関係ないとありますが、これはあくまで Managed ASM をセットアップした後の状況を指しているものと思われます。
今回検証したようなシチュエーションであれば、運用上は Master Authorzed Networks が有効な Private Cluster への Managed ASM のインストールやその他操作を行うために asmcli を実行するインスタンス/端末の外部 IP アドレスを Master Authorzed Networks で許可する必要があるでしょう。

まとめ

今回の検証を通じて確認したことをまとめます。

  • 対象が Public Endpoint の有効な Private Cluster である場合、asmcli は実行元インスタンス/端末がクラスタノードと同一の VPC 上にあったとしても Public Endpoint 経由でアクセスしようとする処理が一部に含まれている
  • 上記の asmcli のふるまいから、Master Authorzed Networks が有効な Private Cluster への Managed ASM のセットアップやその他操作を行うためには、asmcli の実行元の外部 IP アドレスを Master Authorzed Networks で許可する必要がある
  • Managed ASM における Managed Istiod から Istio-proxy への通信はクライアントアクセスではないため、Master Authorized Networks を有効にする場合でも明示的に許可する必要はない

検証を通じて、Managed ASM と Private Cluster の関係性がより理解できました。
この記事が Managed ASM の利用を検討されている方のお役に立てば幸いです。

補足

コンソールの GKE クラスタ作成フォームから Anthos Service Mesh が有効化できるようになりました。

特に明記されていませんがチェックボックスを入れてクラスタを作成すると Managed ASM が有効化されます。
インストールのためだけであれば asmcli の実行元を Master Authorized Networks に登録する必要なく ASM を使い始められそうにみえますが、まだ Private Cluster + Managed ASM の組み合わせに対応していないようで、フォームから ASM を有効化しようとすると Private Cluster の設定が外されてしまいます。

Private Cluster + Managed ASM の構成をお求めの場合は引き続き asmcli でのセットアップが必要になりますね。

f:id:polar3130:20211026133822p:plain