🐙

Argo CD が参照していないリポジトリからイメージバージョンを更新する方法

2025/04/03に公開

クラウドエース北野です。
Argo CD が参照していないアプリケーションのリポジトリから Kubernetes マニフェストのイメージバージョンを更新する CD パイプラインの作成方法を紹介します。

概要

アプリケーションのコードが格納されているリポジトリで次のようなタスクを実行すると、コンテナイメージのプッシュと同時にシステムのポッドのコンテナイメージのバージョンの更新ができます。

  1. アプリケーションのビルド
  2. コンテナイメージのプッシュ
  3. CD パイプラインのリポジトリのチェックアウト
  4. 構成ファイルのイメージバージョン更新
  5. 更新した構成ファイルのコミット

GitHub Actions で上記を実行するとき、3 ~ 5 のジョブのステップを次のように定義できます。

- name: Checkout Kubernetes Manifest Repository
  uses: actions/checkout@v2
  with:
    repository: <Organization Name>/<Repository Name>
    token: ${{ vars.PAT }}
    path: .

- name: Update image version
  working-directory: <MANIFEST FILE DIRECTORY PATH>
  run: |
    sed -i '<IMAGE PATH>/s/.*/\ \ value\:\ <IMAGE PATH>:${{ github.sha }}/' <MANIFEST FILE>
    git config --local user.email ${{ vars.USER_EMAIL }}
    git config --local user.name ${{ vars.USER_NAME }}
    git add <MANIFEST FILE>
    git commit -m <COMMENT>
    git push

はじめに

Argo CD はベストプラクティスとして、アプリケーションのソースコードと、Kubernetes のマニフェストを別々のリポジトリで管理すべきだとしています。これはリポジトリを分けることで CI と CD を分離でき、本番環境へのアプリケーションのリリースを細かく制御できるということがあげられます。

アプリケーションのコードと Kubernetes のマニフェストを分離して管理すると、リリースのフローは次のようになります。

  1. コード開発
  2. コンテナイメージのイメージ格納リポジトリへのプッシュ
  3. Kubernetes マニフェストのイメージバージョンの更新
  4. Kubernetes マニフェストの適用

2 のタスクは GitHub Actions などの CI ツールで、4 のタスクは Argo CD などの CD ツールでそれぞれ自動化できます。そのため、リポジトリのファイルを更新すると、安全に本番環境をリリースできます。

しかし、リポジトリを分けて管理しているため、開発環境も同じフローで更新する必要があります。そのため、開発環境で機能を Kubernetes 上で検証しようと思うと、アプリケーションのコードの更新をして、Kubernetes のマニフェストを更新するという 2 回のコミットが必要となり、非常に煩雑なものとなります。

そこで、この記事では 2 から 3 のタスクをアプリケーションの GitHub リポジトリで自動化する方法を紹介します。この方法を適用すると、アプリケーションのリポジトリを更新すると、Kubernetes マニフェストも同時に更新されるので、1 回のコミットで開発環境の更新が実現されます。

GitHub Actions の設計

アプリケーションのリポジトリで次のような GitHub Actions のジョブを定義します。

  1. アプリケーションのビルド
  2. コンテナイメージのプッシュ
  3. Kubernetes マニフェストリポジトリのチェックアウト
  4. 構成ファイルのイメージバージョン更新
  5. 更新した構成ファイルのコミット

GitHub Actions で別のプライベートリポジトリをチェックアウトするには、次のように token に Personal Access Token (以降 PAT と呼びます)を指定すると実施できます。このとき PAT のスコープには repo を選択したものをご利用ください。

- name: Checkout Kubernetes Manifest Repository
  uses: actions/checkout@v2
  with:
    repository: <Organization Name>/<Repository Name>
    token: ${{ secrets.PAT }}
    path: .

repository で、別のリポジトリを組織/リポジトリ名で指定します。path でチェックアウトするパスを指定します。チェックアウトすると、リポジトリのルート以下の内容がすべてそのパスに展開されます。

GitHub Actions で変更内容をコミットするには、ユーザー情報を設定する必要があるので以下のようにタスクを定義します。

- name: Update image version in Kubernetes Manifest
  working-directory: src/cloudrun/helloworld/overlayers/dev
  run: |
    git config --local user.email ${{ vars.USER_EMAIL }}
    git config --local user.name ${{ vars.USER_NAME }}
    git add <FILES>
    git commit -m <COMMENT>
    git push

user.email と user.name は、PAT を発行した GitHub アカウントのユーザー名と e-mail アドレスを代入してください。

イメージバージョンは、git のコミットハッシュを指定する必要があるため、sed コマンドを使い次のようにバージョンを置換します。

sed -i '/<CONTAINER IMAGE PATH>/s/.*/\ \ value\:\ <CONTAINER IMAGE PATH>:${{ github.sha }}/' <MANIFEST FILE>

これらの内容をまとめると、GitHub Actions は次のようになります。

name: Update image version

on:
  workflow_dispatch:

jobs:
  update_img_ver:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Build and Push Image
        run: |
          docker build -t <IMAGE PATH>:${{ github.sha }} .
          docker push <IMAGE PATH>:${{ github.sha }}

      - name: Checkout Kubernetes Manifest Repository
        uses: actions/checkout@v2
        with:
          repository: <Organization Name>/<Repository Name>
          token: ${{ secrets.PAT }}
          path: .

      - name: Update image version in Kubernetes manifest
        working-directory: src/cloudrun/helloworld/overlayers/dev
        run: |
          sed -i '/<CONTAINER IMAGE PATH>/s/.*/\ \ value\:\ <CONTAINER IMAGE PATH>:${{ github.sha }}/' <MANIFEST FILE>
          git config --local user.email ${{ vars.USER_EMAIL }}
          git config --local user.name ${{ vars.USER_NAME }}
          git add kustomization.yaml
          git commit -m "update hello image version from app_sample_repo GH"
          git push

GitHub Actions の設計内容の実施

GitHub Actions 設計内容を次のようなシステム構成で試してみます。

上記の構成を次のようにして構築します。

  1. Google Cloud 上への Artifact Registry の作成
  2. アプリケーションコードのリポジトリの GitHub Actions から Docker イメージの登録
  3. Google Kubernetes Engine (以降 GKE と呼びます)の作成
  4. GKE 上への Argo CD の作成
  5. Kubernetes マニフェストのリポジトリを SYNC する Argo CD の Application の作成
  6. Argo CD から Kubernetes リソースのデプロイ

さらに検証では、アプリケーションコードのリポジトリの GitHub Actions からイメージの登録と、マニフェストを更新して、Argo CD でイメージが更新されるのかを確認します。

事前準備

GitHub Actions で内容を試すために次の内容を構築します。

  • Google Cloud での設定
    • Artifact Registry の作成
    • Workload Identity の作成
    • GKE の作成
    • Certificate Manager の作成
  • Argo CD の設定
    • Argo CD のインストール
    • Application の作成

GKE を作成し、Argo CD をインストールする作業はこちらの記事を参照してください。

Google Cloud の設定

Docker イメージを登録する Artifact Registry は次のように作成します。

artifact_registry.tf
resource "google_artifact_registry_repository" "main" {
  repository_id = <ARTIFACT REGISTRY REPOSITORY NAME>

  project     = var.project
  location    = "asia-northeast1"
  format      = "DOCKER"
}

GitHub Actions から Artifact Registry にプッシュするためには GitHub Actions を認証する必要があります。この記事では、Workload Identity を使い認証させます。Workload Identity の詳細については、こちらを参照してください。

Workload Identity および、認証に使うサービスアカウントは次のように作成します。

service_account.tf
resource "google_service_account" "main" {
  account_id = <SERVICE ACCOUNT ID>

  project = var.project
}

resource "google_project_iam_member" "main" {
  member = google_service_account.main.member
  
  project = var.project
  role    = "roles/artifactregistry.writer"
}
workload_identity.tf
resource "google_iam_workload_identity_pool" "main" {
  workload_identity_pool_id = <WORKLOAD IDENTITY POOL NAME>

  project = var.project
}

resource "google_iam_workload_identity_pool_provider" "main" {
  workload_identity_pool_provider_id = <WORKLOAD IDENTITY POOL PROVIDER>

  project                   = var.project
  workload_identity_pool_id = google_iam_workload_identity_pool.main.workload_identity_pool_id
  attribute_condition       = "assertion.job_workflow_ref.contains(\"<GITHUB ORGANIZATION>/<GITHUB REPOSITORY>\")"
  attribute_mapping = {
    "google.subject" = "assertion.repository"
  }

  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }
}

resource "google_service_account_iam_member" "main" {
  service_account_id = google_service_account.main.account_id

  role   = "roles/iam.workloadIdentityUser"
  member = format("principal://iam.googleapis.com/%s/subject/%s", google_iam_workload_identity_pool.main.name, <GITHUB ORGANIZATION>/<GITHUB REPOSITORY>)
}

サンプルアプリ

Kubernetes でデプロイするアプリケーションは、次のように Hello World! を返すものとします。

main.go
package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello World!\n")
	})
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}

このアプリケーションのコンテナイメージにする Dockerfile は次の通りです。

Dockerfile
FROM golang as builder

WORKDIR /app

RUN go mod init helloworld
COPY *.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o /helloworld

FROM gcr.io/distroless/base-debian11

WORKDIR /

COPY --from=builder /helloworld /helloworld
EXPOSE 8080
USER nonroot:nonroot
CMD ["/helloworld"]

Kubernetes マニフェスト

このコンテナイメージを次のようなディレクトリ構造の Kustomize でデプロイします。

.
├── base
│   ├── deployment.yaml
│   └── kustomization.yaml
└── overlayes
    ├── deployment.yaml
    └── kustomization.yaml
base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ./deployment.yaml
base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: helloworld
  namespace: default
  labels:
    app: helloworld
    backstage.io/kubernetes-id: helloworld

spec:
  selector:
    matchLabels:
      app: helloworld
  template:
    metadata:
      labels:
        app: helloworld
    spec:
      containers:
        - name: helloworld
          image: $IMAGE
          ports:
            - containerPort: 8080
overlayes/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../base

patchesJson6902:
  - target:
      kind: Deployment
      name: helloworld
    path: ./deployment.yaml
overlayes/deployment.yaml
- op: replace
  path: /spec/template/spec/containers/0/image
  value: <IMAGE PATH>:<VERSION>

Argo CD の設定

Argo CD には Kubernetes のマニフェストを格納しているリポジトリを連携し、リポジトリを連携する次のようなアプリケーションを作成します。

Application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: helloworld
  namespace: argocd

spec:
  destination:
    namespace: default
    server: https://kubernetes.default.svc
  project: default
  source:
    path: <MANIFEST FILE PATH>
    repoURL: <REPOSITORY PATH>
    targetRevision: HEAD
  syncPolicy:
    automated: {}

GitHub Actions

Hello World を表示するアプリケーションのコードを管理しているリポジトリに次の GitHub Actions を作成します。

push_image.yaml
name: Update helloworld image

on:
  workflow_dispatch:

jobs:
  update_img_ver:
    runs-on: ubuntu-latest
    permissions:
      contents: 'read'
      id-token: 'write'
    defaults:
      run:
        working-directory: src/golang/helloworld

    steps:
      - uses: actions/checkout@v4

      - name: Authenticate to google cloud
        id: authorization-googlecloud
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ vars.WORKLOADIDENTITY }}
          service_account: ${{ vars.SERVICEACCOUNT }}

      - name: Set up Cloud SDK
        id: setup-cloudsdk
        uses: google-github-actions/setup-gcloud@v2

      - name: Configure docker client
        run: gcloud auth configure-docker asia-northeast1-docker.pkg.dev

      - name: Build and Push Image
        run: |
          docker build -t ${{ vars.ARTIFACTREGISTRY }}/<IMAGENAME>:${{ github.sha }} .
          docker push ${{ vars.ARTIFACTREGISTRY }}/<IMAGENAME>:${{ github.sha }}

      - name: Checkout Kubernetes Manifest Repository
        uses: actions/checkout@v2
        with:
          repository: AriaAquamarine/kubernetes_studies
          token: ${{ secrets.PAT }}
          path: .

      - name: Update helloworld image version in Kubernetes Manifest
        working-directory: src/k8s/helloworld/overlayes
        run: |
          sed -i '/<IMAGE PATH>/s/.*/\ \ value\:\ <IMAGE PATH>:${{ github.sha }}/' deployment.yaml
          git config --local user.email ${{ vars.USER_EMAIL }}
          git config --local user.name ${{ vars.USER_NAME }}
          git add deployment.yaml
          git commit -m "update hello image version from app_sample_repo GH"
          git push

GitHub Actions の変数、シークレットは次の値を設定します。

変数名 内容
WORKLOADIDENTITY Workload Identity ID
SERVICEACCOUNT サービスアカウント email
ARTIFACTREGISTRY Artifact Registry のパス
PAT GitHub アカウントの PAT
USER_EMAIL GitHub アカウントの email
USER_NAME GitHub アカウント名

アプリケーションリポジトリからのイメージの更新

main.go のコードを 次のように Argo CD Test! と表示されるように変更して、GitHub にプッシュして GitHub Actions を実行してみます。

main.go
package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Argo CD Test!\n")
	})
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}

このときのコミットハッシュは次のようになりました。

git log -1 | tee | head -n1
commit d89bbea08fc332419741046f161d3dc34b0bfe39

GitHub Actions を実行して、ジョブが完了した後に Argo CD の Application を Refresh して DIFF を確認します。


差分を確認すると、コンテナのイメージバージョンが git のコミットハッシュと一致していることが確認できます。

さいごに

Argo CD が参照していないアプリケーションコードのリポジトリから Kubernetes マニフェストのイメージバージョンを更新する方法を紹介しました。この方法を開発環境のデプロイに適用すると、開発を円滑にすすめることができるようになります。
この方法はどの環境にも適用できてしまうので、本番環境に適用してしまうと、安全なデプロイパイプラインを実現できなくなるので、導入には注意してください。また、開発環境に導入しても本番環境に適用できないように GitHub の Rules などを作成して、意図していない GitHub Actions が作成されないように保護してください。

Discussion