CI/CD Setup: Automated Pipeline
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
Why Traditional CI/CD Approaches Fail in Modern Environments
Legacy Jenkins installations with manually configured jobs and shell scripts worked adequately when teams deployed weekly and ran monolithic applications. These approaches collapse under modern constraints. Configuration drift between pipeline definitions and actual infrastructure creates deployment failures that take hours to debug. Manual secret management leads to credentials hardcoded in scripts or stored in unsecured locations. Pipeline definitions stored separately from application code create synchronization problems when multiple teams work on shared services.
The shift to cloud-native architectures exposes additional weaknesses. Container-based deployments require image scanning, vulnerability assessment, and registry management that traditional pipelines weren't designed to handle. Kubernetes deployments need sophisticated rollout strategies with health checks, progressive delivery, and automatic rollbacks—capabilities that require deep integration between CI/CD systems and orchestration platforms. Multi-cloud and hybrid deployments demand pipeline portability that vendor-locked solutions cannot provide.
Compliance requirements have intensified. GDPR, SOC 2, and industry-specific regulations now mandate complete audit trails showing who deployed what code, when, and whether it passed required security scans. Traditional pipelines lack the structured metadata and immutable logging needed for compliance automation. Manual approval processes create bottlenecks that defeat the purpose of automation while failing to provide the granular, policy-based controls that modern governance requires.
Modern CI/CD Pipeline Architecture
A production-grade CI/CD pipeline implementation in 2025 follows a declarative, GitOps-based approach where pipeline definitions live alongside application code, infrastructure is provisioned through code, and every deployment creates an auditable, reproducible artifact. The architecture separates concerns into distinct stages: source control integration, build and test automation, security scanning, artifact management, deployment orchestration, and observability.
The foundation starts with pipeline-as-code using modern tools like GitHub Actions, GitLab CI, or Tekton for Kubernetes-native workflows. Here's a realistic GitHub Actions workflow implementing a secure, multi-stage pipeline for a containerized microservice:
name: Production Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
security-scan:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'
build-and-test:
runs-on: ubuntu-latest
needs: security-scan
outputs:
image-digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests with coverage
run: npm run test:coverage
env:
NODE_ENV: test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
- name: Build application
run: npm run build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=sha,prefix={{branch}}-
- name: Build and push image
id: build
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
container-scan:
runs-on: ubuntu-latest
needs: build-and-test
steps:
- name: Run Trivy container scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-and-test.outputs.image-digest }}
format: 'sarif'
output: 'container-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
deploy-staging:
runs-on: ubuntu-latest
needs: [build-and-test, container-scan]
if: github.ref == 'refs/heads/main'
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/checkout@v4
- name: Set up kubectl
uses: azure/setup-kubectl@v4
with:
version: 'v1.29.0'
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Update kubeconfig
run: |
aws eks update-kubeconfig --name staging-cluster --region us-east-1
- name: Deploy with Helm
run: |
helm upgrade --install myapp ./helm/myapp \
--namespace staging \
--create-namespace \
--set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \
--set image.digest=${{ needs.build-and-test.outputs.image-digest }} \
--set environment=staging \
--wait \
--timeout 5m
- name: Run smoke tests
run: |
kubectl wait --for=condition=ready pod -l app=myapp -n staging --timeout=300s
npm run test:smoke -- --url=https://staging.example.com
deploy-production:
runs-on: ubuntu-latest
needs: deploy-staging
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://example.com
steps:
- uses: actions/checkout@v4
- name: Set up kubectl
uses: azure/setup-kubectl@v4
with:
version: 'v1.29.0'
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_PROD_ROLE_ARN }}
aws-region: us-east-1
- name: Update kubeconfig
run: |
aws eks update-kubeconfig --name prod-cluster --region us-east-1
- name: Deploy with progressive rollout
run: |
helm upgrade --install myapp ./helm/myapp \
--namespace production \
--create-namespace \
--set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \
--set image.digest=${{ needs.build-and-test.outputs.image-digest }} \
--set environment=production \
--set rollout.strategy=canary \
--set rollout.steps[0].weight=20 \
--set rollout.steps[0].pause=300 \
--set rollout.steps[1].weight=50 \
--set rollout.steps[1].pause=300 \
--set rollout.steps[2].weight=100 \
--wait \
--timeout 15m
- name: Verify deployment
run: |
kubectl wait --for=condition=ready pod -l app=myapp -n production --timeout=600s
npm run test:production -- --url=https://example.com
- name: Create deployment record
run: |
curl -X POST ${{ secrets.DEPLOYMENT_TRACKER_URL }} \
-H "Authorization: Bearer ${{ secrets.DEPLOYMENT_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"service": "myapp",
"version": "${{ github.sha }}",
"environment": "production",
"deployed_by": "${{ github.actor }}",
"image_digest": "${{ needs.build-and-test.outputs.image-digest }}",
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
}'
This pipeline implements several critical patterns. Security scanning happens at multiple stages—filesystem scanning before build, container image scanning after build, and runtime vulnerability monitoring in deployed environments. The use of image digests rather than tags ensures immutability and prevents tag manipulation attacks. SBOM (Software Bill of Materials) generation provides supply chain transparency required for compliance and incident response.
The deployment strategy uses progressive rollouts with automated verification at each stage. Staging deployments must pass smoke tests before production deployment begins. Production deployments use canary releases with configurable traffic weights and pause durations, allowing automated or manual rollback if metrics indicate problems.
Infrastructure as Code Integration
Modern CI/CD pipeline implementation requires tight integration with infrastructure provisioning. Terraform or OpenTofu workflows should run within the same pipeline framework, ensuring infrastructure changes undergo the same review, testing, and approval processes as application code:
// infrastructure/main.ts - Using Pulumi for type-safe IaC
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as k8s from "@pulumi/kubernetes";
const config = new pulumi.Config();
const environment = pulumi.getStack();
// Create VPC with proper network segmentation
const vpc = new aws.ec2.Vpc(`${environment}-vpc`, {
cidrBlock: "10.0.0.0/16",
enableDnsHostnames: true,
enableDnsSupport: true,
tags: {
Name: `${environment}-vpc`,
Environment: environment,
ManagedBy: "pulumi",
},
});
// EKS cluster with security best practices
const cluster = new aws.eks.Cluster(`${environment}-cluster`, {
vpcId: vpc.id,
subnetIds: privateSubnets.map(s => s.id),
version: "1.29",
enabledClusterLogTypes: [
"api",
"audit",
"authenticator",
"controllerManager",
"scheduler",
],
encryptionConfig: {
provider: {
keyArn: kmsKey.arn,
},
resources: ["secrets"],
},
tags: {
Environment: environment,
ManagedBy: "pulumi",
},
});
// Node group with spot instances for cost optimization
const nodeGroup = new aws.eks.NodeGroup(`${environment}-nodes`, {
clusterName: cluster.name,
nodeRoleArn: nodeRole.arn,
subnetIds: privateSubnets.map(s => s.id),
capacityType: "SPOT",
instanceTypes: ["t3.large", "t3a.large"],
scalingConfig: {
desiredSize: 3,
maxSize: 10,
minSize: 2,
},
updateConfig: {
maxUnavailable: 1,
},
labels: {
Environment: environment,
NodeType: "application",
},
tags: {
Environment: environment,
ManagedBy: "pulumi",
},
});
// Export cluster details for pipeline consumption
export const clusterName = cluster.name;
export const clusterEndpoint = cluster.endpoint;
export const clusterCertificate = cluster.certificateAuthority.data;
export const kubeconfig = pulumi.secret(
pulumi.interpolate`apiVersion: v1
clusters:
- cluster:
certificate-authority-data: ${cluster.certificateAuthority.data}
server: ${cluster.endpoint}
name: ${cluster.name}
contexts:
- context:
cluster: ${cluster.name}
user: ${cluster.name}
name: ${cluster.name}
current-context: ${cluster.name}
kind: Config
users:
- name: ${cluster.name}
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: aws
args:
- eks
- get-token
- --cluster-name
- ${cluster.name}
`
);
This infrastructure code runs in the pipeline before application deployment, with state stored in remote backends (S3 with DynamoDB locking for Terraform, Pulumi's managed backend, or self-hosted alternatives). The pipeline validates infrastructure changes through plan/preview steps that require approval before applying changes to production environments.
Observability and Pipeline Monitoring
A production CI/CD pipeline implementation must include comprehensive observability. Pipeline metrics, deployment frequency, lead time, change failure rate, and mean time to recovery form the foundation of DORA metrics that measure engineering effectiveness. Modern pipelines emit structured logs and metrics to observability platforms:
```typescript // pipeline-metrics.ts - Custom metrics collection import { CloudWatch } from "@aws-sdk/client-cloudwatch"; import { SSM } from "@aws-sdk/client-ssm";
interface PipelineMetrics { pipelineId: string; repository: string; branch: string; commit: string; startTime: Date; endTime?: Date; status: "running" | "success" | "failed" | "cancelled"; stages: StageMetrics[]; }
interface StageMetrics { name: string; startTime: Date; endTime?: Date; status: string; duration?: number; artifacts?: string[]; }
class PipelineObservability { private cloudwatch: CloudWatch; private ssm: SSM; private namespace = "CustomMetrics/CICD";
constructor() { this.cloudwatch = new CloudWatch({ region: process.env.AWS_REGION }); this.ssm = new SSM({ region: process.env.AWS_REGION }); }
async recordPipelineExecution(metrics: PipelineMetrics): Promise { const duration = metrics.endTime ? (metrics.endTime.getTime() - metrics.startTime.getTime()) / 1000 : 0;
await this.cloudwatch.putMetricData({ Namespace: this.namespace, MetricData: [ { MetricName: "PipelineDuration", Value: duration, Unit: "Seconds", Timestamp: metrics.endTime || new Date(), Dimensions: [ { Name: "Repository", Value: metrics.repository }, { Name: "Branch", Value: metrics.branch }, { Name: "Status", Value: metrics.status }, ], }, { MetricName: "PipelineExecutions", Value: 1, Unit: "Count", Timestamp: metrics.endTime || new Date(), Dimensions: [ { Name: "Repository", Value: metrics.repository }, { Name: "Status", Value: metrics.status }, ], }, ], });
// Record deployment frequency for DORA metrics if (metrics.status === "success" && metrics.branch === "main") { await this.recordDeploymentFrequency(metrics); } }
async recordDeploymentFrequency(metrics: PipelineMetrics): Promise {
const parameterName = /cicd/deployments/${metrics.repository}/last-deployment;
try { const lastDeployment = await this.ssm.getParameter({ Name: parameterName, });
const lastTime = new Date(lastDeployment.Parameter!.Value!); const currentTime = metrics.endTime || new Date(); const timeBetweenDeployments = (currentTime.getTime() - lastTime.getTime()) / 1000 / 60; // minutes
await this.cloudwatch.putMetricData({ Namespace: this.namespace, MetricData: [ { MetricName: "DeploymentFrequency", Value: timeBetweenDeployments, Unit: "Minutes", Timestamp: currentTime, Dimensions: [ { Name: "Repository", Value: metrics.repository }, ], }, ], }); } catch (error) { // First deployment, no previous record }
await this.ssm.putParameter({ Name: parameterName, Value: (metrics.endTime || new Date()).toISOString(), Type: "String", Overwrite: true, }); }
async recordChangeFailureRate( repository: string, failed: boolean ): Promise { await this.cloudwatch.putMetricData({ Namespace: this.namespace, MetricData: [ { MetricName: "ChangeFailureRate", Value: failed ? 1 : 0, Unit: "Count", Timestamp: new Date(), Dimensions: [ { Name: "Repository", Value: repository }, ], }, ], }); } }
export const observability = new PipelineObservability();