Argo CDによるGitOps

Argo CDを使用したGitOpsの設計、導入を行ったので概要を書いていきます。

使用したツール、環境
manifest管理: Kustomize
GitOpsツール: Argo CD, Argo CD Image Updater
CI/CD: GitHub Actions
Container Registry: GitHub Container Registry
monorepo: Nx
k8s: GKE

## 全体構成図
リポジトリはk8sマニフェスト用とアプリケーション用（モノレポ）の２つです。

![gitops.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2333388/c1efb0aa-1eca-5957-14f3-95d3237fc225.jpeg)

## モノレポappsのimageのBuild & Push

mainブランチに変更があった時（PRをマージした時）とReleasesをつくった時にGitHua Actionsが走る様になっています。
1. 変更のあるappを取得
    A. PRマージ時
```npx nx affected --target=build-image --base=origin/main~1 --head=HEAD```
nx affectedで変更に対して影響のあったappだけを取得しています。

    B. Releases作成時
```npx nx affected --target=build-image --base=$previous_tag --head=$latest_tag```
nx affectedで１つ前のタグと最新のタグの差分を取得しています

2. matrixを設定
GitHub Actionsのmatrixを使います。
取得したapp全てに対してcall-build-and-push-image job経由でimage buildのworkflowを走らせます。

2. imageをbuild & push
Build and push image workflowでimageにtagをつけてcontainer registoryにpushします。

```set-matrix.yaml
name: Set Matrix

on:
  push:
    branches:
      - 'main'
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'

jobs:
  set-matrix:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    timeout-minutes: 10

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Get node version
        id: version
        run: |
          node_ver=$( node --version )
          echo "node_version=${node_ver:1}" >> $GITHUB_OUTPUT

      - name: Use the node_modules cache
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ steps.version.outputs.node_version }}-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-${{ steps.version.outputs.node_version }}

      - name: Install dependencies
        run: npm ci
 
      - name: Get affected nx apps
        if: ${{ github.ref == 'refs/heads/main' }}
        run: npx nx affected --target=build-image --base=origin/main~1 --head=HEAD

      - name: Get affected nx apps (tag)
        if: startsWith(github.ref, 'refs/tags/v')
        run: |
          latest_tag=${{ github.ref_name }}
          previous_tag=$(git describe --abbrev=0 --tags $latest_tag^)
          npx nx affected --target=build-image --base=$previous_tag --head=$latest_tag

      # nx affected --target=build-image で取得したappのリストをmatrixで使える形に変換（ゴリ押し気味なので他の方法を検討中）
      - name: Set matrix
        id: set-matrix
        run: |
          file_path="apps.txt"
          if [[ ! -f "$file_path" ]] || [[ ! -s "$file_path" ]]; then
            echo "matrix={}" >> $GITHUB_OUTPUT
          else
            mapfile -t apps_array < apps.txt
            json_string="{\"app\":["
            for ((i=0; i<${#apps_array[@]}; i++)); do
              json_string+="\"${apps_array[i]}\""
              if [ $i -lt $((${#apps_array[@]} - 1)) ]; then
                json_string+=", "
              fi
            done
            json_string+="]}"
            echo "matrix=${json_string}" >> $GITHUB_OUTPUT
          fi

  call-build-and-push-image:
    name: Call build and push image
    needs: set-matrix
    if: ${{ needs.set-matrix.outputs.matrix != '{}' }}
    strategy:
      matrix: ${{ fromJson(needs.set-matrix.outputs.matrix) }}
    uses: ./.github/workflows/build-and-push-image.yaml
    with:
      app: ${{ matrix.app }}
```

```build-and-push-image.yaml
name: Build and push image

on:
  workflow_call:
    inputs:
      app:
        required: true
        type: string

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    timeout-minutes: 10

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Log in to the Container registry
        uses: docker/login-action@v2
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ inputs.app }}
          tags: |
            type=sha
            type=semver,pattern={{version}}

      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        with:
          context: .
          file: tools/${{ inputs.app }}/Dockerfile
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
```

## k8s manifest
Kustomizeを使って環境ごとにディレクトリを分けて管理します。
productionやstagingはbaseのディレクトリ構成とほぼ同じです。
```
k8s/
├── base/
│   ├── apps/
│   │   ├── app1/
│   │   │   ├── deployment.yaml
│   │   │   ├── service.yaml
│   │   │   ├── ...
│   │   │   └── kustomization.yaml
│   │   ├── app2/
│   │   ├── app3/
│   │   ├── ...
│   │   └── kustomization.yaml
│   ├── hoge/
│   │   ├── huga.yaml
│   │   └── kustomization.yaml
│   └── ...
│
└── overlays/
    ├── production/
    │   ├── ...
    │   └── kustomization.yaml
    └── staging/
        ├── ...
        └── kustomization.yaml
```

## Argo CD Image Updaterによってk8sのimageをアップデート
Argo CD Image Updaterがimageの更新を検知してk8sへ変更を反映させます。

Argo CD Image Updaterがimageの更新を検知して自動でmanifestを更新しにいきます。

ApplicationSetのannotationsの主要な部分は以下のようになります。

```staging.yaml
annotations:
  argocd-image-updater.argoproj.io/write-back-method: git
  argocd-image-updater.argoproj.io/1update-strategy: latest
  argocd-image-updater.argoproj.io/ignore-tags: latest
  argocd-image-updater.argoproj.io/allow-tags: regexp:sha-[a-z0-9]+
```
```production.yaml
annotations:
  argocd-image-updater.argoproj.io/write-back-method: git
  argocd-image-updater.argoproj.io/update-strategy: semver
  argocd-image-updater.argoproj.io/ignore-tags: latest
  argocd-image-updater.argoproj.io/git-branch: image-updater{{range .Images}}-{{.Alias}}-{{.NewTag}}{{end}}
```

##### argocd-image-updater.argoproj.io/write-back-method
GitOpsをしたかったのでgitです。アプリケーションのマニフェストをもとにパラメータをオーバーライドします。
##### argocd-image-updater.argoproj.io/ignore-tags
docker/metadata-actionでtag eventの時にlatestをつける設定にしていますが、image updaterには無視させたいのでlatestを指定します。latestタグをつける設定を省けばこちらは不要。
##### argocd-image-updater.argoproj.io/update-strategy
stagingは最新のimageに更新するようにします。（sha-abc01234 のようなtagをつけるので argocd-image-updater.argoproj.io/allow-tagsも設定）
productionはsemantic versioningが最新のimageに更新するようにします。
##### argocd-image-updater.argoproj.io/git-branch
productionはmanifestを自動更新したくなかったため、mainとは別にコミットブランチを作成&ブランチへのコミット作成をトリガーにGitHub ActionsでbotがPRを作成するようにしています。

```create-pr.yaml
name: Create PR

on:
  push:
    branches:
      - 'image-updater-*'

jobs:
  create-pr:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
      - name: Create pull request
        run: |
          gh pr create \
          --base main \
          --head ${{ github.ref_name }} \
          --title 'chore: image update' \
          --body 'This is an automated PR for image update.' \
          --reviewer ${{ vars.IMAGE_UPDATE_REVIEWER }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

## まとめ
変更などあれば更新していこうと思います。

### 参考にさせていただいた記事

https://techblog.zozo.com/entry/measure-argocd-introduction

https://techblog.zozo.com/entry/measure-argocd-image-updater

https://zenn.dev/nekoshita/articles/02c1e59a487fb4

https://techstep.hatenablog.com/entry/2022/12/03/111824