2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Gitea Actions + Argo CDで、OpenShiftにpushから数秒で反映されるCI/CDを組む

2
Last updated at Posted at 2026-11-09

はじめに

この記事はClaude Codeによって生成されています。

目的
自宅ラボのOpenShift(SNO)上で、CI/CDの一連の流れを組みました。

  • Gitへpushすると、テスト → イメージのビルド → レジストリへのpush が自動で走る(CI)
  • マニフェストの変更を検知して、クラスタへ自動でデプロイされる(CD)
  • pushから数秒で同期が始まるよう、GiteaからArgo CDへWebhookで通知する

構築はAnsible Playbookで行い、push → CI → CD → アプリ反映 まで通しで動作を確認しています。

環境

項目 内容
クラスタ Single Node OpenShift(インターネットに出られる構成)
Gitリポジトリ Gitea 1.27.0(クラスタ外の管理ホストでPodman稼働)
CI Gitea Actions + act_runner(Docker-in-Docker)
CD OpenShift GitOps(Argo CD)
イメージレジストリ OpenShiftの内部レジストリ
サンプルアプリ Python(FastAPI)のクイズアプリ

構成

CIはPush型、CDはPull型です。CIにはクラスタの変更権限を持たせず、「Gitに書かれた状態」と「クラスタの状態」の同期はArgo CDに任せます。

リポジトリ構成

アプリとマニフェストを1つのリポジトリに置いています。

app-demo/
├── app/                      # FastAPIアプリ
├── tests/                    # pytest
├── Dockerfile
├── .gitea/workflows/ci.yml   # CIの定義
└── manifests/base/           # Deployment / Service / Route / kustomization.yaml

1. CIランナー: act_runnerはDocker-in-Dockerで動かす

Gitea Actionsを動かすには、セルフホストのランナー(act_runner)が必要です。

ハマり: host実行モードが動かない

最初は、コンテナを使わずホスト上で直接ジョブを実行する「host」モードで動かそうとしました。ところがact_runner v0.6.1(検証時点のlatest)では、素のrun:ステップですら失敗しました。

bash: .cache/act/<hash>/act/workflow/0.sh: No such file or directory

ジョブのスクリプトを書き込むパスと、実行するときに参照するパスが食い違っていました。原因までは追っていませんが、hostモードは使えないと判断し、公式で実績のあるDocker-in-Docker方式に切り替えました。

構成

gitea/act_runner:latest-dindイメージを、Podman(Quadlet)で動かします。環境変数を渡すだけで、起動時にGiteaへ自動登録されます。

項目 値
イメージ docker.io/gitea/act_runner:latest-dind
環境変数 GITEA_INSTANCE_URL、GITEA_RUNNER_REGISTRATION_TOKEN、GITEA_RUNNER_NAME、GITEA_RUNNER_LABELS
コンテナの設定 --privileged、Network=host
ジョブのイメージ catthehacker/ubuntu:act-latest(git・node・python3・docker CLI入り)

登録トークンは、Giteaコンテナ内でgitea actions generate-runner-tokenを実行して取得します。

act_runnerのconfig.yamlでは次の3つを設定しました。

container:
  network: "host"        # ジョブコンテナから管理ホストのGitea(localhost:3000)へ届かせる
  privileged: true
  valid_volumes: ["**"]

networkを既定のbridgeのままにすると、ジョブコンテナのlocalhostは自分自身を指すので、Giteaに届きません。

ハマり: 自己署名のレジストリへdocker pushできない

Docker CLIには、podmanの--tls-verify=falseのようなコマンド単位のオプションがありません。act_runnerコンテナ内のdockerdに、/etc/docker/daemon.jsonをマウントして許可します。

{
  "insecure-registries": ["192.168.150.0/24"]
}

ホスト名ではなくクラスタのサブネットで指定しておくと、レジストリのホスト名が決まる前に設定できます。

2. 内部レジストリをクラスタの外へ公開する

CIは管理ホストからイメージをpushするので、内部レジストリのRouteを公開します。

oc patch configs.imageregistry.operator.openshift.io/cluster \
  --patch '{"spec":{"defaultRoute":true}}' --type=merge

ホスト名はdefault-route-openshift-image-registry.apps.<cluster>.<domain>になります。

push用には、アプリのNamespaceに専用のServiceAccount ci-pusherを作り、system:image-builderロールだけを付与しました。トークンはkubernetes.io/service-account-token型のSecretで発行しています(管理者権限は使わない)。

podman loginにホスト名だけを渡すと、HTTPにフォールバックして失敗することがありました。https://<host>と明示すると通ります。

3. Argo CD(OpenShift GitOps)の導入

redhat-operatorsカタログから、channel latestでOpenShift GitOps Operatorを入れます。openshift-gitopsNamespaceにArgo CDが立ち上がり、Routeも自動で作られます。

SNOのメモリが足りず、application-controllerとapplicationset-controllerがPendingになりました。メモリの割り当てが96%に達していたためで、VMのメモリを16GB→20GBに増やして解消しました。

4. Giteaの準備: リポジトリとCI用アカウント

GiteaのAPIでリポジトリを作り、CI専用のユーザーci-botに、このリポジトリへの書き込み権限だけのトークンを発行します。

ハマり(Gitea API)

  • Ansibleのuriモジュールは、force_basic_auth: trueを付けないとBasic認証を送らず401になる
  • ユーザー作成時のメールアドレスに@localhost(TLDなし)を指定すると422で拒否される。@example.localのようにTLD付きにする
  • Gitea ActionsのSecrets/Variablesで、GITEA_で始まる名前は予約語で使えない(GitHubのGITHUB_と同じ)。MANIFEST_PUSH_TOKEN・REPO_SLUGのように別の名前にする
  • Variables APIは、新規作成がPOST、更新がPUT。存在しないキーにPUTすると404になる(SecretsはPUTで作成・更新の両方ができる)

5. CIのワークフロー

pushされると、テスト → ビルド → push → マニフェストの更新 までを行います。

# .gitea/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    if: "!contains(github.event.head_commit.message, '[skip ci]')"
    runs-on: ubuntu-latest
    steps:
      # ジョブコンテナのDNSにはレジストリのホスト名がないので追記する
      - run: echo "${{ vars.REGISTRY_IP }} ${{ vars.REGISTRY_HOST }}" >> /etc/hosts

      - uses: actions/checkout@v4

      - name: Run tests
        run: |
          python3 -m venv .venv && . .venv/bin/activate
          pip install -q -r requirements-dev.txt
          python -m pytest tests/ -v

      - id: vars
        run: echo "tag=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"

      - name: Build and push
        env:
          REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
        run: |
          IMG=${{ vars.REGISTRY_HOST }}/app-demo/app-demo:${{ steps.vars.outputs.tag }}
          docker build -t $IMG .
          echo "$REGISTRY_TOKEN" | docker login "${{ vars.REGISTRY_HOST }}" -u ci-pusher --password-stdin
          docker push $IMG

      - name: Update manifest image tag
        env:
          MANIFEST_PUSH_TOKEN: ${{ secrets.MANIFEST_PUSH_TOKEN }}
        run: |
          sed -i "s|newTag:.*|newTag: ${{ steps.vars.outputs.tag }}|" manifests/base/kustomization.yaml
          git config user.name "ci-bot"
          git config user.email "ci-bot@localhost"
          git add manifests/base/kustomization.yaml
          git diff --cached --quiet && exit 0
          git commit -m "ci: bump image tag to ${{ steps.vars.outputs.tag }} [skip ci]"
          git push "http://ci-bot:${MANIFEST_PUSH_TOKEN}@localhost:3000/${{ vars.REPO_SLUG }}.git" HEAD:main
  • イメージのタグはgitの短いコミットハッシュにしています
  • CIが自分でマニフェストをpushするので、コミットメッセージに[skip ci]を付けてループを防ぎます

ハマり: pushとpullでレジストリのアドレスを分ける

kustomization.yamlのイメージ名に、pushで使った外部Routeのホスト名を書くと、PodがImagePullBackOffになりました。

x509: certificate signed by unknown authority

ノードがRouteの自己署名証明書を信頼していないためです。pushは外部Route、Podのpullはクラスタ内のサービスアドレスと使い分けます。

# manifests/base/kustomization.yaml
images:
  - name: app-demo
    newName: image-registry.openshift-image-registry.svc:5000/app-demo/app-demo
    newTag: abc1234   # CIが書き換える

注意: CIのコミットで手元のpushが拒否される

CIは毎回mainへコミットを追加するので、手元から次にpushすると! [rejected] (fetch first)になります。不具合ではないので、取り込んでからpushします。

git pull --rebase origin main && git push origin main

6. Argo CDのApplication

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: app-demo
  namespace: openshift-gitops
spec:
  project: default
  source:
    repoURL: http://<GiteaのIP>:3000/admin/app-demo.git
    targetRevision: main
    path: manifests/base
  destination:
    server: https://kubernetes.default.svc
    namespace: app-demo
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
  • repoURLには、クラスタ内のPodから届くGiteaのアドレスを書きます(管理ホストのlocalhostはクラスタからは届かない)
  • 非公開リポジトリなので、ci-botのトークンをargocd.argoproj.io/secret-type: repositoryラベル付きのSecretで登録します

ハマり: Syncがforbiddenで失敗する

Service・Deployment・Routeの作成が、いずれもis forbiddenで失敗しました。

OpenShift GitOpsのArgo CDは、既定ではopenshift-gitops以外のNamespaceを操作できません。デプロイ先のNamespaceにラベルを付けると、Operatorが必要な権限を自動で付与します。

oc label namespace app-demo argocd.argoproj.io/managed-by=openshift-gitops

7. Webhookでpushから数秒で同期させる

Argo CDは既定で約3分ごとにGitを確認するので、そのままではpushから反映まで数分かかります。GiteaのWebhookでArgo CDへ通知すると、数秒で同期が始まります。

7.1 設定

  1. Argo CDにWebhookの共有シークレットを設定し、argocd-serverを再起動する
oc patch secret argocd-secret -n openshift-gitops --type=merge \
  -p '{"stringData":{"webhook.gitea.secret":"<共有シークレット>"}}'
oc rollout restart deployment openshift-gitops-server -n openshift-gitops
  1. Giteaのapp.iniで、プライベートIP宛てのWebhookを許可する。GiteaはSSRF対策として、既定ではプライベートIP宛てのWebhookの送信を拒否します
[webhook]
ALLOWED_HOST_LIST = private
SKIP_TLS_VERIFY = true   ; Argo CDのRouteが自己署名証明書のため
  1. Giteaのリポジトリに、Argo CD宛てのWebhookを作る
項目 値
種類 Gitea
URL https://<Argo CDのRoute>/api/webhook
Content type application/json
シークレット 1.と同じ値
イベント push

7.2 ハマり: Webhookは届くのに同期されない

Webhookの送信は成功しているのに、Argo CDが同期しませんでした。argocd-serverのログを見ると、受信はしています。

Received push event repo: http://localhost:3000/admin/app-demo ...

原因はGiteaのURL設定ROOT_URLです。GiteaはWebhookの中身にROOT_URLを元にしたリポジトリのURLを入れます。Argo CDは、このURLとApplicationのrepoURLを文字列で比較して対象を探すので、localhostと<GiteaのIP>で一致せず、何も起きませんでした。エラーも出ません。

GiteaのROOT_URLを、ApplicationのrepoURLと同じアドレスに変えると解決しました。

[server]
ROOT_URL = http://<GiteaのIP>:3000/

コンテナで動かしているGiteaのapp.iniをホスト側から編集すると、ファイルの所有者がrootに変わります。Giteaはコンテナ内のUID 1000で動くため、permission deniedで起動しなくなりました。編集後はchown 1000:1000 app.iniで所有者を戻します。

Webhookを入れる前は、Gitを更新してもすぐに反映されないことがありました(repo-serverのキャッシュ)。次のコマンドで強制的に再取得できます。
oc patch application app-demo -n openshift-gitops --type=merge -p '{"metadata":{"annotations":{"argocd.argoproj.io/refresh":"hard"}}}'

8. 動作確認

  1. アプリのコードを変更してGiteaへpush
  2. Gitea ActionsでCIが走る(テスト → ビルド → push → kustomization.yamlのタグを新しいコミットハッシュに更新)
  3. Webhookで数秒後にArgo CDが同期し、新しいイメージのPodに入れ替わる

入れ替わったことは、Podのイメージタグか、Argo CDの画面で確認できます。

oc get pod -n app-demo -o jsonpath='{.items[*].spec.containers[*].image}'
# → image-registry.openshift-image-registry.svc:5000/app-demo/app-demo:<新しいコミットハッシュ>

Argo CDの画面では、Applicationの同期先のコミットが、CIが作ったコミット(ci: bump image tag to ...)になっていれば成功です。

ハマりポイントまとめ

# 事象 対応
1 act_runnerのhostモードでrun:ステップすら失敗 Docker-in-Docker方式(act_runner:latest-dind)にする
2 ジョブコンテナから管理ホストのGiteaに届かない container.network: "host"
3 自己署名のレジストリにdocker pushできない dockerdのinsecure-registries
4 podman loginがHTTPにフォールバックする https://を明示
5 Argo CDのコントローラーがPending SNOのメモリを増やす(16GB→20GB)
6 Gitea ActionsでGITEA_で始まる名前が使えない 別の名前にする
7 PodがImagePullBackOff(x509) pullはクラスタ内のサービスアドレスを使う
8 Syncがforbidden Namespaceにargocd.argoproj.io/managed-byラベル
9 Webhookが届くのに同期されない GiteaのROOT_URLをApplicationのrepoURLと一致させる
10 app.ini編集後にGiteaが起動しない 所有者をUID 1000に戻す

まとめ

  • Gitea Actions(Docker-in-Docker)+ Argo CDで、push → テスト → ビルド → デプロイ が自動で流れる構成を組めた
  • CIにはクラスタの権限を持たせず、レジストリへのpushとマニフェストの更新だけをさせる。デプロイはArgo CDが担う
  • Webhookを使うとpushから数秒で同期できる。ただしGiteaのURL(ROOT_URL)とApplicationのrepoURLが一致しないと、エラーなしで無視される
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?