In this guide you'll build a complete CI/CD pipeline on AWS CodePipeline that pulls source from GitHub, builds and unit-tests it in CodeBuild, runs a custom AWS Lambda verification action that smoke-tests the deployed app and reports the result back to the pipeline, then promotes the release through a manual approval to production — while Amazon EventBridge rules react to every pipeline state change to alert your team on Slack or SNS.
A few things have changed since older tutorials, so this walkthrough uses 2026-current names and patterns: CloudWatch Events is now EventBridge, CodeStar Connections is now AWS CodeConnections, AWS has paused new-customer onboarding to CodeCommit (so GitHub or Bitbucket is the sensible source today), and CodePipeline V2 adds pipeline-level variables and richer Git triggers.
At MicroPyramid we've designed and operated AWS delivery pipelines like this across 50+ projects over 12+ years, so the patterns below are the ones we actually ship with our AWS consulting team.
Pipeline anatomy: stages and actions
A pipeline is an ordered set of stages, and each stage holds one or more actions. Every action belongs to a category — Source, Build, Test, Deploy, Approval, or Invoke — reads input artifacts, and writes output artifacts to an encrypted S3 artifact bucket. The pipeline we're building has five stages:
- Source — GitHub via AWS CodeConnections; emits the source artifact.
- Build — CodeBuild runs
buildspec.yml(install deps, lint, unit-test, package). - Verify — an Invoke action calls a custom Lambda to smoke-test the staging deploy.
- Approval — a Manual approval gate before production.
- Deploy — CodeDeploy ships to ECS, Lambda, or EC2 (blue/green or canary).
Choose CodePipeline V2 for new pipelines: it supports pipeline variables, parameterized executions, and Git tag/branch trigger filters. V1 still works for existing pipelines and bills differently — set the pipeline type explicitly when you create it.
Connect a GitHub source with CodeConnections
Because AWS paused new CodeCommit onboarding, connect GitHub, GitHub Enterprise, or Bitbucket through AWS CodeConnections (formerly CodeStar Connections). Create the connection, then finish the OAuth handshake once in the console so its status flips from PENDING to AVAILABLE:
# The CLI verb was previously "aws codestar-connections create-connection"
aws codeconnections create-connection \
--provider-type GitHub \
--connection-name mp-github
# Copy the returned ConnectionArn, then authorize the AWS Connector GitHub App in
# the console: Developer Tools > Settings > Connections > "Update pending connection".
# Confirm it is usable before wiring it into the pipeline:
aws codeconnections get-connection \
--connection-arn arn:aws:codeconnections:us-east-1:111122223333:connection/EXAMPLE-1234
# -> ConnectionStatus must read "AVAILABLE"Build and test in CodeBuild with buildspec.yml
CodeBuild runs your build inside a managed container described by a buildspec.yml at the repo root. Use the reports section so test results surface in the CodeBuild Reports UI, and artifacts to hand the packaged app to the next stage. We write these functions and build scripts in Python — see our Python development services for how we structure them.
version: 0.2
env:
variables:
AWS_DEFAULT_REGION: "us-east-1"
phases:
install:
runtime-versions:
python: 3.13
commands:
- pip install -r requirements.txt -r requirements-dev.txt
pre_build:
commands:
- echo "Lint and unit tests"
- ruff check .
- pytest --junitxml=reports/junit.xml
build:
commands:
- echo "Package the application"
- sam build || zip -r app.zip .
reports:
pytest_reports:
files:
- reports/junit.xml
file-format: JUNITXML
artifacts:
name: build-output
files:
- "**/*"The custom Lambda verification action
This is the most useful pattern in the post: a custom verify step. Add an Invoke action after the staging deploy that calls a Lambda. CodePipeline passes the job to your function under the "CodePipeline.job" key; the function runs whatever verification you want — hit a health endpoint, run integration/smoke tests, or check a canary metric — then must call back exactly once:
put_job_success_resultto let the pipeline continue, orput_job_failure_resultto fail the stage (with afailureDetailsmessage, max 265 characters).
Pass per-action config (such as the URL to check) through the action's UserParameters. For checks that run longer than the Lambda timeout, return a continuation token with put_job_success_result(continuationToken=...) and CodePipeline re-invokes the action until you finish. Keep the function itself tidy by following our AWS Lambda best practices.
import json
import urllib.request
import boto3
codepipeline = boto3.client("codepipeline")
def lambda_handler(event, context):
"""Verify a staging deploy and report the result back to CodePipeline."""
job = event["CodePipeline.job"]
job_id = job["id"]
try:
# UserParameters carries per-action config set on the pipeline (JSON string).
raw = job["data"]["actionConfiguration"]["configuration"].get("UserParameters", "{}")
params = json.loads(raw) if raw else {}
url = params.get("url", "https://staging.example.com/healthz")
# Run the actual verification: a health probe here, smoke tests in real life.
with urllib.request.urlopen(url, timeout=10) as resp:
status = resp.getcode()
if status == 200:
codepipeline.put_job_success_result(jobId=job_id)
else:
codepipeline.put_job_failure_result(
jobId=job_id,
failureDetails={"type": "JobFailed", "message": f"Health check: HTTP {status}"},
)
except Exception as exc: # report any failure back so the stage fails cleanly
codepipeline.put_job_failure_result(
jobId=job_id,
failureDetails={"type": "JobFailed", "message": str(exc)[:265]},
)
return {"jobId": job_id}IAM for the verify action
The Lambda's execution role needs permission to report results, and the pipeline's service role needs permission to invoke the function. Keep both least-privilege. Note that PutJobSuccessResult/PutJobFailureResult do not support resource-level scoping, so their Resource is "*" — tighten everything else in the role instead:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReportBackToCodePipeline",
"Effect": "Allow",
"Action": [
"codepipeline:PutJobSuccessResult",
"codepipeline:PutJobFailureResult"
],
"Resource": "*"
}
]
}The pipeline service role, in turn, needs lambda:InvokeFunction and lambda:ListFunctions scoped to the verify function's ARN so the Invoke action can call it.
React to pipeline events with EventBridge
CodePipeline emits events to Amazon EventBridge (the service formerly branded CloudWatch Events). You can match on three detail-types — Pipeline, Stage, and Action execution state changes — with states such as STARTED, SUCCEEDED, FAILED, CANCELED, and SUPERSEDED. Route matches to SNS for email/SMS, to AWS Chatbot for Slack, or to a notifier Lambda. This is the same event-driven plumbing behind an S3-to-Lambda-to-DynamoDB pipeline. An event pattern that fires on terminal pipeline states:
{
"source": ["aws.codepipeline"],
"detail-type": ["CodePipeline Pipeline Execution State Change"],
"detail": {
"state": ["SUCCEEDED", "FAILED", "CANCELED"]
}
}Point that rule at an SNS topic for email, or at a small notifier Lambda for Slack:
import json
import os
import urllib.request
def lambda_handler(event, context):
"""Post CodePipeline state changes to a Slack incoming webhook."""
detail = event["detail"]
pipeline = detail["pipeline"]
state = detail["state"]
text = f":rocket: Pipeline *{pipeline}* changed state: *{state}*"
body = json.dumps({"text": text}).encode("utf-8")
request = urllib.request.Request(
os.environ["SLACK_WEBHOOK_URL"],
data=body,
headers={"Content-Type": "application/json"},
)
urllib.request.urlopen(request, timeout=5)
return {"ok": True}Manual approval gate before production
Insert a stage with a single Manual approval action ahead of your production deploy. Attach an SNS topic so reviewers are notified, and include a review or changelog URL in the action configuration. The execution pauses until someone approves or rejects it in the console or with aws codepipeline put-approval-result. Because the gate sits after the verify action, only releases that pass your smoke tests ever reach a human for sign-off — an auditable, low-friction quality gate.
Deploy strategies: blue/green and canary
For zero-downtime releases, hand the deploy stage to AWS CodeDeploy:
- ECS / Lambda blue-green — CodeDeploy shifts traffic from the old (blue) version to the new (green) one and auto-rolls-back on a CloudWatch alarm.
- Lambda canary / linear — shift a slice of traffic first (e.g.
CodeDeployDefault.LambdaCanary10Percent5Minutes), then the remainder if alarms stay green. - EC2 in-place or blue-green — via the CodeDeploy agent and an
appspec.yml. - S3 deploy — push static sites or artifacts straight to a bucket fronted by CloudFront.
This deploy stage is also where a cloud migration lands: once workloads run on AWS, the same pipeline keeps shipping them. Define the whole thing as code with CloudFormation, AWS CDK, or Terraform so the pipeline, IAM roles, and EventBridge rules are reviewable and reproducible across staging and production accounts.
CodePipeline vs GitHub Actions vs GitLab CI vs Jenkins
| Tool | Best fit | Runners | Watch-outs |
|---|---|---|---|
| AWS CodePipeline | AWS-native delivery to ECS/Lambda/EC2 governed by IAM + EventBridge | Fully managed | AWS-centric; smaller plugin ecosystem |
| GitHub Actions | Repo-adjacent CI with a huge marketplace | GitHub-hosted or self-hosted | You wire OIDC into AWS; runner minutes add up |
| GitLab CI | All-in-one DevOps when you live in GitLab | GitLab SaaS or self-managed runners | Best value when GitLab is also your SCM |
| Jenkins | Maximum flexibility and plugins, full control | Self-hosted | You own patching, scaling, and security |
Honest take: if your workloads already run on AWS and you want deploys governed by IAM and reacting to EventBridge, CodePipeline is the least-friction choice. If your team lives in GitHub and deploys are simpler, GitHub Actions with OIDC is often faster to adopt. Plenty of teams run both — Actions for CI, CodePipeline for the AWS deploy.
Frequently Asked Questions
Should I use CodePipeline V1 or V2?
Use V2 for new pipelines. It adds pipeline-level variables, parameterized executions, and Git push/tag/branch trigger filters, and it bills per action-run minute rather than per active pipeline per month. V1 still works for existing pipelines — set the pipeline type explicitly when you create or update one.
Can I still use AWS CodeCommit as my source?
If you already have CodeCommit repositories you can keep using them, but AWS has paused onboarding new customers to CodeCommit. For new projects, connect GitHub, GitHub Enterprise, or Bitbucket through AWS CodeConnections (the service formerly called CodeStar Connections) instead.
How does a custom Lambda action tell CodePipeline it passed or failed?
Your function reads the job from event["CodePipeline.job"], runs its checks, then calls exactly one of put_job_success_result(jobId=...) or put_job_failure_result(jobId=..., failureDetails=...) through boto3. If you never call back, the action eventually times out and the stage fails.
What replaced CloudWatch Events for pipeline notifications?
Amazon EventBridge — the same event bus, rebranded and expanded. CodePipeline publishes Pipeline, Stage, and Action execution state-change events that you match with an event pattern and route to SNS, Lambda, Slack via AWS Chatbot, or Step Functions.
How do I add a human approval before deploying to production?
Insert a stage with a single Manual approval action ahead of your production deploy. Attach an SNS topic to notify reviewers and add a review URL in the action configuration. The execution pauses until someone approves or rejects it in the console or with aws codepipeline put-approval-result.
What drives the cost of running CI/CD on AWS?
Cost tracks usage, not a flat fee: CodePipeline V2 action-run minutes (or V1 active-pipeline months), CodeBuild build-minutes and the compute size you pick, Lambda invocations and duration for the verify and notify functions, S3 artifact storage, KMS, and any cross-region or data-transfer charges. Right-size CodeBuild compute, cache dependencies between builds, and expire old artifacts to keep spend low.