Managing Environments in AWS CI/CD Pipelines: Dev, Staging, and Prod
Managing environments in a CI/CD pipeline means controlling how code moves through dev, staging, and production, and what must be true before it can advance. On AWS, this is built around CodePipeline stages, separate accounts per environment, Parameter Store for configuration, and manual approval gates before production. This page is for developers and platform engineers setting up or improving a multi-environment AWS pipeline.
The core principle: build once, then promote the exact same artifact safely through every environment. What changes between environments is not the code or container image. It is the configuration, the AWS account, and the approval requirements.
Simple explanation
Think of your application as a package moving through checkpoints before it reaches a customer. Dev is the internal workshop: fast, low-risk, things break often and that is fine. Staging is a final quality check in conditions that mirror production. Production is where customers are. Each checkpoint catches problems the previous one could not.
In AWS terms: a commit triggers CodeBuild to run tests and produce a Docker image in ECR. CodePipeline deploys that image to dev automatically. To reach staging, a team member clicks Approve. To reach production, someone clicks Approve again. The same image tag travels the entire route. It is never rebuilt, never modified.
Why environment management matters
Without explicit environment structure, code goes from a developer’s branch directly to production. A broken database migration, a misconfigured feature flag, or a slow query hits real users before anyone catches it. Environment promotion gives you two chances to find the problem first.
Environment separation also limits blast radius. When dev and production run in separate AWS accounts, an IAM policy mistake or a runaway script in dev cannot touch production resources. This is one of the most practical security guarantees in AWS at low cost.
How it works end to end
- Commit — a developer merges a pull request to the main branch.
- Build and test — CodeBuild runs the test suite, builds the Docker image, and pushes it to ECR tagged with the git commit SHA.
- Deploy to dev — CodePipeline automatically deploys the image tag to the dev ECS cluster.
- Validate dev — automated smoke tests run, or a developer verifies the feature manually.
- Approve staging — a reviewer clicks Approve in the CodePipeline console. The same image tag deploys to the staging ECS cluster. Rejecting stops the pipeline here.
- Validate staging — integration tests run against staging. The team confirms the release looks healthy.
- Approve production — a second manual approval gate. This is non-negotiable for production workloads.
- Deploy to production — the same image tag tested in dev and staging deploys to the production ECS cluster.
THE GOLDEN RULE
The image tag never changes across environments. What changes at each stage is the ECS cluster, the Parameter Store path the application reads, and who must approve the deployment. If your pipeline rebuilds the image per environment, production is never running what staging tested.
Dev vs staging vs production in AWS
Each environment has a distinct purpose and its infrastructure should reflect that. See Dev vs Staging vs Production in AWS for a detailed breakdown of how each differs in infrastructure sizing, data handling, and access controls. The short version:
- Dev — small instances, synthetic data, frequent deployments, no approval required. Cost should be low; uptime does not matter.
- Staging — production-equivalent instance types, anonymized or synthetic data at production-like volume, deployments gated by manual approval. Uptime matters during pre-release testing windows.
- Production — full infrastructure, real data, manual approval required, alerting enabled, and a documented rollback procedure before every deployment.
Separate AWS accounts vs separate VPCs
You have two options for isolating environments: separate AWS accounts per environment, or separate VPCs within a single account. Both work, but they offer different levels of isolation.
| Factor | Separate accounts | Separate VPCs in one account |
|---|---|---|
| IAM isolation | Complete — a dev IAM role cannot affect production resources | Partial — a permissive IAM policy can span both VPCs |
| Billing visibility | Per-environment cost breakdown automatically | Requires tagging and cost allocation reports |
| Service limit isolation | Dev experiments do not consume production service limits | Shared service limits across all environments |
| Setup complexity | Higher — requires AWS Organizations, cross-account roles | Lower — single account, simpler networking |
| Compliance suitability | Strong — account-level isolation satisfies most audit requirements | Weak — auditors often require stronger isolation evidence |
| Recommended for | Production workloads, regulated industries, any team past early stage | Early-stage projects, internal tools with no compliance requirements |
RECOMMENDATION
Use separate AWS accounts for production workloads. A single account with separate VPCs is acceptable for early-stage or internal projects, but it is harder to migrate away from later. AWS Organizations makes the multi-account model manageable by grouping accounts into OUs with service control policies applied at the organization level.
Recommended architecture by team size and risk level
Small team / early stage (1–5 engineers, no compliance requirements): A single AWS account with separate VPCs for dev and production is acceptable. One pipeline, two deployment targets, one manual approval before production. Keep it simple and plan to add separate accounts when production traffic grows.
Growing team (5–30 engineers, production traffic, basic compliance): Separate AWS accounts for dev, staging, and production under AWS Organizations. Manual approval gates before staging and before production. Environment config in Parameter Store per account.
Production-critical / regulated workloads (SOC 2, PCI, HIPAA): Separate accounts are non-negotiable. Add a dedicated tools account for the pipeline. Use SCPs at the OU level to restrict what can be deployed in production accounts. Require two approvers before production. Log all deployments to CloudTrail.
The right time to add approval gates and account separation is before you have production users, not after. Retrofitting these controls into a pipeline already running without them is harder than building them in from the start.
Example CodePipeline flow for multi-environment promotion
A CodePipeline covering three environments has at minimum: a source stage, a build stage, a dev deployment, an approval before staging, a staging deployment, an approval before production, and a production deployment. For ECS-based workloads, the deployment stages use the ECS action type.
# cloudformation/pipeline.yaml (simplified)
Stages:
- Name: Source
Actions:
- Name: GitHub
ActionTypeId:
Category: Source
Owner: ThirdParty
Provider: GitHub
- Name: Build
Actions:
- Name: BuildAndTest
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
# Builds image tagged with commit SHA, runs tests, pushes to ECR
- Name: DeployDev
Actions:
- Name: Deploy
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: ECS
Configuration:
ClusterName: dev-cluster
ServiceName: my-app-dev
- Name: ApproveStaging
Actions:
- Name: ManualApproval
ActionTypeId:
Category: Approval
Owner: AWS
Provider: Manual
Configuration:
NotificationArn: arn:aws:sns:us-east-1:123456789012:pipeline-approvals
- Name: DeployStaging
Actions:
- Name: Deploy
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: ECS
Configuration:
ClusterName: staging-cluster
ServiceName: my-app-staging
- Name: ApproveProduction
Actions:
- Name: ManualApproval
ActionTypeId:
Category: Approval
Owner: AWS
Provider: Manual
Configuration:
NotificationArn: arn:aws:sns:us-east-1:123456789012:pipeline-approvals
- Name: DeployProduction
Actions:
- Name: Deploy
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: ECS
Configuration:
ClusterName: prod-cluster
ServiceName: my-app-prodEach approval stage sends an SNS notification to the team. A reviewer logs into the console, verifies the previous environment looks healthy, and clicks Approve or Reject. Rejecting leaves the next environment unchanged.
For the production stage, consider pairing this with blue-green deployments or canary releases so traffic shifts gradually. Both strategies make it significantly easier to roll back if something goes wrong after cutover.
Managing environment-specific configuration
Each environment needs different values: different database hostnames, different API endpoints, different feature flags. Store them in AWS Systems Manager Parameter Store, organized by environment prefix:
aws ssm put-parameter \
--name "/dev/database/host" \
--value "dev-db.cluster-abc123.us-east-1.rds.amazonaws.com" \
--type String
aws ssm put-parameter \
--name "/staging/database/host" \
--value "staging-db.cluster-def456.us-east-1.rds.amazonaws.com" \
--type String
aws ssm put-parameter \
--name "/prod/database/host" \
--value "prod-db.cluster-ghi789.us-east-1.rds.amazonaws.com" \
--type SecureStringNEVER DO THIS
Do not hardcode environment config in buildspec.yml, the pipeline CloudFormation template, or as environment variables on the pipeline itself. Database hostnames embedded in a template are hard to rotate, visible to anyone with read access to the repo, and create drift between what is documented and what is actually used.
The application reads its own parameter at startup using an environment prefix injected by the ECS task definition. The pipeline does not carry configuration values — it only deploys the image tag. For credentials and API keys specifically, see Managing Secrets in CI/CD Pipelines on AWS.
# Application reads its own config at startup
import boto3, os
env = os.environ.get("APP_ENV", "dev") # Injected by ECS task definition
ssm = boto3.client("ssm")
db_host = ssm.get_parameter(Name=f"/{env}/database/host")["Parameter"]["Value"]Adding a new environment means adding a new Parameter Store prefix and a new pipeline stage. No changes to application code required.
Promoting the same image tag or artifact
Build once. Promote the same artifact. This is the single most important rule of multi-environment CI/CD.
Rebuilding the Docker image per environment is a common mistake. Even with identical source code, each rebuild can produce a different image: packages may resolve to newer patch versions, build layer caches differ, timestamps change. What staging tested is no longer byte-for-byte identical to what reaches production.
In the build stage, tag the image with the git commit SHA and push it to ECR. Every deploy stage uses that same tag. The image is immutable.
# Build stage — tag with commit SHA, push once
IMAGE_TAG=$CODEBUILD_RESOLVED_SOURCE_VERSION
docker build -t $ECR_REPO:$IMAGE_TAG .
docker push $ECR_REPO:$IMAGE_TAG
# Dev, staging, and production all reference the same IMAGE_TAG
# Passed between stages as a CodePipeline output artifactPass the image tag as a CodePipeline output artifact from the build stage. Each deploy stage reads it as an input artifact. The tag never changes as it moves through the pipeline.
IAM roles and cross-account deployment
The pipeline runs under a pipeline execution role in the tools account. That role should not have direct permissions to modify dev, staging, or production resources. Instead, define a deployment role in each target account and configure the pipeline to assume it per stage.
# Deployment role in the target account (dev, staging, or prod)
# Trusted by the pipeline execution role in the tools account
aws iam create-role \
--role-name CodePipelineDeployRole \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::TOOLS_ACCOUNT_ID:role/CodePipelineExecutionRole"
},
"Action": "sts:AssumeRole"
}]
}'HOW THIS PROTECTS YOU
The pipeline execution role has no production access by default. To reach production, it must explicitly assume the production deployment role via AWS STS temporary credentials. Every assumption is logged to CloudTrail. A compromised pipeline role cannot reach production without also compromising the assume-role chain.
When deploying with AWS CodeDeploy, each environment’s CodeDeploy service role is scoped to that account only. The secure CI/CD pipelines guide covers the full role structure and least-privilege policy design in detail.
When to use this setup
- Early-stage / small teams: A lighter version — one account, two environments, one approval — is acceptable while you have no compliance obligations and low production risk. Plan to add separate accounts when you have paying customers.
- Growing teams: Add a staging account, enforce approval gates for staging and production, and migrate environment config to Parameter Store per account. This is where informal processes break down and explicit structure pays off. Infrastructure as Code keeps environments consistent as the team grows.
- Production-critical workloads: Full separate accounts, SCPs at the OU level, two approvers before production, all deployments logged to CloudTrail, and a documented rollback procedure before every release. You need to be able to reproduce any environment from source.
Common mistakes
- Skipping staging. Staging catches performance regressions that dev misses entirely because dev uses smaller instance types. A slow query that does not appear on a t3.small will show up in staging before it hits customers.
- Rebuilding the Docker image per environment. Tag with the git commit SHA at build time and promote that tag. Rebuilding means the image that reaches production is not what staging tested.
- Storing environment config in the pipeline definition. Database hostnames embedded in a CloudFormation template are hard to rotate and create drift between documentation and reality. Use Parameter Store.
- Removing approval gates to speed up deployments. Approval gates are not a process checkbox. They are an explicit confirmation that staging looked healthy before traffic shifts. Removing them transfers risk, not time.
- Using one account for all environments. A permissive IAM policy in a dev script can reach production resources in a single-account setup. Separate accounts eliminate this class of mistake entirely.
Implementation checklist
- Build the Docker image once and tag it with the git commit SHA
- Promote the same image tag through dev, staging, and production — never rebuild per environment
- Run each environment in a separate AWS account (or a separate VPC at minimum for early-stage projects)
- Store all environment-specific config in Parameter Store, organized by environment prefix
- Add a manual approval stage before staging and before production
- Use cross-account IAM roles for deployment; give the pipeline execution role no direct production access
- Have a rollback procedure in place before every production deployment
Frequently asked questions
Should I use separate AWS accounts or separate VPCs?
Separate AWS accounts per environment is the right choice for production workloads. An IAM misconfiguration in dev cannot affect production if they live in different accounts. VPC isolation within a single account is weaker because a permissive IAM policy can span both. Separate accounts are the AWS-recommended approach and are manageable with AWS Organizations.
Should I rebuild the Docker image for each environment?
No. Build the image once in the CI stage and promote that exact image tag through dev, staging, and production. Rebuilding introduces risk: packages can resolve to different versions, and the image that reaches production is technically different from the one tested in staging. Tag with the git commit SHA at build time and treat that tag as immutable.
How do I handle environment-specific config and secrets?
Store environment-specific values in AWS Systems Manager Parameter Store, organized by environment prefix (for example /dev/db-host, /staging/db-host, /prod/db-host). Use SecureString for anything sensitive. The application reads its own config at startup using the environment prefix. The pipeline does not need to carry these values.
When is a single AWS account acceptable?
A single account with separate VPCs can work for early-stage projects with no compliance requirements where isolation from production is not critical. Once you have paying customers, process regulated data, or need SOC 2 or PCI compliance evidence, separate accounts are the correct choice. Starting with separate accounts from the beginning is easier than migrating later.
Why add manual approvals if the pipeline is already automated?
Automation prevents mistakes during build and test. Human approval before production is a different control. It gives someone the chance to verify that staging looked healthy, that no unexpected changes are going out, and that the timing is right. Pipelines can pass every automated check while deploying something a human would have caught.