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
- Concept: the assembly line for your code
- Why manual deploys fail
- CI, continuous delivery and continuous deployment
- GitHub Actions anatomy
- Designing the pipeline
- Step 1 — the CI workflow
- Step 2 — let GitHub into AWS with no keys (OIDC)
- Step 3 — build and push the image
- Step 4 — database migrations from CI
- Step 5 — roll out and verify
- Rollbacks
- Making the pipeline safe
- Making it fast
- When the pipeline fails
- What this costs
- Interview corner
- Practice task before Lesson 15
- 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.
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.
- Your laptopgit push origin main
- GitHub receives the commitGitHub Actions (CI)installs, lints, runs tests, npm run build
- any step fails → stop here, nothing deployedDocker buildpackages app + Node + deps into one image, pushed to a registry
- same image runs on your laptop and on EC2Deploy (CD)EC2 pulls the image · or Terraform/ASG rolls new servers
- health check passes → old version retiredPM2 / container restartedzero-downtime reload
- Nginx keeps serving throughoutUserssee the new version — minutes after the push, no SSH
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 deploy | What eventually happens | The pipeline fixes it by |
|---|---|---|
| Someone SSHs in and runs commands | Steps get skipped at 11 p.m., and nobody knows what was done | Running the same script every time |
| Tests are “run when we remember” | A broken build reaches customers | Failing the pipeline before deploy |
| Build happens on the production server | It is slow, it competes with live traffic, and it differs from staging | Building once in a clean runner |
| Only two people know how to deploy | Releases wait, and the team is afraid of deploy day | Anyone merges, and it ships |
| No record of who deployed what | “What changed at 14:00?” has no answer | Keeping a history: commit, run, image tag, deployment |
| Rollback is improvised | Rushed decisions under pressure | A rehearsed one-click rollback |
| SSH keys and AWS keys shared around | Leaks, and former employees who still have access | Having 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:
| Term | Automates | A human decides… |
|---|---|---|
| Continuous Integration (CI) | On every push/PR (pull request): install, lint, type-check, test, build | Whether to merge |
| Continuous Delivery | CI, plus it builds a ready-to-release package (an artefact) every time | When to release (with a manual approval button) |
| Continuous Deployment | CI, plus it deploys to production by itself after the checks pass | Nothing. 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.
| Word | Meaning | Example |
|---|---|---|
| Workflow | One YAML file (a text file that uses indentation) in .github/workflows/. It describes one complete automated process | ci.yml, deploy.yml |
| Event (on:) | What starts the workflow | push, pull_request, workflow_dispatch (a manual button), schedule (a timer) |
| Job | A group of steps that runs on one fresh machine. Jobs run at the same time, unless one job needs another | test, build, deploy |
| Runner | The machine that runs a job. GitHub provides them. Each job gets a clean one, and it is deleted afterwards | ubuntu-latest |
| Step | One command (run:) or one ready-made action (uses:) | pnpm install |
| Action | A packaged step, written by someone else, that you can reuse | actions/checkout@v4 |
| Secret / variable | Stored values. Secrets are encrypted and hidden in logs. Variables are plain text | secrets.SOME_TOKEN, vars.AWS_REGION |
| Environment | A named target (such as production) with its own secrets, protection rules and people who approve | environment: production |
| Artifact / cache | An artifact is a file passed between jobs. A cache is a saved folder that makes later runs faster | node_modules cache |
Here is the smallest useful workflow. It shows the shape before we look at the real ones:
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:
| Trigger | What runs | Touches AWS? |
|---|---|---|
| Pull request | CI: lint, types, tests, build | No. It is safe even for forks (a fork is someone else's copy of your repo) |
| Merge to main | CI → build & push image → (approval) → migrate → roll out → smoke test | Yes, through a role that works for a short time |
| Manual button | Rollback to a previous image tag | Yes |
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.
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.comCommit 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.
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.
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.comand the audiencests.amazonaws.com. This is free, and you do it only once.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 subjectsThe
subin the token depends on how the job runs. A job with noenvironment:has…:ref:refs/heads/main. A job that declaresenvironment: productionhas…:environment:productioninstead. 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 theenvironment:productionline.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
myapprepository; - 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”. AllowingSendCommandon*is a simplification. In a strict setup, limit it with a condition to servers that have the tagapp=myapp.- log in to ECR and push images to the
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.)
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| Line | Why it matters |
|---|---|
needs: ci | The pipeline stops at the first failed check. Tests are a gate, not only a report. |
permissions: id-token: write | Without it, GitHub does not issue the OIDC token, and taking on the role fails. |
concurrency … cancel-in-progress: false | Two merges in quick succession wait in line instead of racing each other into production. (GitHub keeps only the newest waiting run.) |
| tag = short commit SHA | A 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=gha | Docker 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 rollout | New HTML never points to a hashed file that is not yet on the CDN (Lesson 11). |
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:
| Approach | How | Verdict |
|---|---|---|
| Open the DB to the internet | Allow GitHub's IP ranges | Never. It cancels what Lesson 8 did |
| Run migrations inside the VPC via SSM | Tell one server to run the migrate command of the new image, with its own env variables | Our choice: it needs nothing new |
| A self-hosted runner in the VPC, or a one-time ECS task | The job runs on a machine that can reach RDS | Good 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.
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
mainbranch. - Least-privilege token. Least privilege means giving only the permissions that are needed. Set
permissions:in every workflow (as above). The defaultGITHUB_TOKENcan 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@v4follows 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
echosecrets. - 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/CODEOWNERSfile. 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-nodewithcache: 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: trueon 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
| Symptom | Likely cause | Fix |
|---|---|---|
Not authorized to perform sts:AssumeRoleWithWebIdentity | The sub condition in the trust policy does not match the run (wrong repo, branch or environment), or permissions: id-token: write is missing | Print 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 permission | Add 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 error | package.json and the lockfile do not match | Run pnpm install on your machine and commit the updated lockfile |
Build passes locally, fails in CI | A 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 start | A YAML error, a wrong branch filter, or (for manual or scheduled runs) the file is not on the default branch | The 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 checks | Read 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 hangs | The job is waiting for an approval, or a command has no time limit | Check the pending review of the environment. Add timeout-minutes to jobs |
Two deploys collide | There is no concurrency control | Use 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
| Item | Approx. price |
|---|---|
| GitHub Actions, public repos | Free |
| GitHub Actions, private repos | 2,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 refresh | Free |
| 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 region | Free |
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
- 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 formain. - Create the OIDC provider, the
github-deployrole with the trust and least-privilege policies, and aproductionenvironment with a required reviewer (yourself). - 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. - 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
sublooks like. - 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.
- Add an ECR lifecycle policy that keeps only the 20 most recent images, and add
.github/CODEOWNERScovering the workflows folder.
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
- Git and GitHub Guide: Commits, Branches, Pull Requests
- DevOps and AWS Guide: From Git Push to Production
Related series