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:
The reason why GitOps is especially suited to deploy cloud native applications is that Kubernetes follows the same declarative way of doing things:
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.
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.
Most commonly used tools are Flux from Weaveworks and ArgoCD. You may find extensive comparisons of both tools but to sum it up:
In this article, we will look to implement a GitOps model using ArgoCD.
Tools
We will implement a GitOps scenario using:
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:
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.
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.
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.
Previously published behind paywall at https://medium.com/@emmanuel.sys/how-to-build-a-gitops-workflow-with-argocd-kustomize-and-github-actions-f919e7443295