GitHubActions + ArgoCD + HELMを使ったGitOps手法でGKEへアプリをデプロイ

# この記事について
半年前からk8sの勉強を始め、GitOps手法でアプリをデプロイする知見を得ることができたので学んだことのまとめ記事です。

# GitOps手法とは？

weave.worksが提唱する開発手法です。
https://www.weave.works/blog/gitops-operations-by-pull-request
> GitOps is a way to manage systems like Kubernetes

訳: GitOpsは、Kubernetesなどのシステムを管理する方法です
・・・とあるようにk8sで構築するシステムの管理手法と考えて良いと思います。
実際にGitOpsのキーワードで色んな記事を読んできましたが、今の所k8s以外は見たことがありません。

リソースは基本的にGitで管理し、本番環境への反映はプルリクによる操作が望ましいとされています。
この記事でもソースを管理するアプリリポジトリとk8sのyamlを管理するマニフェストリポジトリの2つを使用して構築していきたいと思います。

# 今回構築する全体像について
構築する全体像は以下の通りです。
![gitops.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/cb5ee863-c797-5002-23eb-12d2a7544a06.png)

- アプリはReact(create-react-app)を使用します
- Nodeへのスケジューリング内容はあくまでイメージです。必ずしも図のように配置されるわけではありません
- ArgoCDからデプロイされるk8sリソースについてもイメージです。HELMのカスタムValue(後述)の内容によってデプロイされるリソースも変わってきます
- GCPのロードバランサーなど記事の内容と直接関係ない部分については省略しています

---------------------------------------

**ではここからが本題です。全体像を区分けして解説していきます。**

# 1. GitHubActionsを使用してDockerイメージをGCRへpush
まずはCIの部分を構築していきます。
![gitops1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/25c7d90f-1a6d-37a3-b286-0d8c93bf39da.png)

## ここで使用する技術について
- GitHubActions・・・GitHubのCIツールです。GitHubリポジトリでのイベントを検知してテストやDockerfileのbuild・pushなどのワークフローを実行できます。

- GCR・・・GoogleContainerRegistryの略で、Dockerイメージのリポジトリサービスです。要はGCP版のDockerHubのようなもの。

## アプリリポジトリについて
完成版はこちらになります。
https://github.com/Nishi53454367/gitops_app_create-react-app

### create-react-app作成手順について
プロジェクト初期化手順はREADMEに記載してあります。
今回の記事の内容とは関係ないのでこちらの解説は割愛します。

### Dockerfileについて
- docker/Dockerfile.devは開発用コンテナです。docker-compose.ymlで使用しています
- docker/Dockerfileがk8sのPodに配置するコンテナです。これをGitHubActionsのワークフローで使用します。ReactリソースをビルドしてNginxのデフォルトディレクトリにコピーしているだけの必要最小限の作りになっています

### GitHubActionsワークフローについて
#### 作成手順
GitHub上で対象リポジトリのActionsのタブを選択し、テンプレートを選択します。
今回は以下をベースに修正していきました。
![スクリーンショット 2020-12-06 15.02.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/7266b510-f15e-012b-534e-564103ae8a27.png)
完成版は以下のようになっています。

```yaml:.github/workflows/docker-image.yml
name: Docker Image CI

on:
  # mainブランチへのpushをトリガーにworkflowを実行
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2

      # GCPプロジェクトの認証
    - name: GCP Authenticate
      uses: google-github-actions/setup-gcloud@master
      with:
        version: '290.0.1'
        project_id: ${{ secrets.GCP_PROJECT_ID }}
        service_account_key: ${{ secrets.GCP_SA_KEY }}
        export_default_credentials: true
    - name: Configure docker to use the gcloud cli
      run: gcloud auth configure-docker

      # Dockerfileのbuild
    - name: Build the Docker image
      run: docker build . -f docker/Dockerfile -t gcr.io/${{ secrets.GCP_PROJECT_ID }}/create_react_app:${{ github.run_number }}

      # DockerfileをGCRへpush
    - name: Push the docker image
      run: docker push gcr.io/${{ secrets.GCP_PROJECT_ID }}/create_react_app:${{ github.run_number }}
```

- 「GCPプロジェクトの認証」の部分は以下を参考に作成しています
https://github.com/google-github-actions/setup-gcloud/tree/master/setup-gcloud

- GCP_PROJECT_ID、GCP_SA_KEYはこの名前の機密情報をGitHubのSecretsから取得します(後述)

- GCRへDockerイメージをpushする時は`ホスト/プロジェクトID/{任意のイメージ名:任意のtag}`でタグ付けをする必要があります。
  - ホストは米国内データセンターの「gcr.io」と「us.gcr.io」、欧州連合の「eu.gcr.io」、アジアのデータセンターの「asia.gcr.io」と何種類か選択できますが、今回は「gcr.io」を使用します
  - {任意のイメージ名:任意のtag}の部分は`create-react-app：ワークフローの実行回数`としています。任意のtagの部分は1からインクリメントされていきます

## GCP上での作業
### GCPプロジェクトについて
今回は`gitops-react-app`(プロジェクトIDも同様)という名前のプロジェクトを使用します。

### GCRのAPIを有効化
メニューから「Container Registry」を選択して「Container Registry APIを有効化」を押してGCRを使用できる状態にしておきます。

### サービスアカウントキー作成
メニューから「IAM と管理」→「サービス アカウント」を選択し、以下の内容でサービスアカウントを作成します。
![スクリーンショット 2020-12-06 16.27.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/f5d134ba-d64b-2c43-30a0-5a3e326cd8db.png)
サービスアカウント名、説明の内容はなんでも大丈夫です。
IDはサービスアカウント名を元に自動入力されます。
![スクリーンショット 2020-12-06 16.29.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/1f38a3f1-dff4-472b-5d30-af3f67934bc6.png)
似たようなロールで「ストレージオブジェクト管理者」があるので注意です。

作成したサービスアカウントの右側のメニュー(「・・・」を縦並びにしたやつ)から「鍵を作成」でJSON形式を指定してダウンロードします。

## GitHub上での作業
### SecretsにGCP機密情報を設定
先ほど自分のマシンにダウンロードした鍵をbase64でエンコードします。

```
cat {ダウンロードした鍵のパスを指定} | base64
```

鍵本体もですがこの値も簡単にデコードできるので流出しないように注意してください。

この鍵のbase64エンコード値とGCPプロジェクトIDを.github/workflows/docker-image.ymlで使用しているSecrets名でGitHubのSecretsへ設定すれば準備は完了です。

GitHubリポジトリで「Settings」→「Secrets」→「New repository secret」で以下の内容で設定を行います。

| Name | Value |
|:-----------:|:------------:|
| GCP_PROJECT_ID | gitops-react-app |
| GCP_SA_KEY | 鍵のbase64エンコード値 |

以下のように設定できればOKです。
![スクリーンショット 2020-12-06 17.11.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/aef6b756-336c-b3eb-b137-cfd0c8395e52.png)
ここで設定した内容はUpdateを押しても現在の設定値を見ることができず、上書きという形での変更となっています。また、リポジトリをforkしても値が引き継がれることはありません。 

## 動作確認
ではmainブランチへのpushでGCRへDockerイメージが登録されるか確認してみます。

この時点ではGCR上にはまだ何も登録されていません。
![スクリーンショット 2020-12-06 17.00.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/fe2abcf9-ad14-eac9-6f6e-a75942372d4f.png)

任意の更新を行い、mainブランチへpushを行うと、GitHub上の「GitHubActions」タブでワークフローが実行されていることを確認できます。
こんな感じで緑のチェックマークが付いていることが確認できれば成功です。
![スクリーンショット 2020-12-06 17.03.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/95d5c4eb-a24b-ebcc-b28d-7399ba09fba6.png)
GCRを再度確認すると、Dockerイメージが登録されていることが確認できます。
![スクリーンショット 2020-12-06 17.04.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/bf3047f6-3430-2d36-4f08-b9e01bb12cf3.png)
イメージを選択するとDockerイメージをpullする時に必要なリポジトリ情報が確認できます。
以下の部分をHELMのカスタムValue(後述)に設定するので控えておきます。

- リポジトリ：`gcr.io/gitops-react-app/create_react_app`
- タグ：`3`

タグはワークフローの実行回数を指定しているので、これは3回目の実行であることがわかります。
![スクリーンショット 2020-12-06 17.06.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/c71f6c92-8776-d925-aada-1d4f92e4dd26.png)

以上でCIの設定・動作確認が完了です。

# 2. マニフェストリポジトリをArgoCDで監視
次にCDの部分を構築していきます。
![gitops2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/681fa343-60d9-f6a6-998c-8f51da420d3d.png)

## ここで使用する技術について
- HELM・・・k8sリソースのテンプレートパッケージです。穴埋め方式のテンプレートとPodの数やDeploymentでpullするDockerイメージなど環境によって変更したい値の変数を組み合わせk8sリソースをデプロイすることができます。
この変数を定義するファイルのことをこの記事内でカスタムValueファイルと呼んでいます。アプリケーションでいう環境変数ファイル(.env)と似たようなイメージです。
環境に応じたカスタムValueファイルを用意しておくことで、STG環境と本番環境でyamlを1から準備する必要がなくなります。
繰り返しになりますがHELMは穴埋め方式なので、カスタマイズ性が弱いと思います。(やろうとすると分岐だらけの複雑なテンプレートになりそう。)
複数のアプリでテンプレートを使い回したいような時はKustomizeの方が適していると思います。

- ArgoCD・・・k8sクラスタにインストールして使用するpull型のCDツールです。
使い始めて間もないですが、GUIもわかりやすくk8s初学者にとっても使いやすいツールだと思います。

## マニフェストリポジトリについて
完成版はこちらになります。
https://github.com/Nishi53454367/gitops_manifest_helm

### HELM作成手順について
プロジェクト初期化手順はREADMEに記載してあります。
`create helm`コマンドを使用して作成すると、deploymentテンプレートがデフォルトでポート80をListenしているシングルPodの構成となっています。
今回は特別な修正を加えなくてもReactアプリをビルドしたNginxコンテナをこのPod内に配置してあげるだけでOKですが、実際にはアプリケーションの構成に合わせた修正が必要になると思います。

### カスタムValueファイルについて
今回はカスタムValueファイルは1つだけ作成しています。
実際には「開発用、STG用、本番用」や「サービスA用、サービスB用」など複数用意するケースが多いと思います。
カスタムValueに定義がないと`create helm`した時に作成されるvalues.yamlがデフォルト値となります。よって、values.yamlを見ればどんな値をカスタムで設定できるか確認できます。
もちろんtemplates配下を修正して任意の名前でセットできるように修正も可能です。

今回作成したカスタムValueファイルは以下の通りです。最小限の設定です。

```yaml:create-react-app-values.yaml
# Pod
replicaCount: 1

# DockerImage
image:
  repository: gcr.io/gitops-react-app/create_react_app
  tag: "3"

# Service
service:
  type: LoadBalancer
```

DockerImageの部分はCI構築時に確認したDockerリポジトリの情報をセットします
GCRはprivateリポジトリですが、同じGCPプロジェクト内で構築したGKE上で使用するので認証は不要でDockerイメージをpullできます。

ちなみにファイル名は`xxxvalues.yaml`の形式にしておかないとArgoCD管理画面で設定する時(後述)に候補として出てこなかったので、このルールでファイルを作成しておきます。

## GCP上での作業
### GKEでクラスタを作成
メニューから「Kubernetes Engine」を選択し、以下の内容でクラスタを作成します。
節約のため以下の通りにしていますが、今回の記事の内容とは関係ないので、好きに変更して問題ありません。

- ゾーンはus-west1-a
- マシンタイプは全てe2-small
- プリエンティブルノード有効

### クラスタへArgoCDインストール
作成したクラスタを選択して「接続」を押し、CloudShell接続を行い、以下のコマンドを実行していきます。

#### ArgoCD用のネームスペースを作成
```
kubectl create namespace argocd
```

#### ArgoCDインストール
Non-HA構成の場合(今回はこっちでやります)

```
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
```

HA構成の場合

```
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v1.0.1/manifests/ha/install.yaml
```

#### ArgoCD管理画面を外部から接続できるようにpatch
インストールが終わったら以下のコマンドを実行

```
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'
```

## ArgoCD管理画面上での作業

### ログイン
メニューの「Kubernetes Engine」→「Services と Ingress」を選択して`argocd-server`のエンドポイントをクリックしてArgoCDの管理画面を起動します。オレオレ証明書なので警告出ますが続行すると以下の画面が表示されます。
![スクリーンショット 2020-12-06 21.06.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/841f93b9-963b-159f-ce4c-dc61147581bf.png)
Usernameは、`admin`
Passwordは、ArgoCDのPod名がデフォルトのパスワードになっています

CloudShellで以下のコマンドを実行することで一覧表示される`argocd-server-xxxxx`のPod名がパスワードになっています。

```
kubectl get pods --namespace argocd
```

### マニフェストリポジトリ設定
左側のメニューで歯車マークを選択し、「Repositories」を選択します。
![スクリーンショット 2020-12-06 21.17.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/19a6e1ab-5452-bae7-e638-126a99dc1f8a.png)
「CONNECT REPO USING HTTPS」を選択し、リポジトリの内容を設定します
![スクリーンショット 2020-12-06 21.19.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/7a96f1fd-8722-ca16-74bf-9b85ceafb250.png)
Repository URLにマニフェストリポジトリのURLを入力して「CONNECT」を押します。
リポジトリがprivateの場合は、GitアカウントのUsernameとPasswordに入力すれば接続できます。※1
![スクリーンショット 2020-12-06 21.22.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/f7c4d24c-661b-ae07-0aef-7775fa5a6593.png)
以下のようになれば設定完了です。
![スクリーンショット 2020-12-06 21.25.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/5ed1939e-3d26-9289-653c-19538bd21be7.png)

### アプリ作成
![スクリーンショット 2020-12-06 21.28.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/18c616fd-75d8-212f-1600-9b1909c95556.png)
「NEW APP」を選択し、以下の内容で作成を行います。
#### GENERAL
![スクリーンショット 2020-12-06 21.31.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/3edc5c1c-4eb1-b0f1-1954-a244e5a30ffa.png)
Application Nameは、任意の名前でOKです。この名前がデプロイされるk8sリソース名に使用されます。

#### SOURCE
![スクリーンショット 2020-12-06 21.33.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/f378a9be-3c37-dde0-1451-c6e6dd300710.png)

- Repository URLは先ほど設定したマニフェストリポジトリを選択します
- Revisionは監視対象のブランチを指定します。今回はmainブランチとしています
- PathはRevisionを入力するとカレントディレクトリにHELMテンプレートが格納されているので、「.」が選択できるようになります。ここで候補値として「.」が出てこない場合、どこか設定が誤っている可能性があります。

#### DESTINATION
![スクリーンショット 2020-12-06 21.38.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/c37bb3cf-feb0-41d3-0628-b7273c647b0c.png)
Namespaceは今回指定しないのでdefaultにします。

#### HELM
![スクリーンショット 2020-12-06 21.42.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/4d4e3d5d-2034-ca55-b8d9-2fe73e73dd00.png)
VALUES FILESの部分でカスタムValueファイルが選べるようになっているので、デプロイしたい内容のファイルを選択します。

ここまでの内容で「CREATE」を押すと以下のカードが作成されます。
![スクリーンショット 2020-12-06 21.46.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/364832c9-8dab-cc87-7ab2-9909426f0ceb.png)
これでデプロイの手前までが完了です。

# 3. ArgoCDでデプロイ
では最後にデプロイを行います。
引き続きArgoCD管理画面で操作します。
![gitops3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/9a0b742f-13bc-3cd1-6526-ebfddfd27817.png)

## デプロイ実行
作成したカードを選択し、「SYNC」を押します。
![スクリーンショット 2020-12-06 21.48.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/743aca5e-8670-c778-8f0d-104418f1db36.png)
続けて「SYNCHRONIZE」を押すとデプロイが始まります。
![スクリーンショット 2020-12-06 21.49.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/16deea0e-0cc5-8978-6b80-1c53f39d9614.png)
しばらく待って以下のようになれば完了です。
![スクリーンショット 2020-12-06 21.52.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/ff56f428-bd67-4c0d-86b7-445bac5b9d7f.png)

## 接続確認
メニューの「Kubernetes Engine」→「Services と Ingress」を選択するとArgoCDでデプロイしたリソース`create-react-app-web`が追加されています。

エンドポイントをクリックするとReactの初期ページを開くことができます。
![スクリーンショット 2020-12-06 21.57.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/04cee3b4-d792-a2f1-ec45-af240c86a05a.png)
これで一通り完了です。

ArgoCDでデプロイしたリソースであれば、ステータスやイベントログなどArgoCDの管理画面上で確認できます。もちろんkubectlコマンドでの操作も可能です。

## 変更内容の反映
マニフェストリポジトリを変更するとArgoCDが変更を検知します。
試しにカスタムValueファイルで定義しているPod数を変更してpushします。

```yaml:create-react-app-values.yaml
# Pod
replicaCount: 3 # ここを1から3に変更

# DockerImage
image:
  repository: gcr.io/gitops-react-app/create_react_app
  tag: "3"

# Service
service:
  type: LoadBalancer
```

ArgoCDは一定間隔でマニフェストリポジトリを監視しているので、しばらく待っているとステータスがOutOfSyncに変わります。(「REFRESH」を押して即時にOutOfSyncに変更することも可能です。)
![スクリーンショット 2020-12-06 22.12.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/08bec540-1972-2219-5b4a-7f03d89200a4.png)

この状態で先ほどと同様に「SYNC」→「SYNCHRONIZE」するとデプロイが行われ、Podが3つ増えます。
![スクリーンショット 2020-12-06 22.17.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263328/9428e02d-4bbb-daef-df14-a8af249a2b1f.png)
カスタムValueファイルを変更しただけですが、当然templates配下を変更しても検知されます。

アプリの変更を反映したい場合は、
アプリ修正してアプリリポジトリへpush
↓
GitHubActionsのワークフローによってGCRへ新しいタグのDockerイメージが登録
(ワークフローの実行回数をタグに含めているのでタグが3から4にインクリメントされる)
↓
あとは同じ要領でカスタムValueファイルを変更してデプロイするだけです。

カスタムValueの修正例

```yaml:create-react-app-values.yaml
# Pod
replicaCount: 3

# DockerImage
image:
  repository: gcr.io/gitops-react-app/create_react_app
  tag: "4" # ここを3から4に変更

# Service
service:
  type: LoadBalancer
```

# まとめ
今回まとめたのは基本的な使い方のみでしたが、実際にチームで運用する時は、Dockerイメージのタグ付けルールやDBのマイグレーションのタイミングなどなど検討しないといけないことはたくさんあります。運用事例は今後色んな記事を読んだりして勉強したいと思っています。

また、ArgoCDはまだまだ知らない機能が多いと思うので、今後もっと使い込んでいきたいと思います。GUIベースでの操作でしたが、CLI操作も可能なので何か他のサービスと組合わせてデプロイをもっと効率化できないかなーとかも思ったり。

以上、GitHubActionsとArgoCDとHELMを使用したGitOps手法の構築でした。

# 追記

※1 GitHubではパスワードを使用した認証は将来的に廃止されます。
https://github.blog/2020-12-15-token-authentication-requirements-for-git-operations/
したがってGitHubのprivateリポジトリへの認証はGitHub側で発行したアクセストークンを使用するようにしてください。
ArgoCDへのアクセストークンの設定方法は以下の公式ページの通り。
https://argoproj.github.io/argo-cd/user-guide/private-repositories/#access-token
基本的にPassword欄に発行したアクセストークンを設定するだけです。