Creating GitOps Workflow with ArgoCD, Kustomize and GitHub Actions

Written by EmmanuelSys | Published 2020/08/24
Tech Story Tags: kubernetes | gitops | github | programming | tools | automation | learning | devops

TLDR The term GitOps was first coined by Weaveworks in a popular article from August 2017. The problem it intends to solve was how to efficiently and safely deploy a Kubernetes application. The main tenants of this philosophy are:Use a Git repository as the single source of truth. Use a Git commit to commit a change in the form of a commit. When the application state differ from the desired state (that is: what’s in Git), a reconciliation loop detect the drift and try to reach this state. The final pipeline step is then to run a command like kubectl apply.via the TL;DR App

The term GitOps was first coined by Weaveworks in a popular article from August 2017. The problem it intends to solve was how to efficiently and safely deploy a Kubernetes application.
The main tenants of this philosophy are:
  • Use a Git repository as the single source of truth.
  • Any change is made in the form of a Git commit.
  • When the application state differ from the desired state (that is: what’s in Git), a reconciliation loop detect the drift and try to reach this state.directly from Weaveworks
The reason why GitOps is especially suited to deploy cloud native applications is that Kubernetes follows the same declarative way of doing things:
  • You submit a bunch of Kubernetes objects through a declarative document as YAML or JSON
  • Kubernetes Operators process are endlessly evaluating the difference between the submitted objects and their real state in the cluster, adding, modifying or deleting them as needed
When you understand the concept, you can apply the GitOps way not only to Kubernetes application but to anything described with code, for example code infrastructure.

What is the difference between GitOps and the final deploy step of my CICD pipeline ?

Very often your pipeline is triggered by a change in code (if not, it really should be). Therefore, it’s in fact the same starting point as GitOps. Your final pipeline step is then to run a command like kubectl apply. You run an imperative command to reach the desired state.
In GitOps, you won’t do this: it’s an external tool that detects the drift in your Git repository and will run theses commands for you. You can think of it as a “pulling” way of doing things.
Let’s look into these tools.

What tools are available to implement GitOps ?

Most commonly used tools are Flux from Weaveworks and ArgoCD. You may find extensive comparisons of both tools but to sum it up:
  • Flux can only observe one repository at a time, meaning you have generally one flux instance running for each application.
  • ArgoCD may observe multiple repositories, comes with a GUI dashboard, may be federated with an identity provider: it’s more enterprise ready.
In this article, we will look to implement a GitOps model using ArgoCD.

Our GitOps workflow

Tools
We will implement a GitOps scenario using:
  • ArgoCD as the GitOps tool
  • GitHub Actions as the CICD pipeline
  • Kustomize to describe application deployments
Starting point
Code is available on GitHub. You will find step by step instructions on how to make it work for you by installing a Minikube cluster, ArgoCD and setup the required security tokens.
Application
Our application is a simple Go application displaying the good old “Hello World” string.
package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
)

const PORT = 8080

func main() {
	startServer(handler)
}

func startServer(handler func(http.ResponseWriter, *http.Request)){
	http.HandleFunc("/", handler)
	log.Printf("starting server...")
	http.ListenAndServe(fmt.Sprintf(":%d", PORT), nil)
}

func handler(w http.ResponseWriter, r *http.Request){
	log.Printf("received request from %s", r.Header.Get("User-Agent"))
	host, err := os.Hostname()
	if err != nil {
		host = "unknown host"
	}
	resp := fmt.Sprintf("Hello from %s", host)
	_, err = w.Write([]byte(resp))
	if err != nil {
		log.Panicf("not able to write http output: %s", err)
	}
}
The associated Dockerfile is quite simple, building the application then running it in a linux container.
FROM golang:1.14 as build
WORKDIR /build
COPY . .
RUN CGO_ENABLED=0 go build -o hello-gitops cmd/main.go

FROM alpine:3.12
EXPOSE 8080
WORKDIR /app
COPY --from=build /build/hello-gitops .
CMD ["./hello-gitops"]
Code pipeline
Our pipeline uses two jobs:
  • One for running tests, building and pushing the container on Dockerhub
  • The second one will edit the Kustomize patch to bump the expected container tag to the new Docker image and then commit these changes.
name: Go

on:
  push:
    branches: [ master ]

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    
    steps:
    - name: Set up Go 1.x
      uses: actions/setup-go@v2
      with:
        go-version: ^1.14
      
    - name: Check out code
      uses: actions/checkout@v2

    - name: Test
      run: |
        CGO_ENABLED=0 go test ./...
        
    - name: Build and push Docker image
      uses: docker/[email protected]
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}
        repository: ${{ secrets.DOCKER_USERNAME }}/hello-gitops
        tags: ${{ github.sha }}, latest

  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    needs: build

    steps:
    - name: Check out code
      uses: actions/checkout@v2

    - name: Setup Kustomize
      uses: imranismail/setup-kustomize@v1
      with:
        kustomize-version: "3.6.1"

    - name: Update Kubernetes resources
      env:
        DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
      run: |
       cd kustomize/base
       kustomize edit set image hello-gitops=$DOCKER_USERNAME/hello-gitops:$GITHUB_SHA
       cat kustomization.yaml
        
    - name: Commit files
      run: |
        git config --local user.email "[email protected]"
        git config --local user.name "GitHub Action"
        git commit -am "Bump docker tag"

    - name: Push changes
      uses: ad-m/github-push-action@master
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
ArgoCD
ArgoCD must be configured to observe our Git repository. Configuration is rather straightforward and can be done in the included GUI. You need to specify the relative path of the Kustomize patch to use though.

GitOps Magic

Note that at the end of the GitHub Actions pipeline, we don’t run any imperative command to deploy our application, we just changed our container version using Kustomize and auto-pushed these changes into our repository.
If you do any code change, the pipeline is triggered and a new Docker image is pushed, the container version is updated and ArgoCD should catch the change. Everything should be green by then.

Conclusion

GitOps is a powerful and intelligible way of making a change to anything. I think it could be considered as the logical continuation of the “* as code” we see everywhere and the massive industry trend to move to more declarative, easier to understand models.

Published by HackerNoon on 2020/08/24