├── go.mod ├── screenshots ├── failing-tests.png └── successful-tests.png ├── local.yml ├── manifests ├── service.yml ├── kustomization.yml └── deployment.yml ├── Dockerfile ├── foobar ├── foobar.go └── foobar_test.go ├── .github └── workflows │ └── workflow.yml ├── main.go ├── scripts └── generate-kubeconfig.sh └── README.md /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/padok-team/github-actions-tutorial 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /screenshots/failing-tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrismaddalena/github-actions-tutorial/master/screenshots/failing-tests.png -------------------------------------------------------------------------------- /screenshots/successful-tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrismaddalena/github-actions-tutorial/master/screenshots/successful-tests.png -------------------------------------------------------------------------------- /local.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | golang: &goloang 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | image: test_local_golang 9 | -------------------------------------------------------------------------------- /manifests/service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: foobar 5 | spec: 6 | selector: 7 | app: foobar 8 | ports: 9 | - protocol: TCP 10 | port: 8080 11 | -------------------------------------------------------------------------------- /manifests/kustomization.yml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - ./deployment.yml 6 | - ./service.yml 7 | 8 | commonLabels: 9 | app: foobar 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14 AS builder 2 | 3 | WORKDIR /app 4 | COPY . . 5 | RUN CGO_ENABLED=0 go build -o /bin/foobar 6 | 7 | 8 | FROM alpine AS runner 9 | 10 | COPY --from=builder /bin/foobar /bin/foobar 11 | CMD ["/bin/foobar"] 12 | -------------------------------------------------------------------------------- /foobar/foobar.go: -------------------------------------------------------------------------------- 1 | package foobar 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | ) 7 | 8 | // Sequence returns a sequence of numbers from 1 to length, where multiples of 9 | // 3 are replaced by "foo", multiples of 5 are replaced by "bar", and multiples 10 | // of both 3 and 5 are rpelaced by "foobar". 11 | func Sequence(length int) ([]string, error) { 12 | if length < 0 { 13 | return nil, errors.New("length is negative") 14 | } 15 | 16 | seq := make([]string, length) 17 | 18 | for i := range seq { 19 | n := i + 1 20 | switch { 21 | case n%3 == 0 && n%5 == 0: 22 | seq[i] = "foobar" 23 | case n%5 == 0: 24 | seq[i] = "bar" 25 | case n%3 == 0: 26 | seq[i] = "foo" 27 | default: 28 | seq[i] = strconv.Itoa(n) 29 | } 30 | } 31 | 32 | return seq, nil 33 | } 34 | -------------------------------------------------------------------------------- /manifests/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: foobar 5 | labels: 6 | app: foobar 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: foobar 12 | template: 13 | metadata: 14 | labels: 15 | app: foobar 16 | spec: 17 | containers: 18 | - name: foobar 19 | image: REPOSITORY:TAG 20 | ports: 21 | - containerPort: 8080 22 | readinessProbe: 23 | httpGet: 24 | path: /healthz 25 | port: 8080 26 | livenessProbe: 27 | httpGet: 28 | path: /healthz 29 | port: 8080 30 | resources: 31 | requests: 32 | cpu: 10m 33 | memory: 30Mi 34 | limits: 35 | cpu: 1000m 36 | memory: 256Mi 37 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: main-worklfow 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | run-tests: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - 16 | name: Check out source code 17 | uses: actions/checkout@v2 18 | - 19 | name: Set up Go 20 | uses: actions/setup-go@v2.1.3 21 | with: 22 | go-version: "^1.14" # The Go version to download and use 23 | - 24 | name: Print Go version 25 | run: go version 26 | - 27 | name: Run unit tests 28 | run: go test -v ./... 29 | 30 | build-and-release: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - 34 | name: Check out source code 35 | uses: actions/checkout@v2 36 | - 37 | name: Build the stack 38 | run: docker-compose -f local.yml up --build -d 39 | - name: Upload coverage to Codecov 40 | uses: codecov/codecov-action@v2 41 | with: 42 | token: ${{ secrets.CODECOV_TOKEN }} 43 | directory: coverage/reports/ 44 | env_vars: OS,PYTHON 45 | fail_ci_if_error: true 46 | files: coverage.xml 47 | name: codecov-umbrella 48 | path_to_write_report: ./coverage/codecov_report.txt 49 | verbose: true 50 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/padok-team/github-actions-tutorial/foobar" 10 | ) 11 | 12 | const addr = ":8080" 13 | 14 | func main() { 15 | http.HandleFunc("/foobar", foobarHandler) 16 | http.HandleFunc("/healthz", healthHandler) 17 | 18 | log.Printf("Listening for requests on %s\n", addr) 19 | 20 | err := http.ListenAndServe(addr, nil) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | } 25 | 26 | // foobarHandler responds with a FooBar sequence. 27 | // Expects a "length" parameter defining the size of the sequence. 28 | func foobarHandler(w http.ResponseWriter, r *http.Request) { 29 | lengthParam := r.URL.Query().Get("length") 30 | if lengthParam == "" { 31 | http.Error(w, "missing parameter: length", http.StatusBadRequest) 32 | return 33 | } 34 | 35 | length, err := strconv.Atoi(lengthParam) 36 | if err != nil { 37 | http.Error(w, "invalid length", http.StatusBadRequest) 38 | return 39 | } 40 | 41 | seq, err := foobar.Sequence(length) 42 | if err != nil { 43 | http.Error(w, fmt.Sprintf("failed to compute sequence: %s", err.Error()), http.StatusBadRequest) 44 | return 45 | } 46 | 47 | fmt.Fprintf(w, "%s", seq) 48 | } 49 | 50 | // healthHandler reports on the server's health. 51 | func healthHandler(w http.ResponseWriter, r *http.Request) { 52 | fmt.Fprintln(w, "Server is healthy :)") 53 | } 54 | -------------------------------------------------------------------------------- /foobar/foobar_test.go: -------------------------------------------------------------------------------- 1 | package foobar_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/padok-team/github-actions-tutorial/foobar" 7 | ) 8 | 9 | func TestSequence(t *testing.T) { 10 | tests := []struct { 11 | length int 12 | expected []string 13 | expectErr bool 14 | }{ 15 | { 16 | length: 5, 17 | expected: []string{"1", "2", "foo", "4", "bar"}, 18 | expectErr: false, 19 | }, 20 | { 21 | length: 15, 22 | expected: []string{"1", "2", "foo", "4", "bar", "foo", "7", "8", "foo", "bar", "11", "foo", "13", "14", "foobar"}, 23 | expectErr: false, 24 | }, 25 | { 26 | length: 0, 27 | expected: []string{}, 28 | expectErr: false, 29 | }, 30 | { 31 | length: -3, 32 | expected: nil, 33 | expectErr: true, 34 | }, 35 | } 36 | 37 | for _, test := range tests { 38 | actual, err := foobar.Sequence(test.length) 39 | 40 | if err != nil && !test.expectErr { 41 | t.Errorf("got unexpected error: %w", err) 42 | } 43 | if err == nil && test.expectErr { 44 | t.Error("expected error, got nil") 45 | } 46 | 47 | if !sequencesEqual(test.expected, actual) { 48 | t.Errorf("expected %q, got %q", test.expected, actual) 49 | } 50 | } 51 | } 52 | 53 | func sequencesEqual(a, b []string) bool { 54 | if len(a) != len(b) { 55 | return false 56 | } 57 | 58 | for i := range a { 59 | if a[i] != b[i] { 60 | return false 61 | } 62 | } 63 | 64 | return true 65 | } 66 | -------------------------------------------------------------------------------- /scripts/generate-kubeconfig.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | 6 | ### These are the parameters you can set when calling this script: 7 | NAMESPACE="${NAMESPACE:-default}" 8 | ### 9 | 10 | 11 | echo "⏳ Fetching service account credentials..." 12 | SA_SECRET_NAME=$(kubectl get serviceaccount github-actions --namespace "${NAMESPACE}" --output go-template='{{ (index .secrets 0).name }}') 13 | echo "✅ Service account credentials fetched." 14 | echo 15 | 16 | echo "⏳ Adding Kubernetes API server to kubectl configuration..." 17 | KUBECONFIG_SERVER=$(kubectl config view --minify --output go-template='{{ (index .clusters 0).cluster.server }}') 18 | kubectl get secret $SA_SECRET_NAME --namespace "${NAMESPACE}" --output go-template='{{ index .data "ca.crt" }}' | base64 --decode > /tmp/kubeconfig-ca.crt 19 | kubectl --kubeconfig /tmp/kubeconfig.yml config set-cluster production --server=$KUBECONFIG_SERVER --certificate-authority /tmp/kubeconfig-ca.crt --embed-certs=true 20 | rm /tmp/kubeconfig-ca.crt 21 | echo "✅ Kubernetes API server added." 22 | echo 23 | 24 | echo "⏳ Adding authentication token to kubectl configuration..." 25 | KUBECONFIG_TOKEN=$(kubectl get secret $SA_SECRET_NAME --namespace "${NAMESPACE}" --output go-template='{{ .data.token }}' | base64 --decode) 26 | kubectl --kubeconfig /tmp/kubeconfig.yml config set-credentials github-actions --token $KUBECONFIG_TOKEN 27 | kubectl --kubeconfig /tmp/kubeconfig.yml config set-context github-actions-production --cluster production --user github-actions --namespace "${NAMESPACE}" 28 | kubectl --kubeconfig /tmp/kubeconfig.yml config use-context github-actions-production 29 | echo "✅ Authentication token added." 30 | echo 31 | 32 | echo "⏳ Converting configuration to base64..." 33 | KUBECONFIG_B64="$(base64 --input /tmp/kubeconfig.yml)" 34 | rm /tmp/kubeconfig.yml 35 | echo "✅ Configuration converted." 36 | echo 37 | 38 | echo "👌 Configuration file ready!" 39 | echo "👇 Use the following value for your GitHub secret:" 40 | echo 41 | echo "${KUBECONFIG_B64}" 42 | echo 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Github Actions tutorial 2 | 3 | This tutorial will guide you through building a functional CI/CD pipeline with 4 | Github Actions. You will create a workflow that automatically runs unit tests on 5 | all pull requests, and deploys the latest version of the master branch to a 6 | Kubernetes cluster. 7 | 8 | For an introduction to the core concepts behind GitHub Actions, I recommend 9 | reading [this article](https://www.padok.fr/en/blog/github-actions) to learn 10 | the basic vocabulary used in this tutorial. 11 | 12 | ## Requirements 13 | 14 | To complete this tutorial, you will need the following: 15 | 16 | - A Github account. No paid plan is necessary. 17 | - Basic knowledge of git and Github: how to commit changes to branches and open 18 | pull requests. 19 | - A [DockerHub](https://hub.docker.com/) account. Alternatively, you can use any 20 | public container registry for this tutorial. 21 | - A working Kubernetes cluster. You must have the [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/) 22 | command-line tool configured with administrator-level previleges on your 23 | cluster. For example, a fresh [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine) 24 | cluster will work perfectly. This tutorial will work on most cloud-providers. 25 | 26 | ## Step 0: Fork this repository 27 | 28 | The first thing you should do is [fork](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) 29 | this repository. As you advance through the steps below, you can add your work 30 | to your fork. 31 | 32 | Once forked, clone the repository: 33 | 34 | ```bash 35 | git clone https://github.com/YOUR_USERNAME/github-actions-tutorial.git 36 | cd github-actions-tutorial 37 | ``` 38 | 39 | ## Step 1: Explore the repository 40 | 41 | This repository contains code for a simple HTTP server. Here is a quick tour of 42 | the files already in place. Feel free to take a deeper look at the code if you 43 | are interested. 44 | 45 | The `go.mod`, `main.go` files and `foobar/` directory implement a rudimentary 46 | HTTP server, complete with unit tests. The server has two endpoints: `/foobar`, 47 | which responds with a FooBar sequence, and `/healthz`, which reports on the 48 | server's health. 49 | 50 | A `Dockerfile` provides a recipe for compiling the Go code into a [container image](http://www.padok.fr/en/blog/container-docker-oci). 51 | 52 | The `manifests/` directory contains Kubernetes resource specifications and a 53 | [Kustomize](https://kustomize.io/) configuration file. 54 | 55 | ## Step 2: Automatically run unit tests 56 | 57 | The `foobar` package contains unit tests. A good practice is to run unit tests 58 | on all pull requests and on every commit to the master branch. 59 | 60 | Create a `.github/workflows/workflow.yml` file in your repository for your 61 | GitHub Actions **workflow**. In the file, start with a name: 62 | 63 | ```yaml 64 | name: main-worklfow 65 | ``` 66 | 67 | Use the `on` field to trigger your workflow whenever a commit is pushed to the 68 | master branch or a pull request is made: 69 | 70 | ```yaml 71 | on: 72 | push: 73 | branches: 74 | - master 75 | pull_request: 76 | branches: 77 | - master 78 | ``` 79 | 80 | A workflow is composed of independent **jobs**. Create a job called `run-tests` 81 | that will run the application's unit tests: 82 | 83 | ```yaml 84 | jobs: 85 | # Run all unit tests. 86 | run-tests: 87 | ``` 88 | 89 | Every job requires an operating system to run on. For this tutorial, you will be 90 | using Ubuntu. Fill in the `runs-on` field of the job: 91 | 92 | ```yaml 93 | jobs: 94 | # Run all unit tests. 95 | run-tests: 96 | runs-on: ubuntu-latest 97 | ``` 98 | 99 | Jobs contain a list of **steps**, which are executed consecutively. Often, the 100 | first step is to clone your repository to use the source code it contains. To do 101 | this, use an **action** provided by Github, called `actions/checkout`. To use 102 | this action, fill in the `uses` field of the first step: 103 | 104 | ```yaml 105 | jobs: 106 | # Run all unit tests. 107 | run-tests: 108 | runs-on: ubuntu-latest 109 | steps: 110 | # Check out the pull request's source code. 111 | - name: Check out source code 112 | uses: actions/checkout@v2 113 | ``` 114 | 115 | Next, you need to have Go installed to run your unit tests. There is already an 116 | action that sets up everything for you, called `actions/setup-go`. This action 117 | takes a `go-version` parameter, to know which version of Go to install. 118 | Provide parameters to an action by filling in the `with` field: 119 | 120 | ```yaml 121 | steps: 122 | # Check out the pull request's source code. 123 | - name: Check out source code 124 | uses: actions/checkout@v2 125 | 126 | # Install Go. 127 | - name: Set up Go 128 | uses: actions/setup-go@v2-beta 129 | with: 130 | go-version: "^1.14" # The Go version to download and use. 131 | ``` 132 | 133 | In the code above, `^1.14` means `1.14.x`, where `x` can be anything. Each 134 | `1.14.x` release of Go is compatible with your code, so this is not an issue. 135 | That being said, it would be nice to know exactly which version of Go 136 | you are using here. Print the version in the next step. There is no existing 137 | action that does this, so use the `run` field to execute the `go version` 138 | command: 139 | 140 | ```yaml 141 | # Install Go. 142 | - name: Set up Go 143 | uses: actions/setup-go@v2-beta 144 | with: 145 | go-version: "^1.14" # The Go version to download and use. 146 | - name: Print Go version 147 | run: go version 148 | ``` 149 | 150 | The `run` field allows you to run any shell command. Use it again in the job's 151 | final step to run your application's unit tests: 152 | 153 | ```yaml 154 | # Run unit tests. 155 | - name: Run unit tests 156 | run: go test -v ./... 157 | ``` 158 | 159 | At this point, you should have the following code for your workflow: 160 | 161 | ```yaml 162 | name: main-worklfow 163 | 164 | env: {} 165 | 166 | on: 167 | push: 168 | branches: 169 | - master 170 | pull_request: 171 | branches: 172 | - master 173 | 174 | jobs: 175 | # Run all unit tests. 176 | run-tests: 177 | runs-on: ubuntu-latest 178 | steps: 179 | # Check out the pull request's source code. 180 | - name: Check out source code 181 | uses: actions/checkout@v2 182 | 183 | # Install Go. 184 | - name: Set up Go 185 | uses: actions/setup-go@v2-beta 186 | with: 187 | go-version: "^1.14" # The Go version to download and use. 188 | - name: Print Go version 189 | run: go version 190 | 191 | # Run unit tests. 192 | - name: Run unit tests 193 | run: go test -v ./... 194 | ``` 195 | 196 | Commit these changes and push them to the master branch: 197 | 198 | ```bash 199 | git checkout master 200 | git pull 201 | git add .github/workflows/workflow.yml 202 | git commit -m 'Add run-tests job to workflow' 203 | git push 204 | ``` 205 | 206 | ## Step 3: Check workflow results 207 | 208 | When you pushed your commit to Github, it automatically triggered the workflow. 209 | On your repository's page, go to the **Actions** tab. You should see the run in 210 | question. 211 | 212 | If it is still running, it will have a yellow dot next to your commit message. 213 | Once it has finished, depending on the result it will have either a red cross 214 | or a green tick. For this first run, the unit tests should pass and the workflow 215 | should complete successfully. 216 | 217 | ## Step 4: Create a pull request 218 | 219 | Your workflow not only triggers when commits are pushed to master, but also when 220 | developers make pull requests. Check out a new branch called `awesome-feature`: 221 | 222 | ```bash 223 | git checkout -b awesome-feature 224 | ``` 225 | 226 | Edit the `foobar/foobar.go` file to introduce a breaking change. For instance, 227 | replace a `5` with a `7`. Then, commit and push the change to Github: 228 | 229 | ```bash 230 | git add foobar/foobar.go 231 | git commit -m 'Introduce breaking change' 232 | git push -u origin awesome-feature 233 | ``` 234 | 235 | Go to your repository's webpage and [create a pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request) 236 | for the new branch. Make sure to merge into the master branch of your 237 | repository, and not into `padok-team`'s. Once the pull request is created, the 238 | workflow will trigger and Github will display its progress on the pull request's 239 | page: 240 | 241 | ![failing tests](screenshots/failing-tests.png) 242 | 243 | Since you introduced a breaking change, the unit tests are failing and your 244 | workflow as well. Github displays this prominently, so developers are aware of 245 | the issue as soon as possible. 246 | 247 | Fix the issue in `foobar/foobar.go`, then commit and push the fix: 248 | 249 | ```bash 250 | git add foobar/foobar.go 251 | git commit -m 'Fix breaking change' 252 | git push 253 | ``` 254 | 255 | Once the push is through, Github will trigger the workflow again. This time, it 256 | will pass. 257 | 258 | ![successful tests](screenshots/successful-tests.png) 259 | 260 | You can merge your pull request into the master branch, confident that your 261 | awesome feature does not introduce a breaking change. 262 | 263 | ## Step 5: Automatically build and release a container image 264 | 265 | Now that your application is fully tested, time to package it as a container 266 | image, and then push that image to a container registry like DockerHub. 267 | 268 | > You need a DockerHub account for this step. The image repository will 269 | > automatically be created when your workflow pushes the first image. The 270 | > repository should be public, otherwise Kubernetes will not be able to pull any 271 | > images to deploy without credentials. 272 | 273 | Go back to the master branch to keep working on your workflow: 274 | 275 | ```bash 276 | git checkout master 277 | git pull 278 | ``` 279 | 280 | Add a second job to the workflow, called `build-and-release`. This job also runs 281 | on Ubuntu and starts by checking out your source code: 282 | 283 | ```yaml 284 | # Build and release. 285 | build-and-release: 286 | runs-on: ubuntu-latest 287 | steps: 288 | # Check out source code. 289 | - name: Check out source code 290 | uses: actions/checkout@v2 291 | ``` 292 | 293 | [Docker Inc.](https://www.docker.com/company) has published an action that 294 | builds container images and pushes them to a container registry. This is exactly 295 | what you aim to do, so use `docker/build-push-action` in your job's next step. 296 | 297 | This action requires you specify the repository to push your image to. This is a 298 | great usecase for environment variables. At the top of your workflow file, fill 299 | in the `env` field to add an `IMAGE_REPOSITORY` variable equal to the image 300 | repository you wish to store your image in. For example, my DockerHub handle is 301 | `busser` so I wrote: 302 | 303 | ```yaml 304 | env: 305 | IMAGE_REPOSITORY: busser/foobar 306 | ``` 307 | 308 | In order to push images to an image registry, Github Actions requires 309 | credentials to authenticate itself. Sensitive information like usernames and 310 | passwords should _never_ be written inside files commited to a version control 311 | system like git. Thankfully, Github provides a way to manage secret values. 312 | 313 | On your repository's webpage, go the **Settings** tab, then select **Secrets** 314 | in the left-hand menu. There, add two secrets: `DOCKER_USERNAME` and 315 | `DOCKER_PASSWORD`, containing your DockerHub credentials. 316 | 317 | You are now ready to add the final step to your job, using a community-built 318 | action, and without compromising on security. You shouldn't simply use the 319 | `latest` tag for your container image, so tell the `docker/build-push-action` 320 | action to tag your image with the name of the branch and the hash of the commit 321 | that triggered the workflow. 322 | 323 | ```yaml 324 | # Build and release. 325 | build-and-release: 326 | runs-on: ubuntu-latest 327 | steps: 328 | # Check out source code. 329 | - name: Check out source code 330 | uses: actions/checkout@v2 331 | 332 | # Build and push container image. 333 | - name: Build and push container image 334 | uses: docker/build-push-action@v1 335 | with: 336 | username: ${{ secrets.DOCKER_USERNAME }} 337 | password: ${{ secrets.DOCKER_PASSWORD }} 338 | repository: ${{ env.IMAGE_REPOSITORY }} 339 | tag_with_ref: true 340 | tag_with_sha: true # sha-${GITHUB_SHA::7} 341 | ``` 342 | 343 | Whenever you use `${{ ... }}` inside your workflow, Github will dynamically 344 | inject values at run-time for your steps to use. 345 | 346 | Commit the updated worflow to the master branch and push the change to Github: 347 | 348 | ```bash 349 | git checkout master 350 | git pull 351 | git add .github/workflows/workflow.yml 352 | git commit -m 'Add build-and-release job to workflow' 353 | git push 354 | ``` 355 | 356 | This will trigger a run of the updated pipeline. Follow its progress on Github. 357 | You may notice that the `run-tests` and `build-and-release` jobs ran in 358 | parallel. This is by design: since both jobs are independent of one another, 359 | running them at the same time allows your workflow to run faster and developers 360 | to get feedback on their work sooner. 361 | 362 | ## Step 6: Create a `kubectl` configuration file 363 | 364 | To deploy to Kubernetes, we will be using the `kubectl` command-line tool. To 365 | connect and authenticate to the cluster, this will require a configuration file 366 | containing credentials for a service account with sufficient permissions to 367 | deploy. Create a service account called `github-actions` with permission to 368 | edit the `default` namespace: 369 | 370 | ```bash 371 | kubectl create serviceaccount github-actions --namespace default 372 | kubectl create rolebinding github-actions --clusterrole edit --serviceaccount default:github-actions 373 | ``` 374 | 375 | Next, you need to fetch the service account's authentication token and build a 376 | `kubectl` configuration file. The commands below do this for you, since this 377 | isn't the point of this tutorial: 378 | 379 | ```bash 380 | scripts/generate-kubeconfig.sh 381 | ``` 382 | 383 | Add another secret to your Github repository, called `KUBECONFIG`, containing 384 | the base64-encoded string printed by the script you just ran. 385 | 386 | ## Step 7: Automatically deploy to Kubernetes 387 | 388 | Now that your application is tested, built, and released, all that remains is to 389 | deploy it. Add a third job called `deploy` to your workflow: 390 | 391 | ```yaml 392 | # Deploy to Kubernetes. 393 | deploy: 394 | runs-on: ubuntu-latest 395 | ``` 396 | 397 | You only want to deploy to Kubernetes when a new commit is pushed to the master 398 | branch, but your workflow is also triggered by pull requests. Use the job's `if` 399 | field to make sure it runs only when triggered by the master branch: 400 | 401 | ```yaml 402 | # Deploy to Kubernetes. 403 | deploy: 404 | runs-on: ubuntu-latest 405 | if: github.ref == 'refs/heads/master' 406 | ``` 407 | 408 | If one of the first two jobs fails, either because the tests didn't pass or 409 | because Github failed to build a container image, then the `deploy` job 410 | shouldn't run. Use the `needs` field to specify dependencies between jobs: 411 | 412 | ```yaml 413 | # Deploy to Kubernetes. 414 | deploy: 415 | runs-on: ubuntu-latest 416 | if: github.ref == 'refs/heads/master' 417 | needs: 418 | - run-tests 419 | - build-and-release 420 | ``` 421 | 422 | Once more, the first step of this job is to check out your source code: 423 | 424 | ```yaml 425 | # Deploy to Kubernetes. 426 | deploy: 427 | runs-on: ubuntu-latest 428 | if: github.ref == 'refs/heads/master' 429 | needs: 430 | - run-tests 431 | - build-and-release 432 | steps: 433 | # Check out source code. 434 | - name: Check out source code 435 | uses: actions/checkout@v2 436 | ``` 437 | 438 | Use `kubectl` to interact with the Kubernetes cluster. Add a step that downloads 439 | the binary and installs it on the system: 440 | 441 | ```yaml 442 | # Set up kubectl. 443 | - name: Set up kubectl 444 | run: |- 445 | curl -sfLo kubectl https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl 446 | chmod +x kubectl 447 | sudo mv kubectl /usr/local/bin/ 448 | ``` 449 | 450 | Notice that a command above uses an environment variable called 451 | `KUBECTL_VERSION`. Add it to the `env` field a the top of your workflow file: 452 | 453 | ```yaml 454 | env: 455 | IMAGE_REPOSITORY: busser/foobar 456 | KUBECTL_VERSION: "1.14.10" 457 | ``` 458 | 459 | Add a step that decodes the `kubectl` configuration stored in the `KUBECONFIG` 460 | secret and writes it to a file for later use: 461 | 462 | ```yaml 463 | # Configure kubectl. 464 | - name: Configure kubectl 465 | run: echo ${{ secrets.KUBECONFIG }} | base64 --decode > kubeconfig.yml 466 | ``` 467 | 468 | If you took a look at the `manifests/deployment.yml` file of your repository, 469 | you may have noticed this line: 470 | 471 | ```yaml 472 | image: REPOSITORY:TAG 473 | ``` 474 | 475 | Use Kustomize to dynamically inject the name and tag of the image built during 476 | the latest run of your workflow. Add a step that installs the `kustomize` 477 | binary: 478 | 479 | ```yaml 480 | # Set up Kustomize. 481 | - name: Set up Kustomize 482 | run: |- 483 | curl -sfL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${KUSTOMIZE_VERSION}/kustomize_v${KUSTOMIZE_VERSION}_linux_amd64.tar.gz | tar -xzf - 484 | sudo mv kustomize /usr/local/bin/ 485 | ``` 486 | 487 | A command above uses the `KUSTOMIZE_VERSION` environment variable. Add it the 488 | `env` field: 489 | 490 | ```yaml 491 | env: 492 | IMAGE_REPOSITORY: busser/foobar 493 | KUBECTL_VERSION: "1.14.10" 494 | KUSTOMIZE_VERSION: "3.5.4" 495 | ``` 496 | 497 | Now, add a step that edits the `manifests/kustomization.yml` file to specify the 498 | container image to deploy. This step needs to run in the `manifests` directory, 499 | so fill in the step's `working-directory` field accordingly: 500 | 501 | ```yaml 502 | # Kustomize Kubernetes resources. 503 | - name: Kustomize Kubernetes resources 504 | working-directory: ./manifests 505 | run: kustomize edit set image REPOSITORY:TAG=${IMAGE_REPOSITORY}:sha-${GITHUB_SHA::7} 506 | ``` 507 | 508 | Notice the `GITHUB_SHA` environment variable. No need to add it to the `env` 509 | field; this variable is set automatically by Github when running the workflow. 510 | It contains the hash of the commit that triggered this particular run. 511 | 512 | You are now ready to deploy. Add a step that creates (or updates) your 513 | Kubernetes resources: 514 | 515 | ```yaml 516 | # Deploy to Kubernetes. 517 | - name: Deploy to Kubernetes 518 | run: kubectl --kubeconfig kubeconfig.yml apply --kustomize manifests/ 519 | ``` 520 | 521 | > The `--kustomize` flag is available in version 1.14 and above of `kubectl`. 522 | 523 | Now, wait for Kubernetes to finish updating all pods in your deployment. If 524 | after two minutes the pods have not all started, assume the deployment has 525 | failed. Add a step that uses `kubectl` to do this: 526 | 527 | ```yaml 528 | # Validate deployment. 529 | - name: Validate deployment 530 | run: kubectl --kubeconfig kubeconfig.yml rollout status --timeout 120s deployment/foobar 531 | ``` 532 | 533 | The `deploy` job is now ready to deploy your application. Commit the changes you 534 | made to your workflow and push them to Github: 535 | 536 | ```bash 537 | git checkout master 538 | git pull 539 | git add .github/workflows/workflow.yml 540 | git commit -m 'Add deploy job to workflow' 541 | git push 542 | ``` 543 | 544 | ## Step 8: Check workflow results 545 | 546 | Pushing your changes to Github triggered a run of your finalized workflow. 547 | Github will first run your tests and build a container image for your service. 548 | Once these two jobs have completed successfuly, Github will deploy the latest 549 | version of your application to your Kubernetes cluster. 550 | 551 | Github will display the results of the workflow run on your repository's main 552 | page. 553 | 554 | ## Conclusion 555 | 556 | `TODO: ADD CONCLUSION` 557 | 558 | ## Possible upgrades 559 | 560 | If you wish to keep adding more features to this repository, here are a few 561 | ideas: 562 | 563 | - Simple: 564 | - Add a [workflow status badge](https://help.github.com/en/actions/configuring-and-managing-workflows/configuring-a-workflow#adding-a-workflow-status-badge-to-your-repository) 565 | to your repository. 566 | - Add more Kubernetes resources to your manifests, like a [Service](https://kubernetes.io/docs/concepts/services-networking/service/). 567 | - Advanced: 568 | - Use a [Helm chart](https://helm.sh/docs/topics/charts/) instead of `kubectl` 569 | and `kustomize` to deploy your application to Kubernetes. 570 | - Have pull requests trigger deployments to a staging cluster, in separate 571 | [namespaces](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/). 572 | Delete the corresponding namespace when the pull requests is closed or merged. 573 | --------------------------------------------------------------------------------