Terraformを用いてVPCネットワークにGKE限定公開クラスタを構成する

この記事は「[Google Cloud Platform Advent Calendar 2019](https://qiita.com/advent-calendar/2019/gcp)」14日目の記事です。

# はじめに
GCPを勉強し始めて一ヶ月程になり、VPCネットワークについてはまだふわふわした理解だったのですが、そろそろ基本や仕様を理解しないとな〜と思い、**Terraform**を利用して**限定公開クラスタ**を作成したことについて投稿します:writing_hand:

## 限定公開クラスタとは？
**[限定公開クラスタ](https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters?hl=ja)**とは、簡単に述べるとプライベートIPで接続できるGKEクラスタです。（そのまんま:bow:）
通常GKEクラスタを作成すると外部IPが作成されインターネット経由でアクセス可能となりますが、オンプレ環境のようにプライベートなクラスタを利用したい場合は限定公開として作成することが可能です。
下記のような特徴を持っています。

- マスターノードにアクセスするトラフィックを**マスター承認済みネットワーク（ホワイトリスト）**によって制御可能です。
- ワーカーノードにはVPCネットワークの内部IPアドレスを用いてアクセス可能です。

### ■ [マスターへのアクセス](https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters?hl=ja#endpoints)について
マスターへの**パブリックエンドポイント**と**マスター承認済みネットワーク**を組み合わせることで、マスターノードへのアクセスを段階的に制御することが可能となります。

組み合わせとして下表のように３パターン存在しています。（詳細については見出しのリンク先をご確認ください。）
今回は(A)の構成で作成しており、内部IPのみでアクセス可能です。

||パブリックエンドポイント|マスター承認済みネットワーク|説明|
|---|---|---|---|---|
|(A)|無効|（自動有効）|事前定義した内部IPアドレスのみアクセス可能|
|(B)|有効|有効|事前定義した内部/外部IPアドレスがアクセス可能|
|(C)|有効|無効|全ての任意のIPアドレスからアクセス可能|

## Terraformとは？
**[Terraform](https://www.terraform.io/)**はHashiCorp社が提供しているインフラストラクチャをコードとして管理するための**IaC**に分類されるツールです。インフラをコードとして管理可能になるためバージョン管理や設計レビューなどが容易になります。他にもコマンド実行でインフラ環境が構築されるため自動化にも利用しやすい、複雑なリソースの依存関係を解決してくれるなど様々なメリットがあります。

### ■ Terraformによるインフラ定義
Terraformでは**HCL(HashiCorp Configuration Language)**でインフラを定義します。
下記はGCEインスタンスを作成する例です。コマンド叩くとGCEインスタンスがサクッと作成されちゃいます。

```terraform:gce.tf
resource "google_compute_instance" "default" {
  name         = "test"
  machine_type = "n1-standard-1"
  zone         = "us-central1-a"
  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-9"
    }
  }

  // Local SSD disk
  scratch_disk {
    interface = "SCSI"
  }

  network_interface {
    network = "default"

    access_config {
      // Ephemeral IP
    }
  }

  service_account {
    scopes = ["userinfo-email", "compute-ro", "storage-ro"]
  }
}
```

## 作成するネットワーク構成
今回は下図のようなネットワークを構築しました。外部公開インスタンスを踏み台にしていますが、通常はCloud VPNやCloud Interconnectを利用するのかな、と思います。GKEクラスタのネットワークが自分には難しくて間違っていたらすみません:sweat_smile:

![5.cluster-network.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525869/95380ca6-8a24-ad15-032a-6a6134d6f44b.png)

# 構築手順
ここからは長々となりますが、Terraformの設定手順を記載します。

1. 準備
2. ネットワークの作成
3. パブリックインスタンスの作成
4. プライベートインスタンスの作成
5. 限定公開クラスタの作成

※所々Tipsのように書いた説明は、ハマったポイントなので記載しました:scream:

## 環境
- MacOS 10.15.2
- Google Cloud SDK 273.0.0
- tfenv 1.0.2
- terraform v0.12.17

## 1.準備
### Terraformのインストール
手順は省略しますが、**tfenv**経由でインストールしました。

### GCPの準備
GCPプロジェクトを作成して、課金アカウントを設定します。
今回利用する**GCE/GKE/DNSに関するAPI**を有効にします。

```bash
$ gcloud config set project $PROJECT_ID
$ gcloud services enable \
  compute.googleapis.com \
  container.googleapis.com \
  dns.googleapis.com
```

## 2. ネットワークの作成
基本となるVPCネットワーク`my-pub-nw`、`my-priv-nw`を作成し、VPCピアリングを設定します。

![2.network.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525869/4e1d04e4-7d93-d444-9f6f-b433629eea81.png)

### 2-1. ネットワーク作成
サブネットは自動作成させないために`auto_create_subnetworks=false`を指定しています。
外部公開インスタンス用のVPCネットワーク`my-pub-nw`を作成します。

```hcl:my_pub_nw.tf
resource "google_compute_network" "my_pub_nw" {
  name                    = var.my_pub_nw_name
  project                 = var.project
  auto_create_subnetworks = false 
}
```

限定公開インスタンス/クラスタ用のVPCネットワーク`my-priv-nw`を作成します。

```terraform:my_priv_nw.tf
resource "google_compute_network" "my_priv_nw" {
  name                    = var.my_priv_nw_name
  project                 = var.project
  auto_create_subnetworks = false 
}
```

### 2-2. ファイアウォール設定
内部IPアドレス`10.0.0.0/8`からの受信パケットを全て許可するファイアウォールを設定します。
（デフォルトネットワークの`default-allow-internal`と同様のもの）

```terraform:my_pub_nw_firewall.tf
resource "google_compute_firewall" "my_pub_nw_allow_ingress_internal" {
  name          = "my-pub-nw-allow-ingress-internal"
  network       = google_compute_network.my_pub_nw.self_link
  source_ranges = ["10.0.0.0/8"]

  allow {
    protocol = "icmp"
  }

  allow {
    protocol = "tcp"
    ports    = ["0-65535"]
  }

  allow {
    protocol = "udp"
    ports    = ["0-65535"]
  }
}
```

---
#### ■ [暗黙のファイアウォールルール](https://cloud.google.com/vpc/docs/firewalls?hl=ja#default_firewall_rules)について
Cloud Consoleなどの画面上には表示されませんが、暗黙のファイアウォールルールが存在しています。
これらのルールは優先度が限りなく低く設定されているので（65535）、優先度が高い（数値の小さい）ルールでオーバライドが可能です。

- **暗黙の下り許可ルール（EGRESS)**:
　すべてのインスタンスは、任意の宛先にトラフィックを送信できます。
- **暗黙の上り拒否ルール（INGRESS）**:
　受信トラフィックをブロックすることによって、すべてのインスタンスが保護されます。


#### ■ [defaultネットワークの事前設定ルール](https://cloud.google.com/vpc/docs/firewalls?hl=ja#more_rules_default_vpc)について
デフォルトネットワーク`default`に事前設定されるファイアウォールルールの中に`default-allow-internal`があります。
これは事前定義のIPアドレス範囲`10.128.0.0/9`に対してすべてのプロトコルとポートの上り接続を許可し、インスタンス間のトラフィックを受信します。

--- 

### 2-3. VPCピアリング
`my-pub-nw`,`my-priv-nw`間のVPCピアリングを設定します。
双方向に設定することではじめて利用可能となります。（片方だけだと待機状態）

```terraform:my_peering.tf
resource "google_compute_network_peering" "my_pub_priv_peering" {
  name         = "my-pub-priv-peering"
  network      = google_compute_network.my_pub_nw.self_link
  peer_network = google_compute_network.my_priv_nw.self_link
}

resource "google_compute_network_peering" "my_priv_pub_peering" {
  name         = "my-priv-pub-peering"
  network      = google_compute_network.my_priv_nw.self_link
  peer_network = google_compute_network.my_pub_nw.self_link
}

```

### 2-４. DNS設定
限定公開ネットワークからGoogle APIs、Container RegistryにアクセスするためのDNS設定を追加します。
具体的には`*.googleapis.com`と`*.gcr.io`を`restricted.googleapis.com`の`199.36.153.4/30)`に解決させます。

```terraform:google-apis.tf
resource "google_dns_record_set" "google_apis_cname" {
  project      = var.project
  managed_zone = "google-apis"
  name         = "*.${google_dns_managed_zone.google_apis.dns_name}"
  type         = "CNAME"
  ttl          = 300

  rrdatas      = ["restricted.googleapis.com."]
}

resource "google_dns_record_set" "google_apis_a" {
  project      = var.project
  managed_zone = "google-apis"
  name         = "restricted.googleapis.com."
  type         = "A"
  ttl          = 300

  rrdatas      = ["199.36.153.4","199.36.153.5","199.36.153.6","199.36.153.7"]
}

resource "google_dns_managed_zone" "google_apis" {
  project    = var.project
  name       = "google-apis"
  dns_name   = "googleapis.com."
  visibility = "private"

  private_visibility_config {
    networks {
      network_url = var.my_priv_nw_url
    }
  }
}
```

---
#### ■ [GKE 限定公開クラスタのContainerRegistryの設定](https://cloud.google.com/vpc-service-controls/docs/set-up-gke?hl=ja)について
限定公開のクラスタがGCRを利用する場合はDNSの設定が必要となります。
`*.gcr.io`宛ての通信を`199.36.153.4/30`に解決させることでGCRへアクセス可能となります。

---

```terraform:gcr-io.tf
resource "google_dns_record_set" "gcr-io-cname" {
  project      = var.project
  managed_zone = "gcr-io"
  name         = "*.${google_dns_managed_zone.gcr_io.dns_name}"
  type         = "CNAME"
  ttl          = 300

  rrdatas      = ["gcr.io."]
}

resource "google_dns_record_set" "gcr-io-a" {
  project      = var.project
  managed_zone = "gcr-io"
  name         = "gcr.io."
  type         = "A"
  ttl          = 300

  rrdatas      = ["199.36.153.4","199.36.153.5","199.36.153.6","199.36.153.7"]
}

resource "google_dns_managed_zone" "gcr_io" {
  project    = var.project
  name       = "gcr-io"
  dns_name   = "gcr.io."
  visibility = "private"

  private_visibility_config {
    networks {
      network_url = var.my_priv_nw_url
    }
  }
}
```

---

#### ■ [オンプレミス ホスト用の限定公開の Google アクセスの構成](https://cloud.google.com/vpc/docs/configure-private-google-access-hybrid?hl=ja#configuring-dns)について
限定公開のクラスタがGoogle APIsを利用する場合はDNSの設定が必要となります。
`*.googleapis.com`宛のすべてのリクエストを`199.36.153.4/30`に解決させることでGoogle APIsにアクセス可能となります。

---

## 3. パブリックインスタンスの作成
続いて、外部公開用のインスタンスを作成します。
![3.public-instance.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525869/31a0693d-a08c-b0f4-a710-18fc73d60a77.png)

### 3-1. サブネットの設定
外部公開インスタンスを接続するためのサブネット`my-pub-subnet`を作成します。

```terraform:my_pub_nw.tf
resource "google_compute_subnetwork" "my_pub_subnet" {
  name          = var.my_pub_subnet_name
  ip_cidr_range = var.my_pub_subnet_ip_cidr
  region        = var.region
  network       = google_compute_network.my_pub_nw.self_link
}
```

### 3-２. 外部公開インスタンスの作成
外部公開インスタンス`my-pub-instance`を作成します。
`google_compute_address`リソースで静的IPアドレスを作成しインスタンスのNICに適用します。
スコープには`cloud-platform`を指定しており（理由は後述）、これによりCloud SDKのコマンドによるGoogle APIが利用可能となります。

```terraform:my_pub_instance.tf
resource "google_compute_instance" "my_pub_instance" {
  project       = var.project
  zone          = var.zone
  name          = var.my_pub_instance_name # "my-pub-instance"
  machine_type  = var.machine_type

  tags = [var.my_pub_instance_name]

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-9"
    }
  }
  
  network_interface {
    subnetwork         = var.my_pub_subnet_name
    subnetwork_project = var.project
    access_config {
      nat_ip = google_compute_address.static_ip.address # 下記の外部IPを指定
    }
  }

  service_account {
    scopes = ["cloud-platform"]
  }
}   

resource "google_compute_address" "static_ip" { # 外部IPアドレス
  name = "my-pub-instance-address"
}
```

---

#### ■ [アクセススコープ](https://cloud.google.com/compute/docs/access/service-accounts?hl=ja#associating_a_service_account_to_an_instance)について
アクセススコープは、インスタンスの権限を指定するレガシーな方法で、
現在は完全な権限アクセススコープである`cloud-platform`を設定しサービスアカウントに適切なIAM役割を付与してアクセス制御することが推奨されているようです。
また、作成したインスタンスには下記のアクセススコープが自動的に構成されています。

```
- Google Cloud Storage に対する読み取り専用アクセス権
- Compute Engine ログに書き込むための書き込みアクセス権
- Google Cloud プロジェクトに指標データを公開するための書き込みアクセス権
- Google Cloud Endpoints に必要な Service Management 機能に対する読み取り専用アクセス権（アルファ版）
- Google Cloud Endpoints に必要な Service Control 機能に対する読み取り書き込みアクセス権（アルファ版）
```

---

### 3-3. ファイアウォールの設定
外部公開インスタンスへのアクセス`tcp:22`、`icmp`を許可します。

```terraform:my_pub_nw_firewall.tf
resource "google_compute_firewall" "my_pub_nw_allow_ingress_my_instance" {
  name    = "my-pub-nw-allow-ingress-my-instance"
  network = google_compute_network.my_pub_nw.self_link

  allow {
    protocol = "icmp"
  }

  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  target_tags = ["my-pub-instance"]
}
```

これで、外部公開インスタンスの作成は完了です。

## 4. 限定公開インスタンスの作成
続いて、限定公開用のインスタンスを作成します。

![4,private-instance.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525869/190607f8-478b-59ee-21da-bfb87513c438.png)


### 4-1. サブネットの作成
限定公開用のインスタンス/クラスタを接続するサブネット`my-priv-subnet`を作成します。
内部IPからAPIを利用するため`private_ip_google_access = true`を指定します。
GKE用のセカンダリIP範囲を`secondary_ip_range`で指定しておきます。

```terraform:my_priv_nw.tf
resource "google_compute_subnetwork" "my_priv_subnet" {
  name                     = var.my_priv_subnet_name
  ip_cidr_range            = var.my_priv_subnet_ip_cidr
  region                   = var.region
  network                  = google_compute_network.my_priv_nw.self_link
  private_ip_google_access = true
  secondary_ip_range {
    range_name    = "services"
    ip_cidr_range = var.my_priv_subnet_2nd_services_ip_cidr
  }
  secondary_ip_range {
    range_name    = "pods"
    ip_cidr_range = var.my_priv_subnet_2nd_pods_ip_cidr
  }
}
```

---

#### ■ [セカンダリIP範囲](https://cloud.google.com/vpc/docs/alias-ip?hl=ja#subnet_primary_and_secondary_cidr_ranges)について
サブネットに対して複数のIP範囲を持たせることが可能です。プライマリに設定したものがサブネットのデフォルトのIP範囲として利用されますが、セカンダリに指定したIP範囲は今回のようにKubernetesのポッドやサービスのIPアドレスに割り当てることが可能となります。

---

### 4-２. プライベートインスタンスの作成
プライベートインスタンス`my-priv-instance`を作成します。

```terraform:my_priv_instance.tf
resource "google_compute_instance" "my_priv_instance" {
  project       = var.project
  zone          = var.zone
  name          = var.my_priv_instance_name
  machine_type  = var.machine_type

  tags = [var.my_priv_instance_name]

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-9"
    }
  }
  
  network_interface {
    subnetwork         = var.my_priv_subnet_name
    subnetwork_project = var.project
  }

  service_account {
    scopes = ["cloud-platform"]
  }

  scheduling {
    automatic_restart = false
    preemptible       = true
  }
  metadata_startup_script = "apt-get install kubectl"
}   
```

#### 4−３. ファイアウォールの設定
外部公開ネットワーク`10.10.10.0/24`からの`icmp`と`tcp:22,80,443`を許可します。

```terraform:my_priv_nw_firewall.tf
resource "google_compute_firewall" "my_priv_instance_allow_ingress_my_pub_instance" {
  name          = "my-priv-instance-allow-ingress-my-pub-instance"
  network       = google_compute_network.my_priv_nw.self_link
  source_ranges = ["10.10.10.0/24"]
  target_tags   = ["my-priv-instance"]

  allow {
    protocol = "icmp"
  }

  allow {
    protocol = "tcp"
    ports    = ["22", "80", "443"]
  }
}
```

## 5. 限定公開クラスタの作成
いよいよ限定公開クラスタを作成します。

![5.cluster-network.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525869/95380ca6-8a24-ad15-032a-6a6134d6f44b.png)

### 5-1. ファイアウォールの設定
下記のファイアウォールを設定することで、GCRからコンテナイメージをpullできるようになります。
設定に失敗するとポッドのコンテナが立ち上がりません。。

```terraform:my_priv_nw_firewall.tf
resource "google_compute_firewall" "my_priv_nw_allow_egress_google_apis" {
  name               = "my-priv-nw-allow-egress-google-apis"
  network            = google_compute_network.my_priv_nw.self_link
  direction          = "EGRESS"
  destination_ranges = ["199.36.153.4/30"]

  allow {
    protocol = "all"
  }
}
```

また、下記の設定はクラスタを作成する前に作成しておく必要があるようです。
（ワーカーノードが接続する`my_priv_nw`からマスターノードへトラフィックが許可されないため）

```terraform:my_priv_nw_firewall.tf
resource "google_compute_firewall" "my_priv_nw_allow_egress_masternode" {
  name               = "my-priv-nw-allow-egress-masternode"
  network            = google_compute_network.my_priv_nw.self_link
  direction          = "EGRESS"
  destination_ranges = [var.my_priv_cluster_master_ip_cidr] # "172.16.0.0/28"

  allow {
    protocol = "tcp"
    ports    = ["443", "10250"]
  }
}
```

### 5-２. クラスタの作成
限定公開クラスタ`my-priv-cluster`を作成します。今回の肝ですので各blockを確認します。

#### private_cluster_configブロック
private_cluster_configブロックは、限定公開の設定となります。

- **enable_private_nodes**: 各ノードのパブリックIPを無効化します。
- **enable_private_endpoint**: マスターノードのパブリックエンドポイントを無効化します。
- **master_ipv4_cidr_block**: マスターノードに割り当てるIP範囲を指定しています。

#### ip_allocation_policyブロック
ip_allocation_policyブロックは、IP割当設定になります。サブネット作成時に作成したセカンダリIP範囲を利用しています。

- **cluster_secondary_range_name**: クラスタのポッドに割り当てるIP範囲を指定します。
- **services_secondary_range_name**: クラスタのサービスドに割り当てるIP範囲を指定します。

#### master_authorized_networks_configブロック
master_authorized_networks_configブロックはマスター承認済みネットワークに関する設定となります。
ここでは`my-priv-instance`が接続されている`my-priv-subnet`を指定しています。

```terraform:my_priv_cluster.tf
resource "google_container_cluster" "primary" {
  name               = var.my_priv_cluster_name
  location           = var.zone
  initial_node_count = 3
  network            = "my-priv-nw"
  subnetwork         = "my-priv-subnet"

  private_cluster_config {
    enable_private_nodes    = true
    enable_private_endpoint = true
    master_ipv4_cidr_block  = var.my_priv_cluster_master_ip_cidr # "172.16.0.0/28"
  }

  ip_allocation_policy {
    cluster_secondary_range_name   = "pods"
    services_secondary_range_name  = "services"
  }

  master_authorized_networks_config {
    cidr_blocks {
      cidr_block   = var.my_priv_subnet_ip_cidr
      display_name = "net1"
    }
  }
}
```

---

### ■ [マスター認証済ネットワークの設定](https://cloud.google.com/kubernetes-engine/docs/how-to/authorized-networks?hl=ja)について
マスターノードのエンドポイントにアクセスするIP範囲をホワイトリストとして登録することで、登録されていないIPからのトラフィックを制御することが可能となります。

---

以上の手順で限定公開クラスタの作成完了です。
クラスタには内部IPアドレス経由のアクセスのみ可能となっています。

# まとめ
今回は**Terraform**を利用して**GKE限定公開クラスタ**を作成してみました。
多少複雑な点もいくつかありましたが、インフラ経験一週間の私でもお手軽に構成することができました。
限定公開クラスタが必要となるケースは業務でもあるかと思いますので、その際は是非利用してみてはいかがでしょうか。

個人的にはGKE（と言うよりKubernetes）に対する理解不足によりハマったことが多かったのでこれからも勉強します。:muscle:

# 参考
[インターネット接続なしの完全にプライベートなGKEクラスター](https://qiita.com/baby-degu/items/c7cdeef0c91b059a41df)
[GKE/Kubernetes でなぜ Pod と通信できるのか](https://qiita.com/apstndb/items/9d13230c666db80e74d0)