Table of Contents

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 AreaRunners и там берем строчку 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

.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

Что тут прроисходит?

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

Настройка деплоя во внешний кластер

План такой:

переключаемся на контекст тестового кластера:

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