Skip to content

CodePipeline manual approval that does not bottleneck the team

Set up an AWS CodePipeline manual approval that protects production without stalling releases. Staging deploys on its own, production waits for a named approver who gets the diff in the notification, and a second approver covers when the first is away.

A manual approval in AWS CodePipeline pauses a release until a person clicks Approve, and this article is for teams who want that check before production without it blocking every deploy. The trouble starts when the only approver is in a meeting or on vacation. Releases pile up, the notification gives no context, and after seven days the pipeline quietly fails. Before long, someone suggests deleting the approval step.

The better fix keeps the approval and makes it quick to answer. Run staging as its own pipeline with no approval, and put the approval only in the production pipeline. Send the request through Amazon SNS (Simple Notification Service) with the commit, the diff link and the staging URL, so the approver can decide in seconds. Give two named people permission to approve, and set the timeout to match your on-call rotation.

What’s happening

The default AWS::CodePipeline::Pipeline manual approval action does three things badly out of the box:

  • The SNS notification is generic — it says “approval needed” with a link to the console, but no context about what’s being deployed, what changed, or how staging validated.
  • The approval timeout is 7 days, and after timeout the pipeline transitions to Failed, which silently breaks the next several runs because most teams don’t watch for stale failed states.
  • There is no concept of a “secondary approver” in CodePipeline native — if the primary approver is on PTO, releases pile up until they’re back.

The pattern that fixes all three has four moving parts:

  • Two pipelines, not one. Staging is its own pipeline with no approval — pushes to main deploy to staging automatically. Production is a separate pipeline triggered by a release tag (or a manual trigger) with the approval action. This separates “fast iteration” from “considered release.”
  • SNS topic per environment, subscribed to a Slack channel and an email DL. The notification carries the commit SHA, the JIRA ticket (parsed from commit message), the staging URL, and the diff. The approver sees the actual change, not just the pipeline run ID.
  • Approval timeout is intentional, not default. Set the timeout to a number that matches the team’s on-call rotation. For a team with weekly rotations, 5 days is right — it means the same on-call sees the approval and the timeout in the same week.
  • Secondary approver via IAM, not pipeline config. Two named people have codepipeline:PutApprovalResult on the approval action. Either can approve. If the primary is unreachable, the secondary clicks approve and the deploy proceeds.

The end state is a pipeline where approval is a 30-second confirmation, not a meeting blocker.

How to verify

The approval action with proper SNS wiring:

ApprovalTopic:
  Type: AWS::SNS::Topic
  Properties:
    TopicName: !Sub ${AppName}-prod-approval
    Subscription:
      - Endpoint: !Ref ApproverEmail
        Protocol: email
      - Endpoint: !Ref SlackWebhookSubscription
        Protocol: https

ApprovalAction:
  Name: ManualApproval
  ActionTypeId:
    Category: Approval
    Owner: AWS
    Provider: Manual
    Version: '1'
  Configuration:
    NotificationArn: !Ref ApprovalTopic
    CustomData: !Sub |
      Commit: #{SourceVariables.CommitId}
      Author: #{SourceVariables.AuthorDisplayName}
      Message: #{SourceVariables.CommitMessage}
      Staging URL: https://staging.${DomainName}
      Diff: https://github.com/${GitHubOwner}/${GitHubRepo}/commit/#{SourceVariables.CommitId}
    ExternalEntityLink: !Sub https://staging.${DomainName}/healthz

The CustomData substitutions use CodePipeline action variables — SourceVariables.CommitId, SourceVariables.AuthorDisplayName, SourceVariables.CommitMessage — populated by the Source stage from the CodeStar Source action. The approver opens the SNS email and sees the actual commit they’re approving, not a generic “approval needed.”

For the Slack side, the SNS-to-Slack pattern uses a Lambda subscriber that posts a message with two buttons: Approve and Reject. The Lambda calls aws codepipeline put-approval-result against the approval action’s token. The token is included in the SNS payload — the Lambda parses it, the approver clicks the button, the call completes in under a second. The IAM policy on the Lambda’s role:

{
  "Effect": "Allow",
  "Action": "codepipeline:PutApprovalResult",
  "Resource": "arn:aws:codepipeline:ca-central-1:111111111111:app-prod-pipeline/Approval/ManualApproval"
}

Scope the resource to the specific approval action ARN, not *. This is the kind of “any pipeline approval” footgun that turns into a “any user can approve any deploy” finding in the next audit.

Verify the timeout works as expected:

aws codepipeline get-pipeline \
  --name app-prod-pipeline \
  --query 'pipeline.stages[?name==`Approval`].actions[0].timeoutInMinutes'

If the response is null, the action is using the 7-day default. To set an explicit timeout, the CloudFormation property is TimeoutInMinutes on the action — note that CodePipeline manual approval actions support timeouts up to 7 days (10080 minutes). Setting it lower than your on-call cycle is intentional — it forces the timeout to land on someone’s shift, not on a Sunday morning when nobody is paged.

The fix

The full pattern, in order:

  1. Split staging from prod. Staging is its own pipeline triggered on every push to main. Prod is triggered on a release tag (v*), or manually via aws codepipeline start-pipeline-execution. Use the same CodeBuild project for both, the same artifact, the same deploy target shape — only the trigger and the approval differ.

  2. SNS topic per environment. Prod approval pages the on-call AM (Slack + email). Staging pipeline failures page the same SNS topic but with a different subject line. Other environments — dev, qa — go to a low-priority Slack channel only.

  3. Notification carries the diff link. The CustomData field on the approval action is your one shot at giving the approver context. Include the commit SHA, the commit message, the diff link, and the staging healthz URL. If your commit messages are not informative enough to approve from, fix the commit messages — that is upstream of the pipeline problem.

  4. Two named approvers, IAM-scoped. Create an IAM group prod-deploy-approvers with one statement: codepipeline:PutApprovalResult on the approval action ARN. Add two people. When the primary is on PTO, the secondary covers. When both are unreachable, the deploy waits — that is the correct behavior, not a bug to engineer around.

  5. Timeout matches the on-call cycle. Weekly rotation → 5 days. Daily rotation → 24 hours. Match it to whatever cadence guarantees the same person sees both ends of the approval window. After timeout, the pipeline transitions to Failed — a CloudWatch event rule fires on Pipeline Execution State Change with state: FAILED and the SNS notification fires again, this time to the secondary group.

  6. The “automatic on staging, manual on prod” rule is not a religion. A regulated workload that touches financial transactions may need a second approval at staging too. A pure-marketing site may not need approval anywhere. The rule of thumb: if a bad deploy causes data corruption, you need approval. If a bad deploy causes a page to render badly for 10 minutes before the auto-rollback, you do not.

The point of manual approval is to make someone read the diff before prod. Make it easy for them to do that, hard for them to approve without reading, and impossible for the pipeline to silently rot when they’re unreachable.


Stack Harbor builds and operates production CodePipeline workflows as part of Managed AWS onboarding and the ongoing Code Deploy Pipeline operating layer. If your release path is still SSH + a Slack message, start with an infrastructure consult.