CI and CD
At work, we follow the practice of a separate code repo and deployment repo for CI
and CD
. This allows for the decoupling of code and configurations for deployment. However, in my home cluster, I didn’t really have a need to decouple the code and the deployment.
Given that I was already hosting my code on github, the obvious solution for CI
was github actions. I knew that I wanted to create a reusable action so that I wouldn’t have to copy paste the entire flow for each new service I created. Lastly, integrating with other open source build tools, such as docker buildx
for multi-arch image support or cosign
for signing, seemed to be simple as well.
Originally for CD
, I created flows for updating a deploy repo with a new image tag, but I was never truly satisfied with it as a solution. Additionally, I chose to build the kubernetes manfiest using kustomize
and run a kubectl apply
afterwards to apply the new changes in my flows. ArgoCD
eliminated all of these steps and I could self host it as well in my k3s
cluster. Seemed like a win-win for me.
Github Actions
The workflow I am going to build out is available on my github repo.
User Inputs
When creating a workflow, users can provide inputs to the flow assuming you’ve defined what inputs are available.
I need to know the image name, the registry, and a few optional fields
name: build push sign
on:
workflow_call:
inputs:
image:
required: true
type: string
registry:
required: true
type: string
environment:
required: false
type: string
default: Homelab
platforms:
required: true
type: string
default: linux/amd64,linux/arm64,linux/arm
env:
IMAGE_PATH: ${{ inputs.registry }}/${{ inputs.image }}
Logging into a registry
The docker login action allows you to log into a registry for pushing an image. You can use any registry that you would like.
I chose to use github’s container registry
- name: log in to ghcr
uses: docker/login-action@v2
if: github.event_name != 'pull_request'
with:
registry: ${{ inputs.registry }}
username: ${{ github.REPOSITORY_OWNER }}
password: ${{ secrets.GITHUB_TOKEN }}
Docker
My k3s
cluster has a mix of different architectures in it. I have an intel nuc which is amd64
, two 4th gen raspberry pis that are arm64
, and an older 3rd gen raspberry pi that is armhf
. This meant that I needed to be able to build images for all three different architectures or specify a nodeSelector
in my manifests. I chose to learn how to build images for multiple architectures.
Docker has some cool github actions for images:
- metadata-action generates metadata for your images such as labels or tags.
- setup-qemu-action sets up qemu for emulating operating systems.
- setup-buildx-action sets up docker buildx for building the image.
- build-push-action can build and push your image to the registry you logged into earlier. You can also input tags and labels from the metadata-action.
I use all of these actions in my workflow.
- name: Docker metadata
id: meta
uses: docker/metadata-action@v4
with:
images: |
${{ env.IMAGE_PATH }}
tags: |
type=schedule
type=ref,event=branch
type=ref,event=pr
type=sha
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: build and publish image
id: build-and-publish
uses: docker/build-push-action@v3
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: ${{ inputs.platforms }}
tags: |
${{ steps.meta.outputs.tags }}
${{ env.IMAGE_PATH }}:${{ github.SHA }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
Signing
Sigstore
offers a solution for signing artifacts called cosign
. Check out their github action for more details.
I am signing using my github OIDC token, but you can provide your own key as well via a secret.
- name: Install Cosign
uses: sigstore/cosign-installer@main
- name: sign
run: cosign sign --yes ${TAGS}
env:
TAGS: ${{ steps.meta.outputs.tags }}
How do I use my workflow?
You’ll need to create a .github/workflows
folder and create yaml file for the flow, such as ci.yaml
. Then you can point to the workflow so long as it is in a public repository.
For example, here is the yaml this blog uses for CI.
name: Build Push Sign
on:
push:
branches: ["main"]
jobs:
build-push-sign:
uses: kdwils/homelab-workflow/.github/workflows/build-push-sign.yml@main
secrets: inherit
with:
image: kdwils/blog
registry: ghcr.io
environment: Homelab
platforms: linux/amd64,linux/arm64
ArgoCD
Check out the official docs for installation on your cluster.
Argo Applications
An Application
is the definition for telling ArgoCD
how to sync an app to your cluster.
Because ArgoCD
is deployed in my k3s
cluster, we can point the destination server to default kubernetes svc, but you could point to a remote cluster as well.
The manifests for this blog live at https://github.com/kdwils/blog/tree/main/deploy/homelab
so I need to tell ArgoCD to look at the /deploy/homelab
overlay.
Heres the full Application for this blog.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: blog
namespace: argocd
spec:
destination:
namespace: blog
server: "https://kubernetes.default.svc"
source:
path: deploy/homelab
repoURL: "https://github.com/kdwils/blog"
targetRevision: HEAD
sources: []
project: default
syncPolicy:
automated:
prune: true
selfHeal: true
Organizing Applications
I define my apps in a separate repo. This repo also contains manifests for critical infra related software for my cluster, such as metallb
, ingress-nginx
, and cert-manager
. Any new apps can simply be created under the /argocd/apps overlay and they will be automagically sync to my cluster.
For my needs, this setup works nicely as it is extremely simple to add new apps. Additionally, whenever I need to make a change I can simply push the new manifest to the homelab repo.
ArgoCD-image-updater
Unfortunately, ArogCD wont pull image changes if you are using an image tag such as main
or latest
(which I am). ArgoCD-image-updater will handle pulling the latest image for your application if you configure it correctly.
This did feel like a bit of a pain to set up initially. You can see my configurations here for the image-updater installation. Side note, if you’re using ghcr.io
as your registry, you need to use a personal access token as your password.
By adding these annotations for my blog Application
, the image-updater will sync the latest image to my cluster.
annotations:
argocd-image-updater.argoproj.io/image-list: blog=ghcr.io/kdwils/blog
argocd-image-updater.argoproj.io/blog.platforms: linux/arm64,linux/amd64
argocd-image-updater.argoproj.io/blog.update-strategy: latest
This seems to work pretty well, however I believe I need to configure credentials for each registry. It seems you can also have the updater commit a new image sha to your repositories as well so long as you’re using helm
or kustomize
which would eliminte the need to constantly poll the registries.