Cloud Deployments

Learn how to deploy to AWS, GCP, and Azure from GitLab CI/CD. From OIDC authentication to multi-cloud strategies, build secure cloud deployment pipelines that scale across providers.

AWS GCP Azure GitLab CI/CD
Why Cloud Deployments from GitLab?

Cloud providers — AWS, GCP, and Azure — offer hundreds of services for compute, storage, databases, networking, and more. Deploying to them from GitLab CI/CD is a common pattern: the pipeline builds and tests the application, then authenticates to the cloud provider and runs deployment commands (e.g., aws s3 sync, gcloud run deploy, az webapp deploy). This gives you a single pipeline for the entire software delivery process, from commit to production, regardless of which cloud the application runs on.

The challenge is authentication. Historically, cloud deployments used long-lived credentials — AWS access keys, GCP service account keys, Azure client secrets — stored as CI/CD variables. These credentials are a security risk: if leaked, they provide permanent access to your cloud account. The modern approach is OIDC (OpenID Connect), where GitLab generates a short-lived token for each job, and the cloud provider exchanges it for temporary credentials. No static secrets are stored in GitLab .

Multi-cloud strategies add complexity. A team might run its primary application on AWS, its data pipeline on GCP, and its enterprise integration on Azure. Or it might use multiple clouds for resilience. GitLab supports all three providers with the same OIDC pattern, allowing a single pipeline to deploy to multiple clouds. The key is to scope each deployment to the specific cloud service and environment it targets, and to use separate roles or identities for each provider.

Core Principle: Use OIDC for authentication whenever possible. It eliminates long-lived credentials, provides short-lived tokens, and enforces scoping by project, branch, and environment.

OIDC Auth

GitLab generates a JWT per job. Cloud provider verifies it and returns temporary credentials. No static secrets.

AWS

STS AssumeRoleWithWebIdentity. Trust policy scoped to GitLab project/branch/environment.

GCP

Workload Identity Federation. Trust policy scoped to GitLab OIDC issuer and claims.

Azure

Federated credentials on app registration. Trust policy scoped to GitLab issuer.

Multi-Cloud

Deploy to multiple providers from one pipeline. Each provider has its own trust and role.

Scoping

Limit trust policies to specific projects, branches, and environments. Least privilege at identity level.
AWS Deployments

AWS is the most common cloud provider for GitLab deployments. The typical pattern is to use OIDC to assume an IAM role, then use the AWS CLI or Terraform to deploy resources. For static websites, aws s3 sync uploads the built assets to an S3 bucket and optionally invalidates CloudFront. For containerized applications, the pipeline builds an image, pushes it to ECR, and updates an ECS service or EKS deployment. For serverless, the pipeline runs aws lambda update-function-code or uses the Serverless Framework .

The OIDC setup involves creating an IAM OIDC identity provider for GitLab and an IAM role that trusts it. The trust policy specifies which GitLab projects, branches, and environments can assume the role, using the sub claim (e.g., project_path:my-group/my-project:ref_type:branch:ref:main). The pipeline generates the JWT, writes it to a file, and the AWS SDK handles the rest — no explicit sts assume-role command is needed in most cases .

For production deployments, AWS recommends separating the plan and apply roles (or at least the permissions). A plan role might have read-only access to resources; an apply role has write access. A deploy job might also have different permissions than a build job. This separation limits the blast radius if a pipeline is compromised. Combined with GitLab's protected environments, this creates a strong defense in depth.

# AWS deployment with OIDC deploy_aws_s3: stage: deploy image: amazon/aws-cli:latest id_tokens: AWS_OIDC_TOKEN: aud: https://gitlab.com variables: AWS_ROLE_ARN: "arn:aws:iam::123456789012:role/gitlab-deploy-role" AWS_WEB_IDENTITY_TOKEN_FILE: "/tmp/web_identity_token" AWS_REGION: us-east-1 before_script: - echo "$AWS_OIDC_TOKEN" > "$AWS_WEB_IDENTITY_TOKEN_FILE" script: - aws sts get-caller-identity - aws s3 sync dist/ s3://my-bucket/ --delete - aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_ID --paths "/*" environment: name: production url: https://example.com rules: - if: $CI_COMMIT_BRANCH == "main" # AWS IAM trust policy (configure in AWS) # { # "Version": "2012-10-17", # "Statement": [{ # "Effect": "Allow", # "Principal": { # "Federated": "arn:aws:iam::123456789012:oidc-provider/gitlab.com" # }, # "Action": "sts:AssumeRoleWithWebIdentity", # "Condition": { # "StringEquals": { # "gitlab.com:sub": "project_path:my-group/my-project:ref_type:branch:ref:main" # } # } # }] # } # Build and push to ECR build_and_push: stage: build image: docker:24 services: - docker:24-dind id_tokens: AWS_OIDC_TOKEN: aud: https://gitlab.com variables: AWS_ROLE_ARN: "arn:aws:iam::123456789012:role/gitlab-build-role" AWS_WEB_IDENTITY_TOKEN_FILE: "/tmp/web_identity_token" AWS_REGION: us-east-1 before_script: - echo "$AWS_OIDC_TOKEN" > "$AWS_WEB_IDENTITY_TOKEN_FILE" - aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin 123456789012.dkr.ecr.$AWS_REGION.amazonaws.com script: - docker build -t 123456789012.dkr.ecr.$AWS_REGION.amazonaws.com/my-app:$CI_COMMIT_SHORT_SHA . - docker push 123456789012.dkr.ecr.$AWS_REGION.amazonaws.com/my-app:$CI_COMMIT_SHORT_SHA # Update ECS service deploy_aws_ecs: stage: deploy image: amazon/aws-cli:latest id_tokens: AWS_OIDC_TOKEN: aud: https://gitlab.com variables: AWS_ROLE_ARN: "arn:aws:iam::123456789012:role/gitlab-deploy-role" AWS_WEB_IDENTITY_TOKEN_FILE: "/tmp/web_identity_token" AWS_REGION: us-east-1 before_script: - echo "$AWS_OIDC_TOKEN" > "$AWS_WEB_IDENTITY_TOKEN_FILE" script: - aws ecs update-service --cluster my-cluster --service my-app --force-new-deployment - aws ecs wait services-stable --cluster my-cluster --services my-app environment: name: production rules: - if: $CI_COMMIT_BRANCH == "main" # Update Lambda function deploy_lambda: stage: deploy image: amazon/aws-cli:latest id_tokens: AWS_OIDC_TOKEN: aud: https://gitlab.com variables: AWS_ROLE_ARN: "arn:aws:iam::123456789012:role/gitlab-lambda-role" AWS_WEB_IDENTITY_TOKEN_FILE: "/tmp/web_identity_token" AWS_REGION: us-east-1 before_script: - echo "$AWS_OIDC_TOKEN" > "$AWS_WEB_IDENTITY_TOKEN_FILE" script: - zip -r function.zip . -x "*.git*" - aws lambda update-function-code --function-name my-function --zip-file fileb://function.zip - aws lambda update-function-configuration --function-name my-function --environment "Variables={VERSION=$CI_COMMIT_SHORT_SHA}"
AWS Deployment Best Practices:
  • Use OIDC instead of access keys
  • Use separate roles for build, plan, and apply
  • Scope trust policies to specific branches and environments
  • Use aws sts get-caller-identity to verify credentials
  • Wait for deployment completion (aws ecs wait, etc.)
GCP Deployments

Google Cloud uses Workload Identity Federation for OIDC. You create a workload identity pool and provider that trusts GitLab's OIDC issuer, then create a service account that the pool can impersonate. The pipeline generates the JWT, exchanges it for a federated token, and uses it to impersonate the service account. The service account has the permissions to deploy the resources .

The deployment commands depend on the service. For Cloud Run, the pipeline builds a container image, pushes it to Artifact Registry, and runs gcloud run deploy. For GKE, it builds an image, pushes it to Artifact Registry, and runs kubectl set image. For Cloud Functions, it runs gcloud functions deploy. For static sites, it runs gsutil rsync or gcloud storage rsync to a Cloud Storage bucket.

The OIDC setup requires a few GCP resources: a workload identity pool, a provider, a service account, and an IAM binding that allows the pool to impersonate the service account. The pipeline uses the google-github-actions/auth action (which works with GitLab too) or manually exchanges the token. The aud in the id_tokens block must match the provider's allowed audiences .

# GCP deployment with Workload Identity Federation deploy_gcp_cloud_run: stage: deploy image: google/cloud-sdk:alpine id_tokens: GCP_OIDC_TOKEN: aud: https://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/gitlab-pool/providers/gitlab-provider variables: GCP_PROJECT_ID: my-project GCP_SERVICE_ACCOUNT: gitlab-deployer@my-project.iam.gserviceaccount.com GCP_WORKLOAD_IDENTITY_PROVIDER: projects/123456789/locations/global/workloadIdentityPools/gitlab-pool/providers/gitlab-provider before_script: - echo "$GCP_OIDC_TOKEN" > /tmp/oidc_token - gcloud auth login --cred-file=<(echo "{ \"type\": \"external_account\", \"audience\": \"$GCP_WORKLOAD_IDENTITY_PROVIDER\", \"subject_token_type\": \"urn:ietf:params:oauth:token-type:jwt\", \"token_url\": \"https://sts.googleapis.com/v1/token\", \"credential_source\": { \"file\": \"/tmp/oidc_token\" }, \"service_account_impersonation_url\": \"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$GCP_SERVICE_ACCOUNT:generateAccessToken\" }") - gcloud config set project $GCP_PROJECT_ID script: - gcloud run deploy my-app --image gcr.io/$GCP_PROJECT_ID/my-app:$CI_COMMIT_SHORT_SHA --region us-central1 --platform managed environment: name: production url: https://my-app-xxx.run.app rules: - if: $CI_COMMIT_BRANCH == "main" # Simpler: use google-github-actions/auth deploy_gcp_simple: stage: deploy image: google/cloud-sdk:alpine id_tokens: GCP_OIDC_TOKEN: aud: https://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/gitlab-pool/providers/gitlab-provider before_script: - echo "$GCP_OIDC_TOKEN" > /tmp/oidc_token - gcloud auth login --cred-file=/tmp/credentials.json script: - gcloud run deploy my-app --image gcr.io/$GCP_PROJECT_ID/my-app:$CI_COMMIT_SHORT_SHA # Push image to Artifact Registry build_and_push_gcp: stage: build image: docker:24 services: - docker:24-dind id_tokens: GCP_OIDC_TOKEN: aud: https://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/gitlab-pool/providers/gitlab-provider before_script: - echo "$GCP_OIDC_TOKEN" > /tmp/oidc_token - gcloud auth login --cred-file=/tmp/credentials.json - gcloud auth configure-docker us-central1-docker.pkg.dev script: - docker build -t us-central1-docker.pkg.dev/$GCP_PROJECT_ID/my-repo/my-app:$CI_COMMIT_SHORT_SHA . - docker push us-central1-docker.pkg.dev/$GCP_PROJECT_ID/my-repo/my-app:$CI_COMMIT_SHORT_SHA # GCP setup commands (run once): # gcloud iam workload-identity-pools create gitlab-pool \ # --location=global # gcloud iam workload-identity-pools providers create-oidc gitlab-provider \ # --location=global \ # --workload-identity-pool=gitlab-pool \ # --issuer-uri=https://gitlab.com \ # --allowed-audiences=https://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/gitlab-pool/providers/gitlab-provider \ # --attribute-mapping="google.subject=assertion.sub,attribute.project_path=assertion.project_path" # gcloud iam service-accounts add-iam-policy-binding gitlab-deployer@my-project.iam.gserviceaccount.com \ # --role=roles/iam.workloadIdentityUser \ # --member="principalSet://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/gitlab-pool/attribute.project_path/my-group/my-project"
GCP OIDC Notes: