Siddhant Deval
Siddhant DevalAuthor
Senior Full-Stack Engineer·Oct 27, 2026·14 min read

CI/CD, Independent Deployments, and Observability

The point of micro-frontends is independent deployability. If your deployment pipeline for one MFE can block or break another team's release, you have not yet achieved the architecture. This article covers manifest-driven CDN deployments, 30-second rollbacks, and distributed error attribution across independently deployed apps.

Technical Series

Micro-Frontend Architecture

Part 9 of 9

CI/CD, Independent Deployments, and Observability

A micro-frontend is not a smaller app — it is a domain boundary enforced at the deployment layer. If you can't explain the business capability it owns, you haven't drawn the boundary yet.
Here is the test for whether you have actually achieved micro-frontend independence: the Checkout team deploys a new feature at 2pm on a Thursday. The Catalog team is in the middle of their own release at 2pm on the same Thursday. Do they interact at all?
If the answer is "yes, we need to coordinate release windows" — you have not achieved the architecture. You have built a distributed monolith: the runtime complexity of multiple deployments without the independence that justifies that complexity.
This final article is about making the answer "no" — at the pipeline level, at the deployment level, and at the observability level.

1. Pipeline Isolation: The Structural Rule

1.1 One Repository, One Pipeline, One Deployment Unit

The structural rule for independent CI/CD in a micro-frontend system is simple:
A CI/CD pipeline run for one remote must never be able to cause a pipeline run, a deployment, or a failure for another remote.
This rule has direct consequences for how you structure your pipelines:
✅ Correct — fully isolated pipelines
  checkout.ci.yml    → triggers on: push to checkout/**
                     → builds: checkout remote only
                     → deploys: checkout CDN path only
                     → failure: blocks checkout deploy only

  catalog.ci.yml     → triggers on: push to catalog/**
                     → builds: catalog remote only
                     → deploys: catalog CDN path only
                     → failure: blocks catalog deploy only

❌ Incorrect — shared pipeline creates coupling
  deploy-all.yml     → triggers on: push to any path
                     → builds: all remotes
                     → deploys: all remotes if all pass
                     → failure: blocks ALL teams' deploys
yaml
# checkout/.github/workflows/deploy.yml
name: Checkout Remote  CI/CD

on:
  push:
    paths:
      - 'packages/checkout/**'  # Only trigger on checkout changes
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build checkout remote
        run: npx nx build checkout
        env:
          NODE_ENV: production

      - name: Run contract verification
        run: npx pact-broker can-i-deploy
          --pacticipant checkout
          --version ${{ github.sha }}
          --to-environment production
          --broker-base-url ${{ secrets.PACT_BROKER_URL }}

      - name: Deploy to CDN
        run: |
          aws s3 sync dist/checkout s3://cdn-bucket/checkout/${{ github.sha }}/ --cache-control max-age=31536000
          aws s3 cp dist/checkout/mf-manifest.json s3://cdn-bucket/checkout/mf-manifest.json --cache-control max-age=60

      - name: Invalidate manifest cache
        run: aws cloudfront create-invalidation --distribution-id $CDN_ID --paths "/checkout/mf-manifest.json"

1.2 Monorepo MFE CI with Affected Builds

When all remotes live in a monorepo, you achieve logical pipeline isolation using affected-build tooling — only rebuilding and redeploying what changed:
bash
# Nx affected — only builds remotes that have changed or depend on changed code
npx nx affected --target=build --base=origin/main --head=HEAD

# Example output when only checkout/src/Cart.tsx changed:
# ✔  Running target build for 2 projects:
#    - checkout      (has changes)
#    - shared-ui     (checkout depends on it — rebuilt if shared-ui changed)
#
# NOT running for:
#    - catalog       (no changes, no dependency on changed code)
#    - profile       (no changes, no dependency on changed code)
#    - shell         (no changes, no dependency on changed code)
yaml
# .github/workflows/ci.yml — monorepo affected build CI
jobs:
  detect-affected:
    runs-on: ubuntu-latest
    outputs:
      affected: ${{ steps.nx-affected.outputs.affected }}
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - id: nx-affected
        run: |
          AFFECTED=$(npx nx show projects --affected --base=origin/main)
          echo "affected=$AFFECTED" >> $GITHUB_OUTPUT

  deploy-affected:
    needs: detect-affected
    strategy:
      matrix:
        remote: ${{ fromJson(needs.detect-affected.outputs.affected) }}
    steps:
      - name: Deploy ${{ matrix.remote }}
        run: npx nx deploy ${{ matrix.remote }}
Crucial Requirement
In a monorepo, modifications to shared packages (design tokens, event schemas, type utilities) are tracked as dependencies. If @example/design-tokens changes, Nx affected automatically rebuilds all remotes that depend on it. This is the correct behavior — a design token change should rebuild all consumers.

2. Manifest-Driven Deployment

2.1 The Two-File Deployment Strategy

From Part 4, the manifest-based deployment strategy separates the versioned chunk from the pointer file:
CDN bucket structure:
  cdn-bucket/
  ├── checkout/
  │   ├── mf-manifest.json        ← pointer file (short TTL: 60s, invalidated on deploy)
  │   ├── 1.2.3/
  │   │   ├── remoteEntry.js      ← versioned chunk (long TTL: 1 year, immutable)
  │   │   └── Cart.js             ← versioned chunk (long TTL: 1 year, immutable)
  │   ├── 1.2.4/
  │   │   ├── remoteEntry.js
  │   │   └── Cart.js
  │   └── 1.2.5/
  │       ├── remoteEntry.js      ← current version (pointed to by mf-manifest.json)
  │       └── Cart.js
Deploying a new version:
  1. Upload new versioned chunks to /checkout/1.2.5/ — long TTL, no invalidation needed
  2. Update mf-manifest.json to point to 1.2.5 — short TTL, CDN invalidation triggered
  3. The next user page load fetches the new manifest → loads 1.2.5 chunks
Rolling back from 1.2.5 to 1.2.3:
  1. Update mf-manifest.json to point back to 1.2.3 — CDN invalidation triggered
  2. Done. 30 seconds. No rebuild. No redeployment of chunks.
Flow trace diagram showing the CDN manifest-driven deployment and rollback path. Top row labeled 'Deploy v1.2.5 (forward)' with four stages: (1) Build produces dist/checkout/1.2.5/* chunks (green). (2) Upload chunks to cdn-bucket/checkout/1.2.5/ — S3 PUT with max-age: 31536000 (amber, network). (3) Update mf-manifest.json: version → 1.2.5, remoteEntry → /checkout/1.2.5/remoteEntry.js (amber). (4) CloudFront invalidation on /checkout/mf-manifest.json only (cyan). Annotation: 'Forward deploy: ~3 min CI + ~30s CDN propagation'. Bottom row labeled 'Rollback to v1.2.3 (emergency)' with two stages: (1) Update mf-manifest.json: version → 1.2.3, remoteEntry → /checkout/1.2.3/remoteEntry.js (amber). (2) CloudFront invalidation on /checkout/mf-manifest.json (cyan). Annotation: 'Rollback: ~30 seconds — no rebuild, no pipeline'. Green arrow connecting forward deploy and rollback: 'v1.2.3 chunks still in CDN bucket — never deleted'. Caption: 'Versioned chunks are immutable and never deleted — rollback is a manifest pointer update, not a rebuild.'
Versioned chunks are immutable and never deleted — rollback is a manifest pointer update, not a rebuild.

2.2 The Manifest Update Script

bash
#!/bin/bash
# scripts/deploy-manifest.sh — updates mf-manifest.json and invalidates CDN

REMOTE_NAME=$1         # e.g., "checkout"
VERSION=$2             # e.g., "1.2.5"
CDN_BUCKET=$3          # e.g., "s3://cdn-bucket"
CDN_DISTRIBUTION=$4    # e.g., "E1XXXXXXXXX"

# Generate manifest
cat > /tmp/mf-manifest.json << EOF
{
  "id": "${REMOTE_NAME}",
  "name": "${REMOTE_NAME}",
  "version": "${VERSION}",
  "remoteEntry": {
    "name": "remoteEntry",
    "path": "./${REMOTE_NAME}/${VERSION}/",
    "type": "module"
  }
}
EOF

# Upload manifest with short TTL (60s — users get new version within 1 minute)
aws s3 cp /tmp/mf-manifest.json \
  ${CDN_BUCKET}/${REMOTE_NAME}/mf-manifest.json \
  --cache-control "max-age=60, s-maxage=60"

# Invalidate CDN edge cache — forces all edges to re-fetch manifest
aws cloudfront create-invalidation \
  --distribution-id ${CDN_DISTRIBUTION} \
  --paths "/${REMOTE_NAME}/mf-manifest.json"

echo "✓ ${REMOTE_NAME}@${VERSION} deployed — rollback: ./deploy-manifest.sh ${REMOTE_NAME} <previous-version>"

3. Local Development Environment Parity

3.1 The Local Dev Problem

In a polyrepo MFE system, a developer working on the Checkout remote needs to see it composed with the shell and other remotes. Options:
  1. Start all remotes locally — requires cloning 4 repositories, running 4 dev servers, and maintaining them in sync. This is the setup that takes 30 minutes to explain to a new engineer.
  2. Use staging remotes, override only the remote you're developing — you run only the Checkout remote locally; the shell points to staging for all other remotes. This is the correct approach.

3.2 The Local Override Pattern

javascript
// shell/rspack.config.js — environment-driven remote URL resolution
const getRemoteUrl = (remoteName, defaultUrl) => {
  // Check for local override environment variable
  const overrideKey = `LOCAL_REMOTE_${remoteName.toUpperCase()}`
  return process.env[overrideKey] ?? defaultUrl
}

new ModuleFederationPlugin({
  remotes: {
    checkout: `checkout@${getRemoteUrl(
      'checkout',
      'https://staging.cdn.example.com/checkout/mf-manifest.json'
    )}`,
    catalog: `catalog@${getRemoteUrl(
      'catalog',
      'https://staging.cdn.example.com/catalog/mf-manifest.json'
    )}`,
  },
})
bash
# Developer working on Checkout remote — runs checkout locally, uses staging for everything else
LOCAL_REMOTE_CHECKOUT=http://localhost:3001/remoteEntry.js npm run dev:shell

# All other remotes (catalog, profile) load from staging CDN
# Only checkout is served from localhost:3001
Pro Tip & Optimization
Publish the local override environment variable names in your team's engineering docs on day one. This single pattern eliminates the most common onboarding friction in any MFE system: "how do I run this locally?"

4. Observability: Distributed Error Attribution

4.1 The Attribution Problem

In a monolith, an error in production has a clear owner: the commit history points to the last change, and one team is responsible for the entire codebase. In a micro-frontend system with 5 independently deployed remotes, an error in the composed application has five possible owners.
Without deliberate error tagging, a Sentry alert that reads TypeError: Cannot read properties of undefined (reading 'cart') requires manual investigation of 5 separate deployment histories to identify which remote deployed the regression and when.

4.2 Error Attribution Tags

Every error reported from a remote must carry two identifying tags:
typescript
// Each remote bootstraps error tracking with its own identity
// checkout/src/bootstrap.ts
import * as Sentry from '@sentry/react'

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  release: `checkout@${process.env.REMOTE_VERSION}`,  // e.g., "checkout@1.2.5"
  environment: process.env.DEPLOY_ENV,
  initialScope: {
    tags: {
      'remote.name': 'checkout',
      'remote.version': process.env.REMOTE_VERSION,    // git SHA or semver
      'remote.deploy_time': process.env.DEPLOY_TIME,   // ISO timestamp
    },
  },
})
With these tags, Sentry (or Datadog, or any platform) allows filtering:
Sentry issue view:
  TypeError: Cannot read properties of undefined (reading 'cart')
  Tags: remote.name=checkout | remote.version=1.2.5 | remote.deploy_time=2026-09-15T14:03:00Z

  → First occurrence: 14:07:00Z (4 minutes after checkout@1.2.5 deployed)
  → Regression owner: Checkout team, commit abc123
The remote.version tag turns a 3-hour debugging session into a 3-minute attribution.

4.3 Health Check Endpoints for Remote Availability

typescript
// checkout/src/health.ts — expose a health check endpoint
// Used by monitoring to verify the remote is reachable before blaming application code

export async function checkCheckoutRemoteHealth(): Promise<{
  status: 'healthy' | 'degraded' | 'unavailable'
  version: string
  manifestUrl: string
}> {
  try {
    const manifest = await fetch(
      'https://cdn.example.com/checkout/mf-manifest.json',
      { cache: 'no-store' }
    ).then(r => r.json())

    return {
      status: 'healthy',
      version: manifest.version,
      manifestUrl: 'https://cdn.example.com/checkout/mf-manifest.json',
    }
  } catch (error) {
    return {
      status: 'unavailable',
      version: 'unknown',
      manifestUrl: 'https://cdn.example.com/checkout/mf-manifest.json',
    }
  }
}
typescript
// shell/src/monitoring.ts — Shell monitors all remotes at startup
const REMOTES = ['checkout', 'catalog', 'profile']

async function checkAllRemotes() {
  const results = await Promise.allSettled(
    REMOTES.map(async (remote) => {
      const health = await checkRemoteHealth(remote)
      if (health.status === 'unavailable') {
        // Alert before users see Error Boundary fallbacks
        monitoring.alert(`remote.unavailable`, {
          remote,
          manifestUrl: health.manifestUrl,
        })
      }
      return health
    })
  )
  return results
}

// Run at shell startup — fail fast, alert early
checkAllRemotes()

4.4 Deployment Strategies Comparison

Comparison matrix of deployment strategies for micro-frontend remotes across five operational axes. Three rows for strategies: Full Rebuild and Redeploy, CDN Manifest Pointer Update, CDN Invalidation Only. Five columns: Rollback Speed, Downtime Risk, Pipeline Time, Chunk Immutability, Operational Complexity. Full Rebuild: Rollback Speed — red (5-15 min), Downtime Risk — amber (brief during deploy), Pipeline Time — red (full CI run), Chunk Immutability — green (each deploy is a new artifact), Complexity — green (simple). CDN Manifest Update (recommended): Rollback Speed — green (30 sec), Downtime Risk — green (zero downtime), Pipeline Time — green (only manifest update), Chunk Immutability — green (versioned chunks never deleted), Complexity — amber (requires manifest architecture). CDN Invalidation Only: Rollback Speed — amber (2-5 min CDN propagation), Downtime Risk — amber (stale cache during propagation window), Pipeline Time — amber (rebuild not required, but invalidation required), Chunk Immutability — red (overwrites chunks in place), Complexity — green (simple). A 'Recommended' badge on CDN Manifest Update row. Caption: 'Manifest-driven deployment is the only strategy that achieves sub-minute rollback without either downtime or overwriting immutable artifacts.'
Manifest-driven deployment is the only strategy that achieves sub-minute rollback without either downtime or overwriting immutable artifacts.

5. Putting It Together: The Production Topology

Production deployment topology for a 4-remote MFE system:

GitHub (monorepo)
├── packages/shell/      → triggered by: changes to packages/shell/**
│                          deploys to: S3 + CloudFront (full rebuild)
│                          CDN: https://app.example.com
│
├── packages/checkout/   → triggered by: changes to packages/checkout/**
│                          deploys to: S3/checkout/${GIT_SHA}/ + updates mf-manifest.json
│                          CDN: https://cdn.example.com/checkout/
│
├── packages/catalog/    → triggered by: changes to packages/catalog/**
│                          deploys to: S3/catalog/${GIT_SHA}/ + updates mf-manifest.json
│                          CDN: https://cdn.example.com/catalog/
│
└── packages/profile/    → triggered by: changes to packages/profile/**
                           deploys to: S3/profile/${GIT_SHA}/ + updates mf-manifest.json
                           CDN: https://cdn.example.com/profile/

Shell deploy schedule:  When shell code changes (rarely — shell is thin)
Remote deploy schedule: When any remote's code changes (independently, multiple times/day)
Crucial Requirement
The App Shell is deployed with a full rebuild when its code changes. But because the shell uses manifest URLs (not hardcoded remote chunk URLs), remotes can deploy without triggering a shell rebuild. The shell's deployment frequency should trend toward rare — if the shell is deploying multiple times per week, business logic is leaking into it.

Summary

ConceptRule
Pipeline isolationOne CI/CD pipeline per remote; failure in one must not block any other
Affected buildsNx/Turborepo affected achieves logical isolation inside a monorepo
Versioned chunksImmutable, long TTL (1 year), never overwritten
Manifest fileShort TTL (60s), updated on every deploy and rollback
RollbackUpdate mf-manifest.json — 30 seconds, no rebuild
Error attributionEvery error tagged with remote.name and remote.version
Health checksShell monitors all remote manifest URLs at startup — alert before user impact
Local dev overrideLOCAL_REMOTE_<NAME>=http://localhost:<port> — document on day one

Series Complete

This concludes the Micro-Frontend Architecture series. Starting from a decision framework that tells you when MFEs earn their complexity (Part 1), through the bundler foundation that makes Module Federation mechanical (Part 2), through federation configuration, advanced manifests, App Shell design, cross-app state, testing strategy, style isolation, and finally operational independence — you now have the complete mental model and implementation vocabulary for a production-grade MFE system.
Recommended Next Step: The most commonly requested topic not covered in this series is the Strangler Fig migration — how to extract bounded contexts from an existing React monolith into micro-frontends without a big-bang rewrite. This is the path most teams actually take. If this would be valuable to you, reach out or subscribe for updates.

References

  1. Nx — Affected Builds
  2. Turborepo — Filtering
  3. AWS CloudFront — Cache Invalidation
  4. Sentry — Release Tracking
  5. Module Federation — Deployment
  6. GitHub Actions — Path Filtering
Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#Micro-Frontends#CI/CD#Observability#CDN#Module Federation#Deployments#Sentry
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.