← maurobernal.com.ar

Etiqueta: github-actions

  • Docker en mi pipeline de CI/CD: builds reproducibles sin sorpresas

    Antes de Docker, nuestro pipeline de CI era una caja negra. «Funcionó en mi máquina pero el build del CI falló». Después de adoptar Docker en el pipeline, los builds fallidos por diferencias de entorno desaparecieron. Si buildea en CI, buildea en producción. Siempre.

    Por qué Docker transforma el CI/CD

    El problema fundamental del CI tradicional: el servidor de CI tiene su propio entorno — versiones de runtime, librerías del sistema, variables — que puede diferir del de desarrollo y producción. Docker elimina ese problema: el build ocurre dentro de un contenedor con el entorno exacto que vos definís en el Dockerfile.

    GitHub Actions: build, test y push a registry

    # .github/workflows/ci-cd.yml
    name: Build, Test and Deploy
    
    on:
      push:
        branches: [ main, develop ]
      pull_request:
        branches: [ main ]
    
    env:
      REGISTRY: ghcr.io
      IMAGE_NAME: ${{ github.repository }}
    
    jobs:
      build-and-test:
        runs-on: ubuntu-latest
        steps:
          - name: Checkout
            uses: actions/checkout@v4
    
          - name: Set up Docker Buildx
            uses: docker/setup-buildx-action@v3
    
          - name: Login to Container Registry
            if: github.event_name != 'pull_request'
            uses: docker/login-action@v3
            with:
              registry: ${{ env.REGISTRY }}
              username: ${{ github.actor }}
              password: ${{ secrets.GITHUB_TOKEN }}
    
          - name: Extract metadata
            id: meta
            uses: docker/metadata-action@v5
            with:
              images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
              tags: |
                type=ref,event=branch
                type=semver,pattern={{version}}
                type=sha,prefix=sha-
    
          - name: Build and push
            uses: docker/build-push-action@v5
            with:
              context: .
              push: ${{ github.event_name != 'pull_request' }}
              tags: ${{ steps.meta.outputs.tags }}
              labels: ${{ steps.meta.outputs.labels }}
              cache-from: type=gha    # cache de GitHub Actions
              cache-to: type=gha,mode=max
    
      deploy:
        needs: build-and-test
        runs-on: ubuntu-latest
        if: github.ref == 'refs/heads/main'
        steps:
          - name: Deploy to production
            uses: appleboy/ssh-action@v1
            with:
              host: ${{ secrets.PROD_HOST }}
              username: ${{ secrets.PROD_USER }}
              key: ${{ secrets.PROD_SSH_KEY }}
              script: |
                cd /apps/miapp
                docker compose pull
                docker compose up -d --no-build
                docker image prune -f

    GitLab CI: Docker-in-Docker

    # .gitlab-ci.yml
    stages:
      - build
      - test
      - push
      - deploy
    
    variables:
      DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
      DOCKER_TLS_CERTDIR: "/certs"
    
    # Build de la imagen
    build:
      stage: build
      image: docker:24
      services:
        - docker:24-dind
      before_script:
        - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
      script:
        - docker build --cache-from $CI_REGISTRY_IMAGE:latest -t $DOCKER_IMAGE .
        - docker push $DOCKER_IMAGE
    
    # Tests dentro de contenedor
    test:
      stage: test
      image: $DOCKER_IMAGE
      script:
        - dotnet test --no-build --verbosity normal
    
    # Tag como latest en main
    push-latest:
      stage: push
      image: docker:24
      services:
        - docker:24-dind
      only:
        - main
      script:
        - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
        - docker pull $DOCKER_IMAGE
        - docker tag $DOCKER_IMAGE $CI_REGISTRY_IMAGE:latest
        - docker push $CI_REGISTRY_IMAGE:latest
    
    # Deploy al servidor
    deploy-prod:
      stage: deploy
      only:
        - main
      script:
        - ssh -i $SSH_KEY deploy@$PROD_HOST
            "cd /apps/miapp &&
             docker compose pull &&
             docker compose up -d --no-build"

    Optimizar el tiempo de build con cache

    El mayor costo de tiempo en un pipeline Docker es el build. Con la caché bien configurada, un build que tardaba 8 minutos puede bajar a 40 segundos si solo cambió el código de la aplicación.

    # La clave: copiar el .csproj ANTES que el código
    # Así el restore se cachea hasta que cambien las dependencias
    
    FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
    WORKDIR /src
    
    # Esta capa se cachea hasta que cambie el .csproj
    COPY ["MiApi.csproj", "."]
    RUN dotnet restore
    
    # Esta capa se invalida con cada commit
    COPY . .
    RUN dotnet publish -c Release -o /app/publish

    El resultado en números

    Antes de implementar este pipeline: builds inconsistentes, ~20 minutos por build, deploys manuales con SSH y comandos a mano. Después: builds reproducibles, 2-4 minutos con cache (8 min cold), deploy automático en cada push a main, rollback trivial con docker compose up -d apuntando a la imagen anterior. Menos stress, más confianza en cada deploy.


    Artículo anterior: Entornos consistentes | Serie Docker Completo | Próximo: Microservicios →


    Artículo anterior: Cómo uso Docker para tener el mismo entorno en dev, test y producción | Serie Docker Completo | Próximo: Microservicios con Docker: lo que aprendí armando mi primera arquitectura →

Tags

tsql (27)mssql (26)sql (20)devops (20)dotnet (18)docker (15)performance (14)contenedores (11)dotnet10 (10)linux (9)csharp (8)microservicios (7)angular (7)angular21 (7)sql server (6)issabel (6)docker-compose (6)typescript (6)mysql (5).NET (5)