dora.ed
← Projects
03 / CI/CD & Automation

Dual-Track CI/CD Delivery Pipeline

A GitHub Actions pipeline that runs quality gates on every push and deploys via two independent paths: a static Next.js export to AWS S3 + CloudFront, and a containerised build to Kubernetes. IAM OIDC provides keyless AWS authentication — no long-lived credentials stored anywhere.

Pipeline flow
git pushtriggerCIlinttype-checktestbuildsec scanStatic Deploynext build → out/aws s3 sync + invalidateS3 + CDNCloudFrontcache invalidatedK8s Deploydocker build + pushkubectl apply / helm upgradeK8s Clusterrolling updatezero downtimeIAM OIDCkeyless auth
Problem

Manual deployments are slow, error-prone, and don't scale. Storing AWS credentials as long-lived secrets is a security risk. Running no quality gates means bugs and type errors reach production.

Solution

Every push triggers a full CI run. On success, two independent deploy jobs run in parallel — one for the static path, one for Kubernetes. IAM OIDC means no AWS credentials are stored anywhere in GitHub.

Pipeline stages
lint

ESLint checks code quality and catches common errors before the build.

type-check

TypeScript compiler validates all types — no any leaks, no runtime surprises.

test

Unit and integration tests run against the application code.

build

next build runs the production build and catches build-time errors.

security scan

npm audit checks for known vulnerabilities in the dependency tree.

deploy (parallel)

Static path and Kubernetes path run simultaneously — independent failures, faster delivery.

Engineering decisions
Why IAM OIDC over long-lived access keys

Static AWS access keys stored in GitHub Secrets are a major security risk — they never expire, can be leaked in logs, and require manual rotation. IAM OIDC lets GitHub Actions assume an AWS role dynamically for each workflow run using short-lived tokens. No keys to rotate, no secrets to leak.

Why two separate deploy jobs, not one

The static S3 deploy and the Kubernetes deploy are independent concerns. Separating them means a Kubernetes cluster issue won't block the static deployment, and each path can be enabled or disabled without modifying shared logic. Each job also has its own IAM permissions — least-privilege by design.

Why security scanning in CI, not as a separate tool

Shifting security left means catching issues before they reach production. Running a dependency audit and static analysis in every PR build ensures no new vulnerabilities are introduced silently. It also creates a clear audit trail in the GitHub Actions log.

Why CloudFront invalidation is part of the deploy job

Without cache invalidation, CloudFront may serve stale files for up to 24 hours after a deployment. Running aws cloudfront create-invalidation at the end of every deploy ensures users always get the latest version immediately.

Lessons learned
01

IAM OIDC trust policies must include the exact GitHub repo and branch in the condition — too permissive and any repo can assume your role, too strict and your own workflow breaks.

02

CloudFront cache invalidation is not instant — it takes 30–60 seconds. Build your deploy job to trigger it and not wait for confirmation to avoid slowing down the pipeline.

03

Separating CI from CD in different jobs makes it easy to re-run just the deploy without re-running tests — useful when a deploy fails due to an infrastructure issue, not a code issue.

04

Caching node_modules in GitHub Actions using actions/cache can cut CI time by 60–80% on subsequent runs by avoiding a full npm install each time.

Tech stack
GitHub ActionsIAM OIDCAWS S3CloudFrontDockerKubernetesTypeScriptESLintNext.jsTerraform
View repository ↗