↓メインコンテンツへスキップ
  1. Blogs/

TerraformでGKEプライベートクラスタと踏み台VMを作成してみた

3 分
Terraform GoogleCloud GKE IAP
0222-nnn
著者
0222-nnn
猫が好き
目次
Terraform-GoogleCloud - この記事は連載の一部です
パート 23: この記事

概要
#

これまでの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との連携を作成します。

Terraform-GoogleCloud - この記事は連載の一部です
パート 23: この記事

関連記事

TerraformでGKEプライベートクラスタからMemorystore for Redis Clusterへ接続してみた
3 分
Terraform GoogleCloud GKE Redis
TerraformでGKEプライベートクラスタからMemorystore for Redisへ接続してみた
2 分
Terraform GoogleCloud GKE Redis
TerraformでGKE Workload IdentityからCloud Storageへアクセスしてみた
2 分
Terraform GoogleCloud GKE WorkloadIdentity
Terraformで外部IPなしのGCEへIAP経由でSSH接続してみた
2 分
Terraform GoogleCloud GCE IAP
Terraform StateをCloud Storage Remote Backendへ移行してみた
2 分
Terraform GoogleCloud CloudStorage
TerraformからGoogle Cloudへ接続してProject情報を取得してみた
3 分
Terraform GoogleCloud