CI/CD Integration Patterns
Purpose: For platform engineers and app developers, documents how to integrate CI/CD pipelines with the openCenter GitOps model, covering push-based CI and pull-based CD patterns.
Prerequisites
- openCenter cluster with FluxCD running
- Container registry accessible from the cluster (Harbor, GHCR, or other)
- Git repository for application source code
- Git repository for GitOps manifests (customer repo)
Architecture: Push CI + Pull CD
openCenter uses a split model:
| Phase | Model | Tool | Responsibility |
|---|---|---|---|
| CI (build) | Push-based | GitHub Actions, GitLab CI | Build image, run tests, push to registry |
| CD (deploy) | Pull-based | FluxCD | Detect new image, update manifests, reconcile |
Developer → Push code → CI builds image → Push image to registry
↓
FluxCD ← Reconcile ← Git manifest updated ← Image automation updates tag
Pattern 1: Manual Image Tag Update
The simplest pattern. CI builds and pushes the image; a human updates the tag in Git.
GitHub Actions Example
# .github/workflows/build.yaml
name: Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to registry
run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Build and push
run: |
IMAGE=ghcr.io/${{ github.repository }}:${{ github.sha }}
docker build -t $IMAGE .
docker push $IMAGE
echo "IMAGE=$IMAGE" >> $GITHUB_OUTPUT
Then update the GitOps manifest manually:
# In the customer GitOps repo
# applications/overlays/<cluster>/managed-services/my-app/deployment.yaml
spec:
template:
spec:
containers:
- name: my-app
image: ghcr.io/myorg/my-app:abc123def # ← update this
Pattern 2: CI Updates Git Manifest Automatically
CI builds the image and opens a PR to the GitOps repository with the new tag.
GitHub Actions Example
# .github/workflows/build-and-update.yaml
name: Build and Update GitOps
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.meta.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: type=sha,prefix=
- name: Build and push
uses: docker/build-push-action@v5
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
update-gitops:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout GitOps repo
uses: actions/checkout@v4
with:
repository: myorg/cluster-gitops
token: ${{ secrets.GITOPS_PAT }}
- name: Update image tag
run: |
cd applications/overlays/dev-cluster/managed-services/my-app
sed -i "s|image: ghcr.io/myorg/my-app:.*|image: ghcr.io/myorg/my-app:${{ needs.build.outputs.image_tag }}|" deployment.yaml
- name: Commit and push
run: |
git config user.name "ci-bot"
git config user.email "ci@myorg.com"
git checkout -b update/my-app-${{ needs.build.outputs.image_tag }}
git add .
git commit -m "chore: update my-app to ${{ needs.build.outputs.image_tag }}"
git push -u origin update/my-app-${{ needs.build.outputs.image_tag }}
gh pr create --title "Deploy my-app ${{ needs.build.outputs.image_tag }}" --body "Auto-generated by CI"
GitLab CI Example
# .gitlab-ci.yml
stages:
- build
- update-gitops
build:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
rules:
- if: $CI_COMMIT_BRANCH == "main"
update-gitops:
stage: update-gitops
image: alpine/git
needs: [build]
script:
- git clone https://oauth2:${GITOPS_TOKEN}@gitlab.com/myorg/cluster-gitops.git
- cd cluster-gitops
- |
sed -i "s|image: registry.gitlab.com/myorg/my-app:.*|image: registry.gitlab.com/myorg/my-app:${CI_COMMIT_SHORT_SHA}|" \
applications/overlays/dev-cluster/managed-services/my-app/deployment.yaml
- git config user.name "gitlab-ci"
- git config user.email "ci@myorg.com"
- git add .
- git commit -m "chore: update my-app to ${CI_COMMIT_SHORT_SHA}"
- git push origin main
rules:
- if: $CI_COMMIT_BRANCH == "main"
Pattern 3: FluxCD Image Update Automation
FluxCD can automatically detect new images in a registry and update manifests in Git. No CI-to-GitOps integration needed.
Setup
- Install the image automation controllers (included in openCenter FluxCD deployment):
kubectl get pods -n flux-system | grep image
# image-reflector-controller
# image-automation-controller
- Create an ImageRepository to scan:
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
name: my-app
namespace: flux-system
spec:
image: ghcr.io/myorg/my-app
interval: 5m
- Create an ImagePolicy to select tags:
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
name: my-app
namespace: flux-system
spec:
imageRepositoryRef:
name: my-app
policy:
semver:
range: ">=1.0.0"
- Create an ImageUpdateAutomation to commit changes:
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageUpdateAutomation
metadata:
name: my-app
namespace: flux-system
spec:
interval: 5m
sourceRef:
kind: GitRepository
name: customer-repo
git:
checkout:
ref:
branch: main
commit:
author:
name: fluxcd
email: fluxcd@myorg.com
messageTemplate: "chore: update {{.AutomationObject.Name}} images"
push:
branch: main
update:
path: ./applications/overlays/dev-cluster
strategy: Setters
- Mark the image field in your Deployment with a setter comment:
spec:
containers:
- name: my-app
image: ghcr.io/myorg/my-app:1.0.0 # {"$imagepolicy": "flux-system:my-app"}
FluxCD detects new tags matching the policy, updates the manifest in Git, and reconciles.
Verification
# Check image scan results
flux get image repository my-app
# Check selected image
flux get image policy my-app
# Check automation status
flux get image update my-app
# View recent image update commits
git log --oneline --author=fluxcd -5
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
ImageRepository shows scan failed | Registry auth missing | Create docker-registry secret and reference in ImageRepository |
| Image not updating | Policy doesn't match tags | Check tag format matches policy (semver vs sha) |
| Commit not pushed | Git auth for push missing | Verify GitRepository has write-capable deploy key |
| Old image still running | Reconciliation interval not elapsed | flux reconcile kustomization <name> --with-source |