概要 #
これまでのGKE記事(Basic編、Workload Identity編)は、コントロールプレーンも外部から到達できるパブリックエンドポイントのクラスタでした。実務ではコントロールプレーンを公開しないプライベートエンドポイントが基本です。
ただしプライベートエンドポイントにすると、kubectlを直接実行する経路がなくなります。VPCの外からは、Clusterが存在することさえAPIを通して確認できません。
そこでVPC内に踏み台(bastion)VMを置き、kubectlはすべて踏み台経由で実行する構成にします。踏み台自体もIAP TCP forwarding経由でのみSSHでき、外部IPを持ちません。
Terraformの基本操作と、IAP SSH(GCE + IAP編)は前提としています。
Redis Memorystoreとの連携、Internal HTTP Load Balancerは扱いません。Podからの実際のWorkload Identity呼び出しも扱いません。これらは本記事の構成を土台に、別記事で扱う予定です。
検証すること #
- コントロールプレーンもノードも外部IPを持たないGKE Standardクラスタを作成できること
master_authorized_networksで接続元を踏み台Subnetだけに絞れること- 踏み台VM経由でのみ
kubectlが実行できること - Artifact Registryのイメージが実際にPodとして配信できること(Node起動確認とは別)
前提環境 #
- Google Cloud CLI、Terraform、Docker
- Billingが有効な検証用Google Cloud Project
- GKE、Compute Engine、IAM、Artifact Registryを操作できるGoogleアカウント
- 検証時のバージョン:Terraform 1.14.3、google 7.43.0
今回の構成 #
踏み台はGKE用とは別のSubnetに置きます。コントロールプレーンへの接続を、この踏み台SubnetのCIDRだけに絞るためです。IAPとArtifact RegistryはVPCの外にある、Google管理のサービスです。
flowchart TB
Op["Operatorの端末"]
subgraph GCP["Google Cloud"]
IAP["Identity-Aware Proxy
VPC外・Google管理"]
AR["Artifact Registry
VPC外・Google管理"]
subgraph Project["Project"]
subgraph Region["asia-northeast1"]
subgraph VPC["VPC"]
subgraph BastionSubnet["踏み台Subnet: 10.43.0.0/24"]
Bastion["踏み台VM
外部IPなし"]
end
subgraph GkeSubnet["GKE Subnet: 10.40.0.0/24"]
subgraph Cluster["GKE Cluster
private endpoint + private nodes"]
CP["Control Plane
172.16.4.0/28"]
Node["Node
外部IPなし"]
end
end
end
end
end
end
Op -->|"IAP TCP forwarding"| IAP
IAP -->|"TCP:22"| Bastion
Bastion -->|"kubectl
master_authorized_networks: 踏み台Subnetのみ"| CP
Node -.->|"イメージpull
artifactregistry.reader"| AR
踏み台からクラスタへ接続するまでの流れは、次のとおりです。
sequenceDiagram
actor Op as Operator
participant IAP as Identity-Aware Proxy
participant Bastion as 踏み台VM
participant CP as Control Plane(private endpoint)
Op->>IAP: gcloud compute ssh --tunnel-through-iap
IAP->>Bastion: TCP:22(送信元 35.235.240.0/20)
Bastion->>CP: gcloud container clusters get-credentials
Note over Bastion,CP: 踏み台SubnetのCIDRのみ許可
CP-->>Bastion: kubeconfig
Bastion->>CP: kubectl get nodes
CP-->>Bastion: Node一覧
VPCの外からkubectlを直接実行する経路はありません。踏み台を経由しない限り、Clusterには到達できません。
使用するTerraformコード #
プライベートエンドポイントのGKE Cluster #
resource "google_container_cluster" "primary" {
name = var.cluster_name
location = var.zone
network = google_compute_network.vpc.name
subnetwork = google_compute_subnetwork.gke.name
private_cluster_config {
enable_private_nodes = true
enable_private_endpoint = true
master_ipv4_cidr_block = var.master_ipv4_cidr_block
}
master_authorized_networks_config {
cidr_blocks {
cidr_block = var.bastion_subnet_cidr
display_name = "bastion-subnet"
}
}
workload_identity_config {
workload_pool = "${var.project_id}.svc.id.goog"
}
}
enable_private_endpoint = trueにすると、コントロールプレーンの接続先そのものがプライベートIPになります。master_authorized_networks_configは、そのプライベートIPへの接続元を制限する設定です。ここでは踏み台Subnetだけを許可しています。
踏み台VM #
resource "google_service_account" "bastion" {
account_id = "${var.bastion_instance_name}-sa"
display_name = "Bastion for private GKE access"
}
# container.clusterAdminはnodes.listを含まないため、kubectl get nodesがForbiddenになる。
resource "google_project_iam_member" "bastion_gke" {
project = var.project_id
role = "roles/container.admin"
member = "serviceAccount:${google_service_account.bastion.email}"
}
resource "google_compute_instance" "bastion" {
name = var.bastion_instance_name
machine_type = var.bastion_machine_type
zone = var.zone
tags = ["bastion"]
network_interface {
subnetwork = google_compute_subnetwork.bastion.id
# 外部IPなし。IAP経由でのみSSH
}
service_account {
email = google_service_account.bastion.email
scopes = ["https://www.googleapis.com/auth/cloud-platform"]
}
metadata_startup_script = <<-EOT
#!/bin/bash
set -euxo pipefail
apt-get update
apt-get install -y kubectl google-cloud-cli-gke-gcloud-auth-plugin
EOT
}
踏み台のSAにはcontainer.adminを付与します。container.clusterAdminではnodes.list権限が含まれず、踏み台からkubectl get nodesを実行するとForbiddenになるためです。
設定と実行 #
cd Advanced-Examples/11-gke-private-bastion
cp terraform.tfvars.example terraform.tfvars
project_id = "your-project-id"
iap_member = "user:you@example.com"
terraform init
terraform fmt -check
terraform validate
terraform plan
terraform apply
VPC・Subnet・Cloud NAT・踏み台VM・GKE Cluster・Artifact Registryなどが作成されます。合わせて25リソースです。GKE作成に数分かかりました。
Apply complete! Resources: 25 added, 0 changed, 0 destroyed.
Outputs:
artifact_registry_nginx_image = "asia-northeast1-docker.pkg.dev/YOUR_PROJECT_ID/tf-adv-gke-bastion-repo/nginx:latest"
bastion_name = "tf-adv-gke-bastion-vm"
cluster_name = "tf-adv-gke-bastion"
private_endpoint = "172.16.4.2"
workload_pool = "YOUR_PROJECT_ID.svc.id.goog"
踏み台経由で確認する #
踏み台にIAP SSHし、踏み台の中でget-credentialsします。
gcloud compute ssh "$(terraform output -raw bastion_name)" \
--zone="$(terraform output -raw zone)" \
--tunnel-through-iap \
--project="$(terraform output -raw project_id)"
# 踏み台の中で実行
gcloud container clusters get-credentials "$(terraform output -raw cluster_name)" \
--zone="$(terraform output -raw zone)" \
--project="$(terraform output -raw project_id)"
kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP
gke-tf-adv-gke-basti-tf-adv-gke-basti-2f6addf8-hrtp Ready <none> 84s v1.35.7-gke.1027000 10.40.0.3
NodeがReadyになりました。ただしこれは、Nodeが起動していることの確認でしかありません。
Podが実際に配信できることを確認する #
Artifact Registryにイメージがpushでき、ノード用SAに読み取り権限があっても、Podとして実際に動くかは別です。nginxをpushしてdeployし、port-forwardで応答を確認します。
# ローカル(Dockerが使える環境)で実行
gcloud auth configure-docker "$(terraform output -raw region)-docker.pkg.dev" --quiet
docker pull nginx:latest
docker tag nginx:latest "$(terraform output -raw artifact_registry_nginx_image)"
docker push "$(terraform output -raw artifact_registry_nginx_image)"
マニフェストの__IMAGE__をこのURLへ置換し、踏み台へ転送して適用します。
sed "s|__IMAGE__|$(terraform output -raw artifact_registry_nginx_image)|" \
k8s/nginx-deployment.yaml > /tmp/nginx-deployment.yaml
gcloud compute scp /tmp/nginx-deployment.yaml \
"$(terraform output -raw bastion_name):/tmp/nginx-deployment.yaml" \
--zone="$(terraform output -raw zone)" --tunnel-through-iap \
--project="$(terraform output -raw project_id)"
# 踏み台の中で実行
kubectl apply -f /tmp/nginx-deployment.yaml
kubectl get pods -l app=nginx -o wide
NAME READY STATUS RESTARTS AGE IP
nginx-6b9766b7d9-mk8z8 1/1 Running 0 9s 10.41.0.13
port-forwardで実際にHTTP応答を確認します。
kubectl port-forward deployment/nginx 8080:80 &
curl -sS -o /dev/null -w "HTTP %{http_code}\n" http://localhost:8080
HTTP 200
200が返り、Artifact RegistryのイメージがNode上で実際に配信できることを確認できました。
後片付け #
terraform destroy
Destroy complete! Resources: 25 destroyed.
GKE Cluster、Node、踏み台VM、Cloud NATは課金対象です。検証後は必ずdestroyします。
まとめ #
- コントロールプレーンとノードの両方を非公開にしたGKE Clusterを作成できる
master_authorized_networksで接続元を踏み台Subnetだけに絞れる- 踏み台VM経由でしか
kubectlが実行できないことを実際に確認できる - Node起動の確認とは別に、Podが実際に配信できることまで確認できる
参考資料 #
次回 #
この構成を土台に、Memorystore for Redisとの連携を作成します。