Complete CI/CD Guide: AWS & Google Cloud

Complete Setup Guide

Follow these steps to set up your CI/CD pipeline for either AWS or Google Cloud.

AWS Setup Instructions

  1. Create an AWS Account. If you don't have one, sign up at aws.amazon.com.

  2. Create an IAM User. Go to IAM service → Users → Add user. Enable programmatic access and attach these policies: AmazonEC2ContainerRegistryFullAccess, AmazonECS_FullAccess.

  3. Create ECR Repository. Go to ECR → Create repository → Name it (e.g., "my-app").

  4. Create ECS Cluster. Go to ECS → Clusters → Create cluster → Select "Networking only" → Name it (e.g., "my-cluster").

  5. Set Up GitLab Variables. In your GitLab project, go to Settings → CI/CD → Variables and add:

    AWS_ACCESS_KEY_ID – From your IAM user

    AWS_SECRET_ACCESS_KEY – From your IAM user

    AWS_DEFAULT_REGION – e.g., "us-east-1"

    AWS_ACCOUNT_ID – Your 12-digit AWS account ID

    ECR_REPO_NAME – Name of your ECR repository

    ECS_CLUSTER_NAME – Name of your ECS cluster

    ECS_SERVICE_NAME – Will be created automatically

    ECS_TASK_DEFINITION – Will be created automatically

Quick AWS CLI Setup (Alternative)

If you have AWS CLI installed, you can run these commands:

# Configure AWS CLI
aws configure

# Create ECR repository
aws ecr create-repository --repository-name my-app

# Create ECS cluster
aws ecs create-cluster --cluster-name my-cluster

Google Cloud Setup Instructions

  1. Create a Google Cloud Account. If you don't have one, sign up at cloud.google.com.

  2. Create a Project. Go to Console → Select or create project → Name it (e.g., "my-ci-cd-project").

  3. Enable APIs. Enable these APIs: Artifact Registry API, Cloud Run API.

  4. Create Service Account. Go to IAM & Admin → Service Accounts → Create Service Account. Add these roles: Artifact Registry Writer, Cloud Run Admin. Then create and download a JSON key.

  5. Create Artifact Registry. Go to Artifact Registry → Create Repository → Name it (e.g., "my-repo").

  6. Set Up GitLab Variables. In your GitLab project, go to Settings → CI/CD → Variables and add:

    GOOGLE_SERVICE_ACCOUNT_KEY – Base64 encoded content of your JSON key file

    GOOGLE_SERVICE_ACCOUNT_EMAIL – Email of your service account

    GOOGLE_PROJECT_ID – Your Google Cloud project ID

    GOOGLE_DEFAULT_REGION – e.g., "us-central1"

    AR_REPO_NAME – Name of your Artifact Registry repository

Encode Service Account Key

To encode your service account key for GitLab:

# On Linux/Mac
base64 -i key-file.json
# On Windows (PowerShell)
[Convert]::ToBase64String([IO.File]::ReadAllBytes("key-file.json"))

Separate Pipeline Configurations

Choose your cloud provider and use the appropriate pipeline configuration.

AWS GitLab CI/CD Pipeline

Use this pipeline if you're deploying to AWS ECS.

.gitlab-ci.yml (AWS Version)

image: docker:latest

services:
  - docker:dind

stages:
  - build
  - test
  - deploy

variables:
  APP_NAME: "my-webapp"

# AWS TEMPLATE
.aws-job-template: &aws-job-anchor
  variables:
    ECR_URI: ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${ECR_REPO_NAME}
  before_script:
    # Install AWS CLI and dependencies
    - apk add --no-cache python3 py3-pip gettext jq
    - pip3 install awscli
    - export PATH=$PATH:/usr/local/bin
    - aws --version
    
    # Configure AWS credentials
    - aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID"
    - aws configure set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY"
    - aws configure set default.region "$AWS_DEFAULT_REGION"
    
    # Login to AWS ECR
    - echo "Logging into AWS ECR..."
    - aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_URI

# DOCKER BUILD SCRIPTS
.docker-build-scripts: &docker-build-scripts
  - docker build -t ${IMAGE_URI}:$CI_COMMIT_SHORT_SHA -f ./Dockerfile .
  - docker push ${IMAGE_URI}:$CI_COMMIT_SHORT_SHA
  - docker build -t ${IMAGE_URI}:latest -f ./Dockerfile .
  - docker push ${IMAGE_URI}:latest

# -------------------------------
# BUILD STAGE
# -------------------------------

aws-build-job:
  <<: *aws-job-anchor
  stage: build
  script:
    - export IMAGE_URI=$ECR_URI
    - *docker-build-scripts
  only:
    - main
    - develop

# -------------------------------
# TEST STAGE
# -------------------------------

# Python pytest
python-test:
  image: python:3.9
  stage: test
  script:
    - pip install -r requirements.txt
    - pip install pytest pytest-cov
    - python -m pytest tests/ --cov=app --cov-report=xml
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml
  only:
    - main
    - develop
    - merge_requests

# Integration test with Docker Compose
integration-test:
  stage: test
  script:
    - echo "Running app using docker-compose for integration test..."
    - docker-compose up --build -d
    - echo "Waiting for containers to initialize..."
    - sleep 10
    - docker-compose logs
    - echo "Running integration tests..."
    # Add your integration test commands here
  after_script:
    - echo "Cleaning up..."
    - docker-compose down
  only:
    - main
    - develop

# -------------------------------
# DEPLOY STAGE
# -------------------------------

# AWS Deploy
aws-deploy-job:
  <<: *aws-job-anchor
  stage: deploy
  environment: production
  script:
    # Create task definition from template
    - envsubst < aws-task-definition.json > task-definition.json
    - cat task-definition.json
    
    # Register task definition and update service
    - aws ecs register-task-definition --cli-input-json file://task-definition.json
    - aws ecs update-service --cluster $ECS_CLUSTER_NAME --service $ECS_SERVICE_NAME --task-definition $ECS_TASK_DEFINITION
  dependencies:
    - aws-build-job
  only:
    - main

Google Cloud GitLab CI/CD Pipeline

Use this pipeline if you're deploying to Google Cloud Run.

.gitlab-ci.yml (Google Cloud Version)

image: docker:latest

services:
  - docker:dind

stages:
  - build
  - test
  - deploy

variables:
  APP_NAME: "my-webapp"

# GOOGLE TEMPLATE
.google-job-template: &google-job-anchor
  variables:
    AR_URI: $GOOGLE_DEFAULT_REGION-docker.pkg.dev/$GOOGLE_PROJECT_ID/$AR_REPO_NAME/$APP_NAME
  before_script:
    # Install Google Cloud SDK
    - apk add --no-cache tar curl python3 coreutils jq
    - curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-x86_64.tar.gz
    - tar -xf google-cloud-cli-linux-x86_64.tar.gz
    - ./google-cloud-sdk/install.sh --quiet
    - export PATH=$PATH:$(pwd)/google-cloud-sdk/bin
    
    # Authenticate with Google Cloud
    - echo $GOOGLE_SERVICE_ACCOUNT_KEY | base64 -d > /tmp/key.json
    - gcloud auth activate-service-account $GOOGLE_SERVICE_ACCOUNT_EMAIL --key-file=/tmp/key.json
    - gcloud config set project $GOOGLE_PROJECT_ID
    
    # Login to Google Artifact Registry
    - gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin https://$GOOGLE_DEFAULT_REGION-docker.pkg.dev

# DOCKER BUILD SCRIPTS
.docker-build-scripts: &docker-build-scripts
  - docker build -t ${IMAGE_URI}:$CI_COMMIT_SHORT_SHA -f ./Dockerfile .
  - docker push ${IMAGE_URI}:$CI_COMMIT_SHORT_SHA
  - docker build -t ${IMAGE_URI}:latest -f ./Dockerfile .
  - docker push ${IMAGE_URI}:latest

# -------------------------------
# BUILD STAGE
# -------------------------------

google-build-job:
  <<: *google-job-anchor
  stage: build
  script:
    - export IMAGE_URI=$AR_URI
    - *docker-build-scripts
  only:
    - main
    - develop

# -------------------------------
# TEST STAGE
# -------------------------------

# Python pytest
python-test:
  image: python:3.9
  stage: test
  script:
    - pip install -r requirements.txt
    - pip install pytest pytest-cov
    - python -m pytest tests/ --cov=app --cov-report=xml
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml
  only:
    - main
    - develop
    - merge_requests

# Integration test with Docker Compose
integration-test:
  stage: test
  script:
    - echo "Running app using docker-compose for integration test..."
    - docker-compose up --build -d
    - echo "Waiting for containers to initialize..."
    - sleep 10
    - docker-compose logs
    - echo "Running integration tests..."
    # Add your integration test commands here
  after_script:
    - echo "Cleaning up..."
    - docker-compose down
  only:
    - main
    - develop

# -------------------------------
# DEPLOY STAGE
# -------------------------------

# Google Deploy
google-deploy-job:
  <<: *google-job-anchor
  stage: deploy
  environment: production
  script:
    - gcloud run deploy $APP_NAME
      --image=$AR_URI:latest
      --platform=managed
      --region=$GOOGLE_DEFAULT_REGION
      --allow-unauthenticated
      --set-env-vars=ENVIRONMENT=production
  dependencies:
    - google-build-job
  only:
    - main

Project Structure

Your project should have this basic structure:

your-project/
├── .gitlab-ci.yml          # Choose AWS or Google version
├── Dockerfile              # Your application Dockerfile
├── docker-compose.yml      # For integration tests
├── aws-task-definition.json # AWS ECS task definition (for AWS only)
├── requirements.txt        # Python dependencies (if using Python)
├── app/                    # Your application code
│   └── ...
├── tests/                  # Your tests
│   ├── test_app.py        # Python tests
│   └── ...
└── YourBlazorApp.sln      # Blazor solution file (if using Blazor)

Testing Examples

Here are complete testing examples for both Python/pytest and Blazor/bUnit.

Python Testing with pytest

  1. Create requirements.txt

    flask==2.3.3
    pytest==7.4.2
    pytest-cov==4.1.0
    requests==2.31.0
    
  2. Create a Simple Flask App

    # app/__init__.py
    from flask import Flask
    
    app = Flask(__name__)
    
    @app.route('/')
    def hello():
        return {'message': 'Hello, World!'}
    
    @app.route('/health')
    def health():
        return {'status': 'healthy'}
    
    if __name__ == '__main__':
        app.run(host='0.0.0.0', port=5000)
    
  3. Create pytest Tests

    # tests/test_app.py
    import pytest
    from app import app
    
    @pytest.fixture
    def client():
        with app.test_client() as client:
            yield client
    
    def test_hello_route(client):
        response = client.get('/')
        assert response.status_code == 200
        assert response.json == {'message': 'Hello, World!'}
    
    def test_health_route(client):
        response = client.get('/health')
        assert response.status_code == 200
        assert response.json == {'status': 'healthy'}
    
    def test_nonexistent_route(client):
        response = client.get('/nonexistent')
        assert response.status_code == 404
    
  4. Create Dockerfile

    FROM python:3.9-slim
    
    WORKDIR /app
    
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    
    COPY . .
    
    EXPOSE 5000
    
    CMD ["python", "-m", "app"]
    

Blazor Testing with bUnit

  1. Create Blazor Project. Create a new Blazor Server or WebAssembly project.

  2. Add Test Project

    dotnet new blazorserver -o MyBlazorApp
    cd MyBlazorApp
    dotnet new xunit -o MyBlazorApp.Tests
    cd MyBlazorApp.Tests
    dotnet add package bunit
    dotnet add package coverlet.collector
    dotnet add reference ../MyBlazorApp/MyBlazorApp.csproj
    
  3. Create bUnit Tests

    // Tests/HomePageTests.cs
    using Bunit;
    using Xunit;
    using MyBlazorApp.Pages;
    
    namespace MyBlazorApp.Tests
    {
        public class HomePageTests : TestContext
        {
            [Fact]
            public void HomePage_RendersCorrectly()
            {
                // Arrange
                var cut = RenderComponent<Home>();
    
                // Assert
                cut.MarkupMatches("<h1>Hello, world!</h1>");
            }
    
            [Fact]
            public void Counter_StartsAtZero()
            {
                // Arrange
                var cut = RenderComponent<Counter>();
    
                // Assert
                cut.Find("p").MarkupMatches("<p>Current count: 0</p>");
            }
    
            [Fact]
            public void Counter_IncrementsWhenClicked()
            {
                // Arrange
                var cut = RenderComponent<Counter>();
    
                // Act
                cut.Find("button").Click();
    
                // Assert
                cut.Find("p").MarkupMatches("<p>Current count: 1</p>");
            }
        }
    }
    
  4. Create Dockerfile

    FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
    WORKDIR /app
    EXPOSE 80
    
    FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
    WORKDIR /src
    COPY ["MyBlazorApp/MyBlazorApp.csproj", "MyBlazorApp/"]
    RUN dotnet restore "MyBlazorApp/MyBlazorApp.csproj"
    COPY . .
    WORKDIR "/src/MyBlazorApp"
    RUN dotnet build "MyBlazorApp.csproj" -c Release -o /app/build
    
    FROM build AS publish
    RUN dotnet publish "MyBlazorApp.csproj" -c Release -o /app/publish
    
    FROM base AS final
    WORKDIR /app
    COPY --from=publish /app/publish .
    ENTRYPOINT ["dotnet", "MyBlazorApp.dll"]
    

Deployment Configuration

Configure your deployment for either AWS or Google Cloud.

AWS ECS Task Definition

Create this file for AWS ECS deployment.

aws-task-definition.json

{
    "family": "devopsAppTask",
    "containerDefinitions": [
        {
            "name": "webapp",
            "image": "${ECR_URI}:latest",
            "cpu": 0,
            "portMappings": [
                {
                    "name": "webapp-80-tcp",
                    "containerPort": 80,
                    "hostPort": 80,
                    "protocol": "tcp",
                    "appProtocol": "http"
                }
            ],
            "essential": true,
            "environment": [
                { "name": "ENVIRONMENT", "value": "production" },
                { "name": "PORT", "value": "80" }
            ],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/devopsAppTask",
                    "awslogs-create-group": "true",
                    "awslogs-region": "${AWS_DEFAULT_REGION}",
                    "awslogs-stream-prefix": "ecs"
                }
            }
        }
    ],
    "executionRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecsTaskExecutionRole",
    "networkMode": "awsvpc",
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "512",
    "memory": "1024",
    "runtimePlatform": {
        "cpuArchitecture": "X86_64",
        "operatingSystemFamily": "LINUX"
    }
}

Create ECS Task Execution Role

You need to create an ECS task execution role in AWS IAM:

  1. Go to IAM → Roles → Create role.
  2. Select "AWS service" and choose "Elastic Container Service" → "Elastic Container Service Task".
  3. Attach the policy AmazonECSTaskExecutionRolePolicy.
  4. Name the role "ecsTaskExecutionRole".

Google Cloud Run Configuration

For Google Cloud, the deployment is handled directly in the pipeline. Make sure you have:

  • Artifact Registry repository created
  • Cloud Run API enabled
  • Service account with appropriate permissions

Manual Cloud Run Deployment (if needed)

You can manually deploy to Cloud Run using gcloud:

# Build and push image manually
docker build -t us-central1-docker.pkg.dev/my-project/my-repo/my-app:latest .
docker push us-central1-docker.pkg.dev/my-project/my-repo/my-app:latest

# Deploy to Cloud Run
gcloud run deploy my-app \
  --image us-central1-docker.pkg.dev/my-project/my-repo/my-app:latest \
  --platform managed \
  --region us-central1 \
  --allow-unauthenticated

Docker Compose for Integration Tests

Create this file for running integration tests.

docker-compose.yml

version: '3.8'

services:
  webapp:
    build: .
    ports:
      - "5000:5000"
    environment:
      - ENVIRONMENT=test
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  tests:
    build: .
    command: python -m pytest tests/ -v
    depends_on:
      - webapp
    environment:
      - TEST_URL=http://webapp:5000

Next Steps & Troubleshooting

Common Issues & Solutions

Pipeline fails with "no basic auth credentials"

This means Docker can't authenticate with the container registry. Check that your AWS or Google Cloud credentials are correctly set in GitLab CI/CD variables.

ECS service not found

Make sure your ECS cluster and service names match exactly what you set in GitLab variables. The service will be created on first deployment.

Permission denied errors

Check that your IAM user (AWS) or service account (Google) has the necessary permissions. The guide includes the required roles.

Monitoring Your Pipeline

  • Check pipeline status in GitLab → CI/CD → Pipelines
  • View job logs by clicking on each job
  • Set up notifications for failed pipelines

🚀 Ready to Deploy!

You now have a complete CI/CD pipeline that can build, test, and deploy your application to either AWS or Google Cloud. Push your code to GitLab and watch the magic happen!

Enjoyed this? Get the next one by email.

Reconnecting… Connection lost Session expired

Hold on while we get you back online. We couldn't reach the server. Check your connection and try again. Reload the page to pick up where you left off.

An unhandled error has occurred. Reload 🗙