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

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

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

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

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


Архитектура приложения

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

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




Токен вставлю чуть дальше

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

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

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

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



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

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

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

tags: "murr_runner"

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



gitlabUrl: https://gitlab.com/

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

runnerRegistrationToken: "___"

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

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

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



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



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



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

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

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

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



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

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

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



Создам 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/ доступно приложение. Откроем его в браузере.



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

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



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



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



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

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

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

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




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



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

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. Теперь, указав его в браузере, мы получим доступ к нашему приложению из любой точки мира.



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

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

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

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

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

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

Комментарии (7)


  1. gohrytt
    31.03.2022 19:57
    +2

    Сейчас бы запускать программу в том же контейнере где она собиралась... Чисто +300 мегабайт к весу образа.


    1. AlexGluck
      01.04.2022 03:00
      +1

      Но но но, не собирать же бинарник статически, чтобы он прям был distroless и весил мегабайта 4. Вдруг всё станет работать быстрее и надёжнее, мы этого не хотим.


      1. past
        01.04.2022 12:38

        Именно так, статический в distroless


  1. past
    01.04.2022 12:39
    +1

    Посмотрите, как работает ArgoCD/Flux. kubectl apply из CI это всё же не совсем gitops


  1. Negash
    01.04.2022 15:31

    Почему люди не используют GitLab AutoDevOps


  1. shep
    01.04.2022 22:11

    сбилдить контейнер с новым приложением;

    загрузить контейнер в репозиторий;

    Образ, пожалуйста, вы билдите и пушите образ...

    Ну замечание по мультистейдж билд.

    Теги образа в прод (only: [new_prod]) хэшем коммита.

    Конструкция с printf при формировании basic auth - восторг. В той же доке гитлаба более лаконичный пример.

    Какой-то анти деврел у вас. Неужели статьи перед публикацией не вычитываются хоть кем-то?


  1. Owleyeinnose
    02.04.2022 17:33

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