【GKE/Terraform】外部ネットワークからの全てのアクセスを制限した限定公開クラスタを作成し、踏み台サーバーからkubectlする
TL;DR
- こちらのリポジトリを参考にしてください
-> https://github.com/nekoshita/gke-private-cluster-with-no-client-access-to-the-public-endpoint-example
こんな人向け
- 限定公開クラスタについて知りたい
- GKEのセキュリティーを強化したい
- TerraformでGCP・GKEさわりたい
解説
限定公開クラスタ(プライベートクラスタ)とは?
GKEは、主にコントロールプレーン
とノード
から成り立っています。
詳しくはこちら -> https://zenn.dev/articles/6b5dffe3d563e1
限定公開クラスタにすると、ノードに使われるVMインスタンスに外部IPアドレスが割り振られなくなります。
すると、ノードに関しては外部からアクセスすることはできなくなります。
どうやって外部からのアクセスを制限するの?
ノードに関しては、限定公開クラスタにすれば、ノードに外部IPアドレスが付与されなくなるので、外部ネットワークからのアクセスを制限することができます。
限定公開クラスタにするには、enable_private_clusterというオプションを付与してGKEクラスタを作成すればOKです。
次に、コントロールプレーンへのアクセスも制限します。
enable_private_endpointというオプションを指定すると、コントロールプレーンがpublicなエンドポイントを持たなくなるので、外部ネットワークからのアクセスを制限することができます。
どうやってkubectlするの?
外部からのアクセスをできなくしていまうので、外部ネットワークから kubectl
コマンドを実行することはできなくなります。
なので、GKEクラスタと同じネットワーク内に踏み台サーバーとなるVMインスタンスを起動します。
またVMインスタンスにSSH接続するのに今回はCloudIAPを用います。
CloudIAPについて詳しくはこちら -> https://zenn.dev/nekoshita/articles/6c8a3650db4489
イメージ図
- VPCネットワークとsubnetを作成します
- subnet内に限定公開クラスタを作成します
- 各ノードが外部IPアドレスを持ちません
- コントロールプレーンへのアクセス制限をかけ、全ての外部ネットワークからのアクセスを拒否します
- 外部ネットワークからkubectlコマンドが使えなくなります
- subnet内に踏み台サーバーとしてVMインスタンスを起動します
- 踏み台サーバー踏み台サーバーにIAPを用いてSSH接続します
- 踏み台サーバーから、GKEクラスタに対してkubectlコマンドを実行します
Terraform解説
ネットワークとサブネットを作成する
VMインスタンスを設置するためのVPCネットワークとサブネットを作成します。
resource "google_compute_network" "my_network" {
name = "my-network"
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "my_subnetwork" {
name = "my-subnetwork"
region = var.gcp_regions["tokyo"]
# サブネットで使用したい内部IPアドレスの範囲を指定する
ip_cidr_range = "10.0.0.0/16"
network = google_compute_network.my_network.self_link
# CloudLoggingにFlowLogログを出力したい場合は設定する
log_config {
metadata = "INCLUDE_ALL_METADATA"
}
secondary_ip_range {
range_name = "my-subnetwork-for-pods"
ip_cidr_range = "10.1.0.0/16"
}
secondary_ip_range {
range_name = "my-subnetwork-for-services"
ip_cidr_range = "10.2.0.0/16"
}
private_ip_google_access = true
}
サブネット内に限定公開クラスタを作成する
enable_private_nodes
とenable_private_endpoint
のオプションを指定することが重要です。
resource "google_container_cluster" "my_cluster" {
name = "my-cluster"
location = "asia-northeast1-a"
project = "あなたのGCPプロジェクト"
network = google_compute_network.my_network.name
subnetwork = google_compute_subnetwork.my_subnetwork.name
# デフォルトのノードプールがないクラスターを作成することはできないので、
# クラスター作成直後にデフォルトのノードプールを削除する
remove_default_node_pool = true
initial_node_count = 1
# 限定公開クラスタにするための設定
private_cluster_config {
# プライベートノードにするかどうか(ノードに外部IPを付与しない)
enable_private_nodes = true
# kubectlコマンドのアクセスを内部IPアドレスからのみにするかどうか
enable_private_endpoint = true
# クラスターのmasterとILB VIPのためのプライベードIPアドレスを指定する必要がある
# /28 subnetでないといけない
master_ipv4_cidr_block = "192.168.0.0/28"
}
# enable_private_endpointがtrueの場合はmaster_authorized_networks_configを設定する必要があります。
# master_authorized_networks_configにはコントロール プレーンにアクセスできるIPアドレスを指定します。
# enable_private_endpointがtrueの時には、master_authorized_networks_configで指定しなくても、以下の2種類のIPアドレスからコントロールプレーンにアクセス可能です
# 1, GKEクラスタが所属するサブネットのプライマリ範囲
# 2, GKEクラスタのポッドに使用されるセカンダリ範囲
master_authorized_networks_config {}
ip_allocation_policy {
cluster_secondary_range_name = google_compute_subnetwork.my_subnetwork.secondary_ip_range.0.range_name
services_secondary_range_name = google_compute_subnetwork.my_subnetwork.secondary_ip_range.1.range_name
}
}
resource "google_container_node_pool" "my_cluster_nodes" {
name = "my-node-pool"
location = "asia-northeast1-a"
cluster = google_container_cluster.my_cluster.name
node_count = 1
node_config {
preemptible = true
machine_type = "n1-standard-1"
service_account = google_service_account.my_cluster.email
oauth_scopes = [
"https://www.googleapis.com/auth/cloud-platform"
]
}
}
# ノードとして起動されるVMインスタンスのためのサービスアカウント
resource "google_service_account" "my_cluster" {
account_id = "my-cluster"
display_name = "My Service Account For My Cluster"
}
サブネット内に踏み台サーバーとしてVMインスタンスを作成する
resource "google_compute_instance" "bastion" {
name = "bastion"
machine_type = "f1-micro"
zone = var.gcp_zones["tokyo-a"]
# Firewallでタグごとにルールを設定したいので、VMインスタンスにタグを設定しておく
tags = ["bastion-tag"]
boot_disk {
initialize_params {
image = "debian-cloud/debian-10"
}
}
network_interface {
# my_subnetworkにインスタンスを起動する
network = google_compute_network.my_network.name
subnetwork = google_compute_subnetwork.my_subnetwork.name
# 踏み台として使うので外部IPアドレスを割り振る
# 外部IPアドレスを割り振る方法は次の2種類ある。静的外部IPアドレス、エフェメラル外部IPアドレス
# 今回は外部IPアドレスは割り振らないので、access_configはコメントアウトしておく
# access_config {}
}
service_account {
# https://cloud.google.com/compute/docs/access/service-accounts#accesscopesiam
# インスタンスで完全な cloud-platform アクセス スコープを設定すると、すべての Google Cloud リソースに対する完全アクセス権を付与できる
# その後、IAM ロールを使用してサービス アカウントの API アクセス権を安全に制限することをGoogleは推奨している
email = google_service_account.bastion.email
scopes = ["cloud-platform"]
}
scheduling {
# 料金を抑えるためにプリエンプティブルにしておく
preemptible = true
# プリエンプティブルの場合は下のオプションが必須
automatic_restart = false
}
}
resource "google_service_account" "bastion" {
account_id = "bastion"
display_name = "My Service Account For Bastion"
}
Cloud IAPでSSH接続するためのFirewallルールを設定する
Cloud IAPでSSH接続するには、Firewallルールを設定する必要があります。
詳しくはこちら -> https://zenn.dev/nekoshita/articles/6c8a3650db4489#firewallルールを設定する
resource "google_compute_firewall" "my_network" {
name = "my-network-firewall"
network = google_compute_network.my_network.name
direction = "INGRESS"
allow {
protocol = "tcp"
ports = ["22"]
}
# 対象のVMインスタンスのタグを指定する
target_tags = ["bastion-tag"]
# Cloud IAPのバックエンドIPアドレス範囲を指定する
# https://cloud.google.com/iap/docs/using-tcp-forwarding#create-firewall-rule
source_ranges = ["35.235.240.0/20"]
# CloudLoggingにFlowLogログを出力したい場合は設定する
log_config {
metadata = "INCLUDE_ALL_METADATA"
}
}
踏み台サーバーにSSH接続するユーザーに権限を付与する
今回はプロジェクトレベルIAMとリソースレベルIAMを用いて、最小限のカスタムロールを作成して、それをユーザーに紐づけます。
まずはロールを作成します。
# IAPを用いてSSH接続するのに必要な権限
# プロジェクトレベルで付与する必要がある
resource "google_project_iam_custom_role" "use_iap" {
role_id = "UseIAP"
title = "use iap"
permissions = [
# プロジェクト内のリソースにアクセスするのに必要な権限
"compute.projects.get",
# 別のサービスアカウントにアクセスするために必要な権限
# SSH接続するときはVMインスタンスのサービスアカウントにアクセスするので必要になる
"iam.serviceAccounts.actAs",
# IAPに必要な権限
"iap.tunnelInstances.accessViaIAP",
]
}
# 踏み台サーバーにSSHするためのロール
resource "google_project_iam_custom_role" "ssh_to_vm_instance" {
role_id = "SSHToVMInstance"
title = "SSH to VM Instance"
permissions = [
# VMインスタンスを取得するのに必要な権限
"compute.instances.get",
# メタ情報を付与するのに必要な権限
# 初めてSSH接続する時、鍵を渡すために必要
"compute.instances.setMetadata",
]
}
次に、ロールをユーザーに紐づけます
# アクセスを許可するユーザーにプロジェクトレベルで付与するロール
# このロールに含まれる権限はプロジェクトレベルで付与する必要がある
resource "google_project_iam_binding" "bind_use_iap" {
role = google_project_iam_custom_role.use_iap.id
members = [
"user:${var.allowed_user_mail}",
]
}
# 踏み台サーバーにIAP経由でSSHするユーザーにリソースレベルのロールを付与する
resource "google_compute_instance_iam_binding" "enable_ssh_to_vm_instance_for_bastion" {
instance_name = google_compute_instance.bastion.name
zone = var.gcp_zones["tokyo-a"]
role = google_project_iam_custom_role.ssh_to_vm_instance.id
members = [
"user:${var.allowed_user_mail}",
]
}
踏み台サーバーからGKEへアクセスするための権限を付与する
踏み台サーバーのためのサービスアカウントはすでに作成してあるので、そのサービスアカウントにGKEへアクセスするための権限を付与します。
また、GKEへのアクセスするための権限はリソースレベルで付与することはできないので、プロジェクトレベルで付与します。
まずはカスタムロールを作成します。
# kubectlするためのロール
resource "google_project_iam_custom_role" "enable_kubectl" {
role_id = "EnableKubectl"
title = "Enable Kubectl"
permissions = [
# クラスタの認証情報を取得するのに必要な権限
"container.clusters.get",
# podの一覧取得するのに必要な権限,
"container.pods.list"
]
}
次にカスタムロールを踏み台サーバーのサービスアカウントに紐づけます
# プロジェクトレベルで踏み台サーバーにロールを付与する
# GKEはリソースレベルでのロールの付与ができない
resource "google_project_iam_binding" "bind_enable_kubectl" {
role = google_project_iam_custom_role.enable_kubectl.id
members = [
"serviceAccount:${google_service_account.bastion.email}",
]
}
実際にリソースを作ってみる
準備
-
Terraform v0.14.5
のインストール - Google Cloud Platformのprojectを作成する
- GCP StorageのBucketを作成する
-
gcloud CLI
のインストール
リソースを作成してkubectlしてみる
#-------------------
# リポジトリをクローンする
#-------------------
$ git clone https://github.com/nekoshita/gke-private-cluster-with-no-client-access-to-the-public-endpoint-example
$ cd gke-private-cluster-with-no-client-access-to-the-public-endpoint-example
#-------------------
# terraform applyしてリソースを作成する
#-------------------
$ export GCP_PROJECT_ID="your-gcp-project-id"
$ export GCS_BUCKET_NAME="your-gcs-bucket-name"
$ export USER_MAIL="your-google-user-mail-to-allow-ssh-acccess@gmail.com"
$ bin/apply $GCP_PROJECT_ID $GCS_BUCKET_NAME $USER_MAIL
#-------------------
# kubectlを実行する
#-------------------
$ export ZONE="asia-northeast1-a"
$ export VM_INSTANCE_NAME="bastion"
$ export GCP_PROJECT_ID="your-gcp-project-id"
# GCPのprojectを作成したgoogleアカウントでログインする(すでにログインしてる場合は不要)
$ gcloud auth login
# IAPを利用して踏み台サーバーにSSH接続する
$ gcloud beta compute ssh --zone $ZONE $VM_INSTANCE_NAME --project $GCP_PROJECT_ID --tunnel-through-iap
# kubectlコマンドをインストールする
$ sudo apt install kubectl
# クラスタの認証情報を取得する
$ gcloud container clusters get-credentials my-cluster --zone asia-northeast1-a
# podを取得できるかためしてみる
$ kubectl get pod --all-namespaces
#-------------------
# 最後にリソースの削除をお忘れなく!
#-------------------
$ export GCP_PROJECT_ID="your-gcp-project-id"
$ export GCS_BUCKET_NAME="your-gcs-bucket-name"
$ export USER_MAIL="your-google-user-mail-to-allow-ssh-acccess@gmail.com"
$ bin/destroy $GCP_PROJECT_ID $GCS_BUCKET_NAME $USER_MAIL
最後に
- kubectlを外部ネットワークからできなくなるので、よりセキュアになるのは嬉しい
- 踏み台サーバーに接続するのはめんどくさい
- 外部のマネージドのCI系サービス(GitHubActions、CircleCIなど)からkubectlコマンド使えないのは不便かもしれない
- ArgoCDとか使えば、kubectlコマンドでリソース管理することはなくなるので、そうすべきかもしれない
- 間違ってたらご指摘いただけると嬉しいです!
Discussion