Cloud BuildとArgoCDで楽にGitOpsを実装する。

## 前書き。
この記事は[フューチャー Advent Calendar 2019](https://qiita.com/advent-calendar/2019/future) 11日目の記事です。

Cloud Build/ArgoCDの使い方とか初期セットアップとかは他の記事にたくさん情報がありますので、ちょっと凝った？設定やパイプラインの全体像がよくわからんの。って人向けの記事になればと思います。

Cloud Build/ArgoCDそれぞれ3つづつポイントを挙げて紹介していきたいと思います。

## GitOpsって何さ。
当然なのですが、[提唱元の記事](https://www.weave.works/technologies/gitops/)を見ていただくのが一番早いかと。
ザックリ要約（with Google翻訳）してくれているのですが、

> 1. Kubernetesおよびその他のクラウドネイティブテクノロジーのオペレーティングモデル。コンテナ化されたクラスターとアプリケーションの展開、管理、監視を統合するベストプラクティスのセットを提供します。
1. アプリケーションを管理するための開発者エクスペリエンスへの道。エンドツーエンドのCI/CDパイプラインとGitワークフローは、操作と開発の両方に適用されます。

ということです。カタカナが多い・・・。

## ArgoCDって何さ。
これも[公式](https://argoproj.github.io/argo-cd/)から。（with Google翻訳）

> - Argo CDは、Kubernetes向けの宣言的なGitOps継続的デリバリーツールです。
- アプリケーションの定義、構成、および環境は宣言型であり、バージョン管理されている必要があります。アプリケーションの展開とライフサイクル管理は、自動化され、監査可能で、理解しやすいものでなければなりません。

宣言型であり、という点がKubernetes（以下k8s）と非常に相性よさげですね。

## Cloud Buildって何さ。
指定されたリポジトリ&ブランチへのコードPushをトリガーに各StepをDockerコンテナで処理してイメージビルドやそれに付随する作業を行ってくれるサービスです。
お安いしそれなりに高速なので使わない手はない・・・！（ビルド時のマシンタイプも指定できます。）

## 個人的見解。
個人的な見解でGitOps/ArgoCDいいね！と思っているのは、

- 開発者はデプロイを全く意識しなくていい。（Git/Githubの操作だけでなんかデプロイされる。）
- アプリケーション部分（テスト/ビルド）とインフラ部分（デプロイ）を疎に繋げられる。
    - 他のツールに浮気してもよし（差し替えやすい）。
    - 組織体によってリポジトリやリソースの管理者が違う場合でも権限を汚染しない。
- k8sで言うと`kubectl`とかしなくていい。手作業怖すぎ。
- Gitが正義。いつだってそれが現実。（差分検知／自動反映でGitのコードがインフラにある。）

もしあなたの組織 or あなた自身がこれらの悩みに直面しているのであれば、GitOps/ArgoCDを検討するのは大いに価値があるとおもいます。（ArgoCDは他ツールに差し替えてしまっても良いのですし。）

ちなみにテスト＆ビルドサービスはCloud Buildでもいいですし、他のメジャーなCIツールで実施してもいいですし、プラガブルなのがとても良いですね。

## よくあるフローと今回説明するフロー。

**【よくあるフロー】**
GitOpsでググると必ずと行っていいほどCIとCDはセットで出てきます。実際そうあるべきだとも思います。
下記のような図ですね。（特定のサービスが出てきますが、他のものでも全然構いません。）
![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315358/58be0e89-7a04-7ccd-3fab-04c9ebfd5c7c.png)

**【今回説明するフロー】**
CIツールの使い方ーとかそのあたりは本質じゃなかったので、テスト部分を省いてCloud Buildをそのまま使います。
~~決して検証コードを用意するのがめんｄ（ｒｙ~~
とはいえ、例えばCircleCIとCloud Buildはどちらもステップごとにコンテナベースで指示を行っていくので、基本的な考え方は同じになるかと思います。

![図2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315358/a7dbdcdd-8b36-8edd-c432-39c37d8cdc18.png)

ここでは下記GCP謹製ツールを用います。

- ビルドツール： Cloud Build
- コンテナレジストリ： Cloud Container Registory
- リポジトリ管理ツール：Github（をCloud Source Repositoriesに同期）
- デプロイ先：Google Kubernetes Engine

ちょっと補足なのですが、
**Cloud Source Repositories**はプルリクエストのような機能がないため、Githubでそれを代替しています。
また、後述のArgoCDの権限周りなんかもGithubならではの設定となってます。
あえてリポジトリを同期して使っていますが、実際Github単体でもこのアーキテクチャは動作します。

長くなりましたね。ここから本編に入ります。

## CloudBuildでアプリとインフラリポジトリの中継をする。
多分このフローで一番忙しいのはこの子になりますね。アプリ変更検知してビルドしてインフラリポジトリにPRせねばならんので・・・

さて、実際どうやるの、というところですが`cloudbuild.yaml`を書いていく形になります。（~~またYAMLか~~）
ちなみに、Cloud Buildで利用可能なイメージの一覧は[こちら](https://cloud.google.com/cloud-build/docs/cloud-builders?hl=ja)になります。

**今回Cloud Buildでポイントにしておきたいのは以下の点です。**

- ビルドにキャッシュを使いたい。
- Githubの認証鍵をGCSに生で置きたくない。
- 変更してコミットしてPRとか・・・よくわかんない。

早速行きましょう。

### ビルドにキャッシュを使いたい。
Cloud Buildはビルドにキャッシュを利用しない（毎回１から）なので、変更が多いアプリだとかなーりイライラします。
GoとかJavaなら・・・！ですが、PHPとかPython, Rubyあたりは辛いです。。。

ので、キャッシュを使えるようにしましょう。

キャッシュにはkanikoを利用します。（[公式ドキュメント](https://cloud.google.com/cloud-build/docs/kaniko-cache?hl=ja)）
とっても楽なのが、kanikoを使うと勝手にCloud Container RegistoryにPushまでしてくれる・・・！

```yaml
steps:

## イメージビルド with kaniko cache（cache-ttl: default=>2w）
- name: 'gcr.io/kaniko-project/executor:latest'
  id: 'Build & Push docker image'
  args:
  - --dockerfile=Dockerfile
  - --destination=asia.gcr.io/$PROJECT_ID/hogehoge/app:$SHORT_SHA
  - --cache=true
  - --cache-ttl=72h
```

これはCloud Build独自の話ですが、`$PROJECT_ID`、`$SHORT_SHA`は組み込み変数となっており、宣言するだけで使えます。楽ちん。
こちらも[公式ドキュメント](https://cloud.google.com/cloud-build/docs/configuring-builds/substitute-variable-values?hl=ja)があります。

### Githubの認証鍵とかTokenをGCSに生で置きたくない。
認証情報はそのままおいちゃダメ、絶対。
面倒だけれども、Cloud KMSで暗号化したものを配置し、それを復号化して使いましょう。
どっちみち置くんかい！という話ですが、KMSへの権限をIAMで絞れば見ただけでは読めないですよ、ね。
（ベストプラクティスがあれば教えてほしい！）

上のイメージビルドのステップに続いて追記しましょう。
※`$_`で始まる変数はCloud Buildに設定する環境変数になります。
取り回しが必要なファイルは`volumes:`でマウントしたディレクトリに保存していくことで後続のコンテナからも参照できるようになります。

```yaml
# GithubのSSH認証鍵を取得（CloudKMSで暗号化済み）
- name: 'gcr.io/cloud-builders/gsutil'
  id: Get Github auth key from GCS
  entrypoint: /bin/sh
  args:
  - '-c'
  - |
    set -x && \
    gsutil cp gs://$_BUCKET_NAME/id_rsa.enc /root/.ssh/id_rsa.enc && \
    gsutil cp gs://$_BUCKET_NAME/known_hosts /root/.ssh/known_hosts
  volumes:
  - name: 'ssh'
    path: /root/.ssh

# CloudKMSで暗号化されたSSH認証鍵を復号
- name: 'gcr.io/cloud-builders/gcloud'
  id: 'Decrypt ssh key with CloudKMS'
  args:
  - kms
  - decrypt
  - --ciphertext-file=/root/.ssh/id_rsa.enc
  - --plaintext-file=/root/.ssh/id_rsa
  - --location=global
  - --keyring=$_KEYRING_NAME
  - --key=$_KEY_NAME
  volumes:
  - name: 'ssh'
    path: /root/.ssh

# Git configの設定
- name: 'gcr.io/cloud-builders/git'
  id: 'Setup git config'
  entrypoint: 'bash'
  args:
  - '-c'
  - |
    chmod 600 /root/.ssh/id_rsa
    cat <<EOF >/root/.ssh/config
    Hostname github.com
    IdentityFile /root/.ssh/id_rsa
    EOF
  volumes:
  - name: 'ssh'
    path: /root/.ssh
```

### 変更してコミットしてPRとか・・・よくわかんない。
すみません、ここはほんとに人によるかと思います・・・
個人的なおすすめは[hub](https://github.com/github/hub)を使うことですが、自前でコンテナを作りたくなかったのと、シェルのほうがぱぱっとかけるということから下記はシェルでの実装になっています。

前提条件として、

- 自動更新するのはdeploymentのコンテナタグだけ。（他のリソースはCloud Buildから変更させない、しない）
- CommitメッセージやPRメッセージは適当です・・・。
- PRメッセージにアプリのリポジトリ+SHA_HASHを載せると、インフラ側でもここが変わったんだな、と分かってgoodです。

`git log`などでよしなに（bashでゴニョって）情報を載せてあげると良いですね。

さぁ、こちらもワンステップごとに見ていきましょう。

```yaml
# Githubから最新のManifestを取得
- name: 'gcr.io/cloud-builders/git'
  id: 'Clone current manifest file'
  args:
  - clone
  - git@github.com:$_GITHUB_REPO
  volumes:
  - name: 'ssh'
    path: /root/.ssh

# Manifestをアップデート
- name: 'gcr.io/cloud-builders/git'
  id: 'Update manifest'
  entrypoint: /bin/sh
  args:
  - '-c'
  - |
    set -x && \
    cd hogehogeapp-manifest && \
    git config --global user.email $_GITHUB_EMAIL && \
    git config --global user.name $_GITHUB_USER && \
    git remote set-url origin git@github.com:$_GITHUB_REPO && \
    git remote -v && \
    git checkout -b $_RELEASE_BRUNCH && \
    git pull origin $_RELEASE_BRUNCH && \
    sed -i "s/hogehoge\:[a-z0-9]*/hogehoge\:$SHORT_SHA/" hogehoge/deployment.yaml
  volumes:
  - name: 'ssh'
    path: /root/.ssh

# Manifestのコミット
- name: 'gcr.io/cloud-builders/git'
  id: 'Commit manifest'
  entrypoint: /bin/sh
  args:
  - '-c'
  - |
    set -x && \
    cd hogehogeapp-manifest/hogehoge && \
    git add deployment.yaml && \
    git commit -m "Excellent commit message" && \
    git push origin $_RELEASE_BRUNCH
  volumes:
  - name: 'ssh'
    path: /root/.ssh

# GithubのAccess Tokenを取得（CLoudKMSで暗号化済み）
- name: 'gcr.io/cloud-builders/gsutil'
  id: Get Github access token from GCS
  entrypoint: /bin/sh
  args:
  - '-c'
  - |
    set -x && \
    gsutil cp gs://$_BUCKET_NAME/github_access_token.enc /root/.ssh/github_access_token.enc
  volumes:
  - name: 'ssh'
    path: /root/.ssh

# CLoudKMSで暗号化されたAccess Tokenを復号
- name: 'gcr.io/cloud-builders/gcloud'
  id: 'Decrypt access token with CloudKMS'
  args:
  - kms
  - decrypt
  - --ciphertext-file=/root/.ssh/github_access_token.enc
  - --plaintext-file=/root/.ssh/github_access_token
  - --location=global
  - --keyring=$_KEYRING_NAME
  - --key=$_KEY_NAME
  volumes:
  - name: 'ssh'
    path: /root/.ssh

# ManifestのPR
- name: 'gcr.io/cloud-builders/curl'
  id: 'PR manifest'
  entrypoint: 'bash'
  args:
  - '-c'
  - |
    _ACCESS_TOKEN=`cat /root/.ssh/github_access_token`
    repo=`echo -n $_GITHUB_REPO | sed "s/.git//"`
    json=$(cat << EOS
    {"title": "good title", "head": "${_RELEASE_BRUNCH}", "base": "master", "body": "great PR messages"}
    EOS
    )
    curl -H 'Content-Type:application/json' \
        -H "Authorization: token ${_ACCESS_TOKEN}" \
        "https://api.github.com/repos/${repo}/pulls" \
        -d "$json"
  volumes:
  - name: 'ssh'
    path: /root/.ssh
```

### Cloud Buildはここまで！
お疲れさまでした！コード載せたら冗長で長かったですね・・・。
楽じゃないじゃん・・・（泣

## ArgoCDでGKEにデプロイしていく。

さて、デプロイの主役、ArgoCDの登場です。
使ってみた話はあれど、細かい設定もできるよ！という点を紹介していきたいと思います。
ArgoCDを宣言的デプロイでデプロイしても、インストールShellでデプロイしても同じことができますが、
宣言的デプロイをしておいたほうが色々楽です。（ドキュメントは[こちら](https://argoproj.github.io/argo-cd/operator-manual/declarative-setup/)）

**今回Argo CDでポイントにしておきたいのは以下の点です。**

- Github SSO認証を使いたい。（これはN番煎じ・・・）
- GithubのチームごとにRoleを制限したい！
- コンテナデプロイ前と後になにかしたい！
- （番外編）HPAが・・・なんか変な感じになる・・・

早速行きましょう。

### Github SSO認証を使いたい。（これはN番煎じ・・・）
こちらはすでに紹介があるのですが、前提条件なので・・・（GithubでOAuth Appの登録をしておいてください。）
公式ドキュメントは[こちら](https://argoproj.github.io/argo-cd/operator-manual/sso/)

`argocd-cm.yaml`を編集していきます。（ファイル名はデフォルトのものです。）

```yaml:argocd-cm.yaml
apiVersion: v1
data:
  dex.config: |
    connectors:
      - type: github
        id: github
        name: GitHub
        config:
          clientID: Githubで払い出したクライアントID
          clientSecret: $dex.github.clientSecret
          orgs:
          - name: your-org-name
          teams:
          - "Admin User"
          - "ReadOnly User"
```

更に、上記の`clientSecret`で指定した変数を読み込むため、`argocd-secret.yaml`を編集します。
※普通のシークレットでも良いのですが、GithubにBase64でエンコードしただけの認証情報を載せるのもアレなので、今回はSealedSecretを利用して暗号化しています。（なのでkindがSealedSecretになってます。）

```yaml:argocd-secret.yaml
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  creationTimestamp: null
  name: argocd-secret
  namespace: argocd
spec:
  encryptedData:
    dex.github.clientSecret: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEq.....
```

変更を適用し、画面にアクセスすると「LOGIN VIA GITHUB」のボタンが増えます！
SSOがこんなに簡単にできるとは・・・
![2019-12-10_20h26_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315358/15948956-96a3-e602-1efc-ed9e7cdf0e3c.png)

### GithubのチームごとにRoleを制限したい！
RBACの設定になります。
これもヒジョーに簡単にできるので、サクッと実装していきましょう。

公式ドキュメントは[こちら](https://argoproj.github.io/argo-cd/operator-manual/rbac/)です。

実は↑のSSO設定の際シレッと設定していましたが、`teams`の設定が重要になってきます。
ちなみにteamsの名前はGithubのTeamsタブで確認できる名前を指定しましょう。
※私はslugの設定もできるかなと思って頑張ったんですが、なぜかうまく行かず、、、

```yaml:argocd-cm.yaml
apiVersion: v1
data:
  dex.config: |
    connectors:
      - type: github
        id: github
        name: GitHub
        config:
          clientID: Githubで払い出したクライアントID
          clientSecret: $dex.github.clientSecret
          orgs:
          - name: your-org-name
          teams:
          - "Admin User"
          - "ReadOnly User"
```

おもむろに`argocd-rbac-cm.yaml`を編集していきます。

`p`から始まっている部分が各roleに割り当てる権限になります。
下記の例では`role:org-admin`と`role:org-user`にそれぞれ権限を付与しているのがわかると思います。
`g`から始まっている部分が、組織配下のTeamに上記roleを割り当てる行になります。

```yaml:argocd-rbac-cm.yaml
apiVersion: v1
kind: ConfigMap
data:
  policy.default: role:readonly
  policy.csv: |
    p, role:org-admin, applications, get, */*, allow
    p, role:org-admin, applications, update, */*, allow
    p, role:org-admin, applications, delete, */*, allow
    p, role:org-admin, applications, sync, */*, allow
    p, role:org-admin, applications, override, */*, allow
    p, role:org-admin, applications, create, */*, allow
    p, role:org-admin, certificates, create, *, allow
    p, role:org-admin, certificates, update, *, allow
    p, role:org-admin, certificates, delete, *, allow
    p, role:org-admin, clusters, create, *, allow
    p, role:org-admin, clusters, update, *, allow
    p, role:org-admin, clusters, delete, *, allow
    p, role:org-admin, repositories, create, *, allow
    p, role:org-admin, repositories, update, *, allow
    p, role:org-admin, repositories, delete, *, allow
    p, role:org-admin, projects, create, *, allow
    p, role:org-admin, projects, update, *, allow
    p, role:org-admin, projects, delete, *, allow

    p, role:org-user, applications, get, */*, allow
    p, role:org-user, applications, sync, */*, allow
    p, role:org-user, certificates, get, *, allow
    p, role:org-user, clusters, get, *, allow
    p, role:org-user, repositories, get, *, allow
    p, role:org-user, projects, get, *, allow

    g, your-org-name:Admin User, role:org-admin
    g, your-org-name:ReadOnly User, role:org-user
```

上記では結構ザックリ権限付与をしていますが、更に細かく指定していくことも可能です。

### コンテナデプロイ前と後になにかしたい！
コンテナのデプロイの前後でなにかしたい（例えばDBのシードを流し込むとか、デプロイ結果を通知するとか）場合に便利なHookがあります。

- `PreSync`アプリの新しいバージョンをデプロイする前に、フックを使用して例えばデータベースへシードを投入します。
- `PostSync`フックを使用して、例えばSyncの結果を通知するアプリを実装します。

※他にも`Sync`や`SyncFail`、`Skip`などのHookがあります。

フックを実装するときには`argocd.argoproj.io/hook`アノテーションを利用します。
Hookの削除には`argocd.argoproj.io/hook-delete-policy`アノテーションを利用します。

例えばSlackにSyncの完了時に投稿する場合は、下記のようなbatch用YAMLを作成します。

```yaml
apiVersion: batch/v1
kind: Job
metadata:
  generateName: app-slack-notification-
  annotations:
    argocd.argoproj.io/hook: PostSync
    argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
  template:
    spec:
      containers:
      - name: slack-notification
        image: appropriate/curl
        command:
          - "curl"
          - "-X"
          - "POST"
          - "--data-urlencode"
          - "payload={\"channel\": \"#somechannel\", \"username\": \"hello\", \"text\": \"App Sync succeeded\", \"icon_emoji\": \":ghost:\"}"
          - "https://hooks.slack.com/services/..."
      restartPolicy: Never
  backoffLimit: 2
```
こちらは[公式](https://argoproj.github.io/argo-cd/user-guide/resource_hooks/)にマルっと載ってます。

k8s単体だと実現できないような複雑な処理もでき、かゆいところにとっても手が届きます！

### （番外編）HPAが・・・なんか変な感じになる・・・
これはやっちゃったやつなのですが、GitOpsだー！とかウキウキしていたらHPAの設定＋deploymentにreplicasが残っていて、リリースの度HPAが意図する動作にならない＆運用しているとArgoCDの差分に出てくる。。。とか。

HPA＋ArgoCDで運用する場合は`replicas`のような動的にk8sに変えられる値はGitで管理しない（つまりYAMLに書かない）ようにしましょう。

普通に[ドキュメント](https://argoproj.github.io/argo-cd/user-guide/best_practices/)に書いてあるし、、、ArgoCDはドキュメントが結構手厚めなので、隅々まで目を通しておくと良いと思います。

## 最後に
いかがでしたでしょうか。
他の記事では言及されていない？ような部分にフォーカスしてデプロイフローと関連ツールを見てきました。
これどうやるんだろ、どうやってるんだろという点の気付きになればと思います。

より良い手段やツールがあればぜひ教えて下さい！それでは！