CI/CD on AWS
Automate testing and deployment with CodePipeline, CodeBuild, and GitHub Actions, using OIDC to eliminate long-lived access keys from your CI pipelines.
Introduction
A CI/CD pipeline takes code from a git push to production automatically: run tests, build artifacts, and deploy without manual steps. On AWS you have two main options: the native CodePipeline + CodeBuild stack (AWS-native, tight service integration), or GitHub Actions with OIDC (broader ecosystem, preferred when your repo is on GitHub). The security pattern is the same for both: no long-lived access keys in CI.
AWS-Native CI/CD Pipeline
CodePipeline orchestrates; CodeBuild runs tests and packages; CodeDeploy updates the target
1. CodeBuild and buildspec.yml
CodeBuild spins up a managed build environment, runs your buildspec.yml, and shuts down. You pay only for build minutes. Each run is isolated - no shared state between builds.
# buildspec.yml - CodeBuild build instructions
version: 0.2
phases:
install:
runtime-versions:
python: "3.12" # runtime-versions does not support env variable interpolation
commands:
- pip install -r requirements.txt
- pip install pytest
pre_build:
commands:
- echo "Running tests..."
- pytest tests/ -v --tb=short
build:
commands:
- echo "Packaging Lambda..."
- cd lambda && zip -r ../function.zip . && cd ..
- echo "Deploying CDK stack..."
- npm install -g aws-cdk
- cdk deploy --require-approval never --outputs-file outputs.json
post_build:
commands:
- cat outputs.json
- echo "Deployment complete."
artifacts:
files:
- outputs.json
- function.zip2. GitHub Actions with OIDC (No Long-Lived Keys)
The old approach was to store AWS access keys as GitHub secrets. If those secrets leaked, an attacker had permanent access. OIDC (OpenID Connect) eliminates this risk: GitHub's identity provider proves to AWS who is running the workflow, and AWS issues a short-lived token. No secret to leak, no key to rotate.
# .github/workflows/deploy.yml
name: Deploy to AWS
on:
push:
branches: [main]
permissions:
id-token: write # required for OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
# No long-lived access keys needed!
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest tests/ -v
- name: Deploy CDK stack
run: |
npm install -g aws-cdk
cdk deploy --require-approval never3. Creating the OIDC IAM Role
Before GitHub Actions can assume a role, you must add GitHub's OIDC provider to your AWS account (one-time setup) and create the IAM role with a trust policy that restricts which repo and branch can assume it:
# Create the IAM role for GitHub Actions (OIDC)
# The trust policy allows GitHub's OIDC provider to assume this role,
# but only from the specific repo and branch.
aws iam create-role \
--role-name GitHubActionsRole \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub":
"repo:my-org/my-repo:ref:refs/heads/main"
}
}
}]
}'
# Attach permissions the pipeline needs (e.g., CDK deploy)
aws iam attach-role-policy \
--role-name GitHubActionsRole \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccessScope the condition strictly: the StringLike condition on sub ensures only pushes from my-org/my-repo on the main branch can assume the role. Without this, any GitHub user could potentially trigger the workflow and assume your AWS role. Use AdministratorAccess only for CDK pipelines, create least-privilege policies for production.
4. Deployment Strategies
Rolling
Replace old tasks/instances gradually. At any point, some serve the old version and some serve the new. Zero downtime but mixed versions briefly in production. Default for ECS services.
Blue/Green
Run old (blue) and new (green) environments simultaneously. Shift traffic from blue to green. Instant rollback by pointing traffic back to blue. Costs double the resources during cutover.
Canary
Send a small percentage of traffic (e.g., 5%) to the new version. Monitor metrics. If healthy, gradually increase. Excellent for catching issues before they affect all users.
Bonus: core-aws-cdk - ByteCode's CDK Library
core-aws-cdk is an open-source Python library published by ByteCode Solutions that wraps common CDK patterns into reusable base stacks. Instead of writing the same S3 encryption settings, SQS dead-letter queue wiring, and Lambda packaging logic in every project, you inherit from pre-configured base stacks and call simple helper methods.
What it provides:
- Base Stacks - pre-configured CDK stacks with consistent tagging and security defaults
- Lambda - simplified function creation with automatic ZIP packaging and dependency bundling
- S3 - bucket creation with encryption, versioning, and access controls applied by default
- SQS - queue creation with dead-letter queue support in one call
- SNS - topic creation with subscription management
- Network Stack - VPC and networking resources managed as a reusable construct
The following example builds a complete SNS → SQS → Lambda → S3 pipeline. Compare it with the raw CDK approach from lesson 13: the helper methods eliminate repeated boilerplate while the underlying CDK constructs remain fully accessible.
from aws_cdk import App, Environment, Duration
from aws_cdk.aws_lambda import Code, Runtime
from aws_cdk.aws_lambda_event_sources import SqsEventSource
from aws_cdk.aws_sns_subscriptions import SqsSubscription
from core_aws_cdk.stacks.lambdas import BaseLambdaStack
from core_aws_cdk.stacks.s3 import BaseS3Stack
from core_aws_cdk.stacks.sns import BaseSnsStack
from core_aws_cdk.stacks.sqs import BaseSqsStack
# Combine base stacks via multiple inheritance
class IntegratedStack(BaseSnsStack, BaseSqsStack, BaseLambdaStack, BaseS3Stack):
"""Full event-driven pipeline in one stack."""
app = App()
stack = IntegratedStack(app, "IntegratedStack",
env=Environment(account="123456789012", region="us-east-1"))
# Each helper applies security defaults and tagging automatically
bucket = stack.create_bucket(bucket_id="DataBucket")
queue = stack.create_sqs_queue(
queue_id="ProcessQueue",
queue_name="process-queue",
with_dlq=True, # dead-letter queue included
max_receive_count=3,
)
topic = stack.create_sns_topic(topic_id="EventTopic", topic_name="event-topic")
topic.add_subscription(SqsSubscription(queue))
fn = stack.create_lambda(
function_id="Processor",
handler="handler.lambda_handler",
code=Code.from_asset("./lambda"),
runtime=Runtime.PYTHON_3_12,
timeout=Duration.minutes(5),
environment={"BUCKET_NAME": bucket.bucket_name},
)
fn.add_event_source(SqsEventSource(queue))
bucket.grant_write(fn)
app.synth()Key Takeaways
- CodeBuild + buildspec.yml - managed build environment; install, test, package, deploy in phases; pay per minute
- OIDC over access keys - GitHub Actions can assume IAM roles via short-lived tokens; no secrets to store or rotate
- Trust policy scope - restrict the OIDC role to a specific repo and branch to prevent privilege escalation
- Rolling - zero downtime, mixed versions briefly; default for ECS; simple to set up
- Blue/Green - instant rollback, requires double capacity during cutover; use with CodeDeploy for Lambda and ECS
- Canary - gradual traffic shift with automatic rollback on alarm; safest strategy for high-traffic production services