This is the second article in our series on building secure AWS deployment pipelines. In the previous article, we explored why validation-first pipelines matter. Now we’ll implement the technical foundation: GitHub Rule Sets and parallel validation workflows.
Building Parallel Validation Pipelines
The Problem: Monolithic Workflows Don’t Scale
Our original approach used monolithic workflow files—one for develop
branch PRs and another for main
branch deployments. Each workflow would sequentially build code, run tests, package assets, and deploy everything with CDK. This worked for a single project, but became inefficient as our monorepo grew.
The problems were clear:
- Slow feedback: Developers waited 16+ minutes to know if their PR was valid
- Resource waste: A single failing test would block deployment unnecessarily
- Merge bottlenecks: Only one validation could run at a time
The Solution: Split CI from CD
I redesigned the pipeline around two core principles:
- Continuous Integration (CI): Fast validation that runs on PR creation
- Continuous Deployment (CD): Deployment that only runs after PR approval and merge
NEW: Split Pipeline with Parallel Validation
┌─────────────────────────────┐
│ PR Created │
│ ↓ │
│ ┌─────────────────────────┐ │
│ │ CI Pipeline │ │ ← Parallel validation
│ │ │ │
│ │ IaC │ Server │ Webapp │ │ ← Each project
│ │ Build │ Build │ Build │ │ independently
│ │ Test │ Test │ Test │ │
│ └─────────────────────────┘ │
│ PR Created │
│ Status Checks │ ← GitHub Rule Sets
│ ↓ │ verify all pass
│ PR Approved │
│ ↓ │
│ PR Merged │
│ ↓ │
│ CD Pipeline │ ← Deploy only after
│ (Deploy) │ validation passes
└─────────────────────────────┘
Total CI Time: ~2 minutes (parallel)
Implementing GitHub Rule Sets
To make this work, I needed GitHub to understand that a PR is only ready for merge when all project validations pass. This is where Rule Sets shine—they can monitor multiple status checks simultaneously.
{
"name": "Core Branch Protection",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["refs/heads/main", "refs/heads/develop"],
"exclude": []
}
},
"rules": [
{ "type": "pull_request" },
{
"type": "required_status_checks",
"parameters": {
"required_status_checks": [
{
"context": "CI Checks (IaC, cd serverless && npm run build && npm test)"
},
{
"context": "CI Checks (Serverless, cd serverless && npm run build && npm test)"
},
{
"context": "CI Checks (Webapp, cd serverless && npm run build && npm test)"
}
],
"strict_required_status_checks_policy": true
}
},
{ "type": "deletion" },
{ "type": "non_fast_forward" }
],
"bypass_actors": [{ "actor_type": "RepositoryRole", "actor_id": 5 }]
}
💡 Key Insight: Rule Sets are more powerful than legacy Branch Protection because one set can cover multiple branches (
main
anddevelop
) with the same rules.
Setting Up Rule Sets
Unlike GitHub Action workflows, Rule Sets require manual setup via the GitHub REST API:
# Apply the rule set using GitHub CLI
gh api repos/{owner}/{repo}/rulesets
--method POST
--input ruleset.json
Important considerations:
- Create separate rule sets for different responsibilities (core protection vs. review requirements)
- Define Rule Sets as JSON files in your repository for version control
- Allow repository administrators to bypass rules during development (
actor_id: 5
) - Use
strict_required_status_checks_policy: true
to ensure checks run on the latest code
The Matrix Strategy: Parallel Validation
The magic happens in the CI workflow using GitHub’s Matrix Strategy. Instead of running validations sequentially, each project gets its own parallel job:
name: CI/CD Pipeline
on:
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:
permissions:
contents: read
checks: write
pull-requests: read
jobs:
setup:
name: Setup Dependencies
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install all dependencies
run: npm ci
- name: Cache workspace with dependencies
uses: actions/cache/save@v4
with:
path: .
key: node-modules-${{ runner.os }}-${{ github.sha }}
ci-checks:
name: CI Checks
runs-on: ubuntu-latest
needs: [setup]
strategy:
matrix:
task:
- { task: "IaC", command: "cd iac && npm run build && npm test" }
- {
task: "Serverless",
command: "cd serverless && npm run build && npm test",
}
- {
task: "Webapp",
command: "cd webapp && npm run build && npm test",
}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Cache node_modules
uses: actions/cache@v4
with:
path: .
key: node-modules-${{ runner.os }}-${{ github.sha }}
restore-keys: |
node-modules-${{ runner.os }}-
- name: ${{ matrix.task }}
run: ${{ matrix.command }}
cleanup:
name: Cache Cleanup
runs-on: ubuntu-latest
needs: [ci-checks]
if: always()
steps:
- name: Delete workflow cache
run: |
gh cache delete "node-modules-${{ runner.os }}-${{ github.sha }}" --repo ${{ github.repository }} || echo "Cache not found or already deleted"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
How the Matrix Strategy Works
- A single job installs all the dependencies and saves to the GitHub cache—Remember this a monorepo with NPM Workspaces so one
npm ci
is enough for the entire project - The GitHub matrix spawns 3 parallel jobs
- Each job restores the cache, runs its command, and reports its status
- The last job removes the cache to prevent accumulation
Each project now appears as a separate status check in the PR, making it immediately clear which component has issues:
PR Status Checks:
✅ CI Checks (IaC, cd serverless && npm run build && npm test)
✅ CI Checks (Serverless, cd serverless && npm run build && npm test)
❌ CI Checks (Webapp, cd serverless && npm run build && npm test) ← Clear failure point
Cache Management Strategy
GitHub Actions cache doesn’t support TTL (time-to-live), so I implemented manual cleanup:
- Setup job: Creates cache with unique key node-modules-{OS}-{SHA}
- Matrix jobs: Restore from cache, run in parallel (1-2 minutes each)
- Cleanup job: Deletes cache to prevent accumulation
⚠️ Why cleanup matters: Without cleanup, caches accumulate and can hit GitHub’s storage limits. Each PR creates a unique cache that would otherwise persist.
Key Takeaways
- Split CI from CD: Validate quickly, deploy only after approval
- Use Rule Sets: More flexible than legacy branch protection
- Matrix Strategy: Parallel validation scales with project complexity
- Cache Management: Manual cleanup prevents storage issues
- Status Visibility: Each project gets its own check for clear feedback
The next challenge was ensuring that only approved contributors could merge changes—which brings us to our review requirements and code ownership strategy.
What’s Coming Next
We now have parallel validation working, but we need to ensure the right people review the right code. In the next article, we’ll implement branch protection rules with designated reviewers and add cost-effective security scanning.
You’ll learn:
- Setting up review requirements with CODEOWNERS
- Implementing smart security scanning without GitHub’s expensive Advanced Security
- Using path filters to target scans only where needed
- Balancing security with development velocity
Continue with Part 3: Secure Code Review – Branch Protection and Automated Security Scanning →