
先に結論
この記事では、実際に運用しているこのブログのAstroアプリを例に、ソースコードのcommitが本番Podへ届くまでを追います。
現在の経路は、次のようになっています。
[手元のAstroプロジェクト]
│
├─ git push
│ ↓
│ [Forgejo]
│ └─ Astroのソースコードを管理
│
└─ buildctl
↓ kubectl port-forward
[BuildKit]
│
├─ Astroをビルド
├─ コンテナイメージを作成
└─ semverタグを付けてpush
↓
[Harbor]
│
│ 新しいタグを検知
↓
[Argo CD Image Updater]
│
│ KustomizeのnewTagを変更
│ Forgejoへcommit
↓
[Forgejo]
│
↓
[Argo CD]
│ automated sync
↓
[ブログの本番Pod]
たとえば、ブログの修正を含むイメージを0.3.21としてHarborへpushすると、Argo CD Image Updaterがそのタグを検知します。
Image Updaterは本番Podを直接変更するのではなく、デプロイ用リポジトリにあるKustomizeのnewTagを0.3.21へ更新し、その変更をForgejoへcommitします。Argo CDはそのcommitを検知し、本番Podを新しいAstroイメージへ入れ替えます。
人間が行う作業は、大きく2つです。
1つ目は、AstroのコードをForgejoへpushすること。
2つ目は、新しいsemverタグを付けてイメージをビルドし、Harborへpushすることです。
新しいタグの検知、Gitへの書き戻し、本番への反映は自動化しています。
ただし、commitしただけでビルドが始まるわけではありません。ビルドの開始は手動であるため、現状は完全なCI/CDではなく、**「手動ビルド+自動デプロイ」**です。
道のり1:AstroのコードをForgejoへpushする
このブログのフロントエンドはAstroで作っています。
Astroのソースコードと、Kubernetesへデプロイするためのmanifestは、別のGitリポジトリとして管理しています。いずれも正本はForgejoにあり、GitHubは外部ミラーとして利用しています。
コードを修正したら、まずAstro側のリポジトリへcommitし、Forgejoへpushします。
git add .
git commit -m "Update blog application"
git push origin main
この時点では、まだ本番環境には何も起きません。
Argo CDが監視しているのはデプロイ用リポジトリであり、Astroのソースコードをpushしただけでは、コンテナイメージも本番Podも更新されないからです。
次に、Astroのソースコードから新しいコンテナイメージを作ります。
道のり2:BuildKitでAstroのイメージを作る
イメージのビルドには、クラスタ内で動かしているBuildKitを使っています。
手元のマシンではDockerデーモンを動かさず、buildctlからクラスタ内のBuildKitへビルドを依頼します。
Astroのプロジェクトには、依存パッケージのインストール、Astroのビルド、配信用ランタイムの作成を行うDockerfileを置いています。BuildKitはそのDockerfileとソースコードを受け取り、ブログを実行するためのコンテナイメージを作ります。
ただし、BuildKitはKubernetesのClusterIP Serviceとして公開しているため、クラスタ外の作業マシンから直接接続することはできません。
そこで、作業時だけkubectl port-forwardを使います。
kubectl port-forward service/buildkit 1234:1234
別のターミナルから、ローカルへ転送したポートを指定してビルドします。
buildctl \
--addr tcp://127.0.0.1:1234 \
build \
--frontend dockerfile.v0 \
--local context=. \
--local dockerfile=. \
--output type=image,name=<Harbor上のイメージ>:0.3.21,push=true
実際のレジストリ名、プロジェクト名、イメージ名は環境ごとに異なるため、ここでは省略しています。
ビルドが成功すると、Astroの静的ファイルやサーバー処理を含む新しいコンテナイメージが作られ、そのままHarborへpushされます。
ここで実際に遭遇したのが、イメージのビルドは成功するのに、Harborへのpushだけが401で失敗する問題でした。
道のり3:Harborに届いたAstroイメージを検知する
Harborへ0.3.21のような新しいタグが登録されると、Argo CD Image Updaterが定期的にレジストリを確認し、現在のデプロイ設定より新しいバージョンがあることを検知します。
対象にするのは、次のような正式なsemver形式のタグだけです。
0.3.19
0.3.20
0.3.21
latest、dev、一時的な検証用タグは、自動更新の対象にしません。
Image Updaterは、検知したイメージを直接Deploymentへ設定するのではなく、デプロイ用リポジトリにあるKustomize設定を変更します。
変更前が次の状態だったとします。
images:
- name: blog-application
newTag: 0.3.20
新しい0.3.21がHarborへpushされると、Image Updaterは次のように変更し、Forgejoへcommitします。
images:
- name: blog-application
newTag: 0.3.21
Argo CDはこのcommitを検知し、通常のGitOpsと同じ手順で同期します。
DeploymentのPod templateが変更され、新しいAstroイメージを使ったPodが起動します。readiness checkを通過すると、古いPodと入れ替わり、このブログの更新が本番へ反映されます。
つまり、Harborへ新しいタグをpushした後の流れは、次のようになります。
Harborへ0.3.21をpush
↓
Image Updaterが検知
↓
KustomizeのnewTagを更新
↓
Forgejoへcommit
↓
Argo CDが同期
↓
Astroの新しいPodが起動
この仕組みにより、「現在、このブログがどのイメージで動いているか」は、Kubernetesクラスタの状態だけでなく、Gitの履歴からも確認できます。
この事例で分かったこと
Astroアプリを1つデプロイするだけなら、GitHub Actionsと外部レジストリを使った方が簡単です。
それでも自前で組んだことで、イメージが本番へ届くまでには、単にツールを並べるだけでは足りないことが分かりました。
BuildKitへどのように接続するのか。
Harborの認証情報はどこから渡されるのか。
Image Updaterはどのタグを新しいと判断するのか。
同じタグが上書きされないことを、どのように保証するのか。
実際にこのブログのAstroイメージを流してみると、問題になるのは各製品の機能ではなく、製品と製品の間にあるこうした「継ぎ目」でした。
現在は、Astroのコードを修正し、新しいバージョン番号でイメージをHarborへpushすれば、その後はGit履歴を残しながら本番Podまで届きます。
完全自動ではありませんが、自分が操作する範囲と、自動化に任せる範囲が明確なデプロイ経路になっています。



