A CI/CD pipeline turns every change into a repeatable path from commit to production: the code is built, tested, packaged, and deployed the same way each time. GitHub Actions makes it easy to write that path next to the code, and AWS provides the places to run it. A pipeline that merely runs is easy to build. The harder part is a handful of design decisions: what gets built, how the pipeline authenticates, who approves production, and how a bad release is undone.
The Stages of a Practical Pipeline
| Stage | Runs on | Purpose |
|---|---|---|
| Lint and unit tests | Every push and pull request | Fast feedback on mistakes before review |
| Build and package | Every merge to the main branch | Produce one versioned artifact, such as a container image |
| Integration tests | After the build, against the packaged artifact | Verify the artifact works with its database and dependencies |
| Deploy to staging | Automatically after tests pass | Run the release in an environment that mirrors production |
| Deploy to production | After approval, using the same artifact | Release to users with a controlled, reversible change |
Keep the pull request checks fast, ideally a few minutes. Slow checks encourage developers to batch changes together and to merge without waiting, which defeats the purpose of the pipeline. Longer tests can run after merge, as long as their failure blocks the deployment that follows.
Build Once, Deploy the Same Artifact Everywhere
Build the application once per commit and promote that exact artifact from staging to production. Rebuilding for each environment means production runs something that was never tested, because dependency versions, base images, or build tools may have changed between the two builds. Tag container images with the commit SHA rather than with latest, so every running version can be traced back to its source code and redeployed exactly.
Environment differences belong in configuration, not in the artifact. Database addresses, feature flags, and credentials are injected at runtime from the environment or from a secret store such as AWS Secrets Manager or Systems Manager Parameter Store.
A common failure: staging passes every test, then production breaks because the rebuild pulled a newer library version. Promoting one artifact through every environment rules this out.
Authenticate to AWS with OIDC, Not Long-Lived Keys
Storing an AWS access key and secret in GitHub secrets is the most common setup and the riskiest one. The keys do not expire, they are often granted broad permissions, and a leaked key works from anywhere. GitHub Actions supports OpenID Connect: the workflow requests a short-lived token from GitHub, and AWS exchanges it for temporary credentials of an IAM role.
- Register GitHub as an OIDC identity provider in the AWS account and create one IAM role per environment.
- Restrict each role's trust policy to the specific repository and to a branch or GitHub environment, so a workflow on a feature branch cannot assume the production role.
- Grant the role only the permissions the deployment needs, such as pushing to one ECR repository and updating one ECS service.
- Give the workflow the id-token: write permission and use the aws-actions/configure-aws-credentials action with role-to-assume, then delete the old access keys.
Protect Production with Environments and Approvals
GitHub environments let you attach rules and secrets to a deployment target. Configure a production environment with required reviewers, available on plans that support it, so a deployment waits for approval, and limit which branches may deploy to it. Because the environment name can be part of the OIDC token, the production IAM role can be restricted to jobs running in that environment.
Add a concurrency group to the deployment job so two releases never run against the same environment at the same time. Overlapping deployments are a frequent source of half-applied changes that are difficult to diagnose.
Choosing the AWS Deployment Target
| Target | Deployment approach | Fits |
|---|---|---|
| Amazon ECS on Fargate | Push the image to ECR, register a new task definition, update the service | Containerised web services and APIs without managing servers |
| Amazon EC2 | Run the deployment on instances through AWS Systems Manager or CodeDeploy | Existing server-based applications and workloads with special requirements |
| S3 and CloudFront | Upload the static build, then invalidate the changed paths | Single-page applications and static websites |
| AWS Lambda | Publish a new version and shift an alias to it | Event-driven functions and lightweight APIs |
For ECS services, enable the deployment circuit breaker with rollback. If new tasks fail their health checks, ECS stops the deployment and returns to the previous task definition automatically. That only works if the health check is meaningful, so it should verify that the application can serve requests, not merely that the process has started.
Handle Database Migrations Safely
During a rolling deployment, old and new versions of the application run at the same time against the same database. A migration that renames or drops a column breaks the old version immediately. Use the expand and contract pattern: add the new structure first in a backward-compatible migration, deploy code that uses it, and remove the old structure in a later release once nothing depends on it.
- Run migrations as a separate pipeline step before the new version receives traffic, not at application start-up on every instance.
- Make each migration backward compatible with the version currently in production.
- Test migrations against a copy of production-sized data, because a change that is instant on a small table can lock a large one for minutes.
- Take or verify a recent backup before migrations that modify existing data.
Make Rollback a Routine Operation
Rollback should mean redeploying the previous artifact, not reverting code and waiting for a new build. Because each image is tagged with its commit SHA, the previous version is already built and tested. Keep a documented, ideally one-step, way to redeploy it, and practise it before an incident requires it. Combined with backward-compatible migrations, this makes most bad releases a few-minute problem.
Keep the Pipeline Itself Secure
- Pin third-party actions to a full commit SHA rather than a mutable tag, so a compromised or changed action cannot alter your pipeline silently.
- Set the default GITHUB_TOKEN permissions to read-only and grant write access per job only where needed.
- Never expose deployment secrets to workflows triggered by pull requests from forks.
- Scan dependencies and container images in the pipeline, and fail the build on critical vulnerabilities that have a fix available.
- Cache dependencies to keep builds fast, but never cache secrets or build outputs that differ by environment.
How to Introduce CI/CD Step by Step
- 1Add lint and unit tests on every pull request and make them a required check before merging.
- 2Containerise the application and build one image per commit on the main branch, tagged with the commit SHA and pushed to Amazon ECR.
- 3Replace stored AWS keys with OIDC roles, one per environment, each restricted to its repository, branch, or environment.
- 4Deploy automatically to staging, then add a production environment with required approval and a concurrency group.
- 5Introduce migration rules, meaningful health checks, and automatic rollback, then rehearse a manual rollback.
- 6Measure deployment frequency, lead time, failure rate, and recovery time, and use them to decide what to improve next.
Key takeaways
- Keep pull request checks fast, and build one artifact per commit that is promoted unchanged from staging to production.
- Use OIDC and per-environment IAM roles instead of long-lived AWS access keys, restricted to the repository and environment.
- Protect production with a GitHub environment, required approval, and a concurrency group to prevent overlapping deployments.
- Write backward-compatible migrations using expand and contract, and run them as a separate step before traffic shifts.
- Make rollback a redeploy of the previous image, and practise it before it is needed.


