Gitlab CI
В данном практическом занятии рассмотрим построение CI/CD конвейера с помощью инструмента gitlab-ci, который является частью платформы gitlab.
Vagrant
Для работы будем использовать следующий Vagrantfile
:
Vagrant.configure("2") do |config|
config.vm.define "gitlab" do |c|
c.vm.box = "ubuntu/lunar64"
c.vm.provider "virtualbox" do |v|
v.cpus = 2
v.memory = 5120
end
c.vm.hostname = "gitlab"
c.vm.network "forwarded_port", guest: 8888, host: 8888
c.vm.provision "shell", inline: <<-SHELL
apt-get update -q
apt-get install -yq golang-go docker.io
usermod -a -G docker vagrant
host="http://localhost:8888"
curl -LO https://packages.gitlab.com/gitlab/gitlab-ce/packages/ubuntu/jammy/gitlab-ce_16.8.3-ce.0_amd64.deb/download.deb \
&& EXTERNAL_URL=$host dpkg -i download.deb && rm download.deb
gitlab-ctl reconfigure
pass="$(awk '/^Password/{print $2}' /etc/gitlab/initial_root_password)"
otoken=$(curl -sH "Content-Type: application/json" "$host/oauth/token" \
-d '{"grant_type":"password","username":"root","password":"'"$pass"'"}' \
| jq -r '.access_token')
ptoken=$(curl -s "$host/api/v4/users/1/personal_access_tokens" \
-H "Authorization: Bearer $otoken" -d "name=test" -d "scopes[]=api" \
| jq -r '.token')
rtoken=$(curl -sH "PRIVATE-TOKEN: $ptoken" "$host/api/v4/user/runners" \
-d "runner_type=instance_type" -d "tag_list=shared" | jq -r '.token')
curl -LO https://packages.gitlab.com/runner/gitlab-runner/packages/ubuntu/jammy/gitlab-runner_16.8.1_amd64.deb/download.deb \
&& dpkg -i download.deb && rm download.deb \
&& gitlab-runner register --non-interactive --url $host --executor shell --token "$rtoken"
curl -XPUT -sH "PRIVATE-TOKEN: $ptoken" -o /dev/null \
"$host/api/v4/application/settings?auto_devops_enabled=false"
echo "root password: $pass"
usermod -a -G docker gitlab-runner
SHELL
end
end
Данная конфигурация развернет виртуальную машину и установит
пакеты gitlab, а также подключит gitlab-runner. После
развертывания gitlab должен быть доступен по адресу
localhost:8888, для авторизации
необходимо использовать логин root
и пароль, который
выводится на экран в конце развертывания, а также
его можно посмотреть в файле /etc/gitlab/initial_root_password
.
New Project
Создадим новый пустой проект test-ci
на странице
projects/new
и склонируем на виртуальную машину:
$ git clone git@localhost:root/test-ci.git
Cloning into 'test-ci'...
warning: You appear to have cloned an empty repository.
$ cd test-ci/
$ ls -a
. .. .git
На текущий момент наш репозиторий пустой, добавим в него
файл .gitlab-ci.yml
с описанием простого пайплайна, чтобы
проверить работоспособность gitlab-ci:
stages:
- test
test:
stage: test
script:
- echo "it's works"
Сделаем коммит и запушим репозиторий:
$ git add .gitlab-ci.yml
$ git commit -m 'init'
[master (root-commit) f2ee095] init
1 file changed, 7 insertions(+)
create mode 100644 .gitlab-ci.yml
$ git push
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), 259 bytes | 129.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To localhost:root/test-ci.git
* [new branch] master -> master
После чего в описании последнего коммита будет виден статус выполнения пайплайна, в данном случае отмеченного зеленой галочкой: Перейдя по ней можно получить более подробное описание выполнения пайплайна, либо перейдя из левой панели в разделе Build:
Перейдя по статусу Passed
можно увидеть из каких шагов состоял пайплайн
и отдельно рассмотреть каждый шаг:
Перейдя в шаг test
можно увидеть лог выполнения:
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())
}
}
А также изменим скрипт в .gitlab-ci.yml
:
stages:
- test
test:
stage: test
script:
- go test
И отправим в репозиторий, создав модуль:
$ go mod init test
go: creating new go.mod: module test
go: to add module requirements and sums:
go mod tidy
$ git add .
$ git commit -m 'add test ci'
[master cf6ec4d] add test ci
4 files changed, 50 insertions(+), 1 deletion(-)
create mode 100644 go.mod
create mode 100644 main.go
create mode 100644 main_test.go
$ git push
Enumerating objects: 8, done.
Counting objects: 100% (8/8), done.
Delta compression using up to 2 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 909 bytes | 454.00 KiB/s, done.
Total 6 (delta 0), reused 0 (delta 0), pack-reused 0
To localhost:root/test-ci.git
f2ee095..cf6ec4d master -> master
В результате выполнения задачи test
нашего пайплайна увидим результат теста:
Running with gitlab-runner 16.8.1 (a6097117)
on gitlab X9GQT59xD, system ID: s_fd69ce32a6f8
Preparing the "shell" executor 00:00
Using Shell (bash) executor...
Preparing environment 00:00
Running on gitlab...
Getting source from Git repository 00:01
Fetching changes with git depth set to 20...
Reinitialized existing Git repository in /home/gitlab-runner/builds/X9GQT59xD/0/root/test-ci/.git/
Checking out cf6ec4d7 as detached HEAD (ref is master)...
Skipping Git submodules setup
Executing "step_script" stage of the job script 00:01
$ go test
PASS
ok test 0.004s
Job succeeded
Gitlab-ci позволяет производить различную обработку результатов
тестов. Попробуем добавить отображение процента покрытия,
добавив параметр coverage
в .gitlab-ci.yml
:
stages:
- test
test:
stage: test
script:
- go test -cover
coverage: '/coverage: (\d+.\d+)% of statements$/'
В данном скрипте утилита go test
выведет информацию о покрытии,
а с помощью регулярного выражение в параметре coverage
gitlab сможет
его считать и отобразить. Запушим данные изменения и посмотрим результат:
Executing "step_script" stage of the job script 00:01
$ go test -cover
PASS
test coverage: 50.0% of statements
ok test 0.004s
Job succeeded
Build Stage
Добавим процесс сборки нашего приложения новым шагом в .gitlab-ci.yml
:
stages:
- test
- build
test:
stage: test
script:
- go test -cover
coverage: '/coverage: (\d+.\d+)% of statements$/'
build:
stage: build
script:
- go build
И отправим в репозиторий, после чего у нас появится новая стадия:
Но вероятно нам не потребуется делать сборку каждый коммит. Добавим правила, чтобы сборка выполнялась только при наличии тега, либо при ручном запуске:
stages:
- test
- build
test:
stage: test
script:
- go test -cover
coverage: '/coverage: (\d+.\d+)% of statements$/'
build:
stage: build
script:
- go build
rules:
- if: $CI_COMMIT_TAG
- when: manual
Теперь после пуша сборка не запустится сама:
И ее можно будет запустить вручную в gitlab:
Работать с бинарными исполняемыми файлами напрямую после сборки не очень удобно,
переделаем процесс сборки, чтобы в результате получался docker образ. Добавим
в репозиторий 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"]
А скрипт сборки в .gitlab-ci.yml
изменим следующим образом:
stages:
- test
- build
test:
stage: test
script:
- go test -cover
coverage: '/coverage: (\d+.\d+)% of statements$/'
build:
stage: build
script:
- docker build -t test:${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA} .
rules:
- if: $CI_COMMIT_TAG
- when: manual
Данный скрипт после сборки проставит образу тег соответствующий тегу в гит репозитории, либо, при его отсутствии, короткий хэш коммита.
После пуша запустим сборку вручную в gitlab:
Executing "step_script" stage of the job script 00:21
$ docker build -t test:${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA} .
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
Install the buildx component to build images with BuildKit:
https://docs.docker.com/go/buildx/
Step 1/7 : FROM golang:1.22-alpine as builder
---> a2742f74d90f
Step 2/7 : WORKDIR /usr/src
---> Using cache
---> b99e933ad4c2
Step 3/7 : COPY . .
---> faa8e15e14c1
Step 4/7 : RUN go build -o /usr/src/app
---> Running in 8cb8d80d21ac
Removing intermediate container 8cb8d80d21ac
---> d71d3c810c82
Step 5/7 : FROM scratch
--->
Step 6/7 : COPY --from=builder /usr/src/app /app
---> 5cf470e9d810
Step 7/7 : CMD ["/app"]
---> Running in 9a223691c8c6
Removing intermediate container 9a223691c8c6
---> 3a194e61f59b
Successfully built 3a194e61f59b
Successfully tagged test:523f63cb
Job succeeded
На виртуальной машине можно увидеть собранный образ:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
test 523f63cb 3a194e61f59b 5 minutes ago 6.95MB
golang 1.22-alpine a2742f74d90f 3 weeks ago 230MB
Deploy Stage
Test
После сборки возможно нам захочется произвести деплой приложения в тестовую среду, добавим стадию деплоя описав запуск и остановку в тестовой среде:
stages:
- test
- build
- deploy
test:
stage: test
script:
- go test -cover
coverage: '/coverage: (\d+.\d+)% of statements$/'
build:
stage: build
script:
- docker build -t test:${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA} .
rules:
- if: $CI_COMMIT_TAG
- when: manual
deploy_test:
stage: deploy
script:
- docker run -p 8000:8080 -d --name test test:${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}
environment:
name: test
on_stop: stop_test
rules:
- when: manual
stop_test:
stage: deploy
script:
- docker rm -f test
environment:
name: test
action: stop
rules:
- when: manual
После чего у нас появится новая стадия в пайплайне:
Запустим сборку и деплой:
После чего на сервере увидим результат:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6b6e6c9786fc test:017e1343 "/app" 51 seconds ago Up 50 seconds 0.0.0.0:8000->8080/tcp, :::8000->8080/tcp test
$ curl localhost:8000
Hello!
Prod
Также можем добавить деплой в продуктивную среду в этой же стадии, но сделаем обязательными правилами наличие тега с ручным запуском:
stages:
- test
- build
- deploy
test:
stage: test
script:
- go test -cover
coverage: '/coverage: (\d+.\d+)% of statements$/'
build:
stage: build
script:
- docker build -t test:${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA} .
rules:
- if: $CI_COMMIT_TAG
- when: manual
deploy_test:
stage: deploy
script:
- docker run -p 8000:8080 -d --name test test:${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}
environment:
name: test
on_stop: stop_test
rules:
- when: manual
stop_test:
stage: deploy
script:
- docker rm -f test
environment:
name: test
action: stop
rules:
- when: manual
deploy_production:
stage: deploy
script:
- docker run -p 9000:8080 -d --name prod test:$CI_COMMIT_TAG
environment:
name: production
on_stop: stop_production
rules:
- if: $CI_COMMIT_TAG
when: manual
stop_production:
stage: deploy
script:
- docker rm -f prod
environment:
name: production
action: stop
rules:
- if: $CI_COMMIT_TAG
when: manual
На текущий момент в пайплайне не появится возможность деплоя в продуктивную среду, так как необходимо иметь тег:
Добавим и запушим тег:
$ git tag v0.0.1
$ git push origin v0.0.1
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To localhost:root/test-ci.git
* [new tag] v0.0.1 -> v0.0.1
После чего появится новый пайплайн:
Как видно сборка запустилась автоматически при наличии тега, запустим теперь деплой в прод и после проверим результат:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0929d8d7b92a test:v0.0.1 "/app" About a minute ago Up About a minute 0.0.0.0:9000->8080/tcp, :::9000->8080/tcp prod
6b6e6c9786fc test:017e1343 "/app" 32 minutes ago Up 32 minutes 0.0.0.0:8000->8080/tcp, :::8000->8080/tcp test
$ curl localhost:9000
Hello!
Также управление средами развертки можно управлять со страницы Operate/Environments:
Таким образом с помощью пайплайна gitlab-ci
можно построить удобный процесс
разработки, тестирования и развертывания.