Lesson 14 · 22 min read

CI/CD with GitHub Actions

Lint, test, build and deploy on every push — automatically, with no one SSH-ing into a server.

What you'll learn in this lesson

  1. Concept: the assembly line for your code
  2. Why manual deploys fail
  3. CI, continuous delivery and continuous deployment
  4. GitHub Actions anatomy
  5. Designing the pipeline
  6. Step 1 — the CI workflow
  7. Step 2 — let GitHub into AWS with no keys (OIDC)
  8. Step 3 — build and push the image
  9. Step 4 — database migrations from CI
  10. Step 5 — roll out and verify
  11. Rollbacks
  12. Making the pipeline safe
  13. Making it fast
  14. When the pipeline fails
  15. What this costs
  16. Interview corner
  17. Practice task before Lesson 15
  18. Conclusion

Concept

CI/CD means continuous integration and continuous delivery (or deployment). In short, a robot does everything between git push and “it is live”. It runs the checks, builds the image, ships it, tests it, and stops if anything is wrong. Vercel did this for you, out of sight. Now you build the same thing yourself on GitHub Actions. This is the automation service built into GitHub. It runs jobs when something happens in your repository, such as a push. You own every step.

Three more words you will meet often. A pipeline is a series of automatic steps that your code passes through on its way to production. A Docker image is a packed file that holds your app and everything it needs to run, so it works the same on every server. A smoke test is a quick check, after a release, that the main pages still work.

The analogy — a factory line with inspectors

A car does not reach the showroom just because someone feels sure it is fine. It moves along a line. The parts are inspected, the car is built and tested, and only then is it shipped. If an inspector rejects the car, the line stops, and no customer ever sees the problem. A pipeline is this line for code, and the tests are the inspectors.

  1. Your laptop
    git push origin main
  2. GitHub receives the commit
    GitHub Actions (CI)
    installs, lints, runs tests, npm run build
  3. any step fails → stop here, nothing deployed
    Docker build
    packages app + Node + deps into one image, pushed to a registry
  4. same image runs on your laptop and on EC2
    Deploy (CD)
    EC2 pulls the image · or Terraform/ASG rolls new servers
  5. health check passes → old version retired
    PM2 / container restarted
    zero-downtime reload
  6. Nginx keeps serving throughout
    Users
    see the new version — minutes after the push, no SSH
From git push to live: the automated pipeline Lessons 9, 14 and 15 build. Lesson 7 does the same thing by hand first, so you understand every step the automation later performs.

Why manual deploys fail

SSH (Secure Shell) is a way to log in to a server from far away and type commands on it. Remember the ~/deploy.sh script from Lesson 7 and the problems we listed. Each of them has caused real outages:

Manual deployWhat eventually happensThe pipeline fixes it by
Someone SSHs in and runs commandsSteps get skipped at 11 p.m., and nobody knows what was doneRunning the same script every time
Tests are “run when we remember”A broken build reaches customersFailing the pipeline before deploy
Build happens on the production serverIt is slow, it competes with live traffic, and it differs from stagingBuilding once in a clean runner
Only two people know how to deployReleases wait, and the team is afraid of deploy dayAnyone merges, and it ships
No record of who deployed what“What changed at 14:00?” has no answerKeeping a history: commit, run, image tag, deployment
Rollback is improvisedRushed decisions under pressureA rehearsed one-click rollback
SSH keys and AWS keys shared aroundLeaks, and former employees who still have accessHaving no humans and no long-lived keys in the process

Teams that release most often tend to break the least. Small, frequent, automatic releases are easier to test, easier to understand, and very easy to undo. They are the opposite of the “large, risky Friday release”.

CI, continuous delivery and continuous deployment

The letters stand for three different levels of automation. Interviewers like to ask about the difference:

TermAutomatesA human decides…
Continuous Integration (CI)On every push/PR (pull request): install, lint, type-check, test, buildWhether to merge
Continuous DeliveryCI, plus it builds a ready-to-release package (an artefact) every timeWhen to release (with a manual approval button)
Continuous DeploymentCI, plus it deploys to production by itself after the checks passNothing. A merge is a release

A pull request (PR) is a request to merge your branch into another branch, so that others can review the change first. Lint is a check that finds style problems and likely mistakes in code. Type-check is a check that finds wrong kinds of values (for example, text where a number is expected) in TypeScript code.

We build continuous delivery with an approval gate. Everything is automatic up to the door of production. There, a GitHub “environment” asks a person to approve. If you remove the gate later, you have continuous deployment.

GitHub Actions anatomy

A workflow is a YAML file in your repository, in the folder .github/workflows/. Git history, code review and rollback work for your pipeline in the same way as for your code.

WordMeaningExample
WorkflowOne YAML file (a text file that uses indentation) in .github/workflows/. It describes one complete automated processci.yml, deploy.yml
Event (on:)What starts the workflowpush, pull_request, workflow_dispatch (a manual button), schedule (a timer)
JobA group of steps that runs on one fresh machine. Jobs run at the same time, unless one job needs anothertest, build, deploy
RunnerThe machine that runs a job. GitHub provides them. Each job gets a clean one, and it is deleted afterwardsubuntu-latest
StepOne command (run:) or one ready-made action (uses:)pnpm install
ActionA packaged step, written by someone else, that you can reuseactions/checkout@v4
Secret / variableStored values. Secrets are encrypted and hidden in logs. Variables are plain textsecrets.SOME_TOKEN, vars.AWS_REGION
EnvironmentA named target (such as production) with its own secrets, protection rules and people who approveenvironment: production
Artifact / cacheAn artifact is a file passed between jobs. A cache is a saved folder that makes later runs fasternode_modules cache

Here is the smallest useful workflow. It shows the shape before we look at the real ones:

.github/workflows/hello.yml
name: Hello
on: push                         # the event

jobs:
  say-hello:                     # a job
    runs-on: ubuntu-latest       # its fresh machine
    steps:
      - uses: actions/checkout@v4          # step 1: an action that downloads your repo
      - run: echo "Commit ${{ github.sha }} by ${{ github.actor }}"   # step 2: a shell command

${{ … }} is an expression. It reads values from the run, such as github.sha (the commit ID), secrets.X or needs.job.outputs.Y, before the step runs. YAML cares about spaces. Use two spaces for each indent and never use tabs. Wrong spaces are the number-one reason for “my workflow does nothing”.

Designing the pipeline

We use two workflows with two triggers, and they share one set of checks:

TriggerWhat runsTouches AWS?
Pull requestCI: lint, types, tests, buildNo. It is safe even for forks (a fork is someone else's copy of your repo)
Merge to mainCI → build & push image → (approval) → migrate → roll out → smoke testYes, through a role that works for a short time
Manual buttonRollback to a previous image tagYes

Pull requests never get AWS access. This split is the main security rule: code that nobody has reviewed must not be able to touch production. Combine it with branch protection (rules that stop changes from reaching main without a review and passing checks). See GitHub Access Control — The Same Idea as IAM.

Step 1 — the CI workflow

We write it as a reusable workflow (workflow_call). This is a workflow that other workflows can call. So the pull-request check and the deploy pipeline run exactly the same checks. What we test and what we ship cannot drift apart.

.github/workflows/ci.yml
name: CI

on:
  pull_request:
  workflow_call:                 # lets deploy.yml reuse this file

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true       # a new push cancels the now-pointless older run

permissions:
  contents: read                 # least privilege: the built-in token gets only what it needs

jobs:
  checks:
    runs-on: ubuntu-latest
    timeout-minutes: 15          # a stuck job must not use up minutes for 6 hours
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4          # version comes from "packageManager" in package.json

      - uses: actions/setup-node@v4
        with:
          node-version: 22                  # keep identical to your Dockerfile and laptop
          cache: pnpm                       # caches the pnpm store between runs

      - run: pnpm install --frozen-lockfile # fails if the lockfile is out of date (good: the lockfile pins exact versions)
      - run: pnpm lint
      - run: pnpm exec tsc --noEmit
      - run: pnpm test --if-present
      - run: pnpm build
        env:
          NEXT_PUBLIC_API_URL: https://yourapp.com

Commit it on a branch, open a pull request, and watch the checks appear at the bottom of the PR. Then go to the repository's Settings → Branches (or Rules → Rulesets, in newer versions of GitHub) and require the checks job to pass before merging. A check that nobody must obey is only a suggestion.

Step 2 — let GitHub into AWS with no keys (OIDC)

The pipeline must call AWS to push images and roll out servers. The tempting way is to create an IAM user, make an access key, and paste it into GitHub Secrets. Lesson 5 explained why this is a bad habit. (IAM, Identity and Access Management, is the AWS service that controls who may do what. An IAM role is an identity with a set of permissions that a person or program can take on for a short time. ECR, Elastic Container Registry, is the AWS store for Docker images.) The key lives forever. Any compromised package in the pipeline can steal it. And it works from anywhere.

The modern answer: OpenID Connect

OpenID Connect (OIDC) is a standard way for one service to prove an identity to another. GitHub can give each job a short-lived signed identity token. The token says “I am the workflow run for repo X on branch Y”. You tell AWS to trust GitHub as an identity provider, and to let that token assume (take on) one specific IAM role. The job then gets temporary credentials that expire after about an hour. There is no secret to store, change or leak. This is Lesson 5's role idea, extended to GitHub.

  1. Tell AWS to trust GitHub (once per account)

    Go to IAM → Identity providers → Add provider → OpenID Connect. Use the provider URL https://token.actions.githubusercontent.com and the audience sts.amazonaws.com. This is free, and you do it only once.

  2. Create the role, with a trust policy that ties it to your repository

    This is the most important part for security. A trust policy says who may take on the role. Its sub (subject) condition decides exactly which workflow runs may become this role. Without it, any repository on GitHub could use your role.

    github-trust.json
    {
      "Version": "2012-10-17",
      "Statement": [{
        "Effect": "Allow",
        "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
        "Action": "sts:AssumeRoleWithWebIdentity",
        "Condition": {
          "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
          "StringLike": {
            "token.actions.githubusercontent.com:sub": [
              "repo:YOUR-ORG/myapp:ref:refs/heads/main",
              "repo:YOUR-ORG/myapp:environment:production"
            ]
          }
        }
      }]
    }
    Why there are two subjects

    The sub in the token depends on how the job runs. A job with no environment: has …:ref:refs/heads/main. A job that declares environment: production has …:environment:production instead. Our build job is the first kind and the deploy job is the second kind, so the role must trust both. Forgetting one is the most common OIDC error (“Not authorized to perform sts:AssumeRoleWithWebIdentity”).

    In the console, go to IAM → Roles → Create role → Web identity. Pick the GitHub provider and fill in your organisation, repository and branch. The console writes most of the trust policy for you. Name the role github-deploy, then edit the trust policy to add the environment:production line.

  3. Give it only the permissions the pipeline needs

    Attach an inline policy (a policy stored inside the role) that allows exactly the deploy and nothing else:

    • log in to ECR and push images to the myapp repository;
    • upload to _next/static/* in the assets bucket (Lesson 11);
    • update the one parameter /myapp/image-tag;
    • start and watch an instance refresh on the Auto Scaling group (an Auto Scaling group is an AWS feature that keeps a set of servers running. An instance refresh replaces those servers with new ones, a few at a time);
    • send an SSM command to the app servers (for migrations, Step 4).

    Notice what is missing: no IAM permissions, no ec2:*, and no way to read the database or delete anything. If someone ever abused this role, the damage (the “blast radius”) would be limited to “deploy a new image”. Allowing SendCommand on * is a simplification. In a strict setup, limit it with a condition to servers that have the tag app=myapp.

Step 3 — build and push the image

This is the deploy workflow. It calls the CI workflow first. Then it builds the image, pushes it, and rolls it out. Read the settings at the top. Each one protects you from a specific failure. (concurrency means how many runs may go at the same time. A runner is the machine that runs a job.)

.github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

concurrency:
  group: deploy-production       # only ONE deploy runs at a time
  cancel-in-progress: false      # never stop a deploy that is half done

permissions:
  id-token: write                # allow the job to request an OIDC token
  contents: read

jobs:
  ci:
    uses: ./.github/workflows/ci.yml       # the same checks as a pull request

  build:
    needs: ci                              # nothing is built unless the checks pass
    runs-on: ubuntu-latest
    outputs:
      tag: ${{ steps.meta.outputs.tag }}
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-deploy
          aws-region: ap-south-1
      - id: ecr
        uses: aws-actions/amazon-ecr-login@v2
      - id: meta
        run: echo "tag=$(git rev-parse --short=12 HEAD)" >> "$GITHUB_OUTPUT"
      - uses: docker/setup-buildx-action@v3     # needed for the type=gha cache to work
      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: ${{ steps.ecr.outputs.registry }}/myapp:${{ steps.meta.outputs.tag }}
          cache-from: type=gha             # reuse Docker layers from earlier runs
          cache-to: type=gha,mode=max
      # then: copy .next/static out of the image and sync it to S3 (Lesson 11)

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production                # waits for approval; it also changes the OIDC subject
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-deploy
          aws-region: ap-south-1
      # 1. run database migrations inside the VPC (Step 4)
      # 2. set /myapp/image-tag to the new tag and start an instance refresh (Lesson 12)
      # 3. wait for the refresh, then curl https://yourapp.com/api/health — fail the run if it is down
LineWhy it matters
needs: ciThe pipeline stops at the first failed check. Tests are a gate, not only a report.
permissions: id-token: writeWithout it, GitHub does not issue the OIDC token, and taking on the role fails.
concurrency … cancel-in-progress: falseTwo merges in quick succession wait in line instead of racing each other into production. (GitHub keeps only the newest waiting run.)
tag = short commit SHAA commit SHA is the unique ID of one Git commit, and a tag is a label on an image. Every image can be traced to its exact code, and a rollback means “deploy that tag”.
cache-from: type=ghaDocker layers are saved in GitHub's cache, so a small change rebuilds in seconds (the layer order from Lesson 9 pays off here). It needs the buildx step above.
Assets synced before the rolloutNew HTML never points to a hashed file that is not yet on the CDN (Lesson 11).
Where NEXT_PUBLIC_ values come from

These values are built into the app at build time (Lesson 7). So CI must give them to the build as build-args. Take them from GitHub Variables, because these values are public anyway. Never pass a real secret as a build arg, because build args can be seen in the image history. Secrets that are needed at run time stay in SSM, and the servers read them when they boot.

Step 4 — database migrations from CI

Almost every team meets this problem. A migration is a script that changes the structure of the database. The runner is on GitHub's network. The database (RDS, the AWS managed database service) is in a private subnet (Lesson 8). A private subnet is a part of your network with no direct route from the internet. So, by design, the runner cannot reach it. Prisma is a database tool for Node apps, and prisma migrate deploy is its command that applies waiting migrations. It cannot connect from the runner. There are three ways to solve this:

ApproachHowVerdict
Open the DB to the internetAllow GitHub's IP rangesNever. It cancels what Lesson 8 did
Run migrations inside the VPC via SSMTell one server to run the migrate command of the new image, with its own env variablesOur choice: it needs nothing new
A self-hosted runner in the VPC, or a one-time ECS taskThe job runs on a machine that can reach RDSGood for bigger projects

AWS Systems Manager Run Command runs a shell command on a server without SSH and without an open port. A small agent program on the server contacts AWS and asks for work. The servers need the managed policy AmazonSSMManagedInstanceCore on their role (add it to myapp-ec2-role).

The migration step finds one running app server. Then it uses send-command to make that server log in to ECR and run the new image once, with the server's own env file and the command prisma migrate deploy. The step waits for the result. If the migration fails, the deploy fails, before any server switches to the new code.

The migration runs before the new servers start, on the same database that the old servers still use. This is why the “expand then contract” rule from Lesson 8 matters. First you add new things. Later you remove the old things. The old code must keep working with the new schema (the database structure) for the few minutes of the rollout. Also, the image must contain the migration tool and its files. If you build a small standalone image, either include the prisma CLI and the schema in it, or use a second Dockerfile target for migrations.

Rollbacks

Every release is an image that never changes, and it has a tag. So a rollback is not a rebuild. It means “point the servers at the previous tag”. Give the team a button that does this.

Make a small workflow called rollback.yml. You start it by hand from the Actions tab (workflow_dispatch) and give it one input: the tag to go back to. It does only the last two moves of the deploy. It writes that tag to /myapp/image-tag and starts an instance refresh. It uses the same role and the same production environment, so it needs the same approval.

Code rolls back; data does not

Going back to an old image is easy. Going back after a migration has dropped a column is not. This is why migrations should only add things and stay backward compatible. It is also why you should take an RDS snapshot (a saved copy of the database) before any migration that removes data. For risky releases, add aws rds create-db-snapshot as a pipeline step before the migrate step.

Making the pipeline safe

A pipeline that can deploy to production is also a very attractive target for an attacker. Treat it like part of production:

  • Approval gate. Go to Settings → Environments → production → Required reviewers. The deploy job waits, and the reviewer sees exactly what will ship. Also limit the environment to the main branch.
  • Least-privilege token. Least privilege means giving only the permissions that are needed. Set permissions: in every workflow (as above). The default GITHUB_TOKEN can have more power than you need.
  • Never show secrets to untrusted code. Pull requests from forks do not get secrets. Keep it that way. Avoid pull_request_target (an event that runs with secrets) together with a checkout of the PR's code. This is a classic way to lose secrets.
  • Pin third-party actions. A line such as uses: some/action@v4 follows a tag that the author can move. For anything that touches your AWS role, pin the action to a full commit SHA (the exact ID of one version). Let Dependabot (a GitHub tool that opens pull requests to update dependencies) suggest updates. A popular action that was hacked has exposed secrets in many repositories before.
  • Never print secrets. GitHub hides the secrets it knows in logs. But a value that you change (base64, split into parts, JSON-escaped) can get through. Do not echo secrets.
  • Limit the OIDC trust to the branch and environment, as we did. A role that trusts “any workflow in this repo” lets any branch or PR deploy.
  • Protect the workflow files. Add a .github/CODEOWNERS file. It lists who must review changes to certain files. Require a review for .github/workflows/**. Editing the pipeline is the same as editing access to production.

To debug an OIDC failure, add a temporary step that prints the claims (the facts) in the token. Compare its sub with the condition in your trust policy. Remove the step afterwards.

Making it fast

People trust a pipeline that is fast. Aim for under ten minutes from start to finish, and under five minutes for the PR check.

  • Cache dependencies (setup-node with cache: pnpm) and Docker layers (type=gha). This is usually the biggest gain.
  • Run independent work in parallel. Split lint, types and tests into separate jobs. They start together on separate runners.
  • Cancel old runs for pull requests (cancel-in-progress: true on CI, but false for deploys).
  • Skip work that is not needed. Use path filters when a change only touches docs.
  • Set timeout-minutes, so a stuck job cannot use up your free minutes.

When the pipeline fails

SymptomLikely causeFix
Not authorized to perform sts:AssumeRoleWithWebIdentityThe sub condition in the trust policy does not match the run (wrong repo, branch or environment), or permissions: id-token: write is missingPrint the token claims (see below), then fix the condition. Also check the permissions block of the workflow
denied: User … is not authorized to perform ecr:…The deploy role does not have an ECR permissionAdd the missing action to the role's policy. CloudTrail (the AWS activity log) shows the exact action that was denied
pnpm install fails with frozen-lockfile errorpackage.json and the lockfile do not matchRun pnpm install on your machine and commit the updated lockfile
Build passes locally, fails in CIA different Node version, a missing env variable, or a file name with different upper and lower case letters (macOS accepts it, Linux does not)Use the same node-version as on your machine. Set the build env variables. Check the upper and lower case of your imports
Workflow does not startA YAML error, a wrong branch filter, or (for manual or scheduled runs) the file is not on the default branchThe Actions tab shows a parse error. Check the file with actionlint, a tool that finds mistakes in workflow files
Instance refresh ends as Failed (and rolls back, if automatic rollback is turned on)The new servers did not pass the ALB health checksRead the ASG activity history and the target health reasons. Run docker logs on a failed server. Often the cause is a bad env variable or a bad migration
Deploy step hangsThe job is waiting for an approval, or a command has no time limitCheck the pending review of the environment. Add timeout-minutes to jobs
Two deploys collideThere is no concurrency controlUse a concurrency group with cancel-in-progress: false, so that deploys run one after the other

Here is a general method. Open the failed run and open the red step. Read the first error, because later errors are usually only a result of it. Repeat the problem on your machine with the same commands, and fix the cause. For AWS permission errors, CloudTrail (Lesson 18) records the exact action that was denied.

What this costs

ItemApprox. price
GitHub Actions, public reposFree
GitHub Actions, private repos2,000 Linux minutes per month on the Free plan (3,000 on Pro). After that, about $0.006–$0.008 per minute (check the current price)
OIDC role, SSM, instance refreshFree
ECR storage~$0.10 per GB-month (add a lifecycle rule to keep only the last ~20 images)
Data pulled from ECR into EC2, same regionFree

A ten-minute pipeline that runs ten times a day uses about 3,000 minutes a month. So a private project can go over the free amount. At that point, caching and cancelling old runs are no longer just nice. They save money.

Interview corner

Continuous integration vs continuous delivery vs continuous deployment?

CI means changes are merged often and checked by an automatic build and tests. Continuous delivery means every build that passes is ready to release, and a person approves the release. Continuous deployment removes the approval. Every change that passes goes to production automatically.

How does your pipeline authenticate to AWS?

With OIDC. GitHub gives each run a short-lived signed token. An IAM role trusts the GitHub OIDC provider, with conditions on the audience and the subject (repo, branch or environment). The job swaps the token for temporary credentials. No access keys are stored anywhere.

Your CI runner cannot reach the private database to migrate. Options?

I never open the database to the internet. I run the migrations inside the VPC. I can use SSM Run Command on a server, a one-time ECS task, or a self-hosted runner. I use the new image and run the migration before I roll out the servers. The migrations must stay backward compatible.

How do you roll back a bad release?

I deploy the previous image tag again. I update the parameter and run an instance refresh, or I run the pipeline again for the old commit. Data changes cannot simply be undone. They need a new fix going forward, or a restore. This is why migrations should only add things, and why a risky one is preceded by a snapshot.

What are the security risks of a CI/CD pipeline and how do you reduce them?

The risks are: stolen long-lived credentials (the answer is OIDC), untrusted PR code that reaches secrets (give no secrets to forks, avoid pull_request_target), hacked third-party actions (pin them to SHAs), roles with too much power (least privilege), and workflow edits that nobody reviewed (CODEOWNERS, branch protection, environment approvals).

Why tag images with the commit SHA instead of latest?

There are two reasons: you can trace it, and it is predictable. The tag shows exactly which code is running. Servers can never end up on different versions. A rollback means deploying an earlier tag. The latest tag can change at any time and does not say which version it is.

Why is concurrency control important for deployments?

Two deploys that overlap can mix their migrations and rollouts and leave the system in a mixed state. A concurrency group makes them run one after the other. Not cancelling a deploy that is running avoids leaving it half done.


Practice task before Lesson 15

  1. Add ci.yml. Open a pull request with a deliberate lint error and watch the check fail; fix it and watch it pass. Turn on required checks for main.
  2. Create the OIDC provider, the github-deploy role with the trust and least-privilege policies, and a production environment with a required reviewer (yourself).
  3. Add deploy.yml. Merge a small visible change (a text on the home page) and watch: checks → build → approval → migrate → rollout → smoke test → the change is live with no SSH.
  4. Break it on purpose: change the trust policy's repo name and read the error, then fix it. Print the OIDC claims with the debug step to see what sub looks like.
  5. Add a bug that fails the health check in a new release. Watch the instance refresh fail (or the smoke test fail) while the old servers keep serving. Then run the rollback workflow.
  6. Add an ECR lifecycle policy that keeps only the 20 most recent images, and add .github/CODEOWNERS covering the workflows folder.
Optional stretch

Add a pull_request job that builds the Docker image (without pushing) so a broken Dockerfile is caught in review, and a Dependabot config (.github/dependabot.yml) that opens weekly PRs for npm packages and GitHub Actions.

Conclusion

Every release is now the same list of automatic steps. It has a record, an approval gate and a rollback. And nobody needs SSH keys or AWS keys to make it happen.

  • CI checks every PR with the same commands that the deploy uses (a reusable workflow). Branch protection makes the checks mandatory.
  • OIDC, not access keys. The credentials are short-lived. The role is limited to your repo, branch and environment, and has only the permissions that the pipeline needs.
  • Build once, tag with the SHA, ship the artefact. Send assets to S3 first, run migrations from inside the VPC, then do a rolling refresh that waits for health checks, and finish with a smoke test (a quick check that the main pages work).
  • Safety is part of the design. Use approvals, concurrency control, pinned actions, explicit permissions, and reviews of workflow changes.
  • A rollback is a button that you have practised. Migrations stay backward compatible, so the rollback works.

The application is now automated. The infrastructure is still a set of commands that you typed by hand. Next, we describe all of it as code with Terraform.


End of Lesson 14. Next: Lesson 15 — Terraform: Infrastructure as Code.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series