CI/CDパイプラインを構築してReact × DjangoRestFrameworkで作ったSPAをGKEクラスタにデプロイする

# CICDパイプラインを構築してReact × DjangoRestFrameworkで作ったSPAをGKEクラスタにデプロイする

## 概要
##### GitHub ActionsとArgoCDでCICDパイプラインを構築し、フロントエンドにReact TypeScript、バックエンドにDjango Rest Frameworkを使った単純なTodoアプリのSPAを作り、Google Kubernetes Engine上のAutopilotクラスタにデプロイする。

この記事では主に、CICDパイプライン構築の各種設定をしていきます。

なお、現時点ではかなり雑な殴り書きの説明、かつコードに不具合も多数ある状態となっていますのでいずれ書き直す可能性があります。鵜呑みにはせず、あくまでアプローチの仕方の参考程度に留めてください。

  ※ArgoCDはGKEのAutopilotクラスタでは安定動作しないようなので今回はCDまでは作らないことにしました。GitHub ActionsによるCIまで行い、デプロイにはHelmを使います。GKEスタンダードクラスタの場合はArgoCDを使用可能でしたが今回は扱いません。

パイプラインのイメージとしては以下のようになります。
![cicd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288404/07e7bab6-3af8-9f0d-3276-30b0102c665a.png)

こちらが完成したTodoアプリのスクリーンショットです。
![Todo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288404/a3a017b6-542a-772c-0328-7530a47c9b95.png)

## 使用技術
##### Backend:
- Python/Django/Django Rest Framework
- django-environ
##### Frontend:
- React TypeScript
- Create React App
- Prettier
- ESLint
- axios
- Nginx
##### CI/CD:
- GitHub Actions
- Helm
- ArgoCD

##### Google Cloud: 
- Google Kubernetes Engine (GKE)
- Secret Manager
- Artifact Registry
- SQLインスタンス
- Cloud Storage
- Service Accounts
- Workload Identity連携
##### コンテナ技術
- Kubernetes
- Docker

##### 環境
- OS: Windows10
- テキストエディタ: Visual Studio Code
- バージョン管理: Git
- venv
- pip
- npm
- Cloud Console

#### ※ArgoCDに関して
GKEクラスタをAutopilotで構成したところ、CPUとメモリのリソース不足によりArgoCDの動作が安定しないため使用を断念しました。原因はArgoCD公式サイトのGet Startedで扱う「install.yaml」で要求されるリソースが大きすぎるためだと思われます。
もしArgoCDを使用する場合、機密情報が含まれるHelmのvalues.yamlファイルを、プライベートリポジトリとはいえgitに上げる必要があるためセキュリティ面を考慮しなければならないと思います。
また参考までに、Artifact RegistryにHelmのChartsを保管し、それをArgoCDで監視する方法もあるようです。気になった方は試してみてください。

## 動機など
　GKE上でSPAを動かしたかったのですが、ネット上にあまり情報がなかったので学習内容の記録として残しておきます。
参考にしたGoogle Cloudの公式ガイドではContainer Registryが使われていたので移行が推奨されているArtifact Registryに変更してあります。

ソースコードに関して具体的に順を追って説明すると長くなるので今回はほとんど触れていません。コードの意図などは記事最下部に記載した[参考になったサイトなど](https://qiita.com/maeple5/items/0967a7c41115257a1564#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88%E3%81%AA%E3%81%A9)をよく読んでいただければある程度分かるかと思います。
一応軽く必要な説明しておくと、バックエンドに機密情報を渡すのにKubernetesのSecretsリソースを使うのを避ける目的でGoogle CloudのSecret Managerに機密情報をJSON形式で格納し、バックエンドのコードから直接Secretにアクセスさせる、などしています。これがセキュリティ的に正しいかは分かりません。
KubernetesのSecretsリソースの使用を避けたかった理由については別記事を書くかもしれません。
もしSecretsリソースを使う場合は以下のリンクがとても参考になると思います。
- [悩みに悩んだ Kubernetes Secrets の管理方法、External Secrets を選んだ理由](https://tech.plaid.co.jp/nayanda-kubernetes-secret-management)
- [Kubernetes External Secrets が非推奨になるので External Secrets Operator と Secret Storage CSI を比較する](https://mixi-developers.mixi.co.jp/compare-eso-with-secret-csi-846ed8b1c9b)

## ソースコード
- [バックエンドのサンプルリポジトリ](https://github.com/maeple5/portfolio-backend)
- [フロントエンドのサンプルリポジトリ](https://github.com/maeple5/portfolio-frontend)
-  [マニフェストのサンプルリポジトリ](https://github.com/maeple5/portfolio-manifest)

(23/05/15 追記)
3つともテンプレートになっているので、利用したい場合はcloneするのではなく「Use this template」>「Create a new repository」をクリックして新規リポジトリを作成してください。
それと、バックエンドリポジトリのルートディレクトリに「local.env」という名前で.envファイルを作ってください。config.settings.localを見れば環境変数として何が必要か分かるはずなので各自で設定をお願いします。example.envを用意しておけばよかったですがうっかり忘れていました。気が向いたら用意しておきます。
(23/05/16 追記)
「local.example.env」を用意しました。「local.env」にリネームして使ってください。

## 1. アプリケーションリポジトリを用意する
アプリケーションリポジトリ（フロントエンドとバックエンドのソースコード）を作成します。
今回は手順簡略化のためソースコードに関してサンプルテンプレートリポジトリを用意しました。
テンプレートリポジトリから新規リポジトリを作成します。
[バックエンドのサンプルテンプレート](https://github.com/maeple5/portfolio-backend)
[フロントエンドのサンプルテンプレート](https://github.com/maeple5/portfolio-frontend)
「Use this template」>「Create a new repository」をクリックして新規リポジトリを作成してください。
作成するとfeature01ブランチのworkflowが走りますが、失敗します。
これを成功させるため、いくつかSecretsとVariablesを設定する必要があります。
(23/05/15 追記・補足)
テンプレート内の全てのブランチを使って新規リポジトリを作成するとhistoryが一致しないためmergeできなかったのを忘れていました。ですので、mainブランチのみを使って新規リポジトリを作ってください。その後、developブランチとfeature01ブランチを新しく作成してください。

## 2. GitHub Actionsの設定をする（１）

バックエンドリポジトリに以下の手順で設定します。
- GitHubのバックエンドリポジトリのSettingsから"Secrets and variables > Actions と進む。
- Variablesタブから以下の三つの変数を設定する。

|  変数名  |  値  |
| ---- | ---- |
|  DJANGO_DEBUG  |  True  |
|  DJANGO_TIME_ZONE  |  ja  |
|  DJANGO_LANGUAGE_CODE  |  Asia/Tokyo  |

- SecretsタブからDJANGO_SECRET_KEYを設定する。

Secretsタブに移り、DJANGO_SECRET_KEYとして各自で生成したシークレットキーを設定します。　（バックエンドリポジトリのみ）
  バックエンドリポジトリのルートディレクトリにて以下のコマンドを実行して生成します。
  
    python manage.py shell
    from django.core.management.utils import get_random_secret_key
    get_random_secret_key()

|  シークレット名  |  値  |
| ---- | ---- |
|  DJANGO_SECRET_KEY  |  各自で生成した値  |

これでfeature01ブランチにcommit & pushするとworkflowが走り、テストまで実行してくれるようになります。
## 3. Google Cloudの設定をする
続いてGoogle Cloudでの作業に移ります。
- Google Cloudでプロジェクトを新規作成
- Cloud SQLインスタンスを作成する
SQL で"インスタンスを作成" をクリックし、PostgreSQLを選択します。
インスタンスID・パスワードを設定し、最初に使用する構成の選択で"開発環境"を選びます。
リージョンを選択し、ゾーンの可用性はシングルゾーンを選択。プライマリーゾーンは任意で設定します。
インスタンスのカスタマイズからマシンタイプを"共有コア"の"1 vCPU, 1.7 GB"を選択。ストレージはSSDの10GB、"ストレージの自動増量を有効にする"のチェックは念のため外します。
接続 > パブリックIPのチェックを外し、プライベートIPにチェックを入れます。関連付けられたネットワーキングでdefaultを選択するとService Networking APIを有効化することを求められるので有効化し、IPアドレスを自動で割り当てるように設定し、"接続"を作成をクリックします。
最後に"インスタンスを作成"をクリックします（作成し終わるまで15分ほどかかります。待っている間に次の手順に進みGKEクラスタを作成します。）

|  項目  |  値  |
| ---- | ---- |
| データベースの種類  |  PostgreSQL  |
| 構成  |  開発環境  |
| リージョン  |  us-central1  |
| ゾーンの可用性  |  シングルゾーン  |
| プライマリーゾーン  |  任意  |
| マシンタイプ  |  "共有コア", "1 vCPU, 1.7 GB"  |
| ストレージ  |  SSD, 10GB  |
| ストレージの自動増量  |  無効  |

- インスタンスができたらデータベースとユーザーを作成する（手順省略）
## 4. GKE, Artifact Registry, Service Accounts, Workload Identity連携,  Cloud Storage, Secret Managerの設定
##### GKEクラスタを作成
Kubernetes Engine > クラスタ と進み、まずはKubernetes Engine APIを有効化します。その後、AutoPilotでクラスタ作成。（作成し終わるまで10分ほどかかります。）手順は省略します。
開発用クラスターと本番用クラスターの2つを作ります。

##### Artifact Registryでリポジトリを作成
  Artifact Registryで開発用リポジトリと本番用リポジトリをそれぞれ作ります。
  手順は省略。

##### Service Accountを作成し、ロールを割り当てる
  割り当てるロールは以下の6つ。
  
- Artifact Registry 書き込み
- Cloud SQL クライアント
- Kubernetes Engine 管理者
- Secret Manager のシークレット アクセサー
- Workload Identity ユーザー
- ログ書き込み 
![roles.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288404/33756558-8efc-38d1-a0a5-8316ab24d559.png)
##### Workload Identity連携の設定をする。
   workload IdentityのAPIを有効化し、プールを作成する。
  以下のリンクを参考に設定する。
  [Google Cloud の Workload Identity 連携でGitHub Actionsから認証する](https://blog.lacolaco.net/2022/07/github-actions-oidc-google-cloud/)

|  項目  |  値  |
| ---- | ---- |
| google.sub  |  assertion.sub  |
| attribute.actor  |  assertion.actor  |
| attribute.aud  |  assertion.aud  |
| attribute.repository  |  assertion.repository  |

    属性条件
    'githubのユーザーID'==attribute.actor

##### Cloud Storageでバケットを作成する
手順省略
##### Secret ManagerでSecretを登録する

Secret名には"django_settings"という名前を使ってください。
以下のJSON形式で記述します。

    {
    "secrets": {
        "DB": {
            "ENGINE": "django.db.backends.postgresql_psycopg2",
            "HOST": "/cloudsql/YOUR_PROJECT_ID:YOUR_LOCATION:YOUR_INSTANCE_NAME",
            "NAME": "YOUR_DB_NAME",
            "USER": "YOUR_DB_USER",
            "PASSWORD": "YOUR_DB_PASSWORD",
            "PORT": "5432"
            },
        "GS_BUCKET_NAME": "YOUR_MEDIA_BUCKET_NAME",
        "SECRET_KEY":"$(cat /dev/urandom | LC_ALL=C tr -dc '[:alpha:]'| fold -w 50 | head -n1)"
      }
    }
YOUR_LOCATIONにはリージョンを入力する。
YOUR_MEDIA_BUCKET_NAMEには作成したバケット名を指定する。

※使用しているSecret名などはバックエンドリポジトリのconfig.settings.productionにおいてハードコードされているので上記のようにしてください。
※JSON形式にした理由はSecret Managerに登録するSecretの数を減らすためです。環境によって変えたいDB名などは別のSecretに切り出して下さい。現在のままだと各クラスタが使用するDBが同一となるためテストができません。その解決策として、Helmのvalues.yamlで環境ごとに異なる値を与え、KubernetesのConfigMapでバックエンドに環境変数を設定し、その環境変数の値により使用するSecretを分ける、という方法が考えられます。

## 5. GitHub Actionsの設定をする（２）
   フロントエンドリポジトリとバックエンドリポジトリに以下の項目をActionsのSecretsに設定します。

|  項目  |  値  |
| ---- | ---- |
| DEV_REPOSITORY  |  Artifact Registryの開発用リポジトリ名  |
| PROD_REPOSITORY  |  Artifact Registryの本番用リポジトリ名  |
| IMAGE  |  Artifact Registryのリポジトリに格納するdockerイメージ名  |
| GKE_DEV_CLUSTER  |  GKEの開発用クラスター名  |
| GKE_DEV_CLUSTER  |  GKEの本番用クラスター名  |
| GKE_PROJECT  |  Google CloudのプロジェクトID  |
| SERVICE_ACCOUNT  |  Google Cloudのサービスアカウント名。メールアドレス形式のもの  |
| WORKLOAD_IDENTITY_PROVIDER  |  projects/...から始まる文字列  |

Variablesに以下の2つを追加します。ここでは"us-central1"としますが、各自で選んだロケーションを設定してください。

|  項目  |  値  |
| ---- | ---- |
| GAR_LOCATION  |  us-central1  |
| GKE_ZONE  |  us-central1  |

以上の設定をすると、feature01ブランチからdevelopブランチへのpull requestをmergeするとworkflowが走り、dockerイメージをArtifact Registryの開発用リポジトリにpushしてくれるようになります。
また、developブランチからmainブランチへのpull requestをmergeするとworkflowが走り、dockerイメージをArtifact Registryの本番用リポジトリにpushしてくれるようになります。


## 6. マニフェストリポジトリを用意する
   以下のサンプルテンプレートリポジトリから新規リポジトリを作成します。
   [マニフェストテンプレートリポジトリへのリンク](https://github.com/maeple5/portfolio-manifest)
「Use this template」>「Create a new repository」をクリックして新規リポジトリを作成してください。
作成したリポジトリをcloneし、各種yamlファイルを設定していきます。
まず、values.template.yamlを複製し、同階層に"values.yaml"という名前で保存します。
values.yaml中にある各変数の値には、これまでに作成した情報をもとに入力していきます。
手順は省略しますが、各自で書き換えてください。

次に、envディレクトリ配下のdevelop.template.yamlとproduction.template.yamlも同様に複製し、それぞれdevelop.yaml, production.yamlとして保存します。
develop.yamlとproduction.yaml中の"repository"の値は、

    YOUR_LOCATION-docker.pkg.dev/YOUR_PROJECT_ID/YOUR_ARTIFACT_RESISTRY_REPOSITORY_NAME/IMAGE_NAME
の形式にします。
"tag"の値にはArtifact Registryのページで確認したタグ名を記述します。今回はタグ名に連番ではなくSHA256を使っているのでlatestタグは使えません。これによりタグ名の推測による不正行為を困難にするセキュリティ対策にもなっています。


なお、注意点として、今作った3つのyamlファイルはgitに上げません。.gitignoreに既に記載してあるため、命名の際にタイピングミスがなければ管理対象から外れているはずです。
ただし、ArgoCDを使う場合は別です。ArgoCDはリポジトリを監視し、変更を検知するため、機密情報もgitに上げなければなりません。プライベートリポジトリを使えばよいですが、その場合もセキュリティ対策を熟考する必要があります。

## 7. GKEにデプロイする
デプロイの前にCloud SQL Admin API を有効化しておく必要があります。忘れがちなので注意。
そして以下のコマンドを実行します。GSAはGoogleのサービスアカウント、KSAはKubernetesのサービスアカウントを表しています。KSAはHelmのvalues.yamlで設定したものを使用します。NAMESPACEには"default"を設定します。

    gcloud iam service-accounts add-iam-policy-binding GSA_NAME@GSA_PROJECT.iam.gserviceaccount.com \
    --role roles/iam.workloadIdentityUser \
    --member "serviceAccount:PROJECT_ID.svc.id.goog[NAMESPACE/KSA_NAME]"

デプロイの前提としてhelmコマンドを使えるようにする必要があります。
導入手順は省略しますが、Google CloudのCloud Consoleではデフォルトで使えるのでそちらでマニフェストリポジトリをcloneした方が簡単です。

    gcloud container clusters get-credentials YOUR_DEV_CLUSTER --region YOUR_LOCATION
というコマンドでクラスターの認証情報を取得します。そして、
マニフェストリポジトリのディレクトリに移動し

    helm install -f ./spa/values.yaml -f ./spa/env/develop.yaml spa ./spa
を実行します。
これは、まずvalues.yamlを読み込み、次にdevelop.yamlで差分を上書きしています。
ファイルを読み込む順番を逆にすると動作しないので注意してください。

## 8. デプロイしたSPAを確認する
  Kubernetes Engine > Services & IngressからフロントエンドのサービスのIPアドレスをクリックしアクセスする。
![frontend-ip.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3288404/4121ff7c-0979-d434-5d67-2c147acade20.png)

「タスクアプリ」と書かれたページが表示されるはずです。次に、URLの末尾に"/v4"と追記しアクセスすると「タスクアプリ（v4）」と書かれたページと、入力フォームが表示されます。
  その入力ボックスに適当な文字を入力して送信すると、バックエンドのAPIを呼び出し、そのAPIがCloud SQLインスタンスのデータベースにアクセスし、結果的に現在のページにリストとして非同期で描画されます。
  ブラウザの更新ボタンを押しても変わらないこと、ページを表示しているブラウザのタブを削除して再度アクセスしても投稿内容が消えないこと、削除ボタンを押して削除できることを確認します。

##### 本番環境でも動作することを確かめる。

    gcloud container clusters get-credentials YOUR_PROD_CLUSTER --region YOUR_LOCATION
    helm install -f ./spa/values.yaml -f ./spa/env/production.yaml spa ./spa
これで本番環境にデプロイできます。

## 9.  クリーンアップ
  プロジェクトのダッシュボードから「プロジェクト設定に移動」をクリック。
  上部にある「シャットダウン」をクリックし、モーダルウインドウにプロジェクトIDを入力して削除することを確認します。
  これにより今回GoogleCloudで作成したリソースは全て削除されます。
  
## 最後に
今回は以上となります。
省略箇所が多々あり、全てを確認しながら書いているわけではないので重複する説明や記述の抜け等もあると思います。
不親切な説明となってしまい心苦しいですが、どなたかの参考になれば幸いです。
駄文を長々と失礼いたしました。

## 参考になったサイトなど
- [App Engine スタンダード環境での Django の実行](https://cloud.google.com/python/django/appengine?hl=ja)
- [Cloud Run 環境での Django の実行](https://cloud.google.com/python/django/run?hl=ja)
- [Google Kubernetes Engine で Django を実行する](https://cloud.google.com/python/django/kubernetes-engine?hl=ja)
- [Helmチャートで環境変数を取り扱う工夫](https://qiita.com/Naoto_Ito/items/c01fd4b44dcb8fbbebcc)
-  [k8s ConfigMap で設定を Pod から分離できる( nginx の設定を ConfigMap で管理する)](https://www.kimullaa.com/posts/202002191415/)
-  [Workload Identity を使用する](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity?hl=ja)
-  [GitHub Actions でプルリクのマージでワークフローを実行する](https://qiita.com/okazy/items/7ab46f2c20ec341a2836)
-  [Docker マルチステージビルドでNode.jsでコンパイルしたjsファイルをnginxで配信する](https://zukucode.com/2020/08/docker-multi-stage-build.html)
- [Google Cloud の Workload Identity 連携でGitHub Actionsから認証する](https://blog.lacolaco.net/2022/07/github-actions-oidc-google-cloud/)
- [GKE クラスタ内のワークロードから Google Cloud APIs にアクセスする（Workload Identity）](https://blog.g-gen.co.jp/entry/gke-workload-identity)
- [悩みに悩んだ Kubernetes Secrets の管理方法、External Secrets を選んだ理由](https://tech.plaid.co.jp/nayanda-kubernetes-secret-management)
- [Kubernetes External Secrets が非推奨になるので External Secrets Operator と Secret Storage CSI を比較する](https://mixi-developers.mixi.co.jp/compare-eso-with-secret-csi-846ed8b1c9b)