├── .gitignore ├── go.mod ├── doc ├── dockerhub.png ├── argocd_sync.png ├── argocd_project.png ├── dockerhub_token.png └── github_secrets.png ├── Dockerfile ├── kustomize └── base │ ├── service.yaml │ ├── kustomization.yaml │ ├── ingress.yaml │ └── deployment.yaml ├── cmd ├── main.go └── main_test.go ├── .github └── workflows │ └── go.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | script.sh 2 | .idea 3 | 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module hello-gitops 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /doc/dockerhub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esys/workshop-hello-gitops/HEAD/doc/dockerhub.png -------------------------------------------------------------------------------- /doc/argocd_sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esys/workshop-hello-gitops/HEAD/doc/argocd_sync.png -------------------------------------------------------------------------------- /doc/argocd_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esys/workshop-hello-gitops/HEAD/doc/argocd_project.png -------------------------------------------------------------------------------- /doc/dockerhub_token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esys/workshop-hello-gitops/HEAD/doc/dockerhub_token.png -------------------------------------------------------------------------------- /doc/github_secrets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esys/workshop-hello-gitops/HEAD/doc/github_secrets.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14 as build 2 | WORKDIR /build 3 | COPY . . 4 | RUN CGO_ENABLED=0 go build -o hello-gitops cmd/main.go 5 | 6 | FROM alpine:3.12 7 | EXPOSE 8080 8 | WORKDIR /app 9 | COPY --from=build /build/hello-gitops . 10 | CMD ["./hello-gitops"] -------------------------------------------------------------------------------- /kustomize/base/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: hello-gitops 6 | name: hello-gitops 7 | spec: 8 | ports: 9 | - name: http 10 | port: 8080 11 | targetPort: http 12 | selector: 13 | app: hello-gitops 14 | type: ClusterIP 15 | -------------------------------------------------------------------------------- /kustomize/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - deployment.yaml 5 | - service.yaml 6 | - ingress.yaml 7 | namespace: hello-gitops 8 | images: 9 | - name: hello-gitops 10 | newName: emmsys/hello-gitops 11 | newTag: bd0b95a1e7e31fd3a30c0a979ffbfc45761b001f 12 | -------------------------------------------------------------------------------- /kustomize/base/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: hello-gitops 5 | spec: 6 | rules: 7 | - http: 8 | paths: 9 | - path: / 10 | pathType: Prefix 11 | backend: 12 | serviceName: hello-gitops 13 | servicePort: http -------------------------------------------------------------------------------- /kustomize/base/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: hello-gitops 6 | name: hello-gitops 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: hello-gitops 12 | template: 13 | metadata: 14 | labels: 15 | app: hello-gitops 16 | spec: 17 | containers: 18 | - image: hello-gitops 19 | name: hello-gitops 20 | ports: 21 | - name: http 22 | containerPort: 8080 23 | imagePullSecrets: 24 | - name: registry-esys 25 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | const PORT = 8080 11 | 12 | func main() { 13 | startServer(handler) 14 | } 15 | 16 | func startServer(handler func(http.ResponseWriter, *http.Request)){ 17 | http.HandleFunc("/", handler) 18 | log.Printf("starting server...") 19 | http.ListenAndServe(fmt.Sprintf(":%d", PORT), nil) 20 | } 21 | 22 | func handler(w http.ResponseWriter, r *http.Request){ 23 | log.Printf("received request from %s", r.Header.Get("User-Agent")) 24 | host, err := os.Hostname() 25 | if err != nil { 26 | host = "unknown host" 27 | } 28 | resp := fmt.Sprintf("Hello from %s", host) 29 | _, err = w.Write([]byte(resp)) 30 | if err != nil { 31 | log.Panicf("not able to write http output: %s", err) 32 | } 33 | } -------------------------------------------------------------------------------- /cmd/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | func Test_handler(t *testing.T) { 12 | host, _ := os.Hostname() 13 | type args struct { 14 | path string 15 | status int 16 | expected string 17 | } 18 | tests := []struct { 19 | name string 20 | args args 21 | }{ 22 | { 23 | "status OK", 24 | args{"/", http.StatusOK, fmt.Sprintf("Hello from %s", host)}, 25 | }, 26 | } 27 | 28 | rr := httptest.NewRecorder() 29 | for _, tt := range tests { 30 | t.Run(tt.name, func(t *testing.T) { 31 | req, err := http.NewRequest("GET", tt.args.path, nil) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | handler := http.HandlerFunc(handler) 36 | handler.ServeHTTP(rr, req) 37 | if status := rr.Code; status != tt.args.status { 38 | t.Errorf("handler returned wrong status code: got [%v] want [%v]", 39 | status, tt.args.status) 40 | return 41 | } 42 | if rr.Body.String() != tt.args.expected { 43 | t.Errorf("handler returned unexpected body: got [%v] want [%v]", 44 | rr.Body.String(), tt.args.expected) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | name: Build 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Set up Go 1.x 14 | uses: actions/setup-go@v2 15 | with: 16 | go-version: ^1.14 17 | 18 | - name: Check out code 19 | uses: actions/checkout@v2 20 | 21 | - name: Test 22 | run: | 23 | CGO_ENABLED=0 go test ./... 24 | 25 | - name: Build and push Docker image 26 | uses: docker/build-push-action@v1.1.0 27 | with: 28 | username: ${{ secrets.DOCKER_USERNAME }} 29 | password: ${{ secrets.DOCKER_PASSWORD }} 30 | repository: ${{ secrets.DOCKER_USERNAME }}/hello-gitops 31 | tags: ${{ github.sha }}, latest 32 | 33 | deploy: 34 | name: Deploy 35 | runs-on: ubuntu-latest 36 | needs: build 37 | 38 | steps: 39 | - name: Check out code 40 | uses: actions/checkout@v2 41 | 42 | - name: Setup Kustomize 43 | uses: imranismail/setup-kustomize@v1 44 | with: 45 | kustomize-version: "3.6.1" 46 | 47 | - name: Update Kubernetes resources 48 | env: 49 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 50 | run: | 51 | cd kustomize/base 52 | kustomize edit set image hello-gitops=$DOCKER_USERNAME/hello-gitops:$GITHUB_SHA 53 | cat kustomization.yaml 54 | 55 | - name: Commit files 56 | run: | 57 | git config --local user.email "action@github.com" 58 | git config --local user.name "GitHub Action" 59 | git commit -am "Bump docker tag" 60 | 61 | - name: Push changes 62 | uses: ad-m/github-push-action@master 63 | with: 64 | github_token: ${{ secrets.GITHUB_TOKEN }} 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Workshop Hello GitOps 2 | 3 | Example project to demonstrate GitOps using [Kustomize](https://github.com/kubernetes-sigs/kustomize), GitHub Actions and [ArgoCD](https://github.com/argoproj/argo-cd/) 4 | 5 | - [What we want to achieve](#what-we-want-to-achieve) 6 | - [Setup Minikube](#setup-minikube) 7 | - [Setup ArgoCD](#setup-argocd) 8 | - [Setup DockerHub](#setup-dockerhub) 9 | - [Setup GitHub](#setup-github) 10 | - [Configure ArgoCD](#configure-argocd) 11 | - [Test the application](#test-the-application) 12 | 13 | ## What we want to achieve 14 | 15 | 1. Code change is made to a Go application and pushed to master 16 | 1. GitHub Actions workflow is triggered 17 | 1. Code is first unit tested 18 | 1. If tests are OK, the application is packaged as a Docker image and pushed to DockerHub 19 | 1. The kustomize manifests are edited to reference this new image tag 20 | 1. Kustomize changes are committed and pushed automatically by the workflow 21 | 1. As kustomize files have changed, ArgoCD synchronize the application state with the Kubernetes cluster to deploy or update the app 22 | 23 | To implement this scenario, we will use [Minikube](https://github.com/kubernetes/minikube) as the Kubernetes cluster. 24 | 25 | ## Setup Minikube 26 | 27 | - Install Minikube with on MacOS with default driver (hyperkit) 28 | 29 | ``` 30 | brew install minikube 31 | minikube start 32 | ``` 33 | 34 | - Create the K8s namespaces we will use 35 | ``` 36 | kubectl create ns hello-gitops 37 | kubectl create ns argocd 38 | ``` 39 | 40 | - Enable the ingress controller (nginx) 41 | 42 | ``` 43 | minikube addons enable ingress 44 | ``` 45 | 46 | 47 | ## Setup ArgoCD 48 | 49 | Refer to the official [setup instructions](https://argoproj.github.io/argo-cd/getting_started/) for a detailed guide. 50 | To sum it up 51 | ``` 52 | kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml 53 | ``` 54 | 55 | ArgoCD can be interacted with using 56 | - the web UI 57 | - the ArgoCD CLI 58 | - or by editing the CRD yaml 59 | 60 | If you want to install the CLI 61 | ``` 62 | brew tap argoproj/tap 63 | brew install argoproj/tap/argocd 64 | ``` 65 | 66 | For this example, we will use the web UI. It can be made accessible using a port forward 67 | ``` 68 | kubectl port-forward svc/argocd-server -n argocd 8080:443 69 | ``` 70 | 71 | Check you have access to the web UI at http://localhost:8080 using your browser 72 | - Username is `admin` 73 | - Default password can be retrieved by running `kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o name | cut -d'/' -f 2` 74 | 75 | You are of course encouraged to change this password 76 | 77 | ## Setup DockerHub 78 | 79 | Login to your DockerHub account and create a new repository by clicking `Create Repository` 80 | 81 | ![](doc/dockerhub.png) 82 | 83 | It's easier to make the repository public. If you want to use a private repository (like I did) you will need to allow your Minikube cluster to pull image from this repo by creating a secret. Procedure is explained later. 84 | 85 | You will also need to generate a token for use by GitHub by going into your account settings then Security and clicking `New Access Token` 86 | 87 | ![](doc/dockerhub_token.png) 88 | 89 | ## Setup GitHub 90 | 91 | ### Fork the repository 92 | 93 | - First, fork this repository by using the GitHub `Fork` button 94 | 95 | - Then clone your new forked repository 96 | ``` 97 | git clone https://github.com//hello-gitops.git 98 | 99 | ``` 100 | - Then add the `hello-gitops` repository as upstream 101 | ``` 102 | cd hello-gitops 103 | git remote add upstream https://github.com/esys/hello-gitops.git 104 | ``` 105 | 106 | - Update your fork 107 | ``` 108 | git pull upstream master 109 | ``` 110 | 111 | ### Configure GitHub Actions 112 | 113 | You will need to expose your DockerHub credentials as GitHub secrets. 114 | 115 | - From your forked project page, click on `Settings` then `Secrets` 116 | 117 | ![](doc/github_secrets.png) 118 | 119 | - Create two new secrets `DOCKER_USERNAME` and `DOCKER_PASSWORD` (Docker password is the previously generated token for github on DockerHub) 120 | - Check the `.github/workflows/go.yml` to check parameters and familiarize yourself with the workflow 121 | - Checkout the code 122 | - Unit test it 123 | - Build and push the application as a Docker image 124 | - Update the Kustomize manifest with the new Docker tag 125 | 126 | ### Take a look at Kustomize 127 | 128 | Kustomize manifests are in the `kustomize` folder. 129 | The `kustomization.yaml` is updated automatically by the GitHub workflow to add a new `images` transformation setting the right image name and tag. 130 | ``` 131 | images: 132 | - name: hello-gitops 133 | newTag: baadd7d7832f74e3c6d37b3f07b179d7e86c4017 134 | newName: emmsys/hello-gitops 135 | ``` 136 | 137 | No need to touch these settings as they are updated by the workflow. 138 | 139 | ### Allow Minikube to pull the Docker image 140 | 141 | #### Option 1 : your DockerHub repository is public 142 | 143 | Edit `kustomize/base/deployment.yaml` to remove any reference to an `imagePullSecrets` 144 | 145 | ``` 146 | spec: 147 | containers: 148 | - image: hello-gitops 149 | name: hello-gitops 150 | ports: 151 | - name: http 152 | containerPort: 8080 153 | ``` 154 | 155 | Note: you can also make these changes with a Kustomize patch in a new overlay 156 | 157 | #### Option 2 : your DockerHub repository is private 158 | 159 | You will need to create a DockerHub token to allow Minikube pulling this images 160 | - Login to DockerHub website and create a new token in the security settings like we did for GitHub 161 | - Create a Kubernetes secrets with these credentials in the Kubernetes application namespace 162 | ``` 163 | kubectl -n hello-gitops create secret docker-registry my-registry-creds --docker-server="https://index.docker.io/v1/" --docker-username="YOUR_USERNAME" --docker-password="YOUR_DOCKER_TOKEN" --docker-email="YOUR@EMAIL" 164 | ``` 165 | - Edit `kustomize/base/deployment.yaml` to reference this secret as an `imagePullSecrets` 166 | ``` 167 | spec: 168 | containers: 169 | - image: hello-gitops 170 | name: hello-gitops 171 | ports: 172 | - name: http 173 | containerPort: 8080 174 | imagePullSecrets: 175 | - name: my-registry-creds 176 | ``` 177 | 178 | ### Commit your changes 179 | 180 | At this point, you should certainly have made code changes. 181 | 182 | Commit and push them 183 | ``` 184 | git commit -am "customized settings" 185 | git push origin master 186 | ``` 187 | 188 | ## Configure ArgoCD 189 | 190 | At the end of the workflow, Kustomize manifests are referencing the newly built Docker image. We will configure ArgoCD to observe changes to Kustomize files and update the application in the K8s cluster. 191 | - Login to ArgoCD 192 | - See [the official documentation](https://argoproj.github.io/argo-cd/getting_started/) for a step by step guide 193 | - You will need to enter 194 | - your forked GitHub repository address 195 | - `HEAD` or `master` revision 196 | - `kustomize/base` as the `Path` parameter 197 | - `hello-gitops` as namespace 198 | - Application configuration should look like this 199 | 200 | ![](doc/argocd_project.png) 201 | 202 | - If you've pickup up manual synchronization, click the `Sync` button 203 | - Your application should be deployed properly in the Minikube cluster 204 | 205 | ![](doc/argocd_sync.png) 206 | 207 | - You can make some code changes in the project, ArgoCD should catch the changes and redeploy (or detect out of sync if using manual synchronization) 208 | 209 | ## Test the application 210 | 211 | Application is reachable on port 8080 inside the cluster. It displays a simple Hello World string with the hostname. 212 | 213 | You can reach it through a port forward command 214 | ``` 215 | kubectl -n hello-gitops port-forward $(kubectl -n hello-gitops get po -o name) 8111:8080 216 | curl localhost:8111 217 | # Hello from hello-gitops-6f7d4878c9-qg4l6 218 | ``` 219 | 220 | Or by using the ingress controller 221 | ``` 222 | INGRESS_IP=`kubectl -n hello-gitops get ingress -o jsonpath='{.items[0].status.loadBalancer.ingress[0].ip}'` 223 | curl $INGRESS_IP 224 | # Hello from hello-gitops-6f7d4878c9-qg4l6 225 | ``` 226 | --------------------------------------------------------------------------------