Skip to main content

Command Palette

Search for a command to run...

CI/CD Setup: Automated Pipeline

Published
8 min read
T

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();