blog-banner

AWS Lambda with SAM: A CI/CD Pipeline You Can Set Up in 2 Hours

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.

Why Most Lambda CI/CD Setups Fail

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.

The three failure points we see repeatedly:

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.

The Complete CI/CD Pipeline Architecture

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.

Prerequisites (15 Minutes)

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.

Step 1: Create the IAM Roles (30 Minutes)

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]

Step 2: Create the BuildSpec File (20 Minutes)

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).

Step 3: Create S3 Artifact Bucket (5 Minutes)

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.

Step 4: Create CodeBuild Projects (25 Minutes)

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

Step 5: Create the CodePipeline (30 Minutes)

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

Step 6: Configure Notifications (10 Minutes)

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.

What You Get After 2 Hours

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.

The Common Mistakes That Break This

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.

The Ongoing Costs

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.

When This Setup Doesn't Fit

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.

The Bottom Line

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.