← Назад к вопросам

CI/CD pipeline для микросервиса в GitLab CI

2.0 Middle🔥 251 комментариев
#CI/CD и автоматизация

Условие

Создайте .gitlab-ci.yml для микросервиса на Python/Go/Node.js:

Стадии pipeline

  1. lint - проверка кода линтером
  2. test - запуск unit-тестов
  3. build - сборка Docker образа
  4. push - загрузка образа в registry
  5. deploy-staging - деплой на staging (автоматически)
  6. 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 DeliveryContinuous 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 для всей инфраструктуры и приложений.

Ключевые принципы:

  1. Все описано в Git (конфиги, код, инфра)
  2. Изменения делаются через Pull Requests
  3. Система автоматически синхронизируется с Git
  4. Все изменения логируются и аудитируются

Как 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

АспектТрадиционный CDGitOps
Source of truthPipeline конфиги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

Результат:

  1. CI строит образ и пушит в registry
  2. CD обновляет deployment.yaml в Git
  3. ArgoCD видит изменение и синхронизирует Kubernetes
  4. Приложение обновляется
  5. Все изменения видны в Git истории
CI/CD pipeline для микросервиса в GitLab CI | PrepBro