GKE(Google Kubernetes Engine) Autopilotの限定公開クラスタにIAPを利用してアクセスする

エヌデーデーの関口です。
このTech-Techが始まった初期の頃に、限定公開クラスタの紹介をしましたが、当時はAutopilotがなかったので、少々複雑に考えなければならない部分がありました。

今回はAutopilot版の限定公開クラスタを作成してみて、IAPを利用してアクセスするという記事です。

まあ、ネタとしては既に出尽くされた感のあるものですが、最後の方に少しだけ実験的な事をやっていますので、最後までご覧ください。

想定読者

  • Google Cloudに触れている人
  • これからGKEに触れてみたい人

GKE Autopilotとは

GKEはコンテナオーケストレーションのソフトウェアであるKubernetes(k8sと略します)をGoogle Cloud上でマネージドに提供しているサービスです。

k8s自体は、複数のノードと呼ばれる仮想(or物理)マシンを一つのプラットフォームとして、その上で更に複数のコンテナなどを稼働させておくことができるものです。

すでにk8sのことをご存知の方も多いとは思いますが、改めてDockerやDocker Composeと何が違う?との違いを簡単に列挙すると次のようなものになります。

  • Docker — 単一コンテナを起動させるプラットフォーム(複数のコンテナ間の連携は可能だが複雑)
  • Docker Compose — 複数のコンテナ間の連携を考慮したプラットフォーム、ただしコンテナのスケールアップダウンなどは考慮されていない
  • Kubernetes — 複数のコンテナをまとめてPodやServiceという単位で管理できる、さらにそれらのスケールアップダウンも設定により自動化できる

このようにk8sは単一のコンテナのみならず、複数のコンテナを有機的に稼働させて、サービス提供するのに向いているソフトウェアなのですが、代わりに足回り(ネットワークやノード)の知識や管理も必要とされるのです。

GKEはそれらをGoogle Cloud上で提供してくれるので、オンプレでk8sを運用するよりは遙かに楽ではあるのですが、Standardと呼ばれる通常モードだとノードの管理を利用者が把握しておく必要がありました。ノードの管理とはノード作成・更新・削除や、バージョンアップなどの作業です。

Autopilotモードではこれらを完全にGoogle Cloud側に任せることができます。

個人的なGKEのイメージはGoogle Cloudというパブリッククラウドの中で独自に別のクラウドサービスを作るというような印象ですね。

Autopilotの限定公開クラスタを作る

Autopilotの限定公開クラスタを作成するのであれば、実は次のコマンドを実行するだけで簡単にできてしまいます。しかも、このコマンドではGKEが接続されるべきサブネットも一緒に作成してくれます。

# 以下のコマンドはLinux上での実行です。PowerShellの場合は改行文字が異なりますので注意してください
gcloud container clusters create-auto "autocluster" \
--project "{プロジェクト名}" \
--region "{リージョン}" \
--create-subnetwork name {サブネット名} \
--enable-private-nodes \
--enable-private-endpoint \
--enable-master-authorized-networks

しかし、実際のところサブネットなどは先に自前で作成しておいたほうがGKEから他のマネージドサービスにつなぐとき(プライベートサービス接続)など便利な時があるので、ここでは上記のコマンドではなく、先にVPCを作成しておくという手順で進めていきましょう。といっても、実際のVPC作成手順は以前の記事に記載していますので、それらを参考にしてください。

つづいて、この作成したVPCに繋がったAutopilotの限定公開クラスタを作成するコマンドです。コンソールでも作成はできるのですが、その場合作成済みのサブネットをPodやServiceのCIDRに指定することができないので注意してください。

ローカルやCloud Shellから次のコマンドを実行します。

gcloud container clusters create-auto "autocluster" \
--async \
--project "{プロジェクト名}" \
--region "{リージョン}" \
--enable-private-nodes \
--enable-private-endpoint \
--enable-master-authorized-networks \
--network "projects/{プロジェクト名}/global/networks/wp-vpc" \
--subnetwork "projects/{プロジェクト名}/regions/{リージョン}/subnetworks/wp-subnet" \
--cluster-secondary-range-name "pod-subnet" \
--services-secondary-range-name "service-subnet"

コマンド実行の完了まで時間がかかるので、asyncオプションを指定しています。

また--network--subnetwork--cluster-secondary-range-name--services-secondary-range-nameに、作成済みのVPCやサブネット、それからセカンダリIPv4範囲の名前を指定していることに注目してください。

限定公開クラスタに踏み台マシンからアクセスする

上記のように作成したクラスタは、「承認済みネットワークからのアクセスのみ許可する」というオプション(--enable-master-authrized-network)が有効になっているため、そのままではコントロールプレーンに接続できません。

コントロールプレーンというのは、GKE内部で実際にネットワーク(Service/Ingressなど)やコンテナの集合(Pod/Deployment)の作成や廃棄などの管理を行うための"場所"です。もっと直接的に表現するならば、k8sを操作するkubectlというコマンドを受け付けて実行する環境です。

つまり、このままではGKEのクラスタを作成はできたものの、kubectlを使った操作する事ができない状態です。(コンソールからの操作は可能ですが、決して使い勝手がいいとは言えません)

kubectlを使用するためにはコントロールプレーンにアクセスできる「承認されたネットワーク」から使用する必要があります。このネットワークは作成時などに指定して、特定のIPv4範囲を設定することができます。その他にも同じサブネット上からは特に指定していなくてもアクセスが可能なのです。

そこで、今回は同じサブネット上に踏み台となるマシン(GCE)を作成して、そこからアクセスできるようにしてみましょう。

マシン名やネットワークを指定して、次のようなコマンドで作成します。下記の場合OSイメージはデフォルトでDebian 11になります。(2023年8月現在)

gcloud compute instances create k8s-bastion \
--project "{プロジェクト名}" \
--zone asia-northeast1-b \
--machine-type e2-micro \
--network-interface stack-type=IPV4_ONLY,subnet=wp-subnet,no-address \
--scopes https://www.googleapis.com/auth/cloud-platform \
--tags iap 

ここで注目したいのは、指定したサブネット(--network-interfacesubnet)が、先に作成したGKEクラスタの--subnetworkと同一であるという点です。これにより、GKEのコントロールプレーンに接続可能な条件を満たしていることになります。

もう一点、このサブネットは以前の記事で書いたように、ネットワークタグiapを持つGCEへのInternet-Aware Proxy(IAP)からのSSH接続を有効にしています。

今回作成したGCEでも--tags iapと指定しているので、IAPを利用してローカルやCloud ShellからSSHアクセスしてみましょう。

gcloud compute ssh k8s-bastion --project {プロジェクト名} \
--zone asia-northeast1-b --tunnel-through-iap

アクセスできたと思います。それでは踏み台マシン上で次のようなコマンドを実行してみましょう。
なお、NATについても先に紹介した記事にあるとおりに設定していますので、既にインターネットへのアクセスはできるようになっています。

sudo apt update
sudo apt install kubectl
# GKEにアクセスするために認証を取得するには以下のプラグインが必要なので合わせてインストールする
sudo apt-get install google-cloud-sdk-gke-gcloud-auth-plugin

gcloud container clusters get-credentials autocluster --region asia-northeast1

kubectl get ns

次のような出力になるはずです

NAME                       STATUS   AGE
default                    Active   23m
gke-gmp-system             Active   22m
gke-managed-filestorecsi   Active   23m
gmp-public                 Active   22m
kube-node-lease            Active   23m
kube-public                Active   23m
kube-system                Active   23m

どうですか?限定公開クラスタに接続できたと思います。これでkubectl create podなどを実行してオブジェクト作成ができるようになります。

さて、gcloud container clusters get-credentials を実行したときに、そのクレデンシャルは$HOME/.kube/configに保存されています。
こちらを覗いてみると、次のような構成になっています。

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: LS0t.....
    server: https://172.16.59.226
  name: gke_xxxxxxxx_asia-northeast1_autocluster
contexts:
- context:
    cluster: gke_xxxxxxxx_asia-northeast1_autocluster
    user: gke_xxxxxxxx_asia-northeast1_autocluster
  name: gke_xxxxxxxx_asia-northeast1_autocluster
current-context: gke_xxxxxxxx_asia-northeast1_autocluster
kind: Config
preferences: {}
users:
- name: gke_xxxxxxxx_asia-northeast1_autocluster
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      command: gke-gcloud-auth-plugin
      installHint: Install gke-gcloud-auth-plugin for use with kubectl by following
        https://cloud.google.com/blog/products/containers-kubernetes/kubectl-auth-changes-in-gke
      provideClusterInfo: true

このファイルのclusters[0].cluster.serverという部分に作成したクラスタのコントールプレーンへのアドレスが内部IPとして記載されているのがわかると思います。このアドレスに対してkubectlを実行しているというわけです。

後で作業をするので、ひとまずこの状態で踏み台マシンのSSH接続は維持しておいてください。

ローカルから踏み台マシンにトンネルしてGKEにアクセスする

踏み台マシンにSSHしてkubectlが実行できるようにはなったのですが、実際にはkubectl apply -fを利用して、設定用のyamlファイルを利用してGKEに反映させることが多いと思います。

その場合に、いちいち踏み台マシンにyamlファイルを転送して・・・というような事は面倒なので、可能であればローカルから直接kubectlを実行したいですよね。そこで、この踏み台マシンにローカルから通信をトンネルして直接操作ができるようにしてみましょう。

kubectlを実行するとプロトコルとしてはHTTPを通じて、コントロールプレーンに処理実行させています。
そこで、ローカルのHTTP通信を踏み台マシンに向けるようにします。

踏み台マシンにプロキシソフトを入れる

このあたりは実はGoogle Cloudのドキュメントに詳しく掲載されているので、そちらを参考にされても全く問題ありません。
さきほど踏み台マシンにSSHでつないだままの状態から、ドキュメントと同様にTinyproxyをインストールし、設定を変更して再起動します。

sudo apt install tinyproxy -y
# localhost からの通信も許可する
sudo sed -i -e "s/^Allow 127\.0\.0\.1$/Allow 127.0.0.1\nAllow localhost/" /etc/tinyproxy/tinyproxy.conf
sudo service tinyproxy restart

ここまで終えたら、踏み台マシンからは切断してローカルやCloud Shellから、作成した限定公開のGKEクラスタに接続できるか試してみます。

gcloud container clusters get-credentials autocluster \
--project {プロジェクト名} --region asia-northeast1

kubectl get ns

コマンドがタイムアウトになったはずです。(Ctrl + Cでキャンセルしてください)

原因は今の状態ではローカルから限定公開クラスタの内部IPアドレスには到達出来ないためです。

ローカルから踏み台マシンへポートフォワードする

そこで、ローカルでkubectlを実行したときに、先ほど踏み台マシンに立てたプロキシを経由するようにしてみます。kubectlは内部的にはHTTPで通信を行うので、ローカル環境にHTTPのプロキシを設定してやるという訳です。

まず、ローカルのターミナルで次のコマンドを実行します。

# --ssh-flag の内容は次の通り
# -4 -> IPv4 を使う
# -L8888:localhost:8888 -> ローカルの8888ポートを、リモートのlocalhost:8888にフォワードする
# -N -> リモートコマンドを実行しない(つまりポートフォワードのみ)
# -q -> quietモード。警告などを出力しない
# -f -> sshをバックグラウンドで実行
gcloud compute ssh k8s-bastion \
--tunnel-through-iap \
--project "{プロジェクト名}" \
--zone asia-northeast1-b \
--ssh-flag "-4 -L8888:localhost:8888 -N -q -f"

これでローカルの8888ポートへの通信はIAPでトンネルした上で踏み台マシンの8888ポートにポートフォワードする事になります。ちなみに8888ポートはTinyproxyのデフォルト受信ポートです。

この状態で、ローカルから次のコマンドを実行してください。結果が正しく表示されるはずです。

# 環境変数でHTTPS_PROXYを設定する
export HTTPS_PROXY=localhost:8888

kubectl get ns

こうすると、HTTPS通信はローカルの8888ポートをプロキシするので、先ほどのコマンドで実行したポートフォワード対象となり、踏み台マシンを経由していくことになります。ただし、この場合ローカルで実行するHTTPS通信のすべてが踏み台マシン経由になることに注意してください。

HTTPS_PROXYの影響をなくす

環境変数HTTPS_PROXYはローカルで実行する他のHTTPS通信に影響してしまいますので、このクラスタへの接続の時だけ、ローカルの8888ポートを使うように設定します。

まずはHTTPS_PROXYを消して、元の状態に戻してみます。

unset HTTPS_PROXY
# echoで何も表示されない
echo $HTTPS_PROXY
# 次のコマンドを実行するとタイムアウトになる(Ctrl + Cでキャンセルしてください)
kubectl get ns

ここから今回のクラスタに接続する時だけプロキシを経由するように変更していきます。

次のコマンドを実行すると、クラスタの名前が表示されます。

kubectl config get-clusters

複数表示される場合もありますが、今回作成したクラスタは次のような名称で表示されるはずです。
gke_{プロジェクト名}_asia-northeast1_autocluster

次に、下に示したコマンドでこのクラスタに接続するときだけプロキシを経由するように設定します。上記のコマンドで表示されたクラスタの名称にproxy-urlという設定項目を追加しています。

kubectl config set clusters.gke_{プロジェクト名}_asia-northeast1_autocluster.proxy-url http://localhost:8888

kubectl get ns

kubectl get nsの結果が表示されるようになったはずです。これで特定のクラスタに接続するときだけプロキシを経由するということが可能になりました。

後始末として、ポートフォワードしているsshプロセスをkillしましょう。

netstat -lnpt | grep 8888 | awk '{print $7}' | grep -o '[0-9]\+' | sort -u | xargs sudo kill

sshではなくstart-iap-tunnelで同じことができるか?

gcloud compute ssh --tunnel-through-iapでもいいのですが、gcloud compute start-iap-tunnelでも同じようなことができないでしょうか?
やってみましょう。

まずは、IAPからのポート8888をファイアウォールで許可します。
コンソールから[ファイアウォール ポリシー]のページに進み、IAPからの通信を許可しているルール(ここではiap-allow-ingress)を選択します。
[編集]ボタンを押してプロトコルとポートに8888を追加してから保存します。

ローカルから次のコマンドを実行してみます。

gcloud compute start-iap-tunnel k8s-bastion 8888 \
--local-host-port localhost:8888 \
--project "{プロジェクト名}" \
--zone asia-northeast1-b

Listening on port [8888].と表示されてリッスンを開始するようになりました。

次にもう一つターミナルを開いてコマンドを実行してみます。

kubectl get ns

おや?Unable to connect to the server: Access deniedと表示されました。
接続ができてませんね・・・

これは、次のような通信になっていることが原因です。

gcloud compute ssh --tunnel-through-iapの場合はローカルの通信はIAPを経由して踏み台マシンに繋ぎにいきますが、8888ポートはsshにくるまれて、踏み台マシンのローカル通信(localhost:8888)として扱われます。

一方glcoud compute start-iap-tunnelの場合は、下図のようにローカルの通信がIAPを経由して、sshにくるまれる事なく踏み台マシンに到達します。したがって、この場合の8888ポートの通信は「IAPから直接来る8888ポート通信」と解釈されるわけです。

ファイアウォールで8888を許可したので、踏み台マシンまでは到達しているのですが、この時点ではTinyproxyの設定がAllow 127.0.0.1Allow localhostだけで、ローカル通信のみがプロキシ許可されている状態なので「IAPから直接くる8888ポート通信」をプロキシすることは許可されていないのです。

原因がわかったところでTinyproxyの設定を変更してみましょう。
踏み台マシンにログインして、次のコマンドを実行してください。

# IAPのIP範囲からの通信を許可する
sudo sed -i -e "s/^Allow 127\.0\.0\.1$/Allow 127.0.0.1\nAllow 35.235.240.0\/20/" /etc/tinyproxy/tinyproxy.conf
sudo service tinyproxy restart

さあ、ローカルのターミナルにもどってコマンドを実行してみてください。
gcloud compute start-iap-tunnelが実行中でなければ改めて実行しておいてください。

kubectl get ns
NAME                       STATUS   AGE
default                    Active   176m
gke-gmp-system             Active   175m
gke-managed-filestorecsi   Active   175m
gmp-public                 Active   175m
kube-node-lease            Active   176m
kube-public                Active   176m
kube-system                Active   176m

正しく表示されました!

まとめ

Autopilotのクラスタを作成する場合でもネットワークは先に作っておいた方が、後で他のGoogle Cloudのサービスと接続するのに自分たちで設定を把握できるのでよさそうです。

限定公開クラスタの場合、踏み台マシンとhttpプロキシの導入は必要になりますが、IAPを利用することでローカルから安全かつシームレスにクラスタの操作ができるようになります。