Оркестрация пайплайнов для микросервисной архитектуры, monorepo vs multirepo.
Оркестрация пайплайнов для микросервисной архитектуры: monorepo vs multirepo, reusable workflows, service mesh, API gateway.
В этом кейсе мы рассмотрим настройку CI/CD для микросервисной архитектуры. Вы изучите:
Все сервисы хранятся в одном репозитории:
my-company/
├── services/
│ ├── api-gateway/
│ ├── user-service/
│ ├── order-service/
│ └── notification-service/
├── shared/
│ ├── types/
│ ├── utils/
│ └── proto/
├── infrastructure/
│ ├── terraform/
│ └── kubernetes/
└── .github/
└── workflows/
├── ci.yml
└── deploy.yml
Преимущества monorepo:
Недостатки monorepo:
Каждый сервис в отдельном репозитории:
myorg-api-gateway/
myorg-user-service/
myorg-order-service/
myorg-notification-service/
myorg-infra/
Преимущества multirepo:
Недостатки multirepo:
Используйте monorepo для связанных сервисов одной команды, multirepo для независимых сервисов разных команд.
# .github/workflows/reusable-ci.yml в репозитории myorg/.github
name: Reusable CI
on:
workflow_call:
inputs:
node-version:
required: false
type: string
default: '20'
test-command:
required: false
type: string
default: 'npm test'
coverage-enabled:
required: false
type: boolean
default: false
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Run TypeScript check
run: npm run typecheck
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: ${{ inputs.test-command }}
- name: Upload coverage
if: inputs.coverage-enabled
uses: codecov/codecov-action@v4# services/user-service/.github/workflows/ci.yml
name: User Service CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
ci:
uses: myorg/.github/.github/workflows/reusable-ci.yml@main
with:
node-version: '20'
test-command: 'npm test'
coverage-enabled: true.github, применилось ко всем сервисам# .github/workflows/ci-monorepo.yml
name: Monorepo CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
user-service: ${{ steps.filter.outputs.user-service }}
order-service: ${{ steps.filter.outputs.order-service }}
api-gateway: ${{ steps.filter.outputs.api-gateway }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
user-service:
- 'services/user-service/**'
- 'shared/**'
order-service:
- 'services/order-service/**'
- 'shared/**'
api-gateway:
- 'services/api-gateway/**'
- 'shared/**'
test-user-service:
needs: detect-changes
if: needs.detect-changes.outputs.user-service == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: services/user-service
steps:
- uses: actions/checkout@v4
- name: Run tests
run: npm test
test-order-service:
needs: detect-changes
if: needs.detect-changes.outputs.order-service == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: services/order-service
steps:
- uses: actions/checkout@v4
- name: Run tests
run: npm testОпределяет изменённые пути и создаёт булевы флаги для conditional jobs:
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
backend:
- 'backend/**'
frontend:
- 'frontend/**'
docs:
- 'docs/**'
- '*.md'┌─────────────────┐
│ API Gateway │
└────────┬────────┘
│
┌────┴────┐
│ │
┌───▼───┐ ┌──▼──────┐
│ User │ │ Order │
│Service│ │ Service │
└───┬───┘ └────┬────┘
│ │
│ ┌────▼────────┐
│ │ Notification│
│ │ Service │
│ └─────────────┘
│
┌───▼─────────┐
│ Database │
└─────────────┘
# .github/workflows/deploy-all.yml
name: Deploy All Services
on:
push:
branches: [main]
jobs:
deploy-database:
runs-on: ubuntu-latest
steps:
- name: Deploy database migrations
run: ./scripts/deploy-db.sh
deploy-user-service:
needs: deploy-database
runs-on: ubuntu-latest
steps:
- name: Deploy user-service
run: ./scripts/deploy-user-service.sh
deploy-order-service:
needs: [deploy-database, deploy-user-service]
runs-on: ubuntu-latest
steps:
- name: Deploy order-service
run: ./scripts/deploy-order-service.sh
deploy-notification-service:
needs: deploy-order-service
runs-on: ubuntu-latest
steps:
- name: Deploy notification-service
run: ./scripts/deploy-notification-service.sh
deploy-api-gateway:
needs: [deploy-user-service, deploy-order-service, deploy-notification-service]
runs-on: ubuntu-latest
steps:
- name: Deploy api-gateway
run: ./scripts/deploy-api-gateway.shMAJOR.MINOR.PATCH
│ │ │
│ │ └─ обратно совместимые исправления
│ └─────── обратно совместимые новые функции
└───────────── breaking changes
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install semantic-release
run: npm install -g semantic-release
- name: Run semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"@semantic-release/github"
]
}feat: добавить новую функцию # MINOR
fix: исправить ошибку # PATCH
BREAKING CHANGE: изменить API # MAJOR
# kubernetes/kong-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-gateway
annotations:
konghq.com/plugins: rate-limiting
konghq.com/strip-path: "true"
spec:
rules:
- host: api.example.com
http:
paths:
- path: /users
pathType: Prefix
backend:
service:
name: user-service
port: 80
- path: /orders
pathType: Prefix
backend:
service:
name: order-service
port: 80
- path: /notifications
pathType: Prefix
backend:
service:
name: notification-service
port: 80# kubernetes/kong-plugin.yaml
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: rate-limiting
config:
minute: 100
policy: local
plugin: rate-limiting# .github/workflows/deploy-gateway.yml
name: Deploy API Gateway
on:
push:
branches: [main]
paths:
- 'services/api-gateway/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push image
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/myorg/api-gateway:${{ github.sha }}
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/api-gateway \
api-gateway=ghcr.io/myorg/api-gateway:${{ github.sha }} \
-n production
- name: Wait for rollout
run: |
kubectl rollout status deployment/api-gateway -n production
- name: Update Kong routes
run: |
kubectl apply -f kubernetes/kong-routes.yaml# .github/workflows/setup-istio.yml
name: Setup Istio
on:
workflow_dispatch:
jobs:
install:
runs-on: ubuntu-latest
steps:
- name: Install Istio
run: |
istioctl install --set profile=default -y
- name: Enable sidecar injection
run: |
kubectl label namespace production istio-injection=enabled# kubernetes/virtual-service.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: user-service
spec:
hosts:
- user-service
http:
- route:
- destination:
host: user-service
subset: stable
weight: 90
- destination:
host: user-service
subset: canary
weight: 10# kubernetes/destination-rule.yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: user-service
spec:
host: user-service
subsets:
- name: stable
labels:
version: stable
- name: canary
labels:
version: canary# .github/workflows/canary-deploy.yml
name: Canary Deploy
on:
push:
branches: [main]
paths:
- 'services/user-service/**'
jobs:
canary:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Build and push canary image
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/myorg/user-service:canary-${{ github.sha }}
- name: Deploy canary version
run: |
kubectl apply -f kubernetes/user-service-canary.yaml
kubectl set image deployment/user-service-canary \
user-service=ghcr.io/myorg/user-service:canary-${{ github.sha }} \
-n production
- name: Configure Istio traffic split
run: |
kubectl apply -f kubernetes/virtual-service-canary.yaml
- name: Wait for canary pods
run: |
kubectl wait --for=condition=available deployment/user-service-canary -n production --timeout=300s
- name: Run smoke tests
run: |
./scripts/smoke-tests.sh --canary
- name: Monitor metrics (5 minutes)
run: |
./scripts/monitor-canary.sh --duration=300
- name: Promote to stable or rollback
if: always()
run: |
if [ "${{ job.status }}" == "success" ]; then
./scripts/promote-canary.sh
else
./scripts/rollback-canary.sh
fi#!/bin/bash
# scripts/monitor-canary.sh
DURATION=${1:-300}
ERROR_THRESHOLD=1
LATENCY_THRESHOLD=500
echo "Monitoring canary for $DURATION seconds..."
START_TIME=$(date +%s)
END_TIME=$((START_TIME + DURATION))
while [ $(date +%s) -lt $END_TIME ]; do
ERROR_RATE=$(curl -s http://prometheus/api/v1/query?query=rate(http_requests_total{status="500",service="user-service-canary"}[1m]) | jq '.data.result[0].value[1]')
LATENCY=$(curl -s http://prometheus/api/v1/query?query=histogram_quantile(0.95,rate(http_request_duration_seconds_bucket{service="user-service-canary"}[1m])) | jq '.data.result[0].value[1]')
echo "Error rate: $ERROR_RATE%, Latency: $LATENCY ms"
if (( $(echo "$ERROR_RATE > $ERROR_THRESHOLD" | bc -l) )); then
echo "Error rate exceeded threshold!"
exit 1
fi
if (( $(echo "$LATENCY > $LATENCY_THRESHOLD" | bc -l) )); then
echo "Latency exceeded threshold!"
exit 1
fi
sleep 10
done
echo "Canary monitoring passed!"# kubernetes/configmap-staging.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: staging
data:
API_URL: "https://staging-api.example.com"
LOG_LEVEL: "debug"
ENABLE_METRICS: "true"# kubernetes/configmap-production.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: production
data:
API_URL: "https://api.example.com"
LOG_LEVEL: "info"
ENABLE_METRICS: "true"# kubernetes/external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: app-secrets
data:
- secretKey: DATABASE_URL
remoteRef:
key: myapp/production/database
property: url
- secretKey: API_KEY
remoteRef:
key: myapp/production/api
property: key# helm/values-staging.yaml
replicaCount: 1
resources:
limits:
cpu: 200m
memory: 256Mi
environment: staging
logLevel: debug# helm/values-production.yaml
replicaCount: 3
resources:
limits:
cpu: 500m
memory: 512Mi
environment: production
logLevel: info
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10# kubernetes/servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: user-service
namespace: production
spec:
selector:
matchLabels:
app: user-service
endpoints:
- port: http
path: /metrics
interval: 30s# kubernetes/grafana-dashboard.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboard
namespace: monitoring
data:
microservices.json: |
{
"dashboard": {
"title": "Microservices Overview",
"panels": [
{
"title": "Request Rate",
"targets": [
{
"expr": "rate(http_requests_total[5m])"
}
]
},
{
"title": "Error Rate",
"targets": [
{
"expr": "rate(http_requests_total{status=~\"5..\"}[5m])"
}
]
}
]
}
}# kubernetes/jaeger.yaml
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
name: production
spec:
strategy: production
collector:
options:
log-level: info
query:
options:
log-level: info
storage:
type: elasticsearch
options:
es:
server-urls: http://elasticsearch:9200CI/CD для микросервисов включает:
Архитектура репозиториев
Reusable workflows
Оркестрация деплоя
API Gateway
Service Mesh (Istio)
Версионирование
Мониторинг
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.