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マニフェスト用とアプリケーション用(モノレポ)の2つです。
モノレポappsのimageのBuild & Push
mainブランチに変更があった時(PRをマージした時)とReleasesをつくった時にGitHua Actionsが走る様になっています。
-
変更のある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で1つ前のタグと最新のタグの差分を取得しています -
matrixを設定
GitHub Actionsのmatrixを使います。
取得したapp全てに対してcall-build-and-push-image job経由でimage buildのworkflowを走らせます。 -
imageをbuild & push
Build and push image workflowでimageにtagをつけてcontainer registoryにpushします。
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 }}
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の主要な部分は以下のようになります。
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]+
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を作成するようにしています。
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 }}
まとめ
変更などあれば更新していこうと思います。
参考にさせていただいた記事