Lately I've been using GitOps for managing the continous deployment of my applications.
My applications runs in a Kuberentes cluster so they are managed by a couple of different yml files (manifests).
This blog post will demonstrate a complete ci/cd pipeline using GitHub Actions. Whenever I commit new source code, the following will happen:
- A GitHub action runs a build pipeline and create a new docker image
- It then updates another git repository (
JOS.Infra
) which contains the Kubernetes manifests. - Argo CD will see that the
JOS.Infra
repository has been updated and create a new deployment. - The new code will be deployed and run in the cluster.
Pretty neat!
I have created a sample application, JOS.Echo
, that I will use in this post as an example.
Setup
I have two git repositories:
- JOS.Echo - The source code of the application
- JOS.Infra - The kubernetes manifests. This repository will be updated via a GitHub action in the
JOS.Echo
repository.
I also have Argo CD running in my Kubernetes cluster.
Manifests
My manifests are really simple, they look like this:
deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: jos-echo-deployment
spec:
replicas: 3
selector:
matchLabels:
app: jos-echo
template:
metadata:
labels:
app: jos-echo
spec:
imagePullSecrets:
- name: dockerconfig-ghcr.io
containers:
- name: jos-echo-container
image: ghcr.io/joseftw/jos-echo:1.0.21-ci.ga3b2bc3949
resources:
limits:
cpu: "1"
memory: 2048Mi
requests:
cpu: 0m
memory: 128Mi
volumeMounts:
- name: certs-volume
mountPath: /certs
readOnly: true
ports:
- containerPort: 5555
name: http-web-svc
volumes:
- name: certs-volume
secret:
secretName: local-josef-guru-tls
ingress.yml
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: jos-echo-ingress-route
annotations:
kubernetes.io/ingress.class: traefik-internal
spec:
entryPoints:
- websecure
routes:
- match: Host(`echo.local.josef.guru`)
kind: Rule
services:
- name: jos-echo-service
port: 5555
service.yml
apiVersion: v1
kind: Service
metadata:
name: jos-echo-service
spec:
ports:
- name: http
port: 5555
targetPort: http-web-svc
selector:
app: jos-echo
GitHub Action
My GitHub Action looks like this:
name: JOS.Echo
on:
workflow_dispatch:
push:
branches: [ "main" ]
jobs:
build:
name: CI
runs-on: ubuntu-latest
timeout-minutes: 20
outputs:
semver2: ${{ steps.nbgv.outputs.SemVer2 }}
version: ${{ steps.nbgv.outputs.Version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: dotnet/nbgv@master
id: nbgv
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 8.0.x
dotnet-quality: 'preview'
- name: Build
run: |
dotnet test src/JOS.Echo.sln -c Release /p:AssemblyVersion=${{ steps.nbgv.outputs.version }} /p:InformationalVersion=${{steps.nbgv.outputs.semver2}} --verbosity minimal
- name: Docker - Login
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker - Build and Push
uses: docker/build-push-action@v3
with:
context: .
file: ./src/JOS.Echo/Dockerfile
platforms: linux/amd64
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/jos-echo:latest
ghcr.io/${{ github.repository_owner }}/jos-echo:${{ steps.nbgv.outputs.semver2 }}
- uses: actions/checkout@v4
name: GitOps - Checkout
with:
repository: joseftw/jos.infra
ref: 'develop'
token: ${{ secrets.JOS_YODA_PAT }}
fetch-depth: 0
path: jos.infra
- name: GitOps - Patch and push
run: |
git -C jos.infra config user.name "jos-yoda"
git -C jos.infra config user.email "some-secret-email@users.noreply.github.com"
sed -i'.bak' -e 's|/jos-echo:*.*.*-*.*$|/jos-echo:${{steps.nbgv.outputs.semver2}}|g' jos.infra/apps/internal/jos.echo/deployment.yml
git -C jos.infra commit -am "jos-echo ${{steps.nbgv.outputs.semver2}}"
git -C jos.infra push origin develop
Let's break it down:
- Checkout the source code
- Build the project
- Login to GitHub Container Registry
- Build the docker image
- Push it to GitHub Container Registry
- Checkout the JOS.Infra repository
- Patch the deployment.yml file with the new image tag
- Commit and push the changes to JOS.Infra.
Let's talk a bit about the last three steps, the "GitOps portion".
Connecting to the infra repo - Bot user
I've created a "bot" (just a regular GitHub account) that will do the commits for me since I don't want to use my personal account. That's a more "production-like" setup when working in a team for example. The bot account is added as a collaborator to the JOS.Infra repository so it's allowed to clone, commit and push code.
To avoid having to use the bot account password, I've created a PAT (Personal Access Token) and stored it as a secret in the JOS.Echo
repository. You can see it being used here to connect to the JOS.Infra
repository:
- uses: actions/checkout@v4
name: GitOps - Checkout
with:
repository: joseftw/jos.infra
ref: 'develop'
token: ${{ secrets.JOS_YODA_PAT }}
fetch-depth: 0
path: jos.infra
Updating the deployment manifest
The relevant portion of the deployment.yml file looks like this:
containers:
- name: jos-echo-container
image: ghcr.io/joseftw/jos-echo:1.0.21-ci.ga3b2bc3949
We want to update the image version with our newly built one.
I'm using sed for that.
By no means am I an expert on sed, but the following code works...๐
sed -i'.bak' -e 's|/jos-echo:*.*.*-*.*$|/jos-echo:${{steps.nbgv.outputs.semver2}}|g' jos.infra/apps/internal/jos.echo/deployment.yml
When the file has been updated, the bot does a new commit and pushes it to the infra repository.
git -C jos.infra commit -am "jos-echo ${{steps.nbgv.outputs.semver2}}"
git -C jos.infra push origin develop
Argo CD
It's really easy to configure Argo CD. The only thing you need to do is to point it to your repository that contains the manifests, in my case that's JOS.Infra
.
I'm using the cli for this but you can also use the Argo CD UI.
argocd repo add https://github.com/joseftw/jos.infra --name jos.infra --username my-service-user --password github_pat_MyPat
You will then need to create the application in Argo CD...
argocd app create jos.echo --repo https://github.com/joseftw/jos.infra --path apps/internal/jos.echo --dest-namespace jos-apps --dest-server https://kubernetes.default.svc --directory-recurse
...and that's it!
I've configured Argo CD to auto sync my manifests, so whenever it detects a change in my manifests, it will apply them.
If I now add a new feature to my application...Argo CD will eventually see it and it will be reflected on the dashboard.
The fully (automated) process from commiting the code to have it deployed takes roughly 3 minutes, and the best part is that I don't need to do anything (besides setting everything up ๐ ), it just works.