Deployment Pipeline


# Deployment Pipeline

We hebben in de introductie van dit vak al heel vaak gesproken van deployments. Onze developers willen zo snel mogelijk code shippen naar production. Dit proces verliep vroeger manueel, iets wat we tegenwoordig niet meer kunnen permiteren!

Dit deel van onze DevOps automatisatie noemen we de "pipeline", het voorbereiden en shippen van onze code. In een typische omgeving draait dit misschien tot 100 keer per dag.

# CI/CD

Dit process vatten we vaak samen als een "Continuous Integration/Continuous Deployment" (CI/CD) process (alhoewel dit ook een heel cultuur framwork meebrengt).

Laten we heel eventjes down to earth gaan, in essentie is dit eigenlijk "scripten draaien bij Git commits". We gaan via een CI/CD tool scripts uitvoeren om de kwaliteit van de code te garanderen en deze te laten shippen naar de production.

# CI

Continuous Integration (CI) is een techniek die gebruik maakt van een CI/CD tool om de code te valideren en te testen. Dit is niet het vakgebied voor onze opleiding, maar we gaan hier verschillend automatische testen uitvoeren op de code om te kunnen bevestigen dat alles werkt.

# CD

Continous Deployment (CD) zorgt ervoor dat onze code naar de production kan gestuurd worden. Afhankelijk van de ongeving kan dit bij een push naar de "main" branch, of een apparte "production" branch en soms ook bij het taggen van een release versie. Wij gaan het in onze voorbeelden houden op het eerste.

In het CD deel van onze pipeline gaan we de code bouwen en naar onze servers sturen. We kunnen dat bijvoorbeeld met Docker, we maken een container image en pushen die naar Docker Hub of een andere registry. Waarna we een commando naar onze server sturen om deze image binnen te halen. CD schema

# Tools

Een populair voorbeeld van dit soort on-premise tools is Jenkins, een open-source systeem. Jenkins kent al enkele levensjaren, gepaard met een slechte reputatie in de security wereld (tip: Jenkins is DE manier een netwerk binnen te geraken!) verliest Jenkins momenteel snel populariteit die gaat naar modernere en vaak cloud based oplossingen.

Met de opkomst van Cloud zijn er veel SaaS oplossingen verschenen. Deze diensten doen in essentie nog steeds hetzelfde, maar gebruiken tijdelijke VM's in de Cloud om de acties in uit te voeren. Voorbeelden hiervan zijn CircleCI, Bamboo, TravisCI, Azure DevOps,... . Git hosting providers hebben ook vaak een eigen systeem zoals GitHub Actions en GitLab Runners.

De meeste van deze Cloud diensten hebben ook een on-premise versie. Of een hybride versie waar de configuratie en logs in de cloud draaien maar de eigenlijke commando's op je eigen server.

Voor deze cursus zullen we GitHub Actions gebruiken, de Cloud-gebaseerde DevOps pipeline dienst van GitHub. Deze dienst is gratis en onbeperkt voor publieke repositories op GitHub en gratis tot 3000 build minuten voor private repositories. We gaan ook kijken naar de hybride versie met "self-hosted runners" dat het volledig gratis is, en we kunnen dit ook achter onze eigen firewall draaien!

# GitHub Actions

GitHub Actions is de CI/CD tool die in GitHub ingebouwd zit. Deze dienst wint enorm snel aan populariteit en is makkelijk te gebruiken. We gaan GitHub Actions hier gebruiken om een Docker project te bouwen en te pushen. De onderstaande voorbeelden zijn van QuackeJS-Docker (opens new window) een in browser versie van Quake III met een multiplayer server.

# Workflows

In de actions tab op GitHub zien we verschillende "workflows". Een workflow is een CI/CD actie die uitgevoerd kan worden. Dit kan bij een push, pull request of zelfs elke x aantal uren.

Actions page

Al deze acties staan gedefinieerd in de map .github/workflows in je repository. We bekijken hier een simpele workflow die onze server gaat deployen bij een push naar de main branch.

Een workflow file is een YAML bestand waarin we een job gaan beschrijven.

actions overview

Onderstaande actions file is een workflow voor het bouwen en pushen van een Docker image naar de GHCR registry, de Docker Registry van GitHub.

name: Docker Image Build & Push

on:
    push:
        branches: [main]

jobs:
    build:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v2
            - name: Log in to registry
              run: echo ${{ secrets.GHCR_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin
            - name: Build the Docker image
              run: docker build -t ghcr.io/meyskens/quakejs-docker:$GITHUB_SHA .
            - name: Push the image to the registry
              run: docker push ghcr.io/meyskens/quakejs-docker:$GITHUB_SHA
            - name: Push the image to the registry as latest
              run: |
                  docker tag ghcr.io/meyskens/quakejs-docker:$GITHUB_SHA ghcr.io/meyskens/quakejs-docker:latest
                  docker push ghcr.io/meyskens/quakejs-docker:latest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

We zien 3 hoofdonderdelen:

  • name is de naam van de workflow
  • on is de trigger waarop de workflow moet draaien
  • jobs is de lijst met taken en scripts die in de workflow moeten draaien

# On

We kijken even naar on in detail:

on:
    push:
        branches: [main]
1
2
3

Dit gaat ervoor zorgen dat onze workflow alleen gaat triggeren als we een push naar de main branch doen. Er zijn ook andere opties, zoals pull_request deze zien we vaker in het CI gedeelte waar we code kunnen gaan testen voor we ze mergen naar de main branch. Nog een optie is cron dit geeft een cron job mee die het toelaat op regelmatige basis de workflow te draaien.

on:
    cron: "0 0 * * *"
1
2

# Jobs

Een workflow kan meerdere Jobs hebben. Een Job is een stap in de workflow die uitgevoerd kan worden. Deze job heeft een naam, in dit geval build. In een volgend voorbeeld gaan we de stap deploy toevoegen. Een job heeft een runs-on property die aangeeft welke platform de job moet draaien. In dit geval ubuntu-latest. Er zijn ook andere mogelijkheden (opens new window) zoals windows-latest of macos-latest. We kunnen ook een eigen platform opzetten op een self-hosted runner.

Het belangrijkste deel van een job is steps, waar we de stappen van de job kunnen definieren. We kijken even naar 2 in detail:

steps:
    - uses: actions/checkout@v2
    - name: Build the Docker image
      run: docker build -t ghcr.io/meyskens/quakejs-docker:$GITHUB_SHA .
1
2
3
4

De eerste uses: actions/checkout@v2 gebruikt uses, dit laat ons toe om een bepaalde actie die al voorgeprogrameerd is te gebruiken. In dit geval actions/checkout versie 2. Dit is een standaard actie van GitHub zelf die onze repository download. Je kan deze vinden in de Marketplace (opens new window). Een leuk feitje is dat deze onderliggend Docker gebruiken.

De tweede stap start met name: Build the Docker image, dit geeft de stap een naam die zegt aan de gebruiker wat die doet. In dezelfde stap staat run. Wat dit doet is een shell commando te runnen. In dit geval docker build -t ghcr.io/meyskens/quakejs-docker:$GITHUB_SHA .. Zo kunnen we makkelijk shell scripts uitvoeren voor alles wat we nodig hebben.

# Environment Variables

Je hebt het misschien al opgemerkt ook onze environment variabelen komen terug bij het schrijven van workflows. We kunnen deze specifiek meegeven, maar er zijn ook ingebouwde variablen (opens new window) die GitHub Actions meegeeft.

We hebben een paar interessante:

  • GITHUB_SHA is de SHA van de commit, deze is altijd uniek en handig voor een versie toe te kennen aan een Docker image
  • GITHUB_ACTOR is de naam van de gebruiker die de actie uitvoert
  • GITHUB_REPOSITORY is de naam van de repository
  • GITHUB_REF is de branch of tag die gepushed is

# Secrets

Heel vaak moeten we ergens kunnen inloggen, of een server aanspreken of misschien proprietery code downloaden. We willen niet dat onze wachtwoorden en access tokens zomaar voor het rapen liggen in de code of de logs van de workflow. Secrets bieden hiervoor een antwoord. Je kan secrets instellen op de settings pagina van je repository. Je kan ze daarna opvragen met

${{ secrets.<naam van secret> }}
1

in je workflow file. Moest je de value (per ongeluk) laten printen in je logs dan zal GitHub Actions deze vervangen door sterretjes.

In dit voorbeeld gebruiken we een GHCR_TOKEN secret. We gebruiken deze secret om in te loggen op de GHCR registry.

- name: Log in to registry
  run: echo ${{ secrets.GHCR_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin
1
2

Secrets werken alleen in jouw repository waar je ook toestemming geeft om de secrets te gebruiken! Zo kan ook bij open source code attacks via CI/CD vermeden worden. Let ook op met het goedkeuren van een PR met wijzigingen aan code van de CI 😉

# Self-hosted Runners

Buiten de GitHub Actions runners in de cloud kan je deze ook zelf hosten. Dit heeft een aantal voordelen, zo heb je hierbij geen limiet op aantal minuten dan de build kan draaien. We kunnen ook builds achter onze firewall draaien of op een eigen server, zo kunnen we een deployment makkelijk uitvoeren.

We kunnen deze makkelijk aan een repository toevoegen via de settings pagina van de repository. runners-page

Als we op "New self-hosted runner" klikken krijgen we al meteen alle instructies om een runner te maken! setup page

Als je de stappen blijft volgen krijgen we een setup configuratie script. We laten hier alles default alleen voegen we het label deploy toe.

# Runner Registration

Enter the name of the runner group to add this runner to: [press Enter for Default]

Enter the name of runner: [press Enter for deploy-test]

This runner will have the following labels: 'self-hosted', 'Linux', 'X64'
Enter any additional labels (ex. label-1,label-2): [press Enter to skip] deploy

√ Runner successfully added
√ Runner connection is good

# Runner settings

Enter name of work folder: [press Enter for _work]

√ Settings Saved.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Nu staat alles op GitHub stelt ./run.sh voor de runner om te draaien. Echter wij gaan dit als een service installeren:

sudo ./svc.sh install
sudo ./svc.sh start
1
2

Nu draait onze runner! Om deze te gebruiken kunnen we onze gemaakte tag in de workflow zetten met runs-on aan te passen. We doen dit in het voorbeeld hieronder waar we onze container gaan deployen.

# Deploy voorbeeld

In dit deel gaan met behulp van onze CI/CD pipeline onze container bouwen en deployen. Dit met Git, Docker en Docker Compose. Een beetje de kers op de taart van deze cursus.

We baseren ons op een kopie van QuakeJS Docker (opens new window).

Eerst zetten we onze server op met Terraform:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.16"
    }
  }
}

variable "runner_token" {
  type        = string
  description = "The token for the GitHub Actions runner to use"
  default     = ""
}

variable "repo_url" {
  type        = string
  description = "The URL of the GitHub repo to use"
  default     = "https://github.com/xxx/xxx"
}


provider "aws" {
  region = "us-east-1"
}

resource "aws_key_pair" "deployer" {
  key_name   = "deployer-key"
  public_key = file("~/.ssh/id_rsa.pub")
}

resource "aws_security_group" "main" {
  egress = [
    {
      cidr_blocks      = ["0.0.0.0/0", ]
      description      = ""
      from_port        = 0
      ipv6_cidr_blocks = []
      prefix_list_ids  = []
      protocol         = "-1"
      security_groups  = []
      self             = false
      to_port          = 0
    }
  ]
  ingress = [
    {
      cidr_blocks      = ["0.0.0.0/0", ]
      description      = ""
      from_port        = 22
      ipv6_cidr_blocks = []
      prefix_list_ids  = []
      protocol         = "tcp"
      security_groups  = []
      self             = false
      to_port          = 22
    },
    {
      cidr_blocks      = ["0.0.0.0/0", ]
      description      = ""
      from_port        = 80
      ipv6_cidr_blocks = []
      prefix_list_ids  = []
      protocol         = "tcp"
      security_groups  = []
      self             = false
      to_port          = 80
    },
    {
      cidr_blocks      = ["0.0.0.0/0", ]
      description      = ""
      from_port        = 27960
      ipv6_cidr_blocks = []
      prefix_list_ids  = []
      protocol         = "tcp"
      security_groups  = []
      self             = false
      to_port          = 27960
    }
  ]
}


data "aws_ami" "ubuntu" {
  most_recent = true
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
  owners = ["099720109477"] # Canonical
}

resource "aws_instance" "quake" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.large"

  key_name = aws_key_pair.deployer.key_name

  associate_public_ip_address = true
  vpc_security_group_ids      = [aws_security_group.main.id]

  user_data_replace_on_change = true

  user_data = <<-EOF
    #!/bin/bash
    # Install Docker
    wget -O docker.sh https://get.docker.com/
    bash docker.sh
    usermod -aG docker ubuntu
    # Install GH Actions Runner
    sudo -u ubuntu mkdir /home/ubuntu/actions-runner
    cd /home/ubuntu/actions-runner
    sudo -u ubuntu curl -o actions-runner-linux-x64-2.298.2.tar.gz -L https://github.com/actions/runner/releases/download/v2.298.2/actions-runner-linux-x64-2.298.2.tar.gz
    sudo -u ubuntu tar xzf ./actions-runner-linux-x64-2.298.2.tar.gz
    rm ./actions-runner-linux-x64-2.298.2.tar.gz
    sudo -u ubuntu ./config.sh --url ${var.repo_url} --token ${var.runner_token} --name "Github EC2 Runner3" --labels deploy --unattended
    ./svc.sh install
    ./svc.sh start
    EOF
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

We halen uit onze eigen niet-publieke repo onder Settings -> Actions -> Runners -> New Runner de token uit de commando's. Dit wordt daarna automatisch op onze EC2 server geinstalleerd met een scriptje samen met een install van Docker.

We gaan onze repo kopieren naar een eigen repo, we willen niet onze publieke repo gebruiken voor deze demo:

git clone git@github.com:meyskens/quakejs-docker.git
cd quakejs-docker
git remote remove origin

# eigen repo instellen
git remote add origin <new repo>
git push -u origin main
1
2
3
4
5
6
7

Nu passen we de workflow aan:

  1. we veranderen de ghcr.io repo naam naar de naam van onze repository (doe je dit zelf vergeet ook meyskens niet aan te passen!)
  2. we voegen de deploy stap toe op onze self-hosted runner
name: Docker Image Build & Push

on:
    push:
        branches: [main]

jobs:
    build:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v2
            - name: Log in to registry
              run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin
            - name: Build the Docker image
              run: docker build -t ghcr.io/meyskens/quake-deploy-test:$GITHUB_SHA .
            - name: Push the image to the registry
              run: docker push ghcr.io/meyskens/quake-deploy-test:$GITHUB_SHA
            - name: Push the image to the registry as latest
              run: |
                  docker tag ghcr.io/meyskens/quake-deploy-test:$GITHUB_SHA ghcr.io/meyskens/quake-deploy-test:latest
                  docker push ghcr.io/meyskens/quake-deploy-test:latest
    deploy:
        runs-on: deploy
        needs: build
        steps:
            - uses: actions/checkout@v2
            - name: Log in to registry
              run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin
            - name: Pull the image from the registry
              run: docker pull ghcr.io/meyskens/quake-deploy-test:$GITHUB_SHA
            - name: Ensure /opt/quake exists
              run: |
                  mkdir /opt/quake || true
            - name: Change docker-compose to use this image
              run: |
                  sed -i "s/image:.*/image: ghcr.io\/meyskens\/quake-deploy-test:$GITHUB_SHA/" docker-compose.yaml
            - name: Copy the docker-compose file to the deployment directory
              run: cp docker-compose.yaml /opt/quake/docker-compose.yaml
            - name: restart the deployment
              run: |
                  cd /opt/quake
                  docker compose up -d
                  docker compose restart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

NOTE: GITHUB_TOKEN is een built in secret met een token voor GitHub, deze heeft de rechten van de maker van de commit.

Onze deploy stap hier gaat:

  1. De repo binnenhalen
  2. Ingloggen op GHCR
  3. Docker compose aanpassen met de huidige image met een beetje regex + sed magie
  4. De docker-compose file in onze deploy map zetten
  5. CDen naar onze deployment map en alles updaten met docker-compose up -d

Met runs-on: deploy laten we deze stap op onze eigen server met tag deploy uitvoeren. De image zelf bouwen deden we op de cloud servers van GitHub. We zetten ook needs: build anders gaat GitHub deze stappen parallel uitvoeren.

Als we dit committen en pushen gaat GitHub aan de slag:

actions screen

Als GitHub klaar is met de stappen, staat alles op groen en is de deploy klaar!

We bekijken of het gelukt is:

maartje@deploy-test:/opt/quake$ cat docker-compose.yaml

version: "3.9"
services:
  quakejs:
    image: ghcr.io/meyskens/quake-deploy-test:3d4193d5bb794fd7b9e812c9f93f41980b35b886
    ports:
      - "80:80"
      - "27960:27960"
    environment:
      HTTP_PORT: 80


maartje@deploy-test:/opt/quake$ docker ps
CONTAINER ID   IMAGE                                                                         COMMAND            CREATED          STATUS         PORTS                                                                              NAMES
bffca3f763d6   ghcr.io/meyskens/quake-deploy-test:3d4193d5bb794fd7b9e812c9f93f41980b35b886   "/entrypoint.sh"   24 seconds ago   Up 7 seconds   0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:27960->27960/tcp, :::27960->27960/tcp   quake-quakejs-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

We zien dat de docker-compose.yaml onze SHA heeft van onze laatste versie! Als ook dat alles draait en up to date is. QuakeJS is nu online!

# References

Dit hoofdstuk is geen makkelijke om een geisoleerde oefening rond te maken. Het is wel een belangrijke stap in het project.

Last update: October 18, 2022 11:06
Contributors: Maartje Eyskens