microservices requires a deliberate shift from pipeline-per-service to pipeline-per-topology. The architecture below prioritizes isolation, parallelization, and verifiable contracts.
Step 1: Repository Strategy & Pipeline Templates
Use a poly-repo structure with shared pipeline definitions stored in a dedicated ci-templates repository. Each service owns its code but inherits standardized pipeline stages through composition. This prevents configuration drift while preserving service autonomy.
// ci-templates/src/pipeline-config.ts
import { PipelineConfig, Stage } from '@codcompass/pipeline-sdk';
export const microservicePipeline = (service: string): PipelineConfig => ({
name: `${service}-ci`,
trigger: { push: { branches: ['main', 'release/*'] } },
stages: [
Stage.build({ runtime: 'node:20', cache: 'npm' }),
Stage.test({ type: 'unit', parallel: true }),
Stage.contract({ provider: service, registry: 'contracts.internal' }),
Stage.deploy({ strategy: 'canary', target: 'staging' }),
Stage.validate({ type: 'smoke', timeout: '2m' })
],
artifacts: { image: true, signature: 'cosign' }
});
Step 2: Dependency-Aware Pipeline Orchestration
Model service dependencies explicitly. Use a lightweight DAG (Directed Acyclic Graph) resolver to determine which services require revalidation when a shared library or API contract changes.
// ci-templates/src/orchestrator.ts
import { resolveDAG, getAffectedServices } from '@codcompass/dag-resolver';
export async function triggerRelease(repoChanges: string[]) {
const graph = await resolveDAG('./service-graph.json');
const affected = getAffectedServices(graph, repoChanges);
const pipelineRuns = affected.map(service =>
triggerPipeline(service, {
strategy: 'parallel',
timeout: '10m',
retry: { max: 2, backoff: 'exponential' }
})
);
return Promise.allSettled(pipelineRuns);
}
Step 3: Contract Testing Over End-to-End
Replace heavy E2E suites with consumer-driven contract testing. Tools like Pact or OpenAPI validation pipelines verify interface stability without provisioning full infrastructure. Contracts are versioned alongside services and validated in CI before deployment.
Step 4: GitOps Deployment with Progressive Delivery
Push-based deployments create race conditions and secret leakage risks. Adopt GitOps with Argo CD or Flux. The pipeline builds and pushes immutable container images to a signed registry. Argo CD watches the Git repository for deployment manifests and applies them declaratively.
# argocd/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payment-service
spec:
project: default
source:
repoURL: https://github.com/org/deployments
targetRevision: HEAD
path: services/payment/overlays/staging
destination:
server: https://kubernetes.default.svc
namespace: staging
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Step 5: Ephemeral Integration Environments
Shared staging environments cause non-deterministic test failures. Provision ephemeral namespaces per pull request using tools like Kind, k3d, or cloud-native namespace isolation. Run integration tests against live service meshes, then tear down after validation.
Architecture Rationale:
- GitOps over push-based: Declarative state reconciliation eliminates deployment drift and provides audit trails.
- Contract testing over E2E: Validates interfaces at speed; E2E is reserved for production canaries.
- Ephemeral over shared: Isolates test state, reduces flakiness, and enables parallel validation.
- Parallel pipelines over sequential: Dependency graphs prevent unnecessary waits while maintaining correctness.
Pitfall Guide
1. Shared Pipeline State & Cross-Service Coupling
Mistake: Storing build artifacts, environment variables, or test databases in shared pipeline runners or global caches.
Impact: Race conditions, non-reproducible builds, and cascading failures when one service modifies shared state.
Best Practice: Isolate pipeline execution per service. Use ephemeral runners, service-specific artifact storage, and explicit dependency declarations. Never assume pipeline runners are stateless; enforce statelessness through infrastructure-as-code.
2. Ignoring Service Dependency Graphs
Mistake: Triggering all services on every commit or deploying in arbitrary order.
Impact: Wasted compute, deployment conflicts, and silent integration breaks.
Best Practice: Maintain a service-graph.json or OpenAPI dependency manifest. Use DAG resolution to trigger only affected services. Validate downstream consumers before deploying upstream providers.
3. Over-Testing in CI (Heavy E2E Suites)
Mistake: Running full UI/E2E tests in CI for every microservice.
Impact: Pipeline duration exceeds 30 minutes, developer feedback loops break, and flaky tests mask real regressions.
Best Practice: Shift E2E to production canaries or nightly runs. CI should validate unit tests, contract compliance, and static analysis. Integration tests run only in ephemeral environments with mocked external dependencies.
4. Manual Environment Provisioning & Drift
Mistake: Developers manually configure staging or pre-production clusters.
Impact: Environment drift, inconsistent TLS/certificate handling, and deployment failures that only appear in production.
Best Practice: Treat environments as code. Use Terraform, Crossplane, or Pulumi to provision clusters. Sync environment state through GitOps. Never allow manual kubectl apply in shared environments.
5. Skipping Resilience & Chaos Testing
Mistake: Assuming microservices are resilient because they are containerized.
Impact: Cascading failures, circuit breaker misconfiguration, and silent timeout degradation.
Best Practice: Inject latency, drop packets, and simulate dependency failures in staging. Validate retry policies, timeout configurations, and fallback mechanisms. Tools like Chaos Mesh or Litmus should run weekly against non-production clusters.
6. Hardcoded Secrets & Poor Credential Rotation
Mistake: Embedding API keys, database passwords, or registry tokens in pipeline YAML or environment files.
Impact: Credential leakage, compliance violations, and emergency rotation during incidents.
Best Practice: Use external secret managers (HashiCorp Vault, AWS Secrets Manager, Doppler). Inject secrets at runtime via sidecar or CSI driver. Enforce automatic rotation with 24β72 hour TTLs. Never commit secrets to version control, even in encrypted form.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Startup (<10 services) | Poly-repo + GitHub Actions + Argo CD | Low operational overhead, rapid iteration, built-in GitOps | Minimal; leverages free tiers |
| Enterprise (50+ services) | Mono-repo + shared pipeline SDK + Kubernetes-native GitOps | Centralized governance, consistent compliance, reduced config drift | Moderate; requires dedicated platform team |
| High-compliance (HIPAA/SOC2) | Immutable artifacts + signed registries + audit-synced GitOps | Traceability, cryptographic verification, policy-as-code enforcement | Higher; adds scanning/signing steps but reduces audit risk |
| Multi-cloud deployment | Service mesh + environment-agnostic manifests + cross-cluster Argo | Avoids vendor lock-in, enables failover, simplifies rollout | High initial setup; lowers long-term migration cost |
Configuration Template
# .github/workflows/microservice-ci.yml
name: Microservice CI/CD
on:
push:
branches: [main, release/*]
pull_request:
branches: [main]
env:
SERVICE_NAME: ${{ github.event.repository.name }}
REGISTRY: ghcr.io/${{ github.repository }}
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci && npm run build
- run: npm test -- --coverage --runInBand
- run: npm run validate-contracts -- --registry https://contracts.internal/api
containerize:
needs: build-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: ${{ github.actor }}, password: ${{ secrets.GITHUB_TOKEN }} }
- uses: docker/build-push-action@v5
with: { push: true, tags: ${{ env.REGISTRY }}:${{ github.sha }} }
- run: |
cosign sign --yes ${{ env.REGISTRY }}:${{ github.sha }}
trivy image --exit-code 1 --severity HIGH,CRITICAL ${{ env.REGISTRY }}:${{ github.sha }}
deploy-staging:
needs: containerize
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- run: |
kubectl patch application ${{ env.SERVICE_NAME }} -n argocd \
--type merge -p "{\"spec\":{\"source\":{\"targetRevision\":\"${{ github.sha }}\"}}}"
- run: |
kubectl wait application ${{ env.SERVICE_NAME }} -n argocd \
--for=condition=Synced --timeout=120s
Quick Start Guide
- Initialize pipeline templates: Clone the shared CI repository, define base stages in TypeScript/YAML, and publish as a versioned package.
- Generate dependency graph: Run
npx @codcompass/dag-resolver init in your service directory to map upstream/downstream relationships.
- Enable contract validation: Add
npm run validate-contracts to your CI pipeline and point it to your contract registry endpoint.
- Deploy via GitOps: Create an Argo CD Application manifest targeting your service overlay directory. Commit to
deployments/main. Argo CD syncs automatically.
- Verify delivery: Push a commit to
main. Watch the pipeline execute in parallel, validate contracts, sign the image, and sync to staging via GitOps. Total cycle: 6β12 minutes.