https://skaffold.dev/
https://www.youtube.com/watch?v=0XXICbh9Bqs
https://softchris.github.io/pages/dotnet-dockerize.html#create-a-dockerfile
https://intellitect.com/docker-scaffold/
У меня есть три кластера (соотвествующие трем средам - dev, test, prod). В dev кластере должна происходить сборка и первичное тестирование сборок из ветки dev. В кластеры test и prod - должны выкатываться сборки из веток TEST и PROD соответственно.
Установка Shared-раннера GitLab в кластере kubernetes
Проще всего установить shared runner (то есть тот, который может использоваться любым проектом данной инсталляции GitLab) с помощью helm-чарта.
Создадим неймспейс:
kubectl create ns gitlab
Создадим секрет с корневым сертификатом, чтобы доверять инсталляции GitLab. Имя записи в секрете должна быть hostname.crt, где hostname - это имя хоста GitLab (gitlab.domain.local):
kubectl create secret generic -n gitlab ca-cert --from-file=gitlab.domain.local.crt=./CA.cer
Для того, чтобы раннер смог взаимодействовать с GitLab ему нужно передать пару параметров - адрес инсталляции GitLab и registration token.
Чтобы получить registration token идем в Admin Area → Runners и там берем строчку registration token, а затем подствляем в файл с values для чарта:
cat <<EOF > ./gitlab-runner-settings.yaml image: gitlab/gitlab-runner:alpine-v13.8.0 runnerRegistrationToken: "dqjNDHQsum1zW1gVmtXc" logLevel: debug runners: privileged: true config: | [[runners]] [runners.kubernetes] image = "docker:dind" helpers: image: "gitlab/gitlab-runner-helper:x86_64-bleeding" gitlabUrl: https://gitlab.domain.local/ certsSecretName: ca-cert EOF
Устанавливаем GitLabRunner
helm repo add gitlab https://charts.gitlab.io helm upgrade --install gitlab-runner-shared -n gitlab -f ./gitlab-runner-settings.yaml gitlab/gitlab-runner
Дадим прав раннеру в кластере (то есть дадим прав дефолтной ServiceAccount в неймспейсе gitlab):
kubectl apply -f - <<EOF apiVersion: rbac.authorization.k8s.io/v1 kind: "ClusterRoleBinding" metadata: name: gitlab-runner-admin namespace: gitlab roleRef: apiGroup: rbac.authorization.k8s.io kind: "ClusterRole" name: cluster-admin subjects: - kind: ServiceAccount name: default namespace: gitlab EOF
В итоге - данная конфигурация позволит запускать раннер в кластере и позволит раннеру деплоить собранное. Не забываем, что речь иде о dev-кластере.
Настройка CI/CD на базе GitLab и Skaffold
Настройка проекта в GitLab
- Включаем CI/CD для проекта - Settings → General → Visibility, project features, permissions → Pipelines
- Настраиваем переменные - Settings → CI/CD → Variables. Нужно добавить DOCKER_REGISTRY_URL и конфиги для кластеров (переменные KUBECONFIG_PROD и KUBECONFIG_TEST), отличных от того, где работает раннер (в своем кластере раннер имеет права). Если docker registry требует аутентифиуации, то добавляем переменные DOCKER_REGISTRY_LOGIN и DOCKER_REGISTRY_PASS и расскоментируем в .gitlab-ci.yaml соответствующие строчки.
.gitlab-ci.yml
services: # - name: docker.rdleas.ru/docker:dind # Последнюю версию dind (20+) не удается использовать из-за ошибки: https://github.com/containerd/containerd/issues/4837 # Нужно обновлять container.d на хостах # В остальном - всё уже готово. - name: docker.rdleas.ru/docker:19-dind command: - /bin/sh - -c - | openssl s_client -showcerts -connect docker.rdleas.ru:443 </dev/null 2>/dev/null | awk '/BEGIN/ { i++; } /BEGIN/, /END/ { print }' > /usr/local/share/ca-certificates/ca.crt update-ca-certificates unset DOCKER_TLS_CERTDIR dockerd-entrypoint.sh stages: - build variables: DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "" DOCKER_HOST: tcp://0.0.0.0:2375 build: image: name: docker.rdleas.ru/k8s-skaffold/skaffold:v1.19.0 stage: build # before_script: # - docker login -u `echo $DOCKER_REGISTRY_LOGIN` -p `echo $DOCKER_REGISTRY_PASS` `echo $DOCKER_REGISTRY_URL` #https://skaffold.dev/docs/references/cli/ script: - | openssl s_client -showcerts -connect ${DOCKER_REGISTRY_URL}:443 </dev/null 2>/dev/null | awk '/BEGIN/ { i++; } /BEGIN/, /END/ { print }' > /usr/local/share/ca-certificates/ca.crt update-ca-certificates env echo "waiting for the DinD..." timeout 60 bash -c 'until printf "" 2>>/dev/null >>/dev/tcp/$0/$1; do sleep 1; done' 0.0.0.0 2375 if [ "$CI_COMMIT_BRANCH" = "DEV" ]; then skaffold run -f skaffold-run.yaml --default-repo ${DOCKER_REGISTRY_URL}/vrm else skaffold build -f skaffold-build.yaml fi
Что тут прроисходит?
- service: - эта инструкция запускает некую сущность (доступную по сети из скрипта для сборки). У гитлаба написано, что изначально так запускали БД. Теперь так запускаем docker и в дальнейшем передаем ему Dockerfile для сборки. Для того, чтобы этот сервис доверял нашим сертификатам - мы сначала добавляем сертификат в доверенные, а затем стартуем штатный Entrypoint.
- stages: - список этапов конвейера, которые будут объявлены позднее.
- variables: - список переменных среды, которые будут добавлены к другим переменным и доступны в контейнерах, которые будут запускаться в дальнейшем.
- build: - собственно описание на данный момент единственной стадии конвейера. В ней мы стартуем контейнер skaffold, выполняем before_script, который логинится в приватный Docker Registry. Логин, пароль и url этого registry заданны в настройках проекта GitLab в виде Variables. Конструкции вида
`echo $DOCKER_REGISTRY_LOGIN`
сделаны для того, чтобы избежать ошибки:
cannot perform an interactive login from a non TTY device
Дальше - сам скрипт script. В нем тоже добавляется в доверенные сертификат нашего registry, а затем запускается skaffold с параметром run, который выполняет все стадии конвейера - сборку (а в процессе сборки запускается и тестирование) и деплой с помощью чарта.
skaffold.yaml
Тут происходит вот что.
Как понятно из манифеста - это Config для skaffold. Полное описание всего этого доступно https://skaffold.dev/docs/references/yaml/.
Итак. С точки зрения skaffold, работающего в контейнере, мы выполняем сборку локально, на что указывает тег local: {}.
Мы собраем артефакт с помощью клиента докера (тег docker), который будет общаться с сервером Docker, который является запущенным ранее service: и доступен нам благодаря объявленной переменной среды DOCKER_HOST.
Так вот этому докеру будет передан Dockerfile, путь к которому задан с помощью тега dockerfile: относительно пути context:.
А после сборки мы выкатываем релиз с помощью helm (который будет запущен из контейнера skaffold) в тот кластер на котором работает наш runner.
Подробнее о том как скаффолд работат с кластером - тут https://skaffold.dev/docs/environment/kube-context/#kubeconfig-selection
apiVersion: skaffold/v2beta11 kind: Config build: local: {} tagPolicy: envTemplate: template: "{{.CI_COMMIT_BRANCH}}" artifacts: - image: vrm context: . docker: dockerfile: Dockerfile buildArgs: CI_COMMIT_SHORT_SHA: '{{.CI_COMMIT_SHORT_SHA}}' deploy: helm: releases: - name: vrm chartPath: chart/ namespace: vrm valuesFiles: - '{{.VALUES}}' artifactOverrides: image: vrm
Dockerfile
#https://docs.docker.com/engine/examples/dotnetcore/ FROM docker.rdleas.ru/amd64/maven:3.6.3-jdk-11 as builder WORKDIR /app COPY . . RUN mvn -gs /app/.m2/settings.xml -Pnexus clean compile test-compile \ && mvn -gs /app/.m2/settings.xml -Pnexus package FROM docker.rdleas.ru/openjdk:11.0.10-jre-buster ENV CI_COMMIT_SHORT_SHA=$CI_COMMIT_SHORT_SHA WORKDIR /app COPY --from=builder /app/. ./ CMD ["/usr/local/openjdk-11/bin/java", "-jar", "/app/target/vrm-1.0.0-SNAPSHOT.jar"]
Тут у нас двухстадийная сборка (multistage).
Сначала мы запускаем контейнер maven, который получает псевдоним builder, который выкачивает зависимости из локальной proxy-репы maven-central на базе nexus (директория .m2 с файлом settings.xml лежит в репозитории проекта).
А далее мы собираем второй контейнер на базе образа OpenJDK JRE, в который копируем файлики, полученные в результате работы первого контейнера-билдера.
Настройка деплоя во внешний кластер
План такой:
- создаем неймспейс, в который будем деплоить релиз с помощью helm-чарта
- создаем ServiceAccount, который будет иметь права на все объекты в данном неймспейсе.
- Создаем фалик KUBECONFIG, который мы передадим skaffold и с помощью которого будем обращаться к кластеру.
- Созданный файлик KUBECONFIG кодируем в base64 и помещаем в переменную окружения проекта, чтобы передать его skaffold.
переключаемся на контекст тестового кластера:
kubectl config use-context test
Создаем неймспейс:
kubectl create ns vrm
Создаем ServiceAccount. Я создам его в неймспейсе kube-system:
kubectl create sa -n kube-system vrm-ns-admin
Дадим этой SA права в неймспейсе vrm:
kubectl apply -f - <<EOF apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: vrm-ns-admin namespace: vrm rules: - apiGroups: ["*"] resources: ["*"] verbs: ["*"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: "RoleBinding" metadata: name: vrm-ns-admin namespace: vrm roleRef: apiGroup: rbac.authorization.k8s.io kind: "Role" name: vrm-ns-admin subjects: - kind: ServiceAccount name: vrm-ns-admin namespace: kube-system EOF
Дальше получаем имя токена:
TOKEN_NAME=`kubectl get secret -n kube-system -o name | grep vrm-ns-admin-token`
и его содержимое:
TOKEN=`kubectl get $TOKEN_NAME -n kube-system -o "jsonpath={.data.token}" | base64 -d`
а также корневой сертификат кластера:
TOKEN_CA=`kubectl get $TOKEN_NAME -n kube-system -o "jsonpath={.data['ca\.crt']}"`
и URL для control plane:
CONTROL_PLANE=`kubectl cluster-info | grep 'Kubernetes control plane' | awk '{print $NF}' | sed -r "s/[[:cntrl:]]\[([0-9]{1,3};)*[0-9]{1,3}m//g"`
И, наконец, генерируем собственно сам KUBECONFIG:
cat <<EOF > ./vrm-admin-kubeconfig apiVersion: v1 kind: Config preferences: {} # Define the cluster clusters: - cluster: certificate-authority-data: $TOKEN_CA server: $CONTROL_PLANE name: cluster # Define the user users: - name: vrm-ns-admin user: as-user-extra: {} client-key-data: $TOKEN_CA token: $TOKEN # Define the context: linking a user to a cluster contexts: - context: cluster: cluster namespace: vrm user: vrm-ns-admin name: vrm # Define current context current-context: vrm EOF
Проверяем, что полученный kubeconfig работает:
KUBECONFIG=./vrm-admin-kubeconfig kubectl get po -n vrm
Теперь - добавляем содержимое этого файла в свойтсвах проекта в переменную типа File. В итоге -содержимое файла будет доставлено в контейнер skaffold в виде временного файла, а её значение будет указать на расположение этого файла. А gitlab-ci.yml станет такой:
services: # - name: docker.rdleas.ru/docker:dind # Последнюю версию dind (20+) не удается использовать из-за ошибки: https://github.com/containerd/containerd/issues/4837 # Нужно обновлять container.d на хостах # В остальном - всё уже готово. - name: docker.rdleas.ru/docker:19-dind command: - /bin/sh - -c - | openssl s_client -showcerts -connect docker.rdleas.ru:443 </dev/null 2>/dev/null | awk '/BEGIN/ { i++; } /BEGIN/, /END/ { print }' > /usr/local/share/ca-certificates/ca.crt update-ca-certificates unset DOCKER_TLS_CERTDIR dockerd-entrypoint.sh stages: - build variables: DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "" DOCKER_HOST: tcp://0.0.0.0:2375 build: image: name: docker.rdleas.ru/k8s-skaffold/skaffold:v1.19.0 stage: build before_script: - docker login -u `echo $DOCKER_REGISTRY_LOGIN` -p `echo $DOCKER_REGISTRY_PASS` `echo $DOCKER_REGISTRY_URL` #https://skaffold.dev/docs/references/cli/ script: - | openssl s_client -showcerts -connect ${DOCKER_REGISTRY_URL}:443 </dev/null 2>/dev/null | awk '/BEGIN/ { i++; } /BEGIN/, /END/ { print }' > /usr/local/share/ca-certificates/ca.crt update-ca-certificates env echo "waiting for the DinD..." timeout 60 bash -c 'until printf "" 2>>/dev/null >>/dev/tcp/$0/$1; do sleep 1; done' 0.0.0.0 2375 case $CI_COMMIT_BRANCH in DEV) VALUES="chart/values_dev.yaml" skaffold run -f skaffold-run.yaml --default-repo ${DOCKER_REGISTRY_URL}/vrm ;; TEST) VALUES="chart/values_test.yaml" \ KUBECONFIG=$KUBECONFIG_TEST \ skaffold run -f skaffold-run.yaml \ --default-repo ${DOCKER_REGISTRY_URL}/vrm ;; PROD) KUBECONFIG=$KUBECONFIG_PROD \ VALUES="chart/values_prod.yaml" \ skaffold run -f skaffold-run.yaml \ --default-repo ${DOCKER_REGISTRY_URL}/vrm ;; *) skaffold build -f skaffold-build.yaml ;; esac
Discussion