Инструкция: Как построить процесс доставки приложения в Kubernetes, используя gitlab ci и gitlab runner

ls6ts7a_lx5bkvv_yfpj_olbrxc.jpeg

Привет, Хабр! Меня зовут Егор Комаров, я тестировщик в команде #CloudMTS.

Сегодня я расскажу, как настроить процесс развертывания и обновления микросервисного приложения от разработчика до облака в две команды.

Когда в приложении появляется новый функционал (например, изменился ответ от сервера), запускается ряд стандартных действий:

  • получить фичу от разработчика;
  • сбилдить контейнер с новым приложением;
  • загрузить контейнер в репозиторий;
  • изменить и применить манифест кубера.


Эти рутинные действия можно автоматизировать через функционал gitlab ci.
iwqozjj8hpq4xrl8x78kibn_e1a.png
Архитектура приложения

Рассмотрим по шагам процесс доставки приложения в Kubernetes.

Я создал репозиторий на гитлаб и взял токен для гитлаб-раннера:

oyot7myao-bgagpi_gyrwr9unje.png
vsvtuuwbwdpnb2bqa7wuoecrqag.png
w9mhv86bmql8vizyjsf6fwhlobk.png
Токен вставлю чуть дальше

Хелм — это пакетный менеджер для кубернетиса (как pip для питона).

Для юниксов установка стандартная, для винды ситуация интереснее: скачайте Experimental Windows AMD64 по ссылке.

helm repo add gitlab https://charts.gitlab.io
helm repo update


Чтобы соединить гитлаб ui и гитлаб-раннер в кластере кубера,

gjxqyius-psz8bvdhjoaf7wclry.png

я прописываю registration token из пункта выше в конфиг гитлаб-раннера из хелма:

# выведу содержимое конфига в .yml файл
helm show values gitlab/gitlab-runner > murr-gitlab-runner.yml


В murr-gitlab-runner.yml меня интересует три поля:

tags: "murr_runner"


Прописываем свой тег, чтобы идентифицировать гитлаб-раннер. Именно по тегу gitlab поймет, на какой под слать запросы:

xlkwkvzi31eu7mijls82bfj3ufm.png

gitlabUrl: https://gitlab.com/


Проверяем путь к гитлабу (гитлаб можно установить свой).

runnerRegistrationToken: "___"


Прописываем токен из шага выше.

Создам кластер кубернетиса в облачным сервисе.

Этот сервис #CloudMTS предоставляет набор готовых решений, в частности поднимет мне ноду в кубере, выделит стабильный IP-адрес и предоставит бесплатный плагин в виде ingress-nginx (пустит трафик из интернета в кластер).

qcqgsc1oupuynjdbmyav5ngejfq.png

На выходе я скачаю кубконфиг.

5q987vnrxxgvyvyrzevkvfwiplw.png

Переименую в config и закину в C:\Users\Admin\.kube

uaqqk2debplyfffxbyfrmrwsmau.png

Проверю доступность кластера.

kubectl get nodes
NAME                         STATUS   ROLES    AGE   VERSION
liberal-dove-dcddd8-115d1b   Ready       84m   v1.21.11


Устанавливаю раннер в отдельный неймспейс кубера.
Неймспейс — это как виртуальное окружение в питоне, то есть изолированная среда, в которой можно систематизировать сервисы:

kubectl create ns gitlab-runner
helm install --namespace gitlab-runner gitlab-runner -f murr-gitlab-runner.yml gitlab/gitlab-runner


i3zxelvp1fm_yw2fjp5ykzhnfvg.png

Выдам полные права раннеру:

kubectl create clusterrolebinding --clusterrole=cluster-admin -n gitlab-runner --serviceaccount=gitlab-runner:default our-murr-runner


Подожду пару минут и проверю, что раннер запущен:

kubectl get po -n gitlab-runner -w
NAME                                          READY   STATUS    RESTARTS   AGE
gitlab-runner-gitlab-runner-994b96676-bjftj   1/1     Running   0          3m33s


Также увижу раннер в настройках гитлаба:

0hgeuuuzbhqajn2vdnxlzkwbs-u.png

Создам go сервер:

package main

import (
  "fmt"
  "github.com/rs/cors"
  "net/http"
)

func main() {
  fmt.Println("murr_server запущен")
  mux := http.NewServeMux()
  mux.HandleFunc("/murrengan/", func(w http.ResponseWriter, r *http.Request) {
     w.Header().Set("Content-Type", "application/json")
     w.Write([]byte("{\"message\": \"Привет, муррен!\"}"))
     fmt.Println("Вызвана функция по роуту murrengan")
  })
  handler := cors.Default().Handler(mux)
  err := http.ListenAndServe(":1991", handler)
  if err != nil {
     fmt.Println("murr_server упал:", err)
  }
}


Он возвращает json {«message»: «Привет, муррен!»} при гет-запросе с любого IP на :1991/murrengan/

Проверяем локально:

go run main.go
murr_serve запущен


Теперь по адресу 127.0.0.1:1991/murrengan/ доступно приложение. Откроем его в браузере.

ymqxecrevz10tsbevqi8hx4gkwy.png

Или проверим в терминале:

curl http://127.0.0.1:1991/murrengan/
{"message": "Привет, муррен!"}


Завернем наше приложение в Dockerfile и добавим возможность запуска в контейнере:

FROM golang:1.17-alpine

WORKDIR /

COPY go.mod ./
COPY go.sum ./
RUN go mod download

COPY *.go ./

RUN go build -o /murr_server

EXPOSE 1991


ENTRYPOINT ["/murr_server"]


Сбилдим имидж:

docker build -t murr_server_in_docker:0.3.0 .


Проверим готовый имидж:

docker images
REPOSITORY                  TAG        IMAGE ID       CREATED              SIZE
murr_server_in_docker       0.3.0      18965967f809   About a minute ago   308MB


Можно запустить и протестировать локально:

docker run -it -p 1991:1991 murr_server_in_docker:0.3.0


Описываем инструкцию работы gitlab runner в .gitlab-ci.yml

# В работе 2 стадии
stages:
 - build
 - deploy

# https://github.com/GoogleContainerTools/kaniko
# В этой функции мы даем право канико работать с нашим гитлабом.
# Канико позволяет билдить контейнеры в контейнерах.
.docker-login.: &docker-login
 before_script:
   - mkdir -p /kaniko/.docker
   - echo "{\"auths\":{\"${CI_REGISTRY}\":{\"auth\":\"$(printf "%s:%s" "${CI_REGISTRY_USER}" "${CI_REGISTRY_PASSWORD}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json

Build container:
 image: gcr.io/kaniko-project/executor:debug
 stage: build
 <<: *docker-login
 # тег указывали в murr-gitlab-runner.yml
 tags:
   - murr_runner
 only:
   - new_prod
 script:
   # тут канико билдит контейнер и через --destination пушит образ в реджистери
   # каждый пуш уникальный из-за $CI_COMMIT_SHORT_SHA (глобальная переменная гитлаб-раннера)
   - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

Deploy container:
 image:
   # этот образ позволяет запускать kubectl в script
   name: lachlanevenson/k8s-kubectl:latest
   entrypoint: ["/bin/sh", "-c"]
 stage: deploy
 tags:
   - murr_runner
 only:
   - new_prod
 script:
   # в manifest.yaml я указал шаблон image: registry.gitlab.com/murrengan/murr_server:change_thist_tag_on_gitlab_ci
   # и теперь через утилиту sed меняю change_thist_tag_on_gitlab_ci на уникальный коммит
   - sed -i "s|change_thist_tag_on_gitlab_ci|${CI_COMMIT_SHORT_SHA}|" manifest.yaml
   # применяю новый деплоймент
   - kubectl apply -n default -f manifest.yaml


Отдельно можно отметить manifest.yaml. Манифест — это описание состояния кластера кубернетис. Ты пишешь, как хочешь, чтобы было, а кубер старается так сделать.

apiVersion: apps/v1
kind: Deployment
metadata:
 name: murr-server-deployment
spec:
 selector:
   matchLabels:
     app: murr-server
 replicas: 2
 template:
   metadata:
     labels:
       app: murr-server
   spec:
     containers:
       - name: murr-server
         image: registry.gitlab.com/murrengan/murr_server:change_thist_tag_on_gitlab_ci
         imagePullPolicy: Always
         ports:
           - containerPort: 1991
---
kind: Service
apiVersion: v1
metadata:
 name: murr-server-service
spec:
 selector:
   app: murr-server
 ports:
   - port: 1991
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
 name: murr-server-ingress
 annotations:
   kubernetes.io/ingress.class: "nginx"
   nginx.ingress.kubernetes.io/rewrite-target: /$2

spec:
 rules:
   - http:
       paths:
         - path: /murr_server(/|$)(.*)
           pathType: ImplementationSpecific
           backend:
             service:
               name: murr-server-service
               port:
                 number: 1991


ftcaxkgmfy7tziaj939hn9_xb6s.png

Деплоймент следит за количеством экземпляров приложения мурр_сервер, версией и открытыми портами. Когда я обновляю мурр_сервер, именно деплоймент будет терминейтить старые поды и включать новые.

35cpgnffxyemh88qnz2nsexd5uu.png

Сервис указывает, какое приложение на каком порту ждет трафик.

r4m6fmr3wtogcxrxdigxw6uyer8.png

И как раз в ингрессе я через предустановленный плагин получаю трафик и проксирую его на url /murr_server — получается, чтобы обратиться к приложению, url будет выглядеть так: http://EXTERNAL-IP_от_провайдера_/murr_server/murrengan/

Пушим наши изменения в продакшен-ветку — new_prod:

git add .
git commit
git push --set-upstream origin new_prod


Идем в пайплайны и видим запуск:

uyxvofaqjkau8gbiolel2-aljpg.png
h-gnpoj49l5rimmrjxnvaibrkny.png

Ждем окончания второй джобы:

q7-_dmfxypdcf3bh9vbmnsylpgg.png

Ждем запуск сервиса для доступа к приложению:

kubectl get po -w
NAME                                      READY   STATUS    RESTARTS   AGE
murr-server-deployment-748f76bbb8-2hxxn   1/1     Running   0          60s
murr-server-deployment-748f76bbb8-77ldv   1/1     Running   0          60s


Нам надо получить EXTERNAL-IP для murr-server-load-balancer. Теперь, указав его в браузере, мы получим доступ к нашему приложению из любой точки мира.

hbwzi-joatrnuz-3ctypgjvqvj8.png

В данном примере url выглядит так: http://91.185.95.26/murr_server/murrengan/

Теперь приложение доступно во всем мире 24/7.

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

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

Впереди много задач: линтер, тестирование, фронтенд, https и murr_game…

Спасибо за ваш интерес к теме. Пишите в комментариях, если у вас есть вопросы.

© Habrahabr.ru