It is Friday, 6 PM at a company like Swiggy. An engineer SSHes into a production EC2 instance, pulls the latest code by hand, restarts the service, and hopes nothing breaks over the weekend. There is no record of what changed, no automated test ran before this went live, and if it breaks at 2 AM, the on-call engineer has to reconstruct what happened from memory. This is the exact problem CI/CD exists to solve. Manual deploys have no consistent process, no repeatable verification step, and no fast way back to the last known-good version. **CI/CD** replaces that with an automated pipeline: every change is built, tested, and shipped the same way, every time, with a clear trail of what happened. Manual deploy CI/CD pipeline -------------- --------------- SSH in, pull code by hand Automated build on every push No consistent test step Tests run the same way every time No record of what shipped Full pipeline history and logs Rollback means "remember what to do" Rollback is a defined, repeatable step > **Note:** This module assumes you already know Terraform's core workflow from the IaC module, and that you are comfortable with Docker images and basic AWS services like ECS, EC2, and Lambda at a conceptual level. This module does not re-teach those. ### What you should be able to do after this module * Explain the real difference between continuous integration, continuous delivery, and continuous deployment * Build a GitHub Actions workflow using jobs, steps, and runners * Protect production deploys with GitHub Environments, required reviewers, and secrets * Speed up a pipeline using matrix builds and caching * Build a full container pipeline - build, scan, push, deploy - end to end * Wire Terraform's plan/apply cycle into a pull request workflow with a manual approval gate * Compare rolling, blue-green, canary, and immutable deployment strategies and choose correctly between them * Implement blue-green deployment on EC2 with CodeDeploy, and canary deployment for Lambda -
These three terms get used interchangeably, but they describe three different points on the same spectrum, and knowing which one your team actually practices matters for how much you can trust the pipeline. ### Continuous Integration - merging and verifying code constantly **Continuous Integration (CI)** means every engineer merges code into a shared branch frequently, and an automated pipeline builds and tests it on every merge. The goal is catching integration problems early, while they are small and cheap to fix. ```text Engineer pushes code -> pipeline builds -> pipeline runs tests -> pass/fail reported ``` ### Continuous Delivery - always ready to ship, human decides when **Continuous Delivery** extends CI one step further: every change that passes the pipeline is automatically packaged into a deployable artifact and is always in a releasable state. A human still decides when to actually release it. ### Continuous Deployment - every passing change ships automatically **Continuous Deployment** removes the human decision entirely. Every change that passes the pipeline deploys straight to production with no manual approval step at all. CI Continuous Delivery Continuous Deployment ------------------ ------------------------ ------------------------ Build + test only + always deployable + deploys automatically Human deploys by hand Human decides when to ship No human gate at all > 📌 **Remember:** Most real companies, including large ones like Zerodha and Razorpay, stop at continuous delivery, not continuous deployment. A manual approval gate before production is not a sign of an immature pipeline - for anything with real financial or safety consequences, a deliberate human checkpoint before production is usually the correct choice, not a limitation to eliminate. > 🔴 **Common Mistake:** Calling any automated pipeline "CI/CD" without being specific about where on this spectrum it actually sits. A pipeline that only runs tests is CI. Claiming "we do continuous deployment" when a human actually clicks approve every time overstates what the system does and can mislead incident response planning. -
**GitHub Actions** is GitHub's built-in automation platform - it runs pipelines defined in YAML files whenever an event happens in your repository, like a push or a pull request. ### Workflows, jobs, and steps A **workflow** is a YAML file describing an entire automated process, triggered by an event. A workflow contains one or more **jobs**, which run independently and, by default, in parallel. Each job contains **steps**, which run in order, one after another. ```yaml name: build-and-test on: push: branches: [main] ## runs whenever code is pushed to main jobs: test: runs-on: ubuntu-latest ## the runner - see below steps: - name: Check out code uses: actions/checkout@v4 - name: Install dependencies run: npm install - name: Run tests run: npm test ``` > **Note:** `uses` runs a pre-built, reusable action from GitHub's marketplace - `actions/checkout@v4` is the standard action for pulling your repository's code into the runner. `run` executes a raw shell command directly. Most workflows mix both. ### Runners - where the workflow actually executes A **runner** is the machine that executes your workflow's jobs. GitHub provides free hosted runners (`ubuntu-latest`, `windows-latest`, `macos-latest`), or you can register your own **self-hosted runner** for jobs that need specific hardware, network access to a private VPC, or lower cost at high volume. ```yaml jobs: deploy: runs-on: self-hosted ## runs on your own infrastructure, e.g. inside a VPC ``` > 💡 **Tip:** Start with GitHub-hosted runners. Only move to self-hosted runners once you have a concrete reason - needing private network access to internal resources, specific hardware, or cost savings at a scale where GitHub-hosted minutes get expensive. ### Jobs running in parallel vs in sequence By default, every job in a workflow runs in parallel. Use `needs` to force a job to wait for another to finish first. ```yaml jobs: test: runs-on: ubuntu-latest steps: - run: npm test deploy: needs: test ## waits for the test job to succeed before starting runs-on: ubuntu-latest steps: - run: ./deploy.sh ``` > 🔴 **Common Mistake:** Forgetting `needs` and letting a deploy job run in parallel with the test job. Without it, a deploy can complete before the tests even finish, shipping broken code before the pipeline has told you it is broken. -
### Storing secrets safely **GitHub Secrets** store sensitive values - AWS credentials, API keys, database passwords - encrypted, and expose them to a workflow only as environment variables at runtime, never printed in logs by default. ```yaml steps: - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} ## pulled from repo secrets aws-region: ap-south-1 ``` > ⚠️ **Security:** Prefer OIDC federation over long-lived AWS access keys stored as secrets. With OIDC, GitHub Actions requests short-lived, scoped credentials directly from AWS for each run, so there is no static key sitting in your repository settings that could leak and remain valid indefinitely. ### GitHub Environments - naming and protecting a deploy target A **GitHub Environment** represents a deploy target, like `staging` or `production`, and lets you attach protection rules and environment-specific secrets to it. ```yaml jobs: deploy-prod: runs-on: ubuntu-latest environment: production ## triggers any protection rules configured for "production" steps: - run: ./deploy.sh ``` ### Required reviewers as a production gate Configuring **required reviewers** on an environment means the job pauses and waits for a named person or team to click approve in GitHub's UI before it continues - the same human checkpoint continuous delivery relies on. Push to main | v test job runs | v deploy-staging (environment: staging, no reviewers) --> runs immediately | v deploy-prod (environment: production, required reviewers) --> PAUSED | v Reviewer clicks approve in GitHub UI | v deploy-prod continues > 📌 **Remember:** Configure required reviewers on the `production` environment, not on the whole workflow. This lets staging deploy automatically and fast, while production still gets a deliberate human checkpoint - matching the continuous delivery model most teams actually want. -
### Matrix builds - running the same job across many configurations A **matrix build** runs the same job multiple times with different input values - different Node versions, different operating systems, different regions - without duplicating the job definition. ```yaml jobs: test: strategy: matrix: node-version: [18, 20, 22] ## runs this job three times, once per version runs-on: ubuntu-latest steps: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm test ``` > **Note:** Each combination in a matrix runs as a fully separate, parallel job. Testing against 3 Node versions does not take 3x as long in wall-clock time, since GitHub runs them simultaneously - it just uses 3x the total runner minutes. ### Caching - skipping work that has not changed **Caching** stores files between workflow runs, like installed dependencies, so a later run can reuse them instead of redownloading everything from scratch every time. ```yaml steps: - name: Cache node modules uses: actions/cache@v4 with: path: ~/.npm key: npm-${{ hashFiles('package-lock.json') }} ## cache invalidates if lockfile changes ``` > 💡 **Tip:** Key your cache off a lockfile hash, not a fixed string. If the lockfile changes, the cache key changes too, so you automatically get a fresh cache instead of silently reusing outdated dependencies. > 🔴 **Common Mistake:** Caching so aggressively that a stale cache hides a real dependency problem - a build that only passes because of cached artifacts that would fail on a truly clean install. Periodically running a workflow with caching disabled catches this drift before it reaches production. -
This is the pattern most teams running containers on AWS converge on: build the image, scan it for vulnerabilities, push it to a registry, then deploy it. Push to main | v Build Docker image | v Scan image with Trivy | v Push image to ECR | v Deploy to ECS Fargate ### Building and tagging the image ```yaml - name: Build image run: | docker build -t prod-mumbai-api:${{ github.sha }} . ## tagging with the commit SHA makes every image traceable to exact code ``` > **Note:** Tagging images with `github.sha` (the commit hash) instead of a generic tag like `latest` means you can always trace a running container back to the exact commit that produced it - essential during an incident when you need to know exactly what is deployed. ### Scanning the image with Trivy **Trivy** is an open-source vulnerability scanner that checks a container image's OS packages and application dependencies against known vulnerability databases, before that image ever reaches production. ```yaml - name: Scan image with Trivy uses: aquasecurity/trivy-action@0.24.0 with: image-ref: prod-mumbai-api:${{ github.sha }} severity: CRITICAL,HIGH ## fail the pipeline only on serious findings exit-code: 1 ## non-zero exit fails the workflow step ``` > 🔴 **Common Mistake:** Scanning for every severity level including LOW and MEDIUM and failing the build on all of them. This produces so much noise that engineers start ignoring scan results entirely. Fail the pipeline on CRITICAL and HIGH, and track lower-severity findings separately without blocking every deploy. ### Pushing to ECR **Amazon ECR (Elastic Container Registry)** is AWS's managed Docker registry - the storage location your ECS or Lambda deployment pulls images from. ```yaml - name: Push to ECR run: | aws ecr get-login-password --region ap-south-1 | \ docker login --username AWS --password-stdin ${{ secrets.ECR_REGISTRY }} docker push ${{ secrets.ECR_REGISTRY }}/prod-mumbai-api:${{ github.sha }} ``` ### Deploying to ECS Fargate The final step updates the ECS service to run the new image, triggering ECS's own rolling deployment underneath. ```yaml - name: Deploy to ECS run: | aws ecs update-service \ --cluster prod-mumbai-cluster \ --service api-service \ --force-new-deployment ## tells ECS to pull the newly pushed image tag ``` > **Note:** This assumes the ECS task definition already references the image tag your pipeline just pushed, usually updated as an earlier step in the same job. `--force-new-deployment` tells ECS to start replacing running tasks with new ones based on the current task definition. -
It is Friday, 6 PM at a company like Swiggy. An engineer SSHes into a production EC2 instance, pulls the latest code by ...
These three terms get used interchangeably, but they describe three different points on the same spectrum, and knowing w...
GitHub Actions is GitHub's built-in automation platform - it runs pipelines defined in YAML files whenever an event happ...
Storing secrets safely GitHub Secrets store sensitive values - AWS credentials, API keys, database passwords - encrypted...
Matrix builds - running the same job across many configurations A matrix build runs the same job multiple times with dif...
This is the pattern most teams running containers on AWS converge on: build the image, scan it for vulnerabilities, push...
This builds directly on the Terraform core workflow from the IaC module - fmt, validate, plan, apply - wired into a pull...
Once a pipeline can build and push a new version, the next question is how traffic actually moves from the old version t...
AWS CodeDeploy automates blue-green deployments on EC2, including the traffic shift and rollback, without you scripting ...
Lambda's version of canary deployment uses weighted aliases - an alias that splits invocations between two Lambda versio...
The decision framework Two questions drive most of this decision: how much damage can a bad deploy cause, and how fast d...
Create a basic GitHub Actions workflow. Write a workflow triggered on push to main with one job that checks out code and...
Term What it means Continuous Integration Automated build and test on every merge Continuous Delivery Always deployable,...
Aligns directly with DevOps, Site Reliability (SRE), and Platform Engineering job descriptions.