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"