Eng (なりたい)

はやく エンジニア になりたい

Terraform で GKE private cluster をたてる

f:id:kaito2-2:20201202195532p:plain

なぜ書いたか

GKE を本番運用しているなかで、検討しきれていない設定であったり構成を調査する試みの一環です。 最小構成ではじめる GKE + Terraform - Eng (なりたい) を先に読んでいただけるとより理解しやすいと思います。

tl;dr

良いからコードを見せろという方は以下のレポジトリを参照してください。

github.com

本編

概要

今回は以前作成した最小構成クラスタに以下の変更をしてみます。

examples/safer_cluster_iap_bastion を参考にしています。

  1. private cluster にする
    • Node に External IP が付与されないようになる
  2. master authorized networks を設定する
    • 指定されたネットワークからのみ Master にアクセスできるようになる

実際に、本番運用するなら参考にしているのモジュールを使用することになるともいますが、理解のために自分で記述していきます。

変更内容

最終的なディレクトリ構成は以下です。

.
├── README.md
├── create_tf_bucket.sh
├── dev
│   └── main.tf
├── modules
│   ├── bastion.tf
│   ├── gke.tf
│   ├── network.tf
│   └── variables.tf
└── prod
    └── main.tf

modules/gke.tf

module "gke" {
  // Modified
  source  = "terraform-google-modules/kubernetes-engine/google//modules/private-cluster"
  version = "v12.1.0"

  grant_registry_access = true
  ip_range_pods         = local.ip_range_pods_name
  ip_range_services     = local.ip_range_services_name
  name                  = local.cluster_name
  network               = module.vpc.network_name
  node_pools = [
    {
      machine_type = "n2-standard-2"
      name         = "default-node-pool"
    }
  ]
  project_id               = var.project_id
  region                   = local.region
  remove_default_node_pool = true
  subnetwork               = local.subnet_name

  // Added
  enable_private_nodes = true
  master_authorized_networks = [{
    cidr_block   = "${module.bastion.ip_address}/32"
    display_name = "Bastion Host"
  }]
}

module は terraform-google-modules/kubernetes-engine/google//modules/private-cluster を指定します。 private node はベータ機能なので public-cluster module では有効化できないみたいです。

次に master_authorized_networks で後ほど作成する踏み台サーバーの IP アドレスを指定することで、踏み台からのみ Master にアクセス可能になります。

modules/network.tf

module "vpc" {
  source  = "terraform-google-modules/network/google"
  version = "~> 2.5"

  network_name = local.network_name
  project_id   = var.project_id
  secondary_ranges = {
    (local.subnet_name) = [
      {
        range_name    = local.ip_range_pods_name
        ip_cidr_range = "10.1.0.0/17"
      },
      {
        range_name    = local.ip_range_services_name
        ip_cidr_range = "192.168.64.0/18"
      },
    ]
  }
  subnets = [
    {
      subnet_name   = local.subnet_name
      subnet_ip     = "10.2.0.0/17"
      subnet_region = local.region
    },
  ]
}

// Added
module "cloud-nat" {
  source  = "terraform-google-modules/cloud-nat/google"
  version = "~> 1.2"

  project_id    = var.project_id
  region        = local.region
  router        = "safer-router"
  network       = module.vpc.network_self_link
  create_router = true
}

Node に External IP が付与されなくなったので クラスタ内からインターネットへアクセスするためには Cloud NAT を使う必要あります。 そこで terraform-google-modules/cloud-nat/google module を定義しています。

bastion.tf

data "template_file" "startup_script" {
  template = <<-EOF
    sudo apt-get update -y
    sudo apt-get install -y tinyproxy
    EOF
}

module "bastion" {
  source  = "terraform-google-modules/bastion-host/google"
  version = "~> 2.0"

  host_project   = var.project_id
  image_family   = "debian-9"
  image_project  = "debian-cloud"
  machine_type   = "g1-small"
  members        = var.bastion_members
  name           = local.bastion_name
  network        = module.vpc.network_self_link
  project        = var.project_id
  shielded_vm    = "false"
  startup_script = data.template_file.startup_script.rendered
  subnet         = module.vpc.subnets_self_links[0]
  zone           = local.bastion_zone
}

GKE Mater へアクセスするための踏み台になる VM を定義しています。 terraform-google-modules/bastion-host/google module を使うことでかんたんに IAP を利用することができ、間接的に GKE Master へのアクセスを IAM ベースで制御できるようになります。特別な要件はないので各項目はほとんど変更していません。

便利なのですが、IAM ベースでのアクセスは踏み台をたてなくてもできるようになってほしい

variables.tf

// common
variable "project_id" {
  type = string
}
variable "env" {
  type = string
}
locals {
  region = "asia-northeast1"
}

// network
locals {
  network_name           = "sample-vpc"
  subnet_name            = "sample-subnet"
  ip_range_pods_name     = "ip-range-pods"
  ip_range_services_name = "ip-range-services"
}

// bastion
variable "bastion_members" {
  type = list(string)
}
locals {
  bastion_name = format("%s-bastion", local.cluster_name)
  bastion_zone = format("%s-b", local.region)
}

// gke
locals {
  cluster_name = format("minimum-private-cluster-%s", var.env)
}

最後にこれまでの変数をまとめたファイルを作成します。

デプロイ

複数環境を想定しているので dev/ ディレクトリを例にデプロイしていきます。

<YOUR_PROJECT_ID> を適宜 GCP プロジェクトに置き換えてください。

まずは state ファイルを格納しておく GCP バケットを作成します。

$ ./create_tf_bucket.sh <YOUR_PROJECT_ID>

次にここまで記述してきた module を呼び出すメインファイルを記述します。

dev/main.tf

<YOUR_EMAIL_ADDRESS> は自身のものに置き換えてください。

terraform {
  backend "gcs" {
    // tfstate-${GCP_PROJECT_ID}
    bucket = "tfstate-<YOUR_PROJECT_ID>"
  }
}

module "public-cluster" {
  source     = "../modules"
  env        = "dev"
  project_id = "<YOUR_PROJECT_ID>"
  bastion_members = [
    "user:<YOUR_EMAIL_ADDRESS>"
  ]
}

terraform コマンドでデプロイ

$ cd dev
$ terraform init
...
$ terraform apply
...

GKE Master への疎通を確認します。

$ export PROJECT_ID=<YOUR_PROJECT_ID>
$ export REGION=asia-northeast1

$ gcloud container clusters get-credentials \
    --project $PROJECT_ID --region $REGION \
    --internal-ip minimum-private-cluster-dev
$ gcloud beta compute ssh minimum-private-cluster-dev-bastion \
    --tunnel-through-iap --project $PROJECT_ID --zone $REGION-b \
    -- -L8888:127.0.0.1:8888
$ HTTPS_PROXY=localhost:8888 kubectl get pods --all-namespaces
...

Pod の一覧が表示されれば完成です。

ちなみに proxy なしだとアクセスにちゃんと失敗します。

$ kubectl get pods --all-namespaces
Unable to connect to the server: net/http: TLS handshake timeout

まとめ

  1. private cluster にした
    • Node に External IP が付与されないようになった
      • 外部IPの料金を節約できる
      • セキュリティ向上
    • インターネットへアクセスするためには Cloud NAT が必要
  2. master authorized networks を設定する
    • 指定されたネットワークからのみ Master にアクセスできるようになる
      • セキュリティ向上
    • IAP踏み台サーバーが必要

より詳しい仕組みに関しては、もう少しGKEの構成が固まったらまとめようと思います。

最小構成ではじめる GKE + Terraform

f:id:kaito2-2:20201201031557p:plain

なぜ書いたか

現在はGKEの構成管理に Terraform Resource を直接記述しているのですが、公式の terraform-google-kubernetes-engine への移行を検討しています。 そこで、検証のために最小構成からはじめて徐々にプロダクションレディなクラスタに近づけていく試みの第一歩にできたらと思っています。

github.com

tl;dr

良いからコードを見せろという方は以下のレポジトリを参照してください。

github.com

本編

動作環境

とりあえず動かしてみる

作るものたち

  • VPC
    • 今回は説明を割愛
  • GKE cluster
$ mkdir minimum_public_01
$ cd minimum_public_01

main.tf を作成

// common
locals {
  project_id = "<YOUR_PROJECT_ID>"
  region     = "asia-northeast1"
}

// network
locals {
  network_name           = "sample-vpc"
  subnet_name            = "sample-subnet"
  ip_range_pods_name     = "ip-range-pods"
  ip_range_services_name = "ip-range-services"
}

module "vpc" {
  source       = "terraform-google-modules/network/google"
  version      = "~> 2.5"
  project_id   = local.project_id
  network_name = local.network_name
  subnets = [
    {
      subnet_name   = local.subnet_name
      subnet_ip     = "10.0.0.0/17"
      subnet_region = local.region
    },
  ]

  secondary_ranges = {
    (local.subnet_name) = [
      {
        range_name    = local.ip_range_pods_name
        ip_cidr_range = "10.1.0.0/17"
      },
      {
        range_name    = local.ip_range_services_name
        ip_cidr_range = "192.168.64.0/18"
      }
    ]
  }
}

module "gke" {
  source            = "terraform-google-modules/kubernetes-engine/google"
  version           = "v12.1.0"
  project_id        = local.project_id
  name              = "minimum-configuration-cluster"
  region            = local.region
  network           = module.vpc.network_name
  subnetwork        = local.subnet_name
  ip_range_pods     = local.ip_range_pods_name
  ip_range_services = local.ip_range_services_name
}

required な変数以外ほとんど何も設定していないので特筆すべきことは少ないですが、GCPが推奨している Alias IP ranges を使用しています。 Alias IP ranges の主なメリットに関しては以下を参照してください。

cloud.google.com

これだけで terraform コマンドでデプロイできます。

$ terraform init
...
$ terraform apply
...

かんたん!!

もう少し実用的に

もう少し実際の開発を想定して以下の変更を加えます。

  1. 環境(dev, prod etc.)を分けられるように
  2. GCS に state file を置くように
  3. クラスタ作成時に自動で作成される default node pool を自動で削除

ディレクトリ構成

上記の変更のためにディレクトリ構成は以下のようにしました。

.
├── README.md
├── create_tf_bucket.sh
├── dev
│   └── main.tf
├── modules
│   ├── gke.tf
│   ├── network.tf
│   └── variables.tf
└── prod
    └── main.tf
変更内容

複数環境(GCPプロジェクト)にデプロイできるように module を利用します。

具体的には以下の3ファイルからなる module を作成

modules/gke.tf

module "gke" {
  source  = "terraform-google-modules/kubernetes-engine/google"
  version = "v12.1.0"

  grant_registry_access = true
  ip_range_pods         = local.ip_range_pods_name
  ip_range_services     = local.ip_range_services_name
  name                  = "minimum-public-cluster-${var.env}"
  network               = module.vpc.network_name
  node_pools = [
    {
      machine_type = "n2-standard-2"
      name         = "default-node-pool"
    }
  ]
  project_id               = var.project_id
  region                   = local.region
  remove_default_node_pool = true
  subnetwork               = local.subnet_name
}

grant_registry_access もちゃっかり有効化されていますが、これを無効にしてしまうとGKEクラスタホスティングされているGCPプロジェクトの Container Registry からイメージをプルできなくなってしまいます。

remove_default_node_pool を有効にして、自動で作成される default node pool を削除し、明示的に node pool を作成する方法が推奨されているのでそちらに合わせています。

github.com

modules/network.tf

module "vpc" {
  source  = "terraform-google-modules/network/google"
  version = "~> 2.5"

  network_name = local.network_name
  project_id   = var.project_id
  secondary_ranges = {
    (local.subnet_name) = [
      {
        range_name    = local.ip_range_pods_name
        ip_cidr_range = "10.1.0.0/17"
      },
      {
        range_name    = local.ip_range_services_name
        ip_cidr_range = "192.168.64.0/18"
      },
    ]
  }
  subnets = [
    {
      subnet_name   = local.subnet_name
      subnet_ip     = "10.0.0.0/17"
      subnet_region = local.region
    },
  ]
}

modules/variables.tf

// common
variable "project_id" {}
locals {
  region = "asia-northeast1"
}

// network
locals {
  network_name           = "sample-vpc"
  subnet_name            = "sample-subnet"
  ip_range_pods_name     = "ip-range-pods"
  ip_range_services_name = "ip-range-services"
}

最初の構成から内容自体は変わっていませんが、 ファイルを分割して、 project_id, env を変数化しました。 このモジュールを各環境のディレクトリから呼び出します。

dev/main.tf

terraform {
  backend "gcs" {
    # tfstate-${GCP_PROJECT_ID}
    bucket = "tfstate-<YOUR_PROJECT_ID>"
  }
}

module "public-cluster" {
  source     = "../modules"
  env        = "dev"
  project_id = "<YOUR_PROJECT_ID>"
}

dev/main.tf では module の呼び出しと、 state を格納するGCSバケットを指定しています。

<YOUR_PROJECT_ID> は自分の GCPプロジェクトのIDに置き換えてください。

state を格納するGCSバケット自体は手動で作成する必要があるので、 create_tf_bucket.sh を用意します(スクリプト化しておかないと忘れる)。

create_tf_bucket.sh

#!/bin/sh -e

if [ $# -ne 1 ]; then
  cat << EOF
Usage:
    ${0} <GCP_PROJECT_ID>
EOF
  exit 1
fi

readonly GCP_PROJECT=$1

gsutil mb -p "${GCP_PROJECT}" -c multi_regional -l asia "gs://tfstate-${GCP_PROJECT}/"

ファイルが揃ったので開発環境(想定のGCPプロジェクト)にデプロイしてみます。

$ ./create_tf_bucket.sh <YOUR_PROJECT_ID>
$ cd dev
$ terraform init
...
$ terraform apply
...

これで terraform-google-kubernetes-engine モジュールを使った最少設定のクラスタが出来上がりました。 terraform-google-kubernetes-engine モジュールには他にもたくさんのパラメータがあるので、本番環境で GKE を運用するためにはどのような設定にすべきかをここから検討してけたらと思います。

CKAD を受けてきた。

f:id:kaito2-2:20201026212920p:plain
Certificate の一部

昨日 Certified Kubernetes Application Developer (CKAD) を取得しました! n番煎じですが、やったことを振り返るという意味でもCKADの受験記を書いておきます。

背景

k8sはそこそこ触っていたのでコンセプトはざっくり理解していたおり、ググりながらなら典型的な操作はできる状態でした。

一方で、典型的な REST API や Batch Job ばかり作ってきたので、より網羅的に k8s を勉強するモチベーションの一つとして受けました。

試験までにやったこと

意識したこと

↑を意識しつつあとはひたすら演習した。

結果

66% が合格ラインで僕のスコアは 80% でした。

感想

正直もう少し良いスコアが取れると思っていました。 Mock ExamCKAD-exercises よりも fluentd などのミドルウェアに関する指定などが細かい問題があり、手間取ってしまったことが響いたと思われます…。

スコアは少し残念でしたが、全体を通して普段GKEがやってくれているログに関する操作や、PersistentVolumeなどのあまり使ってこなかったリソースに関して理解を深めることができたのは個人的に良かったです。

次は CKA かな!

Terraform で GCP のサービスアカウントを管理する

なぜ書いたか

Terraform Google Provider のIAM周りのリソースはたくさんある。

  • google_project_iam_policy
  • google_project_iam_binding
  • google_project_iam_member
  • google_service_account_iam_policy
  • google_service_account_iam_binding
  • google_service_account_iam_member
  • google_cloud_run_service_iam_policy
  • ...

たくさんある。

「手動でぽちぽちサービスアカウントを作るのも嫌だけど、Terraform のIAMも怖いし…」という気持ちになったので、これを機に整理してみます。もし間違いなどがありましたら、指摘していただけるととても喜びます。

やりたいこと

Terraform で任意の権限を付与した GCP のサービスアカウントを作成(管理)したい。

tl;dr

特定サービス(の全リソース)に対する権限を付与したい

例として、 sample というサービスアカウントに Cloud Run管理者(roles/run.admin) の権限を付与します。

resource "google_service_account" "sample" {
  project      = "<YOUR_PROJECT_ID>"
  account_id   = "sample"
  display_name = "Sample Service Account"
}

resource "google_project_iam_member" "sample" {
  project = "<YOUR_PROJECT_ID>"
  role    = "roles/run.admin"
  member  = "serviceAccount:${google_service_account.sample.email}"
}

https://i.gyazo.com/41d97cb5b368e823e84b48f0ba6e8301.png

想定通りのサービスアカウントができました。

このように 特定サービスの全リソースに対する権限を付与したい場合は google_project_iam_member を使います。

特定サービスの特定リソースに対しての権限を付与したい

例として、 sample というサービスアカウントに sample-bucket-kaito2 というバケットの管理者権限を付与します。

resource "google_storage_bucket" "sample" {
  project = "<YOUR_PROJECT_ID>"
  name    = "sample-bucket-kaito2"
}

resource "google_service_account" "sample" {
  project      = "<YOUR_PROJECT_ID>"
  account_id   = "sample"
  display_name = "Sample Service Account"
}

resource "google_storage_bucket_iam_member" "sample" {
  bucket = google_storage_bucket.sample.name
  role   = "roles/storage.admin"
  member = "serviceAccount:${google_service_account.sample.email}"
}

コンソールから INFO PANEL を確認すると sample-bucket-kaito2 バケットのみに対して想定通りの権限が付与されています。

Image from Gyazo

このようにサービス内の特定のリソースに対してのみ権限を付与したい場合は、 google_*_iam_member を使用します。

今回は GCS のため、 google_storage_bucket_iam_member でした。

まとめ

おまけ

google_service_account_iam_member がややこしい

google_service_account_iam_member特定サービスの特定リソースに対しての権限を付与したい で説明した google_*_iam_member に該当します。つまり、「特定のサービスアカウントに対しての権限を別のサービスアカウントに付与する」という目的で使います。ややこしいので注意してください。

なぜ *_iam_member を使うのか

IAM policy for projectsIAM policy for Cloud Storage Bucketを見ると、権限を付与するためのリソースとして、

  • *_iam_policy
  • *_iam_binding
  • *_iam_member

のバリエーションがあります。

その中で *_iam_member 以外のリソースは "Authoritative" と記載されており、これらは明示的に設定していないものをApply時に削除するという動作をします。

実際にドキュメントにも以下のような記述があるため 、 "Non-Authoritative" な *_iam_member を使うほうが安全です。

Authoritative. Sets the IAM policy for the project and replaces any existing policy already attached.

参考

Terraform(google provider) で Service Account に Role をバインドするときの罠 - Qiita

Terraform x GCP で、IAM権限を全削除してしまった - Qiita

Terraform Registry