Terraform + kubectl (kustomize) + GKEでDjangoデプロイ

今回ありがたいことに業務中にタイトル記載の技術をキャッチアップできる時間を設けてもらったので、その整理のための記事です。

# この記事でできるようになること
- Terraformを使用したGCPリソース作成がちょっとできるようになる
- kubectl と kustomizeを使用したGKEクラスターのリソース管理がちょっとできるようになる
- コンテナデプロイがなんとなく分かる
- Workload Identityがちょっとわかる

# 今回使用するコード
- **Django**
  - https://github.com/butterthon-dev/dev-app
- **Terraform**
  - https://github.com/butterthon-dev/dev-app-tf
- **k8s**
  - https://github.com/butterthon-dev/dev-app-k8s



# 最終的な構成
※ ロードバランサやカスタムドメインは使用しません。
&emsp;アプリケーションのプロトコルもHTTPです。（HTTPS化はしません）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/292a792f-cffb-e4b8-2f9a-51211912efa8.png)

# ローカルでDjangoアプリを確認（startproject, startappの過程は割愛）
- python: 3.9.1
- Django: 4.0.5
- django-environ（.env用）
※ pyenvで仮想環境用意した場合は`pip install --upgrade pip`でpipを更新するの忘れないように

### クローンしたらdev-app/entrypoint.shに実行権限を付与
``` shell
cd dev-app
chmod +x entrypoint.sh
```

### コンテナを起動してブラウザからアクセス
``` shell
docker-compose up -d
```
http://0.0.0.0:8000 にアクセスして下記画面が表示されればOKです。
マイグレーション実行時、サンプルのテーブルにレコードを1件登録するようにしています。
画面に表示されている`example-user@example.com`はそのテーブルから取得したレコードの値です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/030f4be0-e3b5-7edd-a79d-91727af25284.png)

<br />

# Terraformで各種GCPリソース作成
::: note warn
GCPプロジェクトの作成や、terraform/tfenvのインストール方法などは割愛します
:::

### Terraform用のフォルダを作成
``` shell
mkdir dev-app-tf
```

### Terraformで使用するサービスアカウントを作成
[GCPコンソール] --> [サイドバー] --> [IAMと管理] --> [サービスアカウント]
1. 画面上部の「＋サービスアカウントを作成」ボタン押下
    a. サービスアカウント名：dev-app-tf
    b. サービスアカウントID：自動入力
    c. サービスアカウントの説明：適当に入力（空でも良い）
2. 「作成して続行」ボタン押下
3. ロールは下記を選択して「続行」ボタン押下
    ・ Artifact Registry 管理者
    ・ Cloud SQL 管理者
    ・ サービスアカウントの作成
    ・ Project IAM 管理者
    ・ Kubernetes Engine Cluster 管理者
    ・ サービス アカウント ユーザ
    ・ Compute 閲覧者
    ・ Secret Manager 管理者
    ※ ロールは後から着け外しできるので、後続の手順で403エラーが発生したら適宜ロールを見直してください。
4. ユーザロールや管理者ロールも入力せずに「完了」ボタン押下
TerraformでGCPリソースを操作するためのサービスアカウントを作成できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/1604571c-a028-b53a-f746-148cc23dad1d.png)

5. 対象サービスアカウント行の右側にある縦の三点リーダより、「鍵を管理」選択
6. 「鍵を追加」ボタン押下
7. 「新しい鍵を作成」選択
8. キーのタイプは「JSON」を選択
9. 「作成」ボタン押下
JSONファイルがダウンロードされるので、そのファイルを`dev-app-tf`フォルダ直下に移動
``` diff
- このJSONファイルは絶対にGithubをはじめとする公開サイトにアップロードしないでください。不正利用される可能性があります。
```

### 環境変数`GOOGLE_APPLICATION_CREDENTIALS`に先ほどダウンロードしたJSONキーファイルの絶対パスを設定
``` shell
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account.json
```

### 変数定義 および シークレット変数への値割り当て
プロジェクトIDやregionなど、繰り返し記述する可能性がある値を変数定義ファイル`variables.tf`に記述します。
こうすることで、その他tfファイルから`var.project_id`や`var.region`で使用できるようになります。
※ 変数の値割り当てを行うまでもないので今回`variables.tfvars`は用意しません。

``` dev-app-tf/variables.tf
variable "project_id" {
    type        = string
    default     = "{ プロジェクトID }" // 例）cloud-learn-dev
    description = "プロジェクトID"
}

variable "region" {
    type    = string
    default = "asia-northeast1"
}

variable "zone" {
    type    = string
    default = "asia-northeast1-a"
}

// シークレット変数
variable "sql_user_password" {
  type      = string
  sensitive = true
}
```

次にシークレット変数への値割り当てファイル`secret.auto.tfvars`作成します。
※ .gitignoreに記述するなどして、Gitリポジトリへのコミットは回避してください。
``` dev-app-tf/secret.auto.tfvars
sql_user_password = "9!4930g8hjre"
```

### GCPプロビジョニングのためのプロバイダー設定
- terraform.tf と terraform-google.tf, terraform-google-beta.tfを作成
``` dev-app-tf/terraform.tf
terraform {
  required_providers {
    google = {
      source = "hashicorp/google"
      version = "4.23.0"
    }
    google-beta = {
      source  = "hashicorp/google-beta"
      version = "4.23.0"
    }
  }
}
```

``` dev-app-tf/terraform-google.tf
provider "google" {
  project   = var.project_id
  region    = var.region
  zone      = var.zone
}
```

``` dev-app-tf/terraform-google-beta.tf
provider "google-beta" {
  project   = var.project_id
  region    = var.region
  zone      = var.zone
}
```

### ディレクトリを初期化
初期化を行うことでtfファイルに記述したプロバイダーをダウンロードする。
 「Terraform has been successfully initialized!」が出れば初期化完了です。
``` shell
# dev-app-tf直下で
terraform init
```

### Dockerイメージ管理用のArtifact Registry構成ファイルを作成
``` diff
- 事前にArtifact Registryを有効化しておいてください（[GCPコンソール] --> [サイドバー] --> [Artifact Registry]）
```
今回はdockerイメージをデプロイするのでformatもDOCKERです。
``` dev-app-tf/artifact-registry.tf
resource "google_artifact_registry_repository" "dev_app__img_repository" {
  provider      = google-beta
  location      = var.region
  repository_id = "dev-app-img-repository"
  format        = "DOCKER"
}
```

applyしましょう。
「Do you want to perform these actions?」と聞かれたらyesと入力してエンターを押下してください。
``` shell
# dev-app-tfの直下で
terraform apply
```
「Apply complete! Resources: ...」と表示されたら成功です。

GCPコンソールからArtifact Registryを開くと、リポジトリが作成されていることを確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/1276db82-a2b6-0ee5-0f18-a8c60449b19d.png)

### データ保存用のCloud SQL構成ファイルを作成
``` diff
- 事前にCloud SQL Adminを有効化しておいてください（[GCPコンソール] --> [サイドバー] --> [SQL]）
- 有効にしてから5分ほど待ってterraform apply実行した方がいいかも
```

``` dev-app-tf/cloud-sql.tf
// Cloud SQL Admin APIを有効にしておく（5分くらい待った方がいいカモ）
// そもそもSQLインスタンスの生成に時間が掛かるので、applyも時間かかる（15分前後）
resource "google_sql_database" "db_dev_app" {
  name      = "db-dev-app"
  instance  = google_sql_database_instance.db_instance_dev_app.id
}

resource "google_sql_database_instance" "db_instance_dev_app" {
  name              = "db-instance-dev-app"
  region            = var.region
  database_version  = "MYSQL_8_0"

  settings {
    tier = "db-f1-micro"
  }

  deletion_protection = false
}

resource "google_sql_user" "sql_user_dev_app" {
  name      = "butterthon"
  instance  = google_sql_database_instance.db_instance_dev_app.name
  password  = var.sql_user_password
}
```

applyしましょう。
完了まで15分くらい掛かります。
``` shell
# dev-app-tf直下で
terraform apply
```

### GKEクラスタ構成ファイルを作成
``` diff
- 事前にCloud Resource Manager APIを有効化しておいてください（[GCPコンソール] --> [検索テキストボックス] --> [Cloud Resource Managerで検索]）
```

重要なのは`workload_identity_config`と`workload_metadata_config`です。
workload_identityは『k8sサービスアカウントをIAMサービスアカウントと紐づけることで、PodからGCPリソースを操作できるようにする仕組み』のことです。
今回PodからCloudSQLに接続する必要があるため、この`workload_identity`および`workload_metadata_config`の指定が必須です。

tfファイル内でIAMサービスアカウントを作成
``` dev-app-tf/k8s-engine.tf
// GKEクラスタのリソース管理で使用するIAMサービスアカウント
resource "google_service_account" "gke_sa_dev_app" {
  account_id    = "gke-sa-dev-app"
  display_name  = "gke-sa-dev-app"
}

# 上記IAMサービスアカウントにロールをいくつか付与
# ・Cloud SQL クライアント
# ・Secret Manager のシークレット アクセサー
# ・Artifact Registry 管理者
# ・ストレージオブジェクト閲覧者
resource "google_project_iam_member" "sa_sql_client_dev_app" {
  project = var.project_id
  role    = "roles/cloudsql.client"
  member  = "serviceAccount:${google_service_account.gke_sa_dev_app.email}"
}
resource "google_project_iam_member" "sa_secret_accessor_dev_app" {
  project = var.project_id
  role    = "roles/secretmanager.secretAccessor"
  member  = "serviceAccount:${google_service_account.gke_sa_dev_app.email}"
}
resource "google_project_iam_member" "sa_artifact_registry_reader_dev_app" {
  project = var.project_id
  role    = "roles/artifactregistry.reader"
  member  = "serviceAccount:${google_service_account.gke_sa_dev_app.email}"
}
resource "google_project_iam_member" "sa_storage_viewer_dev_app" {
  project = var.project_id
  role    = "roles/storage.objectViewer"
  member  = "serviceAccount:${google_service_account.gke_sa_dev_app.email}"
}

/**
 * クラスターの作成に10分くらい時間が掛かる
 * ノードプールの作成に3分くらい時間が掛かる
 */
resource "google_container_cluster" "cluster_dev_app" {
  name      = "cluster-dev-app"
  location  = var.region

  remove_default_node_pool  = true
  initial_node_count        = 1
  timeouts {
    create = "30m"
    update = "40m"
  }

  # Workload Identityを有効化
  workload_identity_config {
    workload_pool = "${var.project}.svc.id.goog"
  }
}

resource "google_container_node_pool" "node_pool_dev_app" {
  name          = "node-pool-dev-app"
  location      = var.region
  cluster       = google_container_cluster.cluster_dev_app.name
  node_count    = 1
  autoscaling {
    min_node_count = 0
    max_node_count = 1
  }

  node_config {
    workload_metadata_config {
      mode = "GKE_METADATA"
    }
    oauth_scopes = [
      "https://www.googleapis.com/auth/devstorage.read_only",
      "https://www.googleapis.com/auth/logging.write",
      "https://www.googleapis.com/auth/monitoring",
      "https://www.googleapis.com/auth/service.management.readonly",
      "https://www.googleapis.com/auth/servicecontrol",
      "https://www.googleapis.com/auth/trace.append"
  }
}
```

applyしましょう。
これまた完了するのに20分くらい掛かります。
``` shell
# dev-app-tf直下で
terraform apply
```

これでGCPリソースの作成は完了です。
実際にコンテナデプロイ作業に入っていきましょう。

<br />

# DjangoアプリのDockerイメージをビルド＆プッシュ
:::note warn
gcloudコマンドのインストール方法などは割愛します
:::
### イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/c96f97c9-fd98-54a4-4503-e35b718a124f.png)

### ビルド&プッシュ
下記コマンドでDockerイメージをビルドしてArtifact Registryにプッシュします。
``` shell
# dev-app直下で
gcloud builds submit --tag asia-northeast1-docker.pkg.dev/cloud-learn-dev/dev-app-img-repository/sample-image:latest
# gcloud builds submit --tag { リージョン }-docker.pkg.dev/{ プロジェクトID }/{ Artifact RegistryのリポジトリID }/{ 任意のイメージ名称 }:{ タグ }
```

Artifact Registryのリポジトリを見てみると、DockerイメージがPUSHされていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/563085b1-02b5-ed51-6947-af95097b4353.png)

これでコンテナデプロイの準備完了です。


# kubectl と kustomizeでGKEクラスターにリソースを作成
リソースの作成は、k8sの構成ファイルカスタマイズツールであるkustomizeを使用します。
kustomizeには`based（ベース）`と`overlays（オーバーレイ）`の概念があります。
`base`&emsp;&emsp;・・・リソースのセット（deployment.ymlやservice.yml）とkustomization.ymlを含むディレクトリ
`overlays`・・・baseを参照するkustomization.ymlを含むディレクトリ
``` shell
.
├── base
│   ├── deployment.yml
│   ├── service.yml
│   └── kustomization.yml
│
└── overlays
    ├── staging
    │   └── kustomization.yml
    │
    └── production
        └── kustomization.yml
```

kustomizeを使うと、overlays（デプロイ環境）ごとの共通部分をbaseとして切り出すことができるようになります。
また、production環境だけPod数を10個にするといったoverlaysごとのカスタマイズも可能です。

kustomizeを使わない場合、環境ごとに似たようなymlファイルを複製し、修正が発生した場合は全てのymlに同じような修正を入れる作業が発生したりします。
kustomizeはそんな面倒も解消してくれるツールです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/428130a5-e956-4c6e-0a57-96d396ad2549.png)

### クラスター認証
kustomizeを使用してGKEクラスタのリソースを管理するためクラスタ認証を行います
``` shell
# 一応dev-app-k8sの直下で
gcloud container clusters get-credentials cluster-dev-app --zone asia-northeast1
# gcloud container clusters get-credentials { Terraformで作成したGKEクラスタの名前 } --zone asia-northeast1
#
# 以下が表示されればOK
# Fetching cluster endpoint and auth data.
# kubeconfig entry generated for cluster-dev-app.
```

### k8sサービスアカウント作成
前述の通り、k8sサービスアカウントはIAMサービスアカウントとは別物です。
なので、IAMサービスアカウントの権限を借用できるようk8sサービスアカウントをTerraformで作成したIAMサービスアカウントに関連づけます。
それが`annotations`です。
``` dev-app-k8s/overlays/staging/sidecar/gke_service_account.yml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: gke-sa-dev-app
  annotations:
    iam.gke.io/gcp-service-account: gke-sa-dev-app@cloud-learn-dev.iam.gserviceaccount.com
```

一旦k8sサービスアカウントだけ作成します。
``` shell
# dev-app-k8sの直下で
kubectl apply -f overlays/staging/sidecar/gke_service_account.yml
```

サービスアカウントが作成されたか`kubectl get sa`で確かめます。
指定した名前でサービスアカウントが作られていればOKです。
``` shell
kubectl get sa
NAME             SECRETS   AGE
default          1         145m
gke-sa-dev-app   1         38s
```

しかし、annotationsで関連付けただけではIAMサービスアカウントの権限借用はできません。
k8sサービスアカウントとIAMサービスアカウントの間に`IAMポリシーバインディング`を追加します。
これでようやくk8sサービスアカウントがIAMサービスアカウントの権限を借用できるようになります。

``` shell
# 一応dev-app-k8sの直下で
gcloud iam service-accounts add-iam-policy-binding \
    > gke-sa-dev-app@cloud-learn-dev.iam.gserviceaccount.com \
    > --role="roles/iam.workloadIdentityUser" \
    > --member="serviceAccount:cloud-learn-dev.svc.id.goog[default/gke-sa-dev-app]"

# gcloud iam service-accounts add-iam-policy-binding \
#    > { Terraformで作成したIAMサービスアカウントのメールアドレス } \
#    > --role roles/iam.workloadIdentityUser \
#    > --member "serviceAccount:{ プロジェクトID }.svc.id.goog[{ NAMESPACE }/{ k8sサービスアカウントの名前（kubectl get saで確認可能） }]"
```

GCPコンソールから、IAMサービスアカウントの権限を見るとk8sサービスアカウントポリシーバインディングされていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/868e362c-2d69-5a53-a352-a1bededa1f35.png)

### 軽い用語説明
kustomizeのマニフェストを作成する前に軽く用語の説明を入れておきます。
- **クラスター**
    - `マスターノード`と`ワーカーノード`で構成されたk8sシステムのこと
- **マスターノード**
    - `ワーカーノード`上のコンテナを管理するだけ
    - マニフェストの内容でワーカーノードを管理してくれる
    - 開発者がマニフェストを変更すると、その内容を勝手にワーカーノードで反映してくれる
- **ワーカーノード**
    - 実際にコンテナが稼働する場所（サーバーに相当するもの）
    - ワーカーノードの調整はマスターノードの仕事なので、開発者が直接操作することはほとんどない
- **Deployment**
    - `Pod`のデプロイ管理
    - 障害などでPodが停止してしまった場合は自動でPodを増やしたりなど、マニフェストに記載された状態を保つ動きをしてくれます（厳密にはPodの増減はReplicaSetが実施します）
    - マニフェストファイルのPod数を書き換えたら、それに従ってPodの数を勝手に増減してくれます
- **Service**
    - `ワーカーノード`内の`Pod`にアクセスを振り分けるロードバランサーみたいなもの
    - あくまで`ワーカーノード`内の`Pod`に振り分けるだけなので、それぞれのワーカーノードへのアクセス振り分けは本物のロードバランサーなどを使用する必要があります


### 各種baseファイルを作成
``` shell
.
├── base
│   ├── deployment.yml
│   ├── service.yml
│   └── kustomization.yml
│
└── overlays
    ...
```

- deployment.yml
``` dev-app-k8s/base/deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dev-app
spec:
  replicas: 3
  template:
    metadata:
      name: dev-app
    spec:
      serviceAccountName: overlaysごとに書き換える
      containers:
        - name: dev-app-pod
          image: asia-northeast1-docker.pkg.dev/cloud-learn-dev/dev-app-img-repository/sample-image:latest
          imagePullPolicy: Always
          env:
            - name: DEBUG
              value: "True"
            - name: DATABASE_URL
              value: "mysql://{ Terraformで作成したSQLユーザ}:{ Terraformで作成したSQLユーザパスワード }@127.0.0.1:3306/db-dev-app?charset=utf8mb4"
            - name: DJANGO_SECRET_KEY
              value: "django-insecure-zgxe_knf8as%_$o%jq$4yw@i5p6f0d8p74&a!avsuvfctu+)(c"
            - name: DJANGO_ALLOWED_HOSTS
              value: "*"
            - name: DJANGO_SETTINGS_MODULE
              value: config.settings.dev
          ports:
            - containerPort: 8000

        - name: cloud-sql-proxy
          image: gcr.io/cloudsql-docker/gce-proxy:1.28.0
          command:
            - overlaysごとに書き換える
          securityContext:
            runAsNonRoot: true
```

<br />

- service.yml
``` dev-app-k8s/base/service.yml
apiVersion: v1
kind: Service
metadata:
  name: dev-app
spec: 
  type: LoadBalancer
  ports:
    - port: 80 # Serviceのポート
      targetPort: 8000 # Podの内部ポート（gnicornで0.0.0.0:8000にバインドしているので、targetPortも8000）
      protocol: TCP # 通信に使うプロトコル
      nodePort: 30000 # ワーカーノードのポート（30000~32767の間であればなんでも良い）
```

<br />

- kustomization.yml
``` dev-app-k8s/base/kustomization.yml
resources:
  - ./deployment.yml
  - ./service.yml
```

### 各種overlaysファイル作成
``` shell
.
├── base
│   ├── ...
│   ├── ..
│   └── .
│
└── overlays
    └── staging
        ├── sidecar
        │   └─ gke_service_account.yml
        |
        ├── patches
        │   └── deployment.yml
        │
        └── kustomization.yml
```
見慣れないpatchesディレクトリがあります（ディレクトリ名はpatchesでなくても良い）。
これはbaseのdeployment.ymlをstaging用に書き換えるためのものです。

<br />

- deployment.yml
    - baseのdeployment.ymlでstaging用に書き換えたいところだけ修正します。
``` dev-app-k8s/overlays/staging/patches/deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dev-app
spec:
  template:
    spec:
      serviceAccountName: stg-gke-sa-dev-app-001 # Terraformで作成したGKEアカウント
      containers:
        - name: cloud-sql-proxy
          command:
            - "/cloud_sql_proxy"
            - "-log_debug_stdout"
            - "-instances=cloud-learn-dev:asia-northeast1:db-instance-dev-app=tcp:0.0.0.0:3306" # -instances={ CloudSQLの接続名 }=tcp:0.0.0.0:3306
            - "-term_timeout=30s"
```

<br />

- kustomization.yml
``` dev-app-k8s/overlays/staging/patches/service.yml
bases:
  - ../../base

namePrefix: "stg-"
nameSuffix: "-001"
commonLabels:
  stg: dev-app

patches:
  - ./patches/deployment.yml
```

<br />

|  |  |
---|---
| bases | baseディレクトリへの相対パス（baseディレクトリ直下のkustomization.ymlを参照する） |
| namePrefix | 全てのmetadata.name（1階層目に限る）に接頭辞を付与 |
| nameSuffix | 全てのmetadata.name（1階層目に限る）に接尾辞を付与 |
| commonLabels | 全てのリソースにラベルを設定する。<br />このラベル付けが個人的に超便利です。`matchLabelsやセレクターラベル`も付けてくれるので、ラベル付けを意識する必要もなければ、ラベル付けを忘れてデプロイがうまくいかないという凡ミスも防げます。<br /><ul><li>matchLabels：特定ラベルのPodをDeploymentが管理するためのラベル付け</li><li>セレクターラベル：特定ラベルのPodをServiceが管理するためのラベル付け</li><ul> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/5e8b36f1-5684-208e-453a-ecb8fa0232c4.png)|

<br />

この状態で`kubectl kustomize ./overlays/staging`を実行して構成ファイルをコンソール出力してみます。
出力結果を確認すると、baseの内容がoverlays/patchesの内容で書き換えられていることが分かります。

<details><summary>出力結果</summary>

``` yml
apiVersion: v1
kind: Service
metadata:
  labels:               # commonLabelsが反映されている
    stg: dev-app        # commonLabelsが反映されている
  name: stg-dev-app-001 # { overlaysのnamePrefix } + { baseのService名 } + { overlaysのnameSuffix }
spec:
  ports:
  - nodePort: 30000
    port: 80
    protocol: TCP
    targetPort: 8000
  selector:      # commonLabelsが反映されている
    stg: dev-app # commonLabelsが反映されている
  type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:               # commonLabelsが反映されている
    stg: dev-app        # commonLabelsが反映されている
  name: stg-dev-app-001 # { overlaysのnamePrefix } + { baseのDeployment名 } + { overlaysのnameSuffix }
spec:
  replicas: 3
  selector:        # commonLabelsが反映されている
    matchLabels:   # commonLabelsが反映されている
      stg: dev-app # commonLabelsが反映されている
  template:
    metadata:
      labels:        # commonLabelsが反映されている
        stg: dev-app # commonLabelsが反映されている
      name: dev-app
    spec:
      containers:
      - command: # overlaysのcommandで上書きされている
        - /cloud_sql_proxy
        - -log_debug_stdout
        - -instances=cloud-learn-dev:asia-northeast1:db-instance-dev-app=tcp:0.0.0.0:3306
        - -term_timeout=30s
        image: gcr.io/cloudsql-docker/gce-proxy:1.28.0
        name: cloud-sql-proxy
        securityContext:
          runAsNonRoot: true
      - env:
        - name: DEBUG
          value: "True"
        - name: DATABASE_URL
          value: mysql://{ Terraformで作成したSQLユーザ}:{ Terraformで作成したSQLユーザパスワード }@127.0.0.1:3306/db-dev-app?charset=utf8mb4
        - name: DJANGO_SECRET_KEY
          value: django-insecure-zgxe_knf8as%_$o%jq$4yw@i5p6f0d8p74&a!avsuvfctu+)(c
        - name: DJANGO_ALLOWED_HOSTS
          value: '*'
        image: asia-northeast1-docker.pkg.dev/cloud-learn-dev/dev-app-img-repository/sample-image:latest
        imagePullPolicy: Always
        name: dev-app
        ports:
        - containerPort: 8000
      serviceAccountName: gke-sa-dev-app # overlaysのserviceAccountNameで上書きされている
```
</details>

### GKEクラスターにデプロイ
遂にデプロイです。
``` shell
kubectl apply --kustomize ./overlays/staging
# kubectk apply --kustomize { デプロイしたいoverlaysディレクトリパス }
#
# 以下が表示されれば一旦OK
# service/stg-dev-app-001 created
# deployment.apps/stg-dev-app-001 created
```

GCPコンソールよりGKEクラスターを確認してみます。
- **ワークロード**（Deployment）
    - [GCPコンソール] --> [サイドバー] --> [Kubernetes Engine] --> [ワークロード]
    - ステータスが「OK」になっているので問題ありません
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/383405a6-d1d5-706c-97aa-1564e5790a58.png)

- **ServiceとIngress**
    - [GCPコンソール] --> [サイドバー] --> [Kubernetes Engine] --> [ServiceとIngress]
    - こちらもステータスが「OK」になっているので問題ありません
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/711665cf-5901-975d-f655-3ecfd9a6d08e.png)

- **上記Serviceを選択してPodを確認**
    - [GCPコンソール] --> [サイドバー] --> [Kubernetes Engine] --> [ServiceとIngress] より Service名選択
    - 全てステータスが「Running」になっているので問題ありません
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/7a9cda31-a9ce-1ab7-9a0e-03613456c73e.png)

では、Serviceのエンドポイントにブラウザからアクセスしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/17705b93-26c8-1505-932c-82a60783b4e6.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/9e3db8ea-1f79-b3b1-b635-451afdf63abc.png)

無事アクセスできました。
画面に表示されている`example-user@example.com`はサンプルのテーブルから取得したレコードのデータなので、CloudSQLとの接続もできています。

<br />

ただ、k8sのマニフェストファイルの環境変数設定部分がイケてないです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/980428a0-8f95-069a-5879-4c42d4c8484a.png)
このままではシークレット情報をGithubに誤コミットもしくは、デプロイ前にシークレット情報を書き換え、デプロイ後に元に戻す手間が発生しそうです。
なので、シークレット情報はGCPの**SecretManager**で管理するようにしましょう。

# SecretManagerでシークレット情報を管理
### TerraformにSecretManagerの構成ファイル追加
``` dev-app-tf/secret-manager.tf
resource "google_secret_manager_secret" "staging_dev_app_debug" {
  project   = var.project_id
  secret_id = "staging-dev-app_DEBUG"
  replication {
    user_managed {
        replicas { location = var.region }
    }
  }
  labels = { label = "staging" }
}
resource "google_secret_manager_secret" "staging_dev_app_django_settings_module" {
  project   = var.project_id
  secret_id = "staging-dev-app_DJANGO_SETTINGS_MODULE"
  replication {
    user_managed {
        replicas { location = var.region }
    }
  }
  labels = { label = "staging" }
}
resource "google_secret_manager_secret" "staging_dev_app_django_secret_key" {
  project   = var.project_id
  secret_id = "staging-dev-app_DJANGO_SECRET_KEY"
  replication {
    user_managed {
        replicas { location = var.region }
    }
  }
  labels = { label = "staging" }
}
resource "google_secret_manager_secret" "staging_dev_app_database_url" {
  project   = var.project_id
  secret_id = "staging-dev-app_DATABASE_URL"
  replication {
    user_managed {
        replicas { location = var.region }
    }
  }
  labels = { label = "staging" }
}
resource "google_secret_manager_secret" "staging_dev_app_django_allowed_hosts" {
  project   = var.project_id
  secret_id = "staging-dev-app_DJANGO_ALLOWED_HOSTS"
  replication {
    user_managed {
        replicas { location = var.region }
    }
  }
  labels = { label = "staging" }
}

resource "google_secret_manager_secret_version" "v_staging_dev_app_debug" {
  secret        = google_secret_manager_secret.staging_dev_app_debug.id
  secret_data   = "False"
}
resource "google_secret_manager_secret_version" "v_staging_dev_app_django_settings_module" {
  secret        = google_secret_manager_secret.staging_dev_app_django_settings_module.id
  secret_data   = "config.settings.prd"
}
resource "google_secret_manager_secret_version" "v_staging_dev_app_django_secret_key" {
  secret        = google_secret_manager_secret.staging_dev_app_django_secret_key.id
  secret_data   = "django-insecure-zgxe_knf8as%_$o%jq$4yw@i5p6f0d8p74&a!avsuvfctu+)(c"
}
resource "google_secret_manager_secret_version" "v_staging_dev_app_database_url" {
  secret        = google_secret_manager_secret.staging_dev_app_database_url.id
  secret_data   = "mysql://${google_sql_user.sql_user_dev_app.name}:${var.sql_user_password}@127.0.0.1:3306/${google_sql_database.db_dev_app.name}?charset=utf8mb4"
}
resource "google_secret_manager_secret_version" "v_staging_dev_app_django_allowed_hosts" {
  secret        = google_secret_manager_secret.staging_dev_app_django_allowed_hosts.id
  secret_data   = "*"
}
```

### SecretManagerの構成ファイルを指定してapply
普通にapplyしちゃうとGKEクラスタが再作成されちゃって20分くらい待つハメになります（GKEクラスタ構成ファイルのオプションで解決できるのかな）。
また、terraformコマンドにファイル単位でapplyするオプションがないのでshellコマンドで解決しています。
``` shell
terraform apply `cat ./secret-manager.tf | terraform fmt - | grep -E 'resource |module ' | tr -d '"' | awk '{printf("-target=%s.%s ", $2,$3);}'`
#
# １つずつ地道にapplyすることも可能です
# terraform apply -target="google_secret_manager_secret.staging_dev_app_debug"
# terraform apply -target="google_secret_manager_secret.staging_dev_app_django_settings_module"
# terraform apply -target="..."
# ...
# ..
# .
```

SecretManagerを開いて、構成ファイル通りに登録されているか確認しましょう。
[GCPコンソール] --> [サイドバー] --> [セキュリティ] --> [Secret Manager]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/1e76806e-c903-06b9-2251-210ec33ded9b.png)


### External Secretsを使用してSecretManagerの値を読み込む
[External Secrets](https://github.com/external-secrets/external-secrets)は、k8s用の外部シークレット読み込みツールです。
> AWS Secrets Manager、HashiCorp Vault、Google Secrets Manager、 AzureKeyVaultなどの外部シークレット管理システムを統合するKubernetesオペレーターです。オペレーターは外部APIから情報を読み取り、その値をKubernetesシークレットに自動的に挿入します。

[Helm（k8s用のパッケージマネージャ）](https://helm.sh/ja/)を使用してExternal Secretsをインストールします
``` shell
# dev-app-k8s直下で
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets
```

base直下に構成ファイル`external-secret.yml`を作成して`SecretStore`と`ExternalSecret`を記述します。

| リソース名（kind） | 役割 |
---|---
| SecretStore | 外部シークレットとの認証方法を指定します。 |
| ExternalSecret | 外部シークレットからフェッチするものを指定します。 |
| （当記事では使わないけど）<br />ClusterSecretStore | GKEクラスタ内にある全ての名前空間から参照できるグローバルなSecretStore。<br />1つのGKEクラスタに別名前空間 かつ SecretStoreを使用するようなアプリをデプロイする場合はSecretStoreではなくClusterSecretStoreを使った方が良さそうですね。 |

``` dev-app-k8s/base/external-secret.yml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: secretstore
spec:
  provider:
    gcpsm:
      projectID: cloud-learn-dev
      auth:
        workloadIdentity:
          clusterLocation: asia-northeast1
          clusterName: cluster-dev-app
          serviceAccountRef:
            name: overlaysごとに書き換え

---

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: external-secret
spec:
  refreshInterval: 1h

  secretStoreRef:
    kind: SecretStore
    name: secretstore

  target:
    name: overlaysごとに書き換え
    creationPolicy: Owner

  data:
    - secretKey: DATABASE_URL
      remoteRef:
        key: overlaysごとに書き換え（Secret Manager上の名前を指定）
        version: latest

    - secretKey: DEBUG
      remoteRef:
        key: overlaysごとに書き換え（Secret Manager上の名前を指定）
        version: latest

    - secretKey: DJANGO_SECRET_KEY
      remoteRef:
        key: overlaysごとに書き換え（Secret Manager上の名前を指定）
        version: latest

    - secretKey: DJANGO_SETTINGS_MODULE
      remoteRef:
        key: overlaysごとに書き換え（Secret Manager上の名前を指定）
        version: latest

    - secretKey: DJANGO_ALLOWED_HOSTS
      remoteRef:
        key: overlaysごとに書き換え（Secret Manager上の名前を指定）
        version: latest
```

base直下のkustomization.ymlにも追記します。
``` dev-app-k8s/base/kustomization.yml
resources:
  - ./deployment.yml
  - ./service.yml
  - ./external-secret.yml # 追記
```

overlaysにもexternal-secret.ymlを作って該当箇所を書き換えましょう
``` dev-app-k8s/overlays/staging/external-secret.yml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: secretstore
spec:
  provider:
    gcpsm:
      projectID: cloud-learn-dev
      auth:
        workloadIdentity:
          clusterLocation: asia-northeast1
          clusterName: cluster-dev-app
          serviceAccountRef:
            name: gke-sa-dev-app # k8sサービスアカウント名称

---

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: external-secret
spec:
  secretStoreRef:
    name: stg-secretstore-001 # SecretStoreのmetadata.name

  target:
    name: stg-secrets

  data:
    - secretKey: DATABASE_URL
      remoteRef:
        key: staging-dev-app_DATABASE_URL

    - secretKey: DEBUG
      remoteRef:
        key: staging-dev-app_DEBUG

    - secretKey: DJANGO_SECRET_KEY
      remoteRef:
        key: staging-dev-app_DJANGO_SECRET_KEY

    - secretKey: DJANGO_SETTINGS_MODULE
      remoteRef:
        key: staging-dev-app_DJANGO_SETTINGS_MODULE

    - secretKey: DJANGO_ALLOWED_HOSTS
      remoteRef:
        key: staging-dev-app_DJANGO_ALLOWED_HOSTS
```

overlaysのkustomization.ymlにも追記します。
``` dev-app-k8s/overlays/staging/kustomization.yml
bases:
  - ../../base

...省略...

patches:
  - ./patches/deployment.yml
  - ./patches/external-secret.yml # 追記
```

最後に、baseとoverlaysにあるdeployment.ymlの`env`を修正します。
``` dev-app-k8s/base/deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dev-app
spec:
  replicas: 3
  template:
    metadata:
      name: dev-app
    spec:
      ...省略...
      containers:
        - name: dev-app-pod
          ...省略...
          env:
            - name: DEBUG
              valueFrom:
                secretKeyRef:
                  name: overlaysごとに書き換える
                  key: DEBUG # ExternalSecretのdata.secretKey

            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: overlaysごとに書き換える
                  key: DATABASE_URL # ExternalSecretのdata.secretKey

            - name: DJANGO_SECRET_KEY
              valueFrom:
                secretKeyRef:
                  name: overlaysごとに書き換える
                  key: DJANGO_SECRET_KEY # ExternalSecretのdata.secretKey

            - name: DJANGO_ALLOWED_HOSTS
              valueFrom:
                secretKeyRef:
                  name: overlaysごとに書き換える
                  key: DJANGO_ALLOWED_HOSTS # ExternalSecretのdata.secretKey

            - name: DJANGO_SETTINGS_MODULE
              valueFrom:
                secretKeyRef:
                  name: overlaysごとに書き換える
                  key: DJANGO_SETTINGS_MODULE # ExternalSecretのdata.secretKey

          ports:
            - containerPort: 8000

        - name: cloud-sql-proxy
          image: gcr.io/cloudsql-docker/gce-proxy:1.28.0
          command:
            - overlaysごとに書き換える
          securityContext:
            runAsNonRoot: true

```

``` dev-app-k8s/overlays/staging/patches/deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dev-app
spec:
  template:
    spec:
      serviceAccountName: gke-sa-dev-app
      containers:
        ### ここから修正した部分 ###
        - name: dev-app-pod
          env:
            - name: DEBUG
              valueFrom:
                secretKeyRef:
                  name: stg-secrets # ExternalSecretのtarget.name

            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: stg-secrets # ExternalSecretのtarget.name

            - name: DJANGO_SECRET_KEY
              valueFrom:
                secretKeyRef:
                  name: stg-secrets # ExternalSecretのtarget.name

            - name: DJANGO_ALLOWED_HOSTS
              valueFrom:
                secretKeyRef:
                  name: stg-secrets # ExternalSecretのtarget.name

            - name: DJANGO_SETTINGS_MODULE
              valueFrom:
                secretKeyRef:
                  name: stg-secrets # ExternalSecretのtarget.name
        ### 修正ここまで
          ...省略...
```

applyします
``` shell
# apply
kubectl apply -k ./overlays/staging
```
applyしただけではPodが更新されないので、Deploymentを再起動します
（ダウンタイムなしでPodを刷新してくれます）
``` shell
kubectl rollout restart deploy stg-dev-app-001
# kubectl rollout restart deploy { Deploymentの名前 }
```

Djangoがprd用の設定ファイルを読み込むよう、SecretManagerの`DJANGO_SETTINGS_MODULE`の値を`settings.config.prd`としたので、ログに`■■■■■■ prd`と出力されているはずです。
Podのログを確認してみます。
`[GCPコンソール] --> [サイドバー] --> [Kubernetes Engine] --> [ServiceとIngress] より Service名選択 --> [処理元のPod]より適当なPodを選択 --> [コンテナ]よりdev-app-podの「ログを表示」を選択`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/93ed7844-8942-9936-5905-4d69737de0b2.png)

ブラウザからもアクセスできました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357470/24f1f4f6-81cd-6f67-2c78-cf87c5a4a0c8.png)

以上です。


# 参考情報
https://mixi-developers.mixi.co.jp/gke-workload-identity-deep-dive-2244c99c6d8a

https://cloud.google.com/kubernetes-engine/docs/how-to/kubernetes-service-accounts?hl=ja

https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/#overview-of-kustomize

https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity?hl=ja#:~:text=2%20%E3%81%A4%E3%81%AE%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%20%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%AE%E9%96%93%E3%81%AB%20IAM%20%E3%83%9D%E3%83%AA%E3%82%B7%E3%83%BC%20%E3%83%90%E3%82%A4%E3%83%B3%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%97%E3%81%A6%E3%80%81Kubernetes%20%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%20%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%8C%20IAM%20%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%20%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%AE%E6%A8%A9%E9%99%90%E3%81%AE%E5%80%9F%E7%94%A8%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%97%E3%81%BE%E3%81%99%E3%80%82%E3%81%93%E3%81%AE%E3%83%90%E3%82%A4%E3%83%B3%E3%83%89%E3%81%A7%E3%81%AF%E3%80%81Kubernetes%20%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%20%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%8C%20IAM%20%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%20%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%A8%E3%81%97%E3%81%A6%E6%A9%9F%E8%83%BD%E3%81%97%E3%81%BE%E3%81%99%E3%80%82

https://gist.github.com/mAster-rAdio/0749d320a3dffefad4df5629224cd808

https://qiita.com/Veritas666777/items/45383ca2d14a6700d0f6