December 17, 2025
Your Lambda functions are in production, but you're still deploying with sam build && sam deploy from your laptop.
Every deployment is a small ceremony: make sure you're on the right AWS profile, pray you didn't break something, wait 3 minutes for CloudFormation, then manually test in the console. When something goes wrong at 11 PM, your team scrambles to figure out what version is deployed and who changed what.
This is expensive risk. A single bad deployment that takes down checkout for 15 minutes costs more than automating your pipeline correctly.
We've built AWS Lambda CI/CD pipelines for applications handling millions of requests daily. Here's the complete setup that takes 2 hours to implement and eliminates deployment anxiety.
Your team tried setting this up before. Someone spent a day configuring CodePipeline, got halfway through the IAM permissions maze, hit a cryptic error about cross-region artifact buckets, and gave up.
The problem isn't complexity—it's that AWS documentation shows you individual pieces without explaining how they connect. SAM's official CI/CD docs assume you understand CodePipeline's artifact flow, IAM role assumption chains, and CloudFormation service roles.
IAM permission loops: CodePipeline needs permissions to invoke CodeBuild. CodeBuild needs permissions to assume a CloudFormation execution role. CloudFormation needs permissions to create your Lambda functions. Get any of these wrong and you're debugging "Access Denied" errors for hours.
Artifact bucket confusion: CodePipeline stages pass artifacts through S3. If your pipeline creates buckets in us-east-1 but deploys to us-west-2, you'll hit cross-region restrictions. SAM's default behavior creates buckets regionally, but CodePipeline's default behavior doesn't.
Missing testing stages: Teams build pipelines that deploy directly to production with no automated testing. The first time a deployment breaks production, they abandon the pipeline and return to manual deployments.
The correct setup handles all three from the start.
This pipeline implements a proven pattern: commit → build → test → deploy to staging → manual approval → deploy to production.
GitHub repository contains your SAM application (template.yaml, function code, tests).
CodePipeline orchestrates the workflow, triggered on commits to main branch.
CodeBuild runs in two stages:
• Build stage: sam build, run unit tests, package artifacts
• Test stage: deploy to staging, run integration tests
CloudFormation deploys your SAM application to staging and production environments.
SNS topic notifies your team of deployment status and approval requests.
Total AWS services: CodePipeline, CodeBuild, CloudFormation, S3, SNS, IAM. No third-party tools required.
You need these ready before starting:
AWS account with appropriate permissions: You're creating IAM roles, CodePipeline pipelines, and CloudFormation stacks. Admin access or specific IAM permissions for these services.
GitHub repository with your SAM application. Must include:
• template.yaml (SAM template)
• Function code in organized directories
• buildspec.yml (we'll create this)
• Basic unit tests
AWS CLI and SAM CLI installed locally for initial setup commands.
Two SSM parameters for environment configuration:
aws ssm put-parameter --name /lambda-app/staging/config --value '{"LOG_LEVEL":"DEBUG"}' --type String
aws ssm put-parameter --name /lambda-app/production/config --value '{"LOG_LEVEL":"INFO"}' --type String
These parameters let you configure environments differently without hardcoding values.
IAM roles are the foundation. Get these wrong and you'll fight permission errors for days.
CodePipeline service role (codepipeline-lambda-app-role):
This role allows CodePipeline to orchestrate the workflow—triggering CodeBuild, reading from S3, invoking CloudFormation.
# pipeline-role.yaml
Resources:
CodePipelineServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: codepipeline-lambda-app-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codepipeline.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSCodePipelineFullAccess
Policies:
- PolicyName: PipelineExecutionPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:GetBucketLocation
Resource:
- !Sub 'arn:aws:s3:::codepipeline-${AWS::Region}-*/*'
- Effect: Allow
Action:
- codebuild:BatchGetBuilds
- codebuild:StartBuild
Resource: '*'
- Effect: Allow
Action:
- cloudformation:*
Resource: '*'
- Effect: Allow
Action:
- iam:PassRole
Resource: '*'
CodeBuild service role (codebuild-lambda-app-role):
This role allows CodeBuild to build your application, run tests, and create CloudFormation change sets.
# codebuild-role.yaml
Resources:
CodeBuildServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: codebuild-lambda-app-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: CodeBuildPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: '*'
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource:
- !Sub 'arn:aws:s3:::codepipeline-${AWS::Region}-*/*'
- !Sub 'arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*/*'
- Effect: Allow
Action:
- cloudformation:*
Resource: '*'
- Effect: Allow
Action:
- iam:PassRole
Resource: '*'
- Effect: Allow
Action:
- ssm:GetParameter
Resource:
- !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/lambda-app/*'
CloudFormation execution role (cloudformation-lambda-app-role):
This role allows CloudFormation to create your actual Lambda functions, API Gateways, and other resources defined in your SAM template.
# cloudformation-role.yaml
Resources:
CloudFormationExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: cloudformation-lambda-app-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: cloudformation.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSLambda_FullAccess
Policies:
- PolicyName: CloudFormationExecutionPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
Resource: '*'
- Effect: Allow
Action:
- iam:*
Resource: '*'
- Effect: Allow
Action:
- apigateway:*
Resource: '*'
- Effect: Allow
Action:
- logs:*
Resource: '*'
Deploy these roles:
aws cloudformation create-stack --stack-name lambda-app-iam-roles --template-body file://pipeline-role.yaml --capabilities CAPABILITY_NAMED_IAM
aws cloudformation create-stack --stack-name lambda-app-codebuild-role --template-body file://codebuild-role.yaml --capabilities CAPABILITY_NAMED_IAM
aws cloudformation create-stack --stack-name lambda-app-cfn-role --template-body file://cloudformation-role.yaml --capabilities CAPABILITY_NAMED_IAM
Wait for all three stacks to complete: aws cloudformation wait stack-create-complete --stack-name [stack-name]
The buildspec.yml file tells CodeBuild what to do during each phase. This is where your build, test, and packaging logic lives.
Create buildspec.yml in your repository root:
version: 0.2
phases:
install:
runtime-versions:
python: 3.11
commands:
- echo "Installing SAM CLI..."
- pip install aws-sam-cli
- sam --version
pre_build:
commands:
- echo "Running unit tests..."
- pip install pytest pytest-cov
- pytest tests/unit -v --cov=src --cov-report=term-missing
build:
commands:
- echo "Building SAM application..."
- sam build --use-container
- echo "Running sam validate..."
- sam validate
post_build:
commands:
- echo "Packaging application..."
- sam package --s3-bucket $ARTIFACT_BUCKET --output-template-file packaged.yaml
- echo "Package complete"
artifacts:
files:
- packaged.yaml
- template.yaml
name: BuildArtifact
cache:
paths:
- '/root/.cache/pip/**/*'
Critical details:
The --use-container flag builds Lambda functions in Docker containers matching the Lambda runtime. Without this, your local Python packages might differ from Lambda's environment.
The artifact section specifies which files CodePipeline passes to the next stage. packaged.yaml contains S3 references to your built code.
The cache section speeds up subsequent builds by preserving pip packages between runs.
Environment variable ARTIFACT_BUCKET must be set in CodeBuild project configuration (we'll do this in Step 4).
CodePipeline needs an S3 bucket to store artifacts between stages. SAM creates deployment buckets automatically, but CodePipeline needs its own bucket.
aws s3 mb s3://codepipeline-lambda-app-artifacts-$(aws sts get-caller-identity --query Account --output text) --region us-east-1
Enable versioning for artifact history:
aws s3api put-bucket-versioning --bucket codepipeline-lambda-app-artifacts-$(aws sts get-caller-identity --query Account --output text) --versioning-configuration Status=Enabled
This bucket name includes your AWS account ID to ensure global uniqueness.
You need two CodeBuild projects: one for building/testing, one for deploying to staging with integration tests.
Build project (lambda-app-build):
# codebuild-build-project.yaml
Resources:
BuildProject:
Type: AWS::CodeBuild::Project
Properties:
Name: lambda-app-build
ServiceRole: !Sub 'arn:aws:iam::${AWS::AccountId}:role/codebuild-lambda-app-role'
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:7.0
EnvironmentVariables:
- Name: ARTIFACT_BUCKET
Value: !Sub 'aws-sam-cli-managed-default-samclisourcebucket-${AWS::Region}'
Source:
Type: CODEPIPELINE
BuildSpec: buildspec.yml
TimeoutInMinutes: 15
Deploy-and-test project (lambda-app-deploy-staging):
# codebuild-deploy-project.yaml
Resources:
DeployProject:
Type: AWS::CodeBuild::Project
Properties:
Name: lambda-app-deploy-staging
ServiceRole: !Sub 'arn:aws:iam::${AWS::AccountId}:role/codebuild-lambda-app-role'
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:7.0
EnvironmentVariables:
- Name: STACK_NAME
Value: lambda-app-staging
- Name: ENV
Value: staging
Source:
Type: CODEPIPELINE
BuildSpec: buildspec-deploy.yml
TimeoutInMinutes: 15
Create corresponding buildspec-deploy.yml:
version: 0.2
phases:
install:
runtime-versions:
python: 3.11
commands:
- pip install aws-sam-cli boto3
build:
commands:
- echo "Deploying to staging environment..."
- |
sam deploy \
--template-file packaged.yaml \
--stack-name $STACK_NAME \
--capabilities CAPABILITY_IAM \
--parameter-overrides Environment=$ENV \
--no-fail-on-empty-changeset \
--role-arn arn:aws:iam::${AWS::AccountId}:role/cloudformation-lambda-app-role
post_build:
commands:
- echo "Running integration tests..."
- pip install pytest requests
- |
API_ENDPOINT=$(aws cloudformation describe-stacks \
--stack-name $STACK_NAME \
--query 'Stacks[0].Outputs[?OutputKey==`ApiEndpoint`].OutputValue' \
--output text)
- export API_ENDPOINT
- pytest tests/integration -v
Deploy both projects:
aws cloudformation create-stack --stack-name lambda-app-codebuild-projects --template-body file://codebuild-build-project.yaml
Now connect everything together in a CodePipeline that orchestrates the workflow.
# pipeline.yaml
Resources:
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: lambda-app-pipeline
RoleArn: !Sub 'arn:aws:iam::${AWS::AccountId}:role/codepipeline-lambda-app-role'
ArtifactStore:
Type: S3
Location: !Sub 'codepipeline-lambda-app-artifacts-${AWS::AccountId}'
Stages:
- Name: Source
Actions:
- Name: SourceAction
ActionTypeId:
Category: Source
Owner: ThirdParty
Provider: GitHub
Version: '1'
Configuration:
Owner: your-github-username
Repo: your-repo-name
Branch: main
OAuthToken: '{{resolve:secretsmanager:github-token:SecretString:token}}'
OutputArtifacts:
- Name: SourceOutput
- Name: Build
Actions:
- Name: BuildAction
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: '1'
Configuration:
ProjectName: lambda-app-build
InputArtifacts:
- Name: SourceOutput
OutputArtifacts:
- Name: BuildOutput
- Name: DeployToStaging
Actions:
- Name: DeployStagingAction
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: '1'
Configuration:
ProjectName: lambda-app-deploy-staging
InputArtifacts:
- Name: BuildOutput
- Name: ApprovalStage
Actions:
- Name: ManualApproval
ActionTypeId:
Category: Approval
Owner: AWS
Provider: Manual
Version: '1'
Configuration:
CustomData: 'Please review staging deployment before promoting to production'
- Name: DeployToProduction
Actions:
- Name: DeployProductionAction
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: CloudFormation
Version: '1'
Configuration:
ActionMode: CREATE_UPDATE
StackName: lambda-app-production
TemplatePath: BuildOutput::packaged.yaml
Capabilities: CAPABILITY_IAM
RoleArn: !Sub 'arn:aws:iam::${AWS::AccountId}:role/cloudformation-lambda-app-role'
ParameterOverrides: '{"Environment":"production"}'
InputArtifacts:
- Name: BuildOutput
GitHub OAuth token: Store your GitHub personal access token in Secrets Manager:
aws secretsmanager create-secret --name github-token --secret-string '{"token":"ghp_yourtoken"}'
Deploy the pipeline:
aws cloudformation create-stack --stack-name lambda-app-pipeline --template-body file://pipeline.yaml --capabilities CAPABILITY_IAM
You want alerts when builds fail, approvals are needed, or deployments complete.
Create SNS topic and subscribe your team:
aws sns create-topic --name lambda-app-pipeline-notifications
aws sns subscribe --topic-arn arn:aws:sns:us-east-1:ACCOUNT_ID:lambda-app-pipeline-notifications --protocol email --notification-endpoint your-team@company.com
Add notification rules to CodePipeline through the console or CLI—this connects pipeline events to your SNS topic.
Automated deployments on every commit to main: No more manual sam deploy commands. Push code, pipeline handles the rest.
Built-in testing gates: Unit tests must pass before building. Integration tests must pass in staging before production deployment is possible.
Manual approval checkpoint: Someone must explicitly approve production deployments after reviewing staging.
Audit trail: Every deployment tracked in CodePipeline with timestamps, commit SHA, and approval records.
Consistent environments: Staging and production use identical deployment processes, eliminating "works on my machine" issues.
Using the same stack name for staging and production: Your CloudFormation stack names must differ (lambda-app-staging vs lambda-app-production) or deployments conflict.
Forgetting to enable GitHub webhook: CodePipeline needs webhook access to your repository. If you're setting this up via CLI, use the console to finalize the GitHub connection.
Hardcoding region in IAM roles: Use ${AWS::Region} pseudo-parameters so the pipeline works across regions.
Skipping the CloudFormation execution role: If CloudFormation uses your CodeBuild role directly, you'll hit permission issues. The separation is intentional—principle of least privilege.
Not handling SAM managed buckets: SAM creates S3 buckets for deployment artifacts. These must match between your build commands and deployment commands, or you'll deploy stale code.
This pipeline costs approximately $15-30/month for moderate usage (20-30 deployments monthly):
• CodePipeline: $1/active pipeline/month
• CodeBuild: $0.005/build minute (build takes 3-5 minutes typically, so $0.15-0.25 per build)
• S3 storage: $0.023/GB/month for artifacts (usually under 1GB)
• CloudFormation: No charge
With 25 deployments monthly at 4 minutes each: $1 + (25 × 4 × $0.005) + $1 = $2.50/month in direct costs.
Compare this to the cost of a single bad manual deployment breaking production for 15 minutes. Your engineering time debugging and rolling back costs orders of magnitude more.
You need blue-green deployments with automatic rollback: This pipeline does simple stack updates. Blue-green deployments require CodeDeploy integration—add 2-3 hours for that setup.
You have a monorepo with multiple Lambda applications: This single-pipeline-per-app model doesn't scale. You need pipeline orchestration or a more sophisticated artifact strategy.
You're deploying to multiple AWS accounts: Cross-account deployments require additional IAM role assumption chains and artifact bucket policies—add 3-4 hours for that complexity.
You need deployment windows or change management integration: This immediate-deploy model won't work with organizational controls requiring ServiceNow tickets or maintenance windows.
For those scenarios, you're looking at 1-2 days of pipeline setup, not 2 hours.
Manual Lambda deployments are technical debt masquerading as "moving fast." The first time a bad deploy breaks production at midnight, you've paid for this pipeline setup 10x over in incident response costs.
This 2-hour setup eliminates deployment anxiety, creates an audit trail, and forces testing discipline. The ROI is immediate—your first prevented bad deployment justifies the investment.
We've implemented this exact pattern for 20+ Lambda applications. The CloudFormation templates, buildspecs, and IAM policies are solved problems. If you're still deploying Lambdas from your laptop, we should talk about building this properly—in hours, not weeks.
If you’re still deploying Lambda manually, this is the fastest way to regain control — and sleep better after pushing changes.
We'd love to talk about how we can work together
Take control of your AWS cloud costs that enables you to grow!