Skip to content

Deployment Process & CI/CD

Overview

This document outlines the deployment process, CI/CD pipelines, and infrastructure management for our SaaS CRM application.

Deployment Architecture

Environment Structure

Production (PROD)
├── Load Balancer (AWS ALB)
├── Web Servers (EC2 Auto-scaling)
│   ├── Nuxt Application (3 instances min)
│   └── Nginx Reverse Proxy
├── API Servers (EC2 Auto-scaling)
│   ├── PHP Phalcon API (3 instances min)
│   └── PHP-FPM
├── Database Cluster
│   ├── MySQL Primary (RDS Multi-AZ)
│   └── MySQL Read Replicas (2)
├── Cache Layer
│   ├── Redis Cluster (ElastiCache)
│   └── CDN (CloudFront)
└── Supporting Services
    ├── RabbitMQ (Amazon MQ)
    ├── Elasticsearch (AWS OpenSearch)
    └── S3 (File Storage)

Staging (STG)
├── Similar to production (smaller scale)
└── Single instances for cost optimization

Development (DEV)
├── Single server setup
└── Docker Compose environment

CI/CD Pipeline

GitHub Actions Workflow

# .github/workflows/deploy.yml
name: Deploy Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  NODE_VERSION: '18'
  PHP_VERSION: '8.2'

jobs:
  # 1. Code Quality Checks
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ env.NODE_VERSION }}

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ env.PHP_VERSION }}
          extensions: phalcon, mbstring, mysql

      - name: Install dependencies
        run: |
          npm ci
          composer install --no-dev

      - name: Run linting
        run: |
          npm run lint
          composer run-script lint

      - name: Run security audit
        run: |
          npm audit
          composer audit

  # 2. Testing
  test:
    runs-on: ubuntu-latest
    needs: quality

    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: test
          MYSQL_DATABASE: crm_test
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5

      redis:
        image: redis:7
        options: >-
          --health-cmd="redis-cli ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5

    steps:
      - uses: actions/checkout@v3

      - name: Setup test environment
        run: |
          cp .env.test .env
          npm ci
          composer install

      - name: Run database migrations
        run: |
          php artisan migrate --env=test

      - name: Run unit tests
        run: |
          npm run test:unit
          ./vendor/bin/phpunit --testsuite=unit

      - name: Run integration tests
        run: |
          npm run test:integration
          ./vendor/bin/phpunit --testsuite=integration

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info,./coverage.xml

  # 3. Build
  build:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop'

    steps:
      - uses: actions/checkout@v3

      - name: Setup build environment
        run: |
          npm ci
          composer install --no-dev --optimize-autoloader

      - name: Build frontend
        run: |
          npm run build

      - name: Build backend
        run: |
          composer dump-autoload --optimize
          php artisan config:cache
          php artisan route:cache

      - name: Build Docker images
        run: |
          docker build -t crm-frontend:${{ github.sha }} -f docker/frontend/Dockerfile .
          docker build -t crm-backend:${{ github.sha }} -f docker/backend/Dockerfile .

      - name: Push to ECR
        env:
          AWS_REGION: us-east-1
        run: |
          aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_REGISTRY
          docker tag crm-frontend:${{ github.sha }} $ECR_REGISTRY/crm-frontend:${{ github.sha }}
          docker tag crm-backend:${{ github.sha }} $ECR_REGISTRY/crm-backend:${{ github.sha }}
          docker push $ECR_REGISTRY/crm-frontend:${{ github.sha }}
          docker push $ECR_REGISTRY/crm-backend:${{ github.sha }}

  # 4. Deploy to Staging
  deploy-staging:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/develop'
    environment:
      name: staging
      url: https://staging.ourcrm.com

    steps:
      - name: Deploy to staging
        run: |
          aws ecs update-service \
            --cluster staging-cluster \
            --service crm-frontend \
            --force-new-deployment

          aws ecs update-service \
            --cluster staging-cluster \
            --service crm-backend \
            --force-new-deployment

      - name: Run smoke tests
        run: |
          npm run test:smoke -- --url=https://staging.ourcrm.com

      - name: Notify team
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: 'Staging deployment completed'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

  # 5. Deploy to Production
  deploy-production:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://ourcrm.com

    steps:
      - name: Create deployment
        uses: chrnorm/deployment-action@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          environment: production

      - name: Blue-Green Deployment
        run: |
          # Deploy to green environment
          ./scripts/deploy-green.sh ${{ github.sha }}

          # Run health checks
          ./scripts/health-check.sh green

          # Switch traffic
          ./scripts/switch-traffic.sh green

          # Monitor for 5 minutes
          ./scripts/monitor.sh 300

      - name: Rollback on failure
        if: failure()
        run: |
          ./scripts/switch-traffic.sh blue
          ./scripts/notify-rollback.sh

Deployment Scripts

Blue-Green Deployment

#!/bin/bash
# scripts/deploy-green.sh

VERSION=$1
ENVIRONMENT="green"

echo "Deploying version $VERSION to $ENVIRONMENT environment"

# Update task definition
aws ecs register-task-definition \
  --family crm-app-$ENVIRONMENT \
  --container-definitions "[{
    \"name\": \"frontend\",
    \"image\": \"$ECR_REGISTRY/crm-frontend:$VERSION\",
    \"memory\": 512,
    \"cpu\": 256,
    \"essential\": true
  },{
    \"name\": \"backend\",
    \"image\": \"$ECR_REGISTRY/crm-backend:$VERSION\",
    \"memory\": 1024,
    \"cpu\": 512,
    \"essential\": true
  }]"

# Update service
aws ecs update-service \
  --cluster production-cluster \
  --service crm-$ENVIRONMENT \
  --task-definition crm-app-$ENVIRONMENT

# Wait for deployment
aws ecs wait services-stable \
  --cluster production-cluster \
  --services crm-$ENVIRONMENT

echo "Deployment complete"

Health Check Script

#!/bin/bash
# scripts/health-check.sh

ENVIRONMENT=$1
URL="https://$ENVIRONMENT.internal.ourcrm.com"

echo "Running health checks for $ENVIRONMENT"

# API health check
API_STATUS=$(curl -s -o /dev/null -w "%{http_code}" $URL/api/health)
if [ $API_STATUS -ne 200 ]; then
  echo "API health check failed: $API_STATUS"
  exit 1
fi

# Database connectivity
DB_CHECK=$(curl -s $URL/api/health/db | jq -r .status)
if [ "$DB_CHECK" != "healthy" ]; then
  echo "Database check failed"
  exit 1
fi

# Redis connectivity
REDIS_CHECK=$(curl -s $URL/api/health/redis | jq -r .status)
if [ "$REDIS_CHECK" != "healthy" ]; then
  echo "Redis check failed"
  exit 1
fi

echo "All health checks passed"

Traffic Switch Script

#!/bin/bash
# scripts/switch-traffic.sh

TARGET=$1  # blue or green

echo "Switching traffic to $TARGET environment"

# Update ALB target group
aws elbv2 modify-rule \
  --rule-arn $PROD_ALB_RULE_ARN \
  --actions Type=forward,TargetGroupArn=$TARGET_GROUP_ARN

# Update Route53 weighted routing
aws route53 change-resource-record-sets \
  --hosted-zone-id $HOSTED_ZONE_ID \
  --change-batch "{
    \"Changes\": [{
      \"Action\": \"UPSERT\",
      \"ResourceRecordSet\": {
        \"Name\": \"api.ourcrm.com\",
        \"Type\": \"A\",
        \"SetIdentifier\": \"$TARGET\",
        \"Weight\": 100,
        \"AliasTarget\": {
          \"HostedZoneId\": \"$ALB_ZONE_ID\",
          \"DNSName\": \"$TARGET-alb.ourcrm.com\",
          \"EvaluateTargetHealth\": true
        }
      }
    }]
  }"

echo "Traffic switched to $TARGET"

Docker Configuration

Frontend Dockerfile

# docker/frontend/Dockerfile
FROM node:18-alpine AS builder

WORKDIR /app

# Copy package files
COPY package*.json ./
RUN npm ci --only=production

# Copy application files
COPY . .

# Build application
RUN npm run build

# Production image
FROM node:18-alpine

WORKDIR /app

# Copy built application
COPY --from=builder /app/.output .output
COPY --from=builder /app/node_modules node_modules

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js

EXPOSE 3000

CMD ["node", ".output/server/index.mjs"]

Backend Dockerfile

# docker/backend/Dockerfile
FROM php:8.2-fpm-alpine

# Install extensions
RUN docker-php-ext-install pdo_mysql opcache
RUN pecl install phalcon redis && \
    docker-php-ext-enable phalcon redis

# Install composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

WORKDIR /var/www/html

# Copy application
COPY . .

# Install dependencies
RUN composer install --no-dev --optimize-autoloader

# Configure PHP
COPY docker/backend/php.ini /usr/local/etc/php/conf.d/custom.ini

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost/health || exit 1

EXPOSE 9000

CMD ["php-fpm"]

Docker Compose

# docker-compose.yml
version: '3.8'

services:
  frontend:
    build:
      context: .
      dockerfile: docker/frontend/Dockerfile
    ports:
      - "3000:3000"
    environment:
      NUXT_PUBLIC_API_BASE: http://backend:9000
    depends_on:
      - backend
    networks:
      - crm-network

  backend:
    build:
      context: .
      dockerfile: docker/backend/Dockerfile
    ports:
      - "9000:9000"
    environment:
      DB_HOST: mysql
      DB_DATABASE: crm
      DB_USERNAME: root
      DB_PASSWORD: secret
      REDIS_HOST: redis
    depends_on:
      - mysql
      - redis
    networks:
      - crm-network

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./docker/nginx/ssl:/etc/nginx/ssl
    depends_on:
      - frontend
      - backend
    networks:
      - crm-network

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: crm
    volumes:
      - mysql-data:/var/lib/mysql
    ports:
      - "3306:3306"
    networks:
      - crm-network

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    networks:
      - crm-network

volumes:
  mysql-data:
  redis-data:

networks:
  crm-network:
    driver: bridge

Kubernetes Deployment

Deployment Manifest

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: crm-frontend
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: crm-frontend
  template:
    metadata:
      labels:
        app: crm-frontend
    spec:
      containers:
      - name: frontend
        image: crm-frontend:latest
        ports:
        - containerPort: 3000
        env:
        - name: NUXT_PUBLIC_API_BASE
          valueFrom:
            configMapKeyRef:
              name: crm-config
              key: api.base.url
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: crm-frontend-service
  namespace: production
spec:
  selector:
    app: crm-frontend
  ports:
  - protocol: TCP
    port: 80
    targetPort: 3000
  type: LoadBalancer

Horizontal Pod Autoscaler

# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: crm-frontend-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: crm-frontend
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

Infrastructure as Code

Terraform Configuration

# terraform/main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  backend "s3" {
    bucket = "crm-terraform-state"
    key    = "production/terraform.tfstate"
    region = "us-east-1"
  }
}

# VPC Configuration
module "vpc" {
  source = "terraform-aws-modules/vpc/aws"

  name = "crm-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway = true
  enable_vpn_gateway = true

  tags = {
    Environment = "production"
    Application = "CRM"
  }
}

# RDS MySQL
resource "aws_db_instance" "mysql" {
  identifier     = "crm-mysql"
  engine         = "mysql"
  engine_version = "8.0"
  instance_class = "db.r5.xlarge"

  allocated_storage     = 100
  storage_encrypted     = true
  storage_type          = "gp3"

  db_name  = "crm"
  username = var.db_username
  password = var.db_password

  vpc_security_group_ids = [aws_security_group.rds.id]
  db_subnet_group_name   = aws_db_subnet_group.main.name

  backup_retention_period = 30
  backup_window          = "03:00-04:00"
  maintenance_window     = "sun:04:00-sun:05:00"

  multi_az               = true
  publicly_accessible    = false

  enabled_cloudwatch_logs_exports = ["error", "general", "slowquery"]

  tags = {
    Name        = "CRM Database"
    Environment = "production"
  }
}

# ElastiCache Redis
resource "aws_elasticache_cluster" "redis" {
  cluster_id           = "crm-redis"
  engine              = "redis"
  node_type           = "cache.r6g.xlarge"
  num_cache_nodes     = 1
  parameter_group_name = "default.redis7"
  port                = 6379

  subnet_group_name = aws_elasticache_subnet_group.main.name
  security_group_ids = [aws_security_group.redis.id]

  snapshot_retention_limit = 5
  snapshot_window         = "03:00-05:00"

  tags = {
    Name        = "CRM Cache"
    Environment = "production"
  }
}

# Auto Scaling Group
resource "aws_autoscaling_group" "app" {
  name                = "crm-app-asg"
  vpc_zone_identifier = module.vpc.private_subnets
  target_group_arns   = [aws_lb_target_group.app.arn]
  health_check_type   = "ELB"
  min_size            = 3
  max_size            = 10
  desired_capacity    = 3

  launch_template {
    id      = aws_launch_template.app.id
    version = "$Latest"
  }

  tag {
    key                 = "Name"
    value               = "CRM App Server"
    propagate_at_launch = true
  }
}

# Application Load Balancer
resource "aws_lb" "main" {
  name               = "crm-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets           = module.vpc.public_subnets

  enable_deletion_protection = true
  enable_http2              = true

  tags = {
    Name        = "CRM Load Balancer"
    Environment = "production"
  }
}

Deployment Checklist

Pre-Deployment

  • Code review completed
  • All tests passing
  • Security scan completed
  • Performance testing done
  • Documentation updated
  • Database migrations prepared
  • Rollback plan documented
  • Team notified

During Deployment

  • Maintenance page enabled (if needed)
  • Database backup taken
  • Deployment started
  • Health checks passing
  • Smoke tests completed
  • Monitoring active

Post-Deployment

  • Application fully functional
  • Performance metrics normal
  • Error rates acceptable
  • Customer communication sent
  • Deployment documented
  • Lessons learned captured

Rollback Procedures

Immediate Rollback

#!/bin/bash
# Immediate rollback to previous version

PREVIOUS_VERSION=$(aws ecs describe-services \
  --cluster production-cluster \
  --services crm-app \
  --query 'services[0].taskDefinition' \
  --output text | sed 's/.*://')

echo "Rolling back to version $PREVIOUS_VERSION"

aws ecs update-service \
  --cluster production-cluster \
  --service crm-app \
  --task-definition crm-app:$((PREVIOUS_VERSION - 1))

# Wait for rollback
aws ecs wait services-stable \
  --cluster production-cluster \
  --services crm-app

echo "Rollback complete"

Database Rollback

-- Rollback migration
START TRANSACTION;

-- Reverse schema changes
ALTER TABLE customers DROP COLUMN new_field;

-- Restore data if needed
UPDATE customers SET status = 'active' 
WHERE status = 'migrated';

-- Verify rollback
SELECT COUNT(*) FROM customers WHERE status = 'migrated';

COMMIT;

Monitoring & Alerts

CloudWatch Alarms

# cloudwatch/alarms.yaml
HighErrorRate:
  MetricName: 4XXError
  Threshold: 10
  Period: 300
  EvaluationPeriods: 2
  Actions:
    - !Ref SNSAlertTopic

HighLatency:
  MetricName: TargetResponseTime
  Threshold: 1000  # milliseconds
  Period: 300
  EvaluationPeriods: 2
  Actions:
    - !Ref SNSAlertTopic

LowDiskSpace:
  MetricName: DiskSpaceUtilization
  Threshold: 80  # percentage
  Period: 300
  EvaluationPeriods: 1
  Actions:
    - !Ref SNSAlertTopic

Last Updated: January 2025 Version: 1.0.0