← Назад к вопросам
CI/CD pipeline для микросервиса в GitLab CI
2.0 Middle🔥 251 комментариев
#CI/CD и автоматизация
Условие
Создайте .gitlab-ci.yml для микросервиса на Python/Go/Node.js:
Стадии pipeline
- lint - проверка кода линтером
- test - запуск unit-тестов
- build - сборка Docker образа
- push - загрузка образа в registry
- deploy-staging - деплой на staging (автоматически)
- deploy-production - деплой на production (manual)
Требования
- Используйте кэширование зависимостей
- Образ должен тегироваться по commit hash и latest
- Deploy на production только для тегов вида v*
- Добавьте environment для staging и production
Вопросы
- Чем отличается Continuous Integration от Continuous Deployment?
- Как обеспечить безопасность credentials в pipeline?
- Что такое GitOps и как он связан с CI/CD?
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
1. .gitlab-ci.yml - Полный pipeline
# GitLab CI/CD Pipeline для микросервиса
image: docker:latest
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
REGISTRY_IMAGE: "registry.gitlab.com/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME"
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_VERIFY: ""
services:
- docker:dind
stages:
- lint
- test
- build
- push
- deploy-staging
- deploy-production
# ============================================================================
# LINT Stage
# ============================================================================
lint:python:
stage: lint
image: python:3.11-slim
cache:
key:
files:
- requirements.txt
paths:
- .venv/
before_script:
- apt-get update && apt-get install -y git
- pip install --cache-dir .cache virtualenv
- virtualenv .venv
- source .venv/bin/activate
- pip install -r requirements.txt
script:
- pip install flake8 black isort
- echo "Running flake8..."
- flake8 src/ --max-line-length=120 --exclude=.git,__pycache__
- echo "Running black check..."
- black --check src/
- echo "Running isort check..."
- isort --check-only src/
tags:
- docker
only:
- merge_requests
- branches
lint:yaml:
stage: lint
image: alpine:latest
before_script:
- apk add --no-cache yamllint
script:
- echo "Checking YAML files..."
- yamllint -d relaxed .
tags:
- docker
allow_failure: true
# ============================================================================
# TEST Stage
# ============================================================================
test:unit:
stage: test
image: python:3.11-slim
cache:
key:
files:
- requirements.txt
paths:
- .cache/pip
- .venv/
before_script:
- pip install --cache-dir .cache -r requirements.txt
- pip install pytest pytest-cov pytest-mock
script:
- echo "Running unit tests..."
- pytest tests/unit -v --cov=src --cov-report=xml --cov-report=html
coverage: '/TOTAL.*\s+(\d+%)$/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
paths:
- htmlcov/
expire_in: 1 week
tags:
- docker
only:
- merge_requests
- branches
test:integration:
stage: test
image: docker:latest
services:
- docker:dind
- postgres:15
variables:
POSTGRES_DB: "test_db"
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "postgres"
script:
- echo "Running integration tests..."
- docker run --rm --network host \
-e DATABASE_URL="postgresql://postgres:postgres@localhost:5432/test_db" \
python:3.11-slim bash -c "
pip install -r requirements.txt && \
pip install pytest && \
pytest tests/integration -v
"
tags:
- docker
only:
- merge_requests
- branches
allow_failure: true
# ============================================================================
# BUILD Stage
# ============================================================================
build:docker:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- echo "Building Docker image..."
script:
- |
docker build \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg VCS_REF=$CI_COMMIT_SHA \
--build-arg VERSION=$CI_COMMIT_TAG \
-t $REGISTRY_IMAGE:$CI_COMMIT_SHA \
-t $REGISTRY_IMAGE:latest \
-f Dockerfile .
- docker image ls
artifacts:
paths:
- .
expire_in: 1 hour
tags:
- docker
only:
- branches
- tags
# ============================================================================
# PUSH Stage
# ============================================================================
push:docker-registry:
stage: push
image: docker:latest
services:
- docker:dind
dependencies:
- build:docker
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
script:
- |
docker build \
-t $REGISTRY_IMAGE:$CI_COMMIT_SHA \
-t $REGISTRY_IMAGE:latest \
-f Dockerfile .
- docker push $REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $REGISTRY_IMAGE:latest
- echo "Image pushed to $REGISTRY_IMAGE:$CI_COMMIT_SHA"
tags:
- docker
only:
- branches
- tags
push:docker-registry-tag:
stage: push
image: docker:latest
services:
- docker:dind
dependencies:
- build:docker
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
script:
- |
docker build \
-t $REGISTRY_IMAGE:$CI_COMMIT_TAG \
-f Dockerfile .
- docker push $REGISTRY_IMAGE:$CI_COMMIT_TAG
- echo "Image pushed to $REGISTRY_IMAGE:$CI_COMMIT_TAG"
tags:
- docker
only:
- tags
# ============================================================================
# DEPLOY STAGING
# ============================================================================
deploy:staging:
stage: deploy-staging
image: alpine:latest
environment:
name: staging
url: https://staging-api.example.com
deployment_tier: staging
auto_stop_in: 1 week
before_script:
- apk add --no-cache curl
- apk add --no-cache openssh-client
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H $STAGING_SERVER >> ~/.ssh/known_hosts 2>/dev/null
script:
- echo "Deploying to Staging..."
- |
ssh -i ~/.ssh/id_rsa deploy@$STAGING_SERVER bash -s << EOF
set -e
cd /app/myservice
# Pull latest image
docker pull $REGISTRY_IMAGE:latest
# Stop old container
docker-compose down || true
# Update docker-compose.yml with new image
sed -i "s|image:.*|image: $REGISTRY_IMAGE:latest|g" docker-compose.yml
# Start new container
docker-compose up -d
# Wait for service to be ready
for i in {1..30}; do
if curl -f http://localhost:8000/health; then
echo "Service is ready"
exit 0
fi
sleep 1
done
echo "Service failed to start"
exit 1
EOF
tags:
- docker
only:
- main
- develop
when: on_success
retry:
max: 2
when:
- script_failure
# ============================================================================
# DEPLOY PRODUCTION
# ============================================================================
deploy:production:
stage: deploy-production
image: alpine:latest
environment:
name: production
url: https://api.example.com
deployment_tier: production
kubernetes:
namespace: production
before_script:
- apk add --no-cache curl
- apk add --no-cache openssh-client
- apk add --no-cache kubectl
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H $PROD_SERVER >> ~/.ssh/known_hosts 2>/dev/null
script:
- echo "Deploying to Production..."
- |
ssh -i ~/.ssh/id_rsa deploy@$PROD_SERVER bash -s << EOF
set -e
cd /app/myservice
echo "Deploying version $CI_COMMIT_TAG"
# Pull image
docker pull $REGISTRY_IMAGE:$CI_COMMIT_TAG
# Blue-Green deployment
docker-compose -f docker-compose.prod.yml up -d myapp-green
# Wait for new version to be healthy
for i in {1..60}; do
if curl -f http://localhost:8001/health; then
echo "New version is healthy"
break
fi
sleep 1
done
# Switch traffic
docker-compose -f docker-compose.prod.yml up -d myapp-blue
# Remove old version
docker-compose -f docker-compose.prod.yml down myapp-green
echo "Deployment completed"
EOF
tags:
- docker
only:
- tags
when: manual
environment:
auto_stop_in: never
# ============================================================================
# CLEANUP
# ============================================================================
cleanup:old-images:
stage: .post
image: docker:latest
services:
- docker:dind
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
script:
- echo "Cleaning up old images..."
- |
REGISTRY_URL="$CI_REGISTRY_HOST/api/v4"
REPO_ID=$CI_PROJECT_ID
# Delete images older than 30 days (requires cleanup_policy)
docker image prune -a -f --filter "until=720h"
tags:
- docker
only:
- schedules
allow_failure: true
2. Dockerfile для Python микросервиса
# Multi-stage build
FROM python:3.11-slim as builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
FROM python:3.11-slim
ARG BUILD_DATE
ARG VCS_REF
ARG VERSION
LABEL org.label-schema.build-date=$BUILD_DATE \
org.label-schema.vcs-ref=$VCS_REF \
org.label-schema.vcs-url="https://github.com/user/repo" \
org.label-schema.version=$VERSION
WORKDIR /app
RUN addgroup --gid 1001 appuser && \
adduser --uid 1001 --gid 1001 --disabled-password appuser
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser:appuser . .
ENV PATH=/home/appuser/.local/bin:$PATH \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:8000/health')"
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
3. docker-compose.yml для запуска
version: '3.9'
services:
api:
image: registry.gitlab.com/mygroup/myservice:latest
container_name: myservice_api
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:password@db:5432/myservice
- LOG_LEVEL=info
depends_on:
db:
condition: service_healthy
networks:
- backend
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
db:
image: postgres:15-alpine
container_name: myservice_db
environment:
POSTGRES_DB: myservice
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
networks:
backend:
driver: bridge
4. Continuous Integration vs Continuous Deployment
Continuous Integration (CI)
Определение: Автоматизация процесса интеграции кода от нескольких разработчиков несколько раз в день.
Что это делает:
- Запускает автоматическую сборку кода после каждого коммита
- Запускает тесты
- Проверяет качество кода (линт, анализ уязвимостей)
- Создает артефакты (Docker образы, бинарники)
Преимущества:
- Ранее обнаружение конфликтов слияния
- Постоянная обратная связь разработчикам
- Сокращение времени от кода к готовому артефакту
# CI Pipeline
Code committed → Lint → Unit tests → Integration tests → Build artifact
↓ ↓ ↓ ↓ ↓
GitHub Success Success Success Artifact ready
(auto) (Docker image)
Continuous Deployment (CD)
Определение: Автоматическое развертывание кода в production после успешной CI.
Что это делает:
- Автоматически развертывает готовые артефакты на production
- Запускает дым-тесты на production
- Откатывает в случае проблем
Преимущества:
- Очень частые деплои (несколько раз в день)
- Минимальная задержка между разработкой и production
- Автоматические откаты при проблемах
Риски:
- Требует отличного мониторинга
- Требует высокой тестовой покрытия
- Может привести к нестабильности
# CD Pipeline (автоматический деплой)
CI успешна → Develop → Staging → Production (автоматически)
↓ ↓ ↓ ↓
Image built Deploy Test Live users
(auto) (auto) (auto)
Continuous Delivery vs Continuous Deployment
| Аспект | Continuous Delivery | Continuous Deployment |
|---|---|---|
| Деплой staging | Автоматический | Автоматический |
| Деплой production | Ручной (кнопка) | Полностью автоматический |
| Частота деплоев | 1-2 раза в день | Много раз в день |
| Risk | Ниже (контроль) | Выше (авто) |
| Требует | Хороший мониторинг | Отличный мониторинг |
| Лучше для | Enterprise, финансы | Tech startups, SaaS |
5. Безопасность credentials в pipeline
Проблема
# ❌ НИКОГДА не коммитьте credentials
echo "API_KEY=sk_live_12345678" >> .env
git add .env && git commit -m "Add credentials"
# Теперь credentials в истории git НАВСЕГДА!
Решение 1: GitLab CI Variables (встроено)
# В GitLab: Settings → CI/CD → Variables
# Добавляем переменные:
API_KEY = sk_live_12345678
DATABASE_PASSWORD = super_secret
REGISTRY_PASSWORD = registry_password
# В .gitlab-ci.yml используем переменные
script:
- export API_KEY=$API_KEY # Из переменной
- python main.py
Преимущества:
- Простой встроенный способ
- GitLab хранит переменные зашифрованными
- Не попадают в логи по умолчанию
Недостатки:
- Видно в UI
- Все переменные доступны всем job'ам
Решение 2: Protected Variables
# В GitLab: Settings → CI/CD → Variables
# Отметить опцию "Protected"
# Теперь переменная доступна только:
# - На protected branches (main, production)
# - На тегах
Решение 3: Masked Variables
# Отметить опцию "Masked"
# Переменная НЕ будет видна в логах
echo "$API_KEY" # Будет видно как: ****
Решение 4: HashiCorp Vault
# Интеграция с внешним хранилищем
deploy:production:
image: vault:latest
script:
- |
export VAULT_TOKEN=$(vault login -method=jwt -path=jwt-prod \
-token=$CI_JOB_JWT -format=json | jq -r '.auth.client_token')
API_KEY=$(vault kv get -field=api_key secret/myapp)
DATABASE_PASSWORD=$(vault kv get -field=password secret/database)
python deploy.py
Решение 5: AWS Secrets Manager
deploy:production:
image: aws:latest
script:
- |
API_KEY=$(aws secretsmanager get-secret-value \
--secret-id prod/api_key \
--query SecretString \
--output text)
python deploy.py
Best practices
# 1. НИКОГДА не коммитьте credentials
# 2. Используйте GitLab protected + masked переменные
# 3. Для production используйте HashiCorp Vault или AWS Secrets Manager
# 4. Регулярно ротируйте credentials
# 5. Логируйте доступ к credentials
# 6. Используйте IAM/RBAC для контроля доступа
# .gitignore
.env
.env.local
.env.*.local
secrets/
*.key
*.pem
6. GitOps и его связь с CI/CD
Определение GitOps
GitOps — подход, при котором Git репозиторий является source of truth для всей инфраструктуры и приложений.
Ключевые принципы:
- Все описано в Git (конфиги, код, инфра)
- Изменения делаются через Pull Requests
- Система автоматически синхронизируется с Git
- Все изменения логируются и аудитируются
Как GitOps связан с CI/CD
# CI/CD + GitOps
Developer commits code
↓
Git push → CI Pipeline (lint, test, build)
↓
Create PR
↓
Approve and merge to main
↓
CI builds Docker image → pushes to registry
↓
Update kubernetes-configs/deployment.yaml with new image
↓
Git commit → kubernetes-configs repo
↓
GitOps operator (ArgoCD) detects change
↓
ArgoCD автоматически синхронизирует Kubernetes
↓
Приложение обновляется в production
Традиционный CD vs GitOps
| Аспект | Традиционный CD | GitOps |
|---|---|---|
| Source of truth | Pipeline конфиги | Git репозиторий |
| Как деплоить | Из pipeline (push) | Operator из Git (pull) |
| Откат | Повторный деплой | Откат коммита в Git |
| Аудит | Pipeline логи | Git история |
| Disaster recovery | Сложно | Просто (git clone) |
| Multi-cluster | Сложно | Просто (разные репо) |
Инструменты GitOps
ArgoCD — самый популярный
# Создаем Application в ArgoCD
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/kubernetes-configs
targetRevision: main
path: myapp/
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Что происходит:
- ArgoCD следит за репо kubernetes-configs
- При каждом изменении в Git, ArgoCD синхронизирует Kubernetes
- Если кто-то вручную изменит что-то в Kubernetes, ArgoCD откатит
Практический пример GitOps pipeline
# .gitlab-ci.yml
deploy:gitops:
stage: deploy
image: alpine:latest
script:
- apk add --no-cache git
- git clone https://gitlab.com/myorg/kubernetes-configs.git
- cd kubernetes-configs
- |
# Update deployment image
sed -i "s|image: .*|image: $REGISTRY_IMAGE:$CI_COMMIT_SHA|" \
myapp/deployment.yaml
- git add myapp/deployment.yaml
- git commit -m "Deploy $CI_COMMIT_SHA from $CI_PROJECT_NAME"
- git push https://oauth:$CI_PUSH_TOKEN@gitlab.com/myorg/kubernetes-configs.git main
only:
- main
Результат:
- CI строит образ и пушит в registry
- CD обновляет deployment.yaml в Git
- ArgoCD видит изменение и синхронизирует Kubernetes
- Приложение обновляется
- Все изменения видны в Git истории