Docker deploy pipeline: сборка, push, kubectl rollout

Published: 2026-03-07

Минимальный жизнеспособный пайплайн деплоя: собрать образ → отправить в реестр → обновить Deployment в Kubernetes. Ниже полная реализация на GitLab CI для Python/FastAPI с self-hosted реестром: управление тегами, проверка выкатки, работа с несколькими окружениями.


Структура пайплайна

yamlstages:
  - build
  - deploy

variables:
  REGISTRY: registry.example.com
  DOCKER_TLS_CERTDIR: "/certs"
  DOCKER_BUILDKIT: "1"

Два стейджа. Build создаёт образ с тегом SHA коммита. Deploy применяет его и ждёт завершения.


Сборка и push

yamlbuild:
  stage: build
  image: docker:27
  services:
    - name: docker:27-dind
      alias: docker
  before_script:
    - echo "${REGISTRY_PASSWORD}" | docker login "${REGISTRY}" -u "${REGISTRY_USER}" --password-stdin
  script:
    - |
      IMAGE="${REGISTRY}/${CI_PROJECT_NAME}:${CI_COMMIT_SHORT_SHA}"
      LATEST="${REGISTRY}/${CI_PROJECT_NAME}:latest"
      docker buildx build \
        --platform linux/amd64 \
        --cache-from "${LATEST}" \
        --build-arg BUILDKIT_INLINE_CACHE=1 \
        --tag "${IMAGE}" \
        --tag "${LATEST}" \
        --push \
        .
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

--cache-from latest переиспользует layer-кэш от предыдущей сборки. --build-arg BUILDKIT_INLINE_CACHE=1 встраивает метаданные кэша в образ.

Тег с коротким SHA неизменяем — он идентифицирует конкретный коммит. latest изменяемый, используется только для кэша.


Job деплоя

yamldeploy:
  stage: deploy
  image: bitnami/kubectl:latest
  needs: [build]
  before_script:
    - echo "${KUBECONFIG_BASE64}" | base64 -d > /tmp/kubeconfig
    - export KUBECONFIG=/tmp/kubeconfig
  script:
    - |
      IMAGE="${REGISTRY}/${CI_PROJECT_NAME}:${CI_COMMIT_SHORT_SHA}"
      kubectl set image deployment/${CI_PROJECT_NAME} \
        app="${IMAGE}" --namespace="${NAMESPACE}"
      kubectl rollout status deployment/${CI_PROJECT_NAME} \
        --namespace="${NAMESPACE}" --timeout=120s
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

kubectl set image обновляет образ контейнера без редактирования манифестов. kubectl rollout status --timeout=120s блокирует выполнение до завершения выкатки или сбоя. Если новые поды уходят в crash-loop, CI-джоб падает, а старые поды продолжают работать.


Секрет kubeconfig

Сохраните kubeconfig как CI-переменную KUBECONFIG_BASE64:

bashcat ~/.kube/my-cluster.yaml | base64 -w0

Используйте выделенный ServiceAccount с минимальным RBAC — никогда не используйте admin-kubeconfig в CI:

yamlapiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: ci-deployer
  namespace: app
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "patch", "update"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]

Мультиокружения

yaml.deploy-template: &deploy-template
  stage: deploy
  image: bitnami/kubectl:latest
  before_script:
    - echo "${KUBECONFIG_BASE64}" | base64 -d > /tmp/kubeconfig
    - export KUBECONFIG=/tmp/kubeconfig
  script:
    - kubectl set image deployment/${CI_PROJECT_NAME}
        app="${REGISTRY}/${CI_PROJECT_NAME}:${CI_COMMIT_SHORT_SHA}"
        --namespace="${NAMESPACE}"
    - kubectl rollout status deployment/${CI_PROJECT_NAME}
        --namespace="${NAMESPACE}" --timeout=120s

deploy-dev:
  <<: *deploy-template
  variables:
    NAMESPACE: dev
    KUBECONFIG_BASE64: "${KUBECONFIG_DEV_BASE64}"
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy-prod:
  <<: *deploy-template
  variables:
    NAMESPACE: prod
    KUBECONFIG_BASE64: "${KUBECONFIG_PROD_BASE64}"
  when: manual
  environment:
    name: production
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

Dev деплоится автоматически на каждый push в main. Prod — только вручную через GitLab UI.


Откат

При сбое деплоя:

bashkubectl rollout undo deployment/my-app -n app
kubectl rollout status deployment/my-app -n app --timeout=60s

# На конкретную ревизию
kubectl rollout history deployment/my-app -n app
kubectl rollout undo deployment/my-app -n app --to-revision=3

Smoke-тесты после деплоя

yamlsmoke-test:
  stage: deploy
  image: curlimages/curl:latest
  needs: [deploy-dev]
  script:
    - |
      for i in $(seq 1 10); do
        STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://dev.example.com/healthz)
        if [ "$STATUS" = "200" ]; then
          echo "Smoke test passed"
          exit 0
        fi
        sleep 5
      done
      echo "Smoke test failed"
      exit 1
  rules:
    - if: $CI_COMMIT_BRANCH == "main"