LoginSignup
2
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-06-18

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

この記事でできるようになること

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

今回使用するコード

最終的な構成

※ ロードバランサやカスタムドメインは使用しません。
 アプリケーションのプロトコルもHTTPです。(HTTPS化はしません)
image.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に実行権限を付与

cd dev-app
chmod +x entrypoint.sh

コンテナを起動してブラウザからアクセス

docker-compose up -d

http://0.0.0.0:8000 にアクセスして下記画面が表示されればOKです。
マイグレーション実行時、サンプルのテーブルにレコードを1件登録するようにしています。
画面に表示されているexample-user@example.comはそのテーブルから取得したレコードの値です。
image.png


Terraformで各種GCPリソース作成

GCPプロジェクトの作成や、terraform/tfenvのインストール方法などは割愛します

Terraform用のフォルダを作成

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

  5. 対象サービスアカウント行の右側にある縦の三点リーダより、「鍵を管理」選択

  6. 「鍵を追加」ボタン押下

  7. 「新しい鍵を作成」選択

  8. キーのタイプは「JSON」を選択

  9. 「作成」ボタン押下
    JSONファイルがダウンロードされるので、そのファイルをdev-app-tfフォルダ直下に移動

- このJSONファイルは絶対にGithubをはじめとする公開サイトにアップロードしないでください。不正利用される可能性があります。

環境変数GOOGLE_APPLICATION_CREDENTIALSに先ほどダウンロードしたJSONキーファイルの絶対パスを設定

export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account.json

変数定義 および シークレット変数への値割り当て

プロジェクトIDやregionなど、繰り返し記述する可能性がある値を変数定義ファイルvariables.tfに記述します。
こうすることで、その他tfファイルからvar.project_idvar.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!」が出れば初期化完了です。

# dev-app-tf直下で
terraform init

Dockerイメージ管理用のArtifact Registry構成ファイルを作成

- 事前に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と入力してエンターを押下してください。

# dev-app-tfの直下で
terraform apply

「Apply complete! Resources: ...」と表示されたら成功です。

GCPコンソールからArtifact Registryを開くと、リポジトリが作成されていることを確認できます。
image.png

データ保存用のCloud SQL構成ファイルを作成

- 事前に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分くらい掛かります。

# dev-app-tf直下で
terraform apply

GKEクラスタ構成ファイルを作成

- 事前にCloud Resource Manager APIを有効化しておいてください([GCPコンソール] --> [検索テキストボックス] --> [Cloud Resource Managerで検索])

重要なのはworkload_identity_configworkload_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分くらい掛かります。

# dev-app-tf直下で
terraform apply

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


DjangoアプリのDockerイメージをビルド&プッシュ

gcloudコマンドのインストール方法などは割愛します

イメージ

image.png

ビルド&プッシュ

下記コマンドでDockerイメージをビルドしてArtifact Registryにプッシュします。

# 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

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

kubectl と kustomizeでGKEクラスターにリソースを作成

リソースの作成は、k8sの構成ファイルカスタマイズツールであるkustomizeを使用します。
kustomizeにはbased(ベース)overlays(オーバーレイ)の概念があります。
base  ・・・リソースのセット(deployment.ymlやservice.yml)とkustomization.ymlを含むディレクトリ
overlays・・・baseを参照するkustomization.ymlを含むディレクトリ

.
├── 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

クラスター認証

kustomizeを使用してGKEクラスタのリソースを管理するためクラスタ認証を行います

# 一応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サービスアカウントだけ作成します。

# dev-app-k8sの直下で
kubectl apply -f overlays/staging/sidecar/gke_service_account.yml

サービスアカウントが作成されたかkubectl get saで確かめます。
指定した名前でサービスアカウントが作られていればOKです。

kubectl get sa
NAME             SECRETS   AGE
default          1         145m
gke-sa-dev-app   1         38s

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

# 一応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

軽い用語説明

kustomizeのマニフェストを作成する前に軽く用語の説明を入れておきます。

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

各種baseファイルを作成

.
├── 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

  • 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の間であればなんでも良い)

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

各種overlaysファイル作成

.
├── base
│   ├── ...
│   ├── ..
│   └── .
│
└── overlays
    └── staging
        ├── sidecar
        │   └─ gke_service_account.yml
        |
        ├── patches
        │   └── deployment.yml
        │
        └── kustomization.yml

見慣れないpatchesディレクトリがあります(ディレクトリ名はpatchesでなくても良い)。
これはbaseのdeployment.ymlをstaging用に書き換えるためのものです。


  • 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"

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

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

patches:
  - ./patches/deployment.yml

bases baseディレクトリへの相対パス(baseディレクトリ直下のkustomization.ymlを参照する)
namePrefix 全てのmetadata.name(1階層目に限る)に接頭辞を付与
nameSuffix 全てのmetadata.name(1階層目に限る)に接尾辞を付与
commonLabels 全てのリソースにラベルを設定する。
このラベル付けが個人的に超便利です。matchLabelsやセレクターラベルも付けてくれるので、ラベル付けを意識する必要もなければ、ラベル付けを忘れてデプロイがうまくいかないという凡ミスも防げます。
  • matchLabels:特定ラベルのPodをDeploymentが管理するためのラベル付け
  • セレクターラベル:特定ラベルのPodをServiceが管理するためのラベル付け
    • image.png

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

出力結果
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で上書きされている

GKEクラスターにデプロイ

遂にデプロイです。

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
  • ServiceとIngress

    • [GCPコンソール] --> [サイドバー] --> [Kubernetes Engine] --> [ServiceとIngress]
    • こちらもステータスが「OK」になっているので問題ありません
      image.png
  • 上記Serviceを選択してPodを確認

    • [GCPコンソール] --> [サイドバー] --> [Kubernetes Engine] --> [ServiceとIngress] より Service名選択
    • 全てステータスが「Running」になっているので問題ありません
      image.png

では、Serviceのエンドポイントにブラウザからアクセスしてみます。
image.png
image.png

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


ただ、k8sのマニフェストファイルの環境変数設定部分がイケてないです。
image.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コマンドで解決しています。

terraform apply `cat ./secret-manager.tf | terraform fmt - | grep -E 'resource |module ' | tr -d '"' | awk '{printf("-target=%s.%s ", $2,$3);}'`
#
# 1つずつ地道に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

External Secretsを使用してSecretManagerの値を読み込む

External Secretsは、k8s用の外部シークレット読み込みツールです。

AWS Secrets Manager、HashiCorp Vault、Google Secrets Manager、 AzureKeyVaultなどの外部シークレット管理システムを統合するKubernetesオペレーターです。オペレーターは外部APIから情報を読み取り、その値をKubernetesシークレットに自動的に挿入します。

Helm(k8s用のパッケージマネージャ)を使用してExternal Secretsをインストールします

# 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を作成してSecretStoreExternalSecretを記述します。

リソース名(kind) 役割
SecretStore 外部シークレットとの認証方法を指定します。
ExternalSecret 外部シークレットからフェッチするものを指定します。
(当記事では使わないけど)
ClusterSecretStore
GKEクラスタ内にある全ての名前空間から参照できるグローバルなSecretStore。
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します

# apply
kubectl apply -k ./overlays/staging

applyしただけではPodが更新されないので、Deploymentを再起動します
(ダウンタイムなしでPodを刷新してくれます)

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

ブラウザからもアクセスできました。
image.png

以上です。

参考情報

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0