Jenkins

В данном практическом занятии рассмотрим построение CI/CD конвейера аналогичного предыдущему занятию, но с использованием инструмента jenkins.

Vagrant

Для работы будем использовать следующий Vagrantfile:

Vagrant.configure("2") do |config|
  config.vm.define "jenkins" do |c|
    c.vm.box = "ubuntu/lunar64"
    c.vm.provider "virtualbox" do |v|
      v.cpus = 2
      v.memory = 4096
    end
    c.vm.hostname = "jenkins"
    c.vm.network "forwarded_port", guest: 8888, host: 8888
    c.vm.network "forwarded_port", guest: 8889, host: 8889
    c.vm.provision "shell", inline: <<-SHELL
      apt-get update -q
      apt-get install -yq golang-go docker.io
      chmod o+rw /var/run/docker.sock
      docker run --name gitea -p 8889:3000 -d --restart on-failure \
        -e GITEA__security__INSTALL_LOCK=true -e GITEA__webhook__ALLOWED_HOST_LIST=private gitea/gitea
      docker run --name jenkins -p 8888:8080 -d --restart on-failure \
        -v /var/run/docker.sock:/var/run/docker.sock \
        -e JAVA_OPTS=-Djenkins.install.runSetupWizard=false jenkins/jenkins:2.448-jdk17
      docker exec jenkins jenkins-plugin-cli --verbose \
        -p workflow-aggregator git generic-webhook-trigger
      docker exec jenkins sh -c 'cd /var/jenkins_home/ && curl -LO https://go.dev/dl/go1.21.8.linux-amd64.tar.gz && tar xf go1.21.8.linux-amd64.tar.gz'
      docker exec jenkins sh -c 'cd /var/jenkins_home/ && curl -LO https://download.docker.com/linux/static/stable/x86_64/docker-25.0.3.tgz && tar xf docker-25.0.3.tgz && mv docker/docker go/bin/'
      docker restart jenkins
    SHELL
  end
end

Данная конфигурация развернет виртуальную машину с контейнерами jenkins localhost:8888 с рядом плагинов и gitea localhost:8889.

New Project

Необходимо создать нового пользователя в gitea user/sign_up, а после новый репозиторий repo/create с именем test.

В jenkins создадим пайплайн кликнув New Item или по ссылке view/all/newjob, выбрав тип Pipeline и задав имя test.

После чего в появившейся конфигурации в разделе Pipeline в поле Script слева выберем Hello World и сохраним кнопкой Save.

Запустим сборку нажав Build Now, чтобы проверить работу пайплайна. После чего результат можно будет увидеть в панели Build History:

Из которой можно открыть сам билд или его консольный вывод:

Pipeline from SCM

Свяжем наш git проект с пайплайном в jenkins. Для этого опишем код пайплайна в Jenkinsfile:

pipeline {
    agent any
    stages {
        stage('Hello') {
            steps {
                echo 'Hello World'
            }
        }
    }
}

И отправим в наш репозиторий:

$ git clone http://localhost:8889/alex/test.git
Cloning into 'test'...
warning: You appear to have cloned an empty repository.
$ cd test/
$ cat <<EOF>Jenkinsfile
> pipeline {
    agent any

    stages {
        stage('Hello') {
            steps {
                echo 'Hello World'
            }
        }
    }
}
EOF
$ git add Jenkinsfile
$ git commit -m 'init'
[main (root-commit) d2b8ed9] init
 1 file changed, 11 insertions(+)
 create mode 100644 Jenkinsfile
$ git push
Username for 'http://localhost:8889': alex
Password for 'http://alex@localhost:8889':
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Delta compression using up to 2 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 276 bytes | 276.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote: . Processing 1 references
remote: Processed 1 references in total
To http://localhost:8889/alex/test.git
 * [new branch]      main -> main

Теперь сконфигурируем наш пайплайн в jenkins, задав тип Pipeline script from SCM, указав систему контроля версий - git, в качестве адреса - адрес докер интерфейса и путь к репозиторию http://172.17.0.1:8889/alex/test.git, а также ветку */main.

После чего можно повторно запустить сборку и посмотреть результат:

Webhook

Запускать сборку каждый раз вручную не очень удобно, сделаем триггер для запуска сборки через webhook, который будет реагировать на события пуша в гит репозиторий. В jenkins в конфигурации пайплайна в разделе Build Triggers выберем Generic Webhook Trigger, где в появившейся конфигурации определим токен, который будет идентифицировать наш пайплайн, например test.

В gitea в Settings проекта в разделе Webhooks создадим новый хук кнопкой Add Webhook и выберем тип Gitea.

В конфигурации заполним адрес хука: http://172.17.0.1:8888/generic-webhook-trigger/invoke?token=test

После создания хука можно открыть в него и сделать тестовый запуск кнопкой Test Delivery.

В jenkins же увидим запущенный билд, в котором будет указано, что запуск был инициирован плагином generic webhook.

Теперь попробуем запустить триггер пушем в репозиторий, для этого добавим новый файл go.mod:

$ go mod init test
go: creating new go.mod: module test
$ git add go.mod
$ git commit -m 'add go.mod'
[main 27891cc] add go.mod
 1 file changed, 3 insertions(+)
 create mode 100644 go.mod
$ git push
Username for 'http://localhost:8889': alex
Password for 'http://alex@localhost:8889':
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 2 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 286 bytes | 286.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote: . Processing 1 references
remote: Processed 1 references in total
To http://localhost:8889/alex/test.git
   d2b8ed9..27891cc  main -> main

После чего в jenkins запустится сборка, а также видно какие изменения произошли в репозитории.

Test Stage

Наконец добавим код приложения main.go и тесты к нему main_test.go, чтобы запускать при каждом пуше.

package main

import (
        "log"
        "net/http"
)

func Handler(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("Hello!\n"))
}

func main() {
        if err := http.ListenAndServe("0.0.0.0:8080", http.HandlerFunc(Handler));err != nil {
                log.Fatal(err)
        }
}
package main

import (
        "net/http"
        "net/http/httptest"
        "testing"
)

func TestHandler(t *testing.T) {
        req, err := http.NewRequest("GET", "/", nil)
        if err != nil {
                t.Fatal(err)
        }

        rec := httptest.NewRecorder()

        http.HandlerFunc(Handler).ServeHTTP(rec, req)
        if rec.Code != http.StatusOK {
                t.Errorf(
                        "wrong status code: want %v got %v",
                        http.StatusOK, rec.Code,
                )
        }

        if rec.Body.String() != "Hello!\n" {
                t.Errorf("wrong body: got %s", rec.Body.String())
        }
}

А также изменим Jenkinsfile:

pipeline {
    agent any
    environment {
      PATH = "/var/jenkins_home/go/bin:${env.PATH}"
    }
    stages {
        stage('test') {
            steps {
                sh 'go test'
            }
        }
    }
}

В jenkins запустится новая сборка со следующим результатом:

Build Stage

Добавим сборку приложения новым шагом.

pipeline {
    agent any
    environment {
      PATH = "/var/jenkins_home/go/bin:${env.PATH}"
    }
    stages {
        stage('test') {
            steps {
                sh 'go test'
            }
        }
        stage('build') {
            steps {
                sh 'go build'
            }
        }
    }
}

После пуша появится новая сборка:

Для деплоя нам будет удобнее работать с docker образами, так что изменим сборку:

pipeline {
    agent any
    environment {
      PATH = "/var/jenkins_home/go/bin:${env.PATH}"
    }
    stages {
        stage('test') {
            steps {
                sh 'go test'
            }
        }
        stage('build') {
            steps {
                sh 'docker build -t test:$(git rev-parse --short HEAD) .'
            }
        }
    }
}

А также добавим Dockerfile:

FROM golang:1.22-alpine as builder

WORKDIR /usr/src

COPY . .
RUN go build -o /usr/src/app

FROM scratch
COPY --from=builder /usr/src/app /app

CMD ["/app"]

После отправки в репозиторий в jenkins запустится новый билд, в котором соберется docker образ:

И на нашей виртуальной машине появится новый образ:

$ docker images test
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
test         1e2775f   4311123c6ee4   5 minutes ago   6.95MB

Deploy stage

В конечном итоге нам необходимо произвести деплой образа, для этого добавим стадию deploy в наш пайплайн:

pipeline {
    agent any
    environment {
      PATH = "/var/jenkins_home/go/bin:${env.PATH}"
    }
    stages {
        stage('test') {
            steps {
                sh 'go test'
            }
        }
        stage('build') {
            steps {
                sh 'docker build -t test:$(git rev-parse --short HEAD) .'
            }
        }
        stage('deploy') {
            steps {
                sh 'docker rm -f test'
                sh 'docker run -p 9000:8080 -d --name test test:$(git rev-parse --short HEAD)'
            }
        }
    }
}

После очередного пуша в результате сборки увидим, что контейнер запустился:

$ docker ps
CONTAINER ID   IMAGE                         COMMAND                  CREATED          STATUS          PORTS                                                  NAMES
59586e5d8983   test:c70828c                  "/app"                   18 seconds ago   Up 17 seconds   0.0.0.0:9000->8080/tcp, :::9000->8080/tcp              test
$ curl localhost:9000
Hello!