Gitless GitOpsをFluxでやってみる

Gitless GitOpsとは何か?OCI中心のセキュアな新構成 という記事を読んで、Gitless GitOpsを試してみました。今回の内容はFluxのセットアップをするまでです。

環境

  • kind: v0.20.0 kind clusterに対して操作を行います
  • flux: v2.5.1

Gitless GitOpsの概要

元記事が詳しいのですが、ざっくり解説すると「Git RepositoryからOCI RepositoryにmanifestをCIでpushして、CDツールはOCI Repositoryを定期的に見に行くようにする」という内容になっています。

最近流行りのOCI Repositoryにコンテナイメージ以外も突っ込んじゃうやつだ!と思いました。
OCI Repositoryにすることで嬉しいことは2点あり、Git Repositoryにはないもの(OCI artifactの署名、検証や脆弱性スキャン)が使えるようになってサプライチェーンセキュリティの観点でセキュアになるというのと、git pull をするわけではないので大規模な場合にパフォーマンスの向上が見込める、ということのようです。

個人的なモチベーションとしてサプライチェーンセキュリティに興味があるので触ってみることにしました。

Fluxのセットアップ

fluxflux コマンドを使用するのが流儀っぽいのですが、kubectl を使いたいので、今回 flux bootstrap は使いません。

1. flux operatorのインストール

Flux Operator Installation のkubectlのところを参照。

$ kubectl apply -f https://github.com/controlplaneio-fluxcd/flux-operator/releases/latest/download/install.yaml

これで kind: FluxInstance とかが使えるようになります。

2. OCI Repositoryの作成

今回はghcr.ioを使用します。GitRepositoryからOCI Repositoryへの移行ガイド が参考になります。

移行ガイドではGitHub Actionsを使ってGit Repository上のmanifestをOCI Repositoryにpushする方法が書いてあります。
以下のようなworkflowになっていました。

name: publish-artifact

on:
  workflow_dispatch:
  push:
    branches:
      - 'main'
      - 'oci-artifacts'

permissions:
  packages: write

jobs:
  flux-push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Flux CLI
        uses: fluxcd/flux2/action@main       
      - name: Push immutable artifact
        run: |
          flux push artifact \
            oci://ghcr.io/${{ github.repository }}/manifests:$(git rev-parse --short HEAD) \
            --source="$(git config --get remote.origin.url)" \
            --revision="$(git branch --show-current)@sha1:$(git rev-parse HEAD)" \
            --creds flux:${{ secrets.GITHUB_TOKEN }} \
            --path="./"
      - name: Tag artifact as latest
        run: |
          flux tag artifact \
            oci://ghcr.io/${{ github.repository }}/manifests:$(git rev-parse --short HEAD) \
            --creds flux:${{ secrets.GITHUB_TOKEN }} \
            --tag latest

気になりポイントとして、

  • fluxcd/flux2/action@main でmainブランチ指定しているのは個人的に好みではない
    • hash指定するのが良い
  • flux push artifactpath には何を置けばいいのか
    • エントリーポイントになるところが何か分からない

1つめのmainブランチ指定については、単に flux コマンドを使いたいだけのようだったので aqua で代替しました。

2つめの path については、Exampleを参考にするとkustomizeの kustomization.yaml を置くようだと分かりました。ドキュメント:Generating a kustomization.yaml fileによると、pathを見に行く挙動は以下のようになっているようです。

  • Fluxがpathを見に行く
  • kustomization.yamlがあればそれを使う
  • なければそのディレクトリでkustomization.yamlを生成してそれを参照する
    • YAMLを列挙したような内容になるっぽい?

今回はJsonnet(Grafana tanka)を使っているので、以下のようにCIでYAMLにrenderingしつつ、kustomization.yamlを生成するようにしました。

コード例: PR

    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Install aqua
        uses: aquaproj/[email protected]
        with:
          aqua_version: v2.48.3
      - name: Generate artifact
        run: |
          mkdir out
          tk export ./out environments/default
          cd out && kustomize create --autodetect --recursive
        working-directory: ./home-k8s-app/example

# flux push artifactでは `--path="./home-k8s-app/example/out"` を指定する

これでmainブランチ指定を回避しつつ、Jsonnetを使ってmanifestをOCI Repositoryにpushできました。

注意: 僕は公開リポジトリ uta8a/infra で作業しているので、外部からワークフローをキックされないようにGitHubの設定 Settings > Actions > General から “Require approval for all external contributors” を選択してSaveしています。

3. FluxInstanceを作成する

これでghcr.ioにmanifestが配置されたので、手元のk8sクラスタからそれを引っ張ってきます。
ghcr.ioからpullするために認証情報が必要ですが、まずはそれ無しでFluxInstanceを作成します。

FluxInstanceはクラスタ当たり通常1つだけ作成するもので、pathを指定してmanifestのエントリーポイントに対応するものになります。ArgoCDのApplicationに対応するような複数のAppsは、kustomizeの中のResourceとして定義するようです。なので、FluxInstanceはApplicationとは紐づかない、上位概念だと思います。

重要な部分だけ書きます。全体はこちら

apiVersion: fluxcd.controlplane.io/v1
kind: FluxInstance
...
  sync:
    kind: OCIRepository
    url: "oci://ghcr.io/uta8a/infra/manifests" # ghcr.ioを指定
    ref: "latest"
    path: "." # pathは必須
    pullSecret: "ghcr-auth"

kubectl applyすると、FluxInstanceが作成されました。

$ k logs kustomize-controller-655f85f896-z6pq7 -n flux-system
{"level":"info","ts":"2025-04-18T12:05:22.948Z","msg":"Source artifact not found, retrying in 30s","controller":"kustomization","controllerGroup":"kustomize.toolkit.fluxcd.io","controllerKind":"Kustomization","Kustomization":{"name":"flux-system","namespace":"flux-system"},"namespace":"flux-system","name":"flux-system","reconcileID":"a9834208-3cf8-4fd9-bda4-3a094544d42b"}

source artifactが見つからないというエラーが出ていますね。これはghcr-authという名前のSecretでghcr.ioからpullできるような認証情報を与えると解決します。

GitHub classic PATで払い出した ghp_xxx みたいなトークンを使って以下のような dockerconfig.json を作成します。

{
    "auths": {
        "ghcr.io": {
            "username": "username",
            "password": "ghp_xxx",
            "auth": "<base64-encode(username:ghp_xxx)>"
        }
    }
}

これを cat dockerconfig.json | tr -d '\n' | base64 でbase64エンコードして、以下のようなSecretを作成します。
(この方法は秘密にしたい情報がそのままYAMLに載ってしまっているので、公開情報にはできないです)

apiVersion: v1
kind: Secret
metadata:
  name: ghcr-auth
  namespace: flux-system
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: <base64-encoded-dockerconfig.json>

このYAMLを kubectl apply すると、少し待つと無事にOCI Repositoryからmanifestをpullできました。

{"level":"info","ts":"2025-04-18T12:05:52.949Z","msg":"Source artifact not found, retrying in 30s","controller":"kustomization","controllerGroup":"kustomize.toolkit.fluxcd.io","controllerKind":"Kustomization","Kustomization":{"name":"flux-system","namespace":"flux-system"},"namespace":"flux-system","name":"flux-system","reconcileID":"1980b545-2041-454b-bcec-8b4a528476c3"}
{"level":"info","ts":"2025-04-18T12:06:07.119Z","msg":"server-side apply completed","controller":"kustomization","controllerGroup":"kustomize.toolkit.fluxcd.io","controllerKind":"Kustomization","Kustomization":{"name":"flux-system","namespace":"flux-system"},"namespace":"flux-system","name":"flux-system","reconcileID":"0fa2e279-96f0-44cf-a66c-2e99b1c3d25a","output":{"Deployment/default/podinfo":"created","Service/default/podinfo":"created"},"revision":"latest@sha256:19a31ccc8ae375e0e6fcd89205bfcefd044d8a1cfc4a02c22a8930a5aa065002"}
{"level":"info","ts":"2025-04-18T12:06:07.134Z","msg":"Reconciliation finished in 168.982584ms, next run in 10m0s","controller":"kustomization","controllerGroup":"kustomize.toolkit.fluxcd.io","controllerKind":"Kustomization","Kustomization":{"name":"flux-system","namespace":"flux-system"},"namespace":"flux-system","name":"flux-system","reconcileID":"0fa2e279-96f0-44cf-a66c-2e99b1c3d25a","revision":"latest@sha256:19a31ccc8ae375e0e6fcd89205bfcefd044d8a1cfc4a02c22a8930a5aa065002"}

server-side apply completed みたいな表示が確認できます。

これで実際にmanifestが反映されているのが確認できました。

その他

  • FluxはPlugin機構が無さそうです。今回のtankaで tk export してkustomizeを生成する方法よりもいい感じにやるのは難しいかも。

Is there a Plugin support for Flux?
https://github.com/fluxcd/flux2/discussions/2762

No custom plugin support so can’t really use tk directly. Instead you can pre-render your manifests (save them to Git) and have Flux use those. I ended up generating via tk export and wrapping with a kustomization.yaml that Flux v2 can use natively.
https://github.com/grafana/tanka/discussions/483

  • Fluxはkustomizeに寄っている感じがします。
    • ArgoCDにOCIサポート入ったらArgoCDに移行するかも。Jsonnetで書きたいので…

終わりに

Gitless GitOps, GitからのOCI移行はあったけどOCIからスタートする例は見当たらなかったのでブログとして残しておきました。
今回はkindで手元でやるだけでしたが、そのうちおうちk8sにFlux入れようと思います。複数Appを定義するためにディレクトリ構成を見直したいです。