490 lines
15 KiB
Markdown
490 lines
15 KiB
Markdown
# 11 — CI/CD avec GitHub Actions
|
|
|
|
Pipeline complet : commit → build Docker → push GHCR → déploiement k3s → vérification.
|
|
|
|
---
|
|
|
|
## Architecture du pipeline
|
|
|
|
```
|
|
Push sur main
|
|
│
|
|
├── Job: test
|
|
│ ├── npm run backend:lint
|
|
│ ├── npm run backend:test
|
|
│ └── npm run frontend:lint
|
|
│
|
|
├── Job: build (si tests OK)
|
|
│ ├── docker buildx build backend → ghcr.io/<org>/xpeditis-backend:sha + :latest
|
|
│ └── docker buildx build frontend → ghcr.io/<org>/xpeditis-frontend:sha + :latest
|
|
│
|
|
└── Job: deploy (si build OK)
|
|
├── kubectl set image deployment/xpeditis-backend ...
|
|
├── kubectl set image deployment/xpeditis-frontend ...
|
|
├── kubectl rollout status ...
|
|
└── Health check final
|
|
```
|
|
|
|
---
|
|
|
|
## Secrets GitHub à configurer
|
|
|
|
Dans votre repo GitHub → Settings → Secrets and variables → Actions → New repository secret :
|
|
|
|
| Secret | Valeur | Usage |
|
|
|---|---|---|
|
|
| `HETZNER_KUBECONFIG` | Contenu de `~/.kube/kubeconfig-xpeditis-prod` (base64) | Accès kubectl |
|
|
| `GHCR_TOKEN` | Personal Access Token GitHub (scope: `write:packages`) | Push images |
|
|
| `SLACK_WEBHOOK_URL` | URL webhook Slack (optionnel) | Notifications |
|
|
|
|
```bash
|
|
# Encoder le kubeconfig en base64 pour GitHub Secrets
|
|
cat ~/.kube/kubeconfig-xpeditis-prod | base64 -w 0
|
|
# Copier le résultat dans HETZNER_KUBECONFIG
|
|
|
|
# Créer le Personal Access Token GitHub
|
|
# https://github.com/settings/tokens/new
|
|
# Scopes : write:packages, read:packages, delete:packages
|
|
```
|
|
|
|
---
|
|
|
|
## Workflow principal — `.github/workflows/deploy.yml`
|
|
|
|
```yaml
|
|
# .github/workflows/deploy.yml
|
|
name: Build & Deploy to Hetzner
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
pull_request:
|
|
branches:
|
|
- main
|
|
|
|
env:
|
|
REGISTRY: ghcr.io
|
|
IMAGE_BACKEND: ghcr.io/${{ github.repository_owner }}/xpeditis-backend
|
|
IMAGE_FRONTEND: ghcr.io/${{ github.repository_owner }}/xpeditis-frontend
|
|
|
|
jobs:
|
|
# ============================================================
|
|
# JOB 1 : Tests & Lint
|
|
# ============================================================
|
|
test:
|
|
name: Tests & Lint
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "20"
|
|
cache: "npm"
|
|
|
|
- name: Install dependencies
|
|
run: npm run install:all
|
|
|
|
- name: Lint backend
|
|
run: npm run backend:lint
|
|
|
|
- name: Lint frontend
|
|
run: npm run frontend:lint
|
|
|
|
- name: Test backend (unit)
|
|
run: npm run backend:test -- --passWithNoTests
|
|
|
|
- name: TypeScript check frontend
|
|
run: |
|
|
cd apps/frontend
|
|
npm run type-check
|
|
|
|
# ============================================================
|
|
# JOB 2 : Build & Push Docker Images
|
|
# ============================================================
|
|
build:
|
|
name: Build Docker Images
|
|
runs-on: ubuntu-latest
|
|
needs: test
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
|
|
outputs:
|
|
backend_tag: ${{ steps.meta-backend.outputs.version }}
|
|
frontend_tag: ${{ steps.meta-frontend.outputs.version }}
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Login to GitHub Container Registry
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
# ── Backend ──
|
|
- name: Extract metadata (backend)
|
|
id: meta-backend
|
|
uses: docker/metadata-action@v5
|
|
with:
|
|
images: ${{ env.IMAGE_BACKEND }}
|
|
tags: |
|
|
type=sha,prefix=sha-,format=short
|
|
type=raw,value=latest,enable={{is_default_branch}}
|
|
type=raw,value={{date 'YYYY-MM-DD'}},enable={{is_default_branch}}
|
|
|
|
- name: Build & Push backend
|
|
uses: docker/build-push-action@v5
|
|
with:
|
|
context: .
|
|
file: apps/backend/Dockerfile
|
|
push: true
|
|
tags: ${{ steps.meta-backend.outputs.tags }}
|
|
labels: ${{ steps.meta-backend.outputs.labels }}
|
|
cache-from: type=gha,scope=backend
|
|
cache-to: type=gha,mode=max,scope=backend
|
|
platforms: linux/amd64 # Changer en linux/amd64,linux/arm64 si vous utilisez des CAX
|
|
|
|
# ── Frontend ──
|
|
- name: Extract metadata (frontend)
|
|
id: meta-frontend
|
|
uses: docker/metadata-action@v5
|
|
with:
|
|
images: ${{ env.IMAGE_FRONTEND }}
|
|
tags: |
|
|
type=sha,prefix=sha-,format=short
|
|
type=raw,value=latest,enable={{is_default_branch}}
|
|
type=raw,value={{date 'YYYY-MM-DD'}},enable={{is_default_branch}}
|
|
|
|
- name: Build & Push frontend
|
|
uses: docker/build-push-action@v5
|
|
with:
|
|
context: .
|
|
file: apps/frontend/Dockerfile
|
|
push: true
|
|
tags: ${{ steps.meta-frontend.outputs.tags }}
|
|
labels: ${{ steps.meta-frontend.outputs.labels }}
|
|
cache-from: type=gha,scope=frontend
|
|
cache-to: type=gha,mode=max,scope=frontend
|
|
platforms: linux/amd64
|
|
build-args: |
|
|
NEXT_PUBLIC_API_URL=https://api.xpeditis.com
|
|
NEXT_PUBLIC_APP_URL=https://app.xpeditis.com
|
|
NEXT_PUBLIC_API_PREFIX=api/v1
|
|
|
|
# ============================================================
|
|
# JOB 3 : Deploy vers k3s Hetzner
|
|
# ============================================================
|
|
deploy:
|
|
name: Deploy to Hetzner k3s
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
environment:
|
|
name: production
|
|
url: https://app.xpeditis.com
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Configure kubectl
|
|
run: |
|
|
mkdir -p ~/.kube
|
|
echo "${{ secrets.HETZNER_KUBECONFIG }}" | base64 -d > ~/.kube/config
|
|
chmod 600 ~/.kube/config
|
|
kubectl cluster-info
|
|
|
|
- name: Deploy Backend
|
|
run: |
|
|
BACKEND_TAG="sha-$(echo ${{ github.sha }} | cut -c1-7)"
|
|
|
|
kubectl set image deployment/xpeditis-backend \
|
|
backend=${{ env.IMAGE_BACKEND }}:${BACKEND_TAG} \
|
|
-n xpeditis-prod
|
|
|
|
kubectl rollout status deployment/xpeditis-backend \
|
|
-n xpeditis-prod \
|
|
--timeout=300s
|
|
|
|
echo "✅ Backend deployed: ${BACKEND_TAG}"
|
|
|
|
- name: Deploy Frontend
|
|
run: |
|
|
FRONTEND_TAG="sha-$(echo ${{ github.sha }} | cut -c1-7)"
|
|
|
|
kubectl set image deployment/xpeditis-frontend \
|
|
frontend=${{ env.IMAGE_FRONTEND }}:${FRONTEND_TAG} \
|
|
-n xpeditis-prod
|
|
|
|
kubectl rollout status deployment/xpeditis-frontend \
|
|
-n xpeditis-prod \
|
|
--timeout=300s
|
|
|
|
echo "✅ Frontend deployed: ${FRONTEND_TAG}"
|
|
|
|
- name: Health Check
|
|
run: |
|
|
sleep 15 # Laisser le temps au LB de propager
|
|
|
|
# Test API backend
|
|
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" \
|
|
https://api.xpeditis.com/api/v1/health)
|
|
if [ "$STATUS" != "200" ]; then
|
|
echo "❌ Backend health check failed (HTTP $STATUS)"
|
|
exit 1
|
|
fi
|
|
echo "✅ Backend healthy (HTTP $STATUS)"
|
|
|
|
# Test frontend
|
|
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" \
|
|
https://app.xpeditis.com/)
|
|
if [ "$STATUS" != "200" ]; then
|
|
echo "❌ Frontend health check failed (HTTP $STATUS)"
|
|
exit 1
|
|
fi
|
|
echo "✅ Frontend healthy (HTTP $STATUS)"
|
|
|
|
- name: Notify Slack (success)
|
|
if: success()
|
|
run: |
|
|
if [ -n "${{ secrets.SLACK_WEBHOOK_URL }}" ]; then
|
|
curl -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \
|
|
-H 'Content-type: application/json' \
|
|
--data '{
|
|
"text": "✅ Xpeditis déployé en production",
|
|
"attachments": [{
|
|
"color": "good",
|
|
"fields": [
|
|
{"title": "Commit", "value": "${{ github.sha }}", "short": true},
|
|
{"title": "Auteur", "value": "${{ github.actor }}", "short": true},
|
|
{"title": "Message", "value": "${{ github.event.head_commit.message }}", "short": false}
|
|
]
|
|
}]
|
|
}'
|
|
fi
|
|
|
|
- name: Notify Slack (failure)
|
|
if: failure()
|
|
run: |
|
|
if [ -n "${{ secrets.SLACK_WEBHOOK_URL }}" ]; then
|
|
curl -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \
|
|
-H 'Content-type: application/json' \
|
|
--data '{
|
|
"text": "❌ Échec du déploiement Xpeditis",
|
|
"attachments": [{
|
|
"color": "danger",
|
|
"fields": [
|
|
{"title": "Commit", "value": "${{ github.sha }}", "short": true},
|
|
{"title": "Job", "value": "${{ github.workflow }}", "short": true}
|
|
]
|
|
}]
|
|
}'
|
|
fi
|
|
|
|
- name: Rollback on failure
|
|
if: failure()
|
|
run: |
|
|
echo "⏮️ Rollback en cours..."
|
|
kubectl rollout undo deployment/xpeditis-backend -n xpeditis-prod
|
|
kubectl rollout undo deployment/xpeditis-frontend -n xpeditis-prod
|
|
kubectl rollout status deployment/xpeditis-backend -n xpeditis-prod --timeout=120s
|
|
echo "✅ Rollback terminé"
|
|
```
|
|
|
|
---
|
|
|
|
## Workflow de staging (PR preview) — `.github/workflows/staging.yml`
|
|
|
|
```yaml
|
|
# .github/workflows/staging.yml
|
|
name: Deploy to Staging
|
|
|
|
on:
|
|
pull_request:
|
|
branches:
|
|
- main
|
|
|
|
jobs:
|
|
build-staging:
|
|
name: Build Staging
|
|
runs-on: ubuntu-latest
|
|
if: github.event_name == 'pull_request'
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: docker/setup-buildx-action@v3
|
|
- uses: docker/login-action@v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Build & Push (staging tag)
|
|
uses: docker/build-push-action@v5
|
|
with:
|
|
context: .
|
|
file: apps/backend/Dockerfile
|
|
push: true
|
|
tags: ghcr.io/${{ github.repository_owner }}/xpeditis-backend:pr-${{ github.event.pull_request.number }}
|
|
build-args: NODE_ENV=staging
|
|
|
|
- name: Comment PR
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
github.rest.issues.createComment({
|
|
issue_number: context.issue.number,
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
body: '🐳 Image Docker staging buildée : `pr-${{ github.event.pull_request.number }}`'
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
## Mise à jour des manifests Kubernetes
|
|
|
|
Alternativement, vous pouvez mettre à jour les fichiers YAML dans Git et les appliquer :
|
|
|
|
```bash
|
|
# Dans le workflow CI, mettre à jour le tag d'image dans les manifests
|
|
- name: Update image in manifests
|
|
run: |
|
|
IMAGE_TAG="sha-$(echo ${{ github.sha }} | cut -c1-7)"
|
|
|
|
# Mettre à jour les fichiers YAML
|
|
sed -i "s|image: ghcr.io/.*/xpeditis-backend:.*|image: ${{ env.IMAGE_BACKEND }}:${IMAGE_TAG}|g" \
|
|
k8s/03-backend-deployment.yaml
|
|
|
|
sed -i "s|image: ghcr.io/.*/xpeditis-frontend:.*|image: ${{ env.IMAGE_FRONTEND }}:${IMAGE_TAG}|g" \
|
|
k8s/05-frontend-deployment.yaml
|
|
|
|
# Committer les changements (GitOps)
|
|
git config user.name "GitHub Actions"
|
|
git config user.email "actions@github.com"
|
|
git add k8s/
|
|
git commit -m "chore: update image tags to ${IMAGE_TAG} [skip ci]"
|
|
git push
|
|
```
|
|
|
|
---
|
|
|
|
## Dockerfile final — Backend
|
|
|
|
```dockerfile
|
|
# apps/backend/Dockerfile
|
|
FROM node:20-alpine AS deps
|
|
RUN apk add --no-cache python3 make g++
|
|
WORKDIR /app
|
|
COPY package*.json ./
|
|
COPY apps/backend/package*.json apps/backend/
|
|
RUN npm ci --workspace=apps/backend
|
|
|
|
FROM node:20-alpine AS builder
|
|
WORKDIR /app
|
|
COPY --from=deps /app/node_modules ./node_modules
|
|
COPY --from=deps /app/apps/backend/node_modules ./apps/backend/node_modules
|
|
COPY . .
|
|
RUN cd apps/backend && npm run build
|
|
|
|
FROM node:20-alpine AS runner
|
|
RUN apk add --no-cache dumb-init
|
|
RUN addgroup -g 1001 -S nodejs && adduser -S nestjs -u 1001
|
|
WORKDIR /app
|
|
COPY --from=builder --chown=nestjs:nodejs /app/apps/backend/dist ./dist
|
|
COPY --from=builder --chown=nestjs:nodejs /app/apps/backend/node_modules ./node_modules
|
|
COPY --from=builder --chown=nestjs:nodejs /app/apps/backend/package.json ./
|
|
USER nestjs
|
|
EXPOSE 4000
|
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
|
CMD wget -qO- http://localhost:4000/api/v1/health || exit 1
|
|
ENTRYPOINT ["dumb-init", "--"]
|
|
CMD ["node", "dist/main.js"]
|
|
```
|
|
|
|
## Dockerfile final — Frontend
|
|
|
|
```dockerfile
|
|
# apps/frontend/Dockerfile
|
|
FROM node:20-alpine AS deps
|
|
WORKDIR /app
|
|
COPY package*.json ./
|
|
COPY apps/frontend/package*.json apps/frontend/
|
|
RUN npm ci --workspace=apps/frontend
|
|
|
|
FROM node:20-alpine AS builder
|
|
WORKDIR /app
|
|
COPY --from=deps /app/node_modules ./node_modules
|
|
COPY --from=deps /app/apps/frontend/node_modules ./apps/frontend/node_modules
|
|
COPY . .
|
|
ARG NEXT_PUBLIC_API_URL
|
|
ARG NEXT_PUBLIC_APP_URL
|
|
ARG NEXT_PUBLIC_API_PREFIX=api/v1
|
|
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL \
|
|
NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL \
|
|
NEXT_PUBLIC_API_PREFIX=$NEXT_PUBLIC_API_PREFIX \
|
|
NEXT_TELEMETRY_DISABLED=1
|
|
RUN cd apps/frontend && npm run build
|
|
|
|
FROM node:20-alpine AS runner
|
|
RUN apk add --no-cache dumb-init
|
|
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
|
|
WORKDIR /app
|
|
COPY --from=builder --chown=nextjs:nodejs /app/apps/frontend/.next/standalone ./
|
|
COPY --from=builder --chown=nextjs:nodejs /app/apps/frontend/.next/static ./.next/static
|
|
COPY --from=builder --chown=nextjs:nodejs /app/apps/frontend/public ./public
|
|
USER nextjs
|
|
EXPOSE 3000
|
|
ENV NODE_ENV=production PORT=3000 HOSTNAME="0.0.0.0" NEXT_TELEMETRY_DISABLED=1
|
|
ENTRYPOINT ["dumb-init", "--"]
|
|
CMD ["node", "server.js"]
|
|
```
|
|
|
|
---
|
|
|
|
## Test du pipeline en local
|
|
|
|
```bash
|
|
# Simuler le build Docker localement
|
|
docker build \
|
|
-f apps/backend/Dockerfile \
|
|
-t xpeditis-backend:local \
|
|
.
|
|
|
|
docker build \
|
|
-f apps/frontend/Dockerfile \
|
|
-t xpeditis-frontend:local \
|
|
--build-arg NEXT_PUBLIC_API_URL=http://localhost:4000 \
|
|
--build-arg NEXT_PUBLIC_APP_URL=http://localhost:3000 \
|
|
.
|
|
|
|
# Tester l'image backend
|
|
docker run --rm -p 4000:4000 \
|
|
-e NODE_ENV=production \
|
|
-e DATABASE_HOST=<votre_db_host> \
|
|
-e DATABASE_USER=xpeditis \
|
|
-e DATABASE_PASSWORD=<password> \
|
|
-e DATABASE_NAME=xpeditis \
|
|
-e REDIS_HOST=<votre_redis_host> \
|
|
-e REDIS_PASSWORD=<password> \
|
|
-e JWT_SECRET=test-secret \
|
|
-e SMTP_HOST=localhost \
|
|
-e SMTP_PORT=25 \
|
|
-e SMTP_USER=test \
|
|
-e SMTP_PASS=test \
|
|
xpeditis-backend:local
|
|
|
|
curl http://localhost:4000/api/v1/health
|
|
```
|