My Simple Github Actions CI/CD Pipeline:
Tags: kubernetes, argocd, cicd, go
Published: 2022-06-08 00:00:52

---
title: My Simple Github Actions CI/CD Pipeline: 
published: true
description: Simple CICD pipeline using github actions and deploying to kubernetes cluster.
tags: kubernetes, ArgoCD, cicd, golang
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
---

A little story of how I combined 2 repositories with github actions' `repository_dispatch` webhook to manage my deployment pipeline.

##Background.
I have always wanted to deploy a new image to kubernetes once my build is build is successful, but I always need to update the deployment file to achieve this.

I started using [ArgoCD](https://argo-cd.readthedocs.io/en/stable/#:~:text=Argo%20CD%20is%20implemented%20as,target%20state%20is%20considered%20OutOfSync%20.) recently, and I enjoy the simplicity and the insights I get from the UI. Only problem was that I need to initialize it from my github repo. My repository is private, got some challenges with that, but that's for another time.

##Challenge
The challenge here is that I want my deployment repository to be separate from my application (app) repository (repo). Also, because I am using branches in ArgoCD, I don't get to run my tests or build my image before the changes get picked up by ArgoCD. For these reasons, it makes sense to separate the repos and find a way to notify the deployment repo once the app repo has completed all the checks and the docker image is built, we can then proceed to communicate with the deployment repo, sending the new image tag over. The deployment repo then updates the image in kubernetes deployment file, checks out the code and pushes. ArgoCD picks up the new change and syncs automagically.
Enough Talks, here is the actions code.

## Application
Application was written in go, but this can be swapped out for other services as well.

`app_repo/.github/workflows/ci.yml`
```
name: Test and Build

on:
  push:
    branches: [ master, develop, staging, testing ]
  pull_request:
    branches: [ master ]
jobs:
  lint:
    name: Linting
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v2

      - name: Lint and Vet
        uses: golangci/golangci-lint-action@v2
        with:
          version: latest
          args: --timeout=3m
  test:
    name: Test
    runs-on: ubuntu-18.04
    services:
      mysql:
        image: mysql:5.7
        env:
          MYSQL_DATABASE: app_name
          MYSQL_USER: cicd-user
          MYSQL_PASSWORD: password
          MYSQL_ROOT_PASSWORD: password
        ports:
          - 3306:3306
    steps:
      - uses: actions/checkout@v2

      - name: Set up Go
        uses: actions/setup-go@v2
        with:
          go-version: 1.17

      - name: Test
        run: go test ./...
        env:
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          ENVIRONMENT: ci-cd
          DB_USERNAME: cicd-user
          DB_PASSWORD: password
          DATABASE: app_name
          TOKEN_STRING: "hello hello world"

  build:
    name: Build Image
    runs-on: ubuntu-latest
    needs:
      - test
      - lint
    steps:
      - name: Extract branch name
        shell: bash
        run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
        id: extract_branch

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Login to Dockerhub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and Push
        uses: docker/build-push-action@v2
        with:
          file: .docker/Dockerfile
          push: true
          tags: <<docker_repo>>/<<image_name>>:${{ steps.extract_branch.outputs.branch }}-${{ github.run_id }}-${{ github.run_number }}


  deploy:
    name: Trigger New Deployment
    runs-on: ubuntu-latest
    needs:
      - build
    steps:
      - name: Extract branch name
        shell: bash
        run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
        id: extract_branch

      - name: Deployment
        uses: mvasigh/dispatch-action@main
        with:
          token: ${{ secrets.G_ACCESS_TOKEN }}
          repo: <<deploy_repo>>
          owner: <<owner>>
          event_type: update_image
          message: |
            {
              "branch": "${{ steps.extract_branch.outputs.branch }}",
              "tag": "${{ steps.extract_branch.outputs.branch }}-${{ github.run_id }}-${{ github.run_number }}"
            }
```
##Deployment.

Here, I keep a folder for each environment:
* production
* staging
* testing


There is a stubs folder called `stubs` with this sample file for each environment:

`./stubs/environment.yaml`
```
apiVersion: apps/v1
kind: Deployment
metadata:
  name: <<name>>
  namespace: <<environment>>
spec:
  replicas: 2
  selector:
    matchLabels:
      tier: backend
      app: <<app_name>>
  template:
    metadata:
      labels:
        tier: backend
        app: <<app_name>>
    spec:
      containers:
        - name: <<app_name>>
          image: <<docker_repo>>/<<image>>:#TAG#
          imagePullPolicy: Always
          envFrom:
            - configMapRef:
                name: <<app_config>>
            - secretRef:
                name: <<app_secret>>
```

PS: all data between `<<` and `>>` should be swapped with their actual value. Only `#TAG` should be the placeholder.

`deploy_repo/.github/workflows/deploy.yml`

```
name: Deploy
on:
  repository_dispatch:
    branches: ["master"]
    types: [update_image]

jobs:
  build:
    name: Run API
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v3
        with:
          ref: ${{github.event.client_payload.message.branch}}

      - name: Update Stub With New Tag
        shell: bash
        run: |
          cp stubs/${{ github.event.client_payload.message.branch }}.yaml ${{ github.event.client_payload.message.branch }}/deployment.yaml.stub
          sed -i "s/#TAG#/${{ github.event.client_payload.message.tag }}/g" ${{ github.event.client_payload.message.branch }}/deployment.yaml.stub
          cp ${{github.event.client_payload.message.branch}}/deployment.yaml.stub ${{github.event.client_payload.message.branch}}/deployment.yaml

      - name: Commit files
        run: |
          git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git commit -m "Update Tag ${{ github.event.client_payload.message.tag }}" -a

      - name: Push changes
        uses: ad-m/github-push-action@master
        with:
          github_token: ${{ secrets.G_ACCESS_TOKEN }}
          branch: ${{ github.event.client_payload.message.branch }}        

      - name: All Done
        run: |
          echo "All is Well"
```

Here is a breakdown of the deploy.yml action:
* Checks out the repo using the provided branch
* Copies the environment stub into a `deployment.yaml.stub` file.
* Updates the `#TAG#` placeholder with the new tag from the `repository_dispatch`
* Commit the files using the bot email and using the latest tag as the commit message. This allows us to compare the sync in argocd
* Push the changes using `ad-m/github-push-action`.
* Congratulatory message from me :)

Please if there is an easier way to achieve this, please share in the comment. I will love to learn how to make my life easier.