Triển Khai Laravel Lên AWS Qua Console (Phần 5): CI/CD Với GitHub Actions & Zero-Downtime Deploy

· 11 min read

Phần 4, chúng ta đã cấu hình ALB, CloudFront, và SSL. Hạ tầng hoàn chỉnh. Giờ hãy tự động hóa quy trình deploy và thiết lập monitoring.

Lưu ý: Phần này kết hợp Console (cho monitoring/alarms) và code (cho CI/CD pipeline). GitHub Actions và Envoy phải cấu hình bằng file — không có giao diện Console cho chúng.

Cấu trúc thư mục đã chuẩn bị ở Phần 2:

/var/www/laravel/
├── current → releases/20260406_120000/    (symlink, chuyển nguyên tử)
├── releases/
│   ├── 20260406_120000/                   (mới nhất)
│   ├── 20260405_150000/                   (trước đó)
│   └── ...
└── shared/
    ├── .env
    └── storage/

Zero-downtime vì:

  1. Code mới deploy vào thư mục releases/ hoàn toàn mới
  2. Mọi thiết lập chạy ở thư mục mới trong khi thư mục cũ vẫn phục vụ traffic
  3. Symlink current chuyển nguyên tử (ln -sfn)
  4. PHP-FPM reload graceful — hoàn thành request đang xử lý trước khi dùng code mới

Laravel Envoy (Script Deploy)

Cài Envoy trong dự án Laravel:

composer require laravel/envoy --dev

Tạo Envoy.blade.php ở thư mục gốc:

@servers(['production' => 'ec2-user@your-domain.com'])

@setup
    $repository = 'git@github.com:your/repo.git';
    $branch = $branch ?? 'main';
    $appDir = '/var/www/laravel';
    $releasesDir = $appDir . '/releases';
    $sharedDir = $appDir . '/shared';
    $release = date('Ymd_His');
    $newReleaseDir = $releasesDir . '/' . $release;
    $keepReleases = 5;
@endsetup

@story('deploy')
    clone
    composer
    shared
    build
    optimize
    migrate
    activate
    cleanup
    health-check
@endstory

@task('clone')
    echo "🚀 Đang clone repository ({{ $branch }})..."
    git clone --depth 1 --branch {{ $branch }} {{ $repository }} {{ $newReleaseDir }}
@endtask

@task('composer')
    echo "📦 Đang cài đặt Composer dependencies..."
    cd {{ $newReleaseDir }}
    composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist
@endtask

@task('shared')
    echo "🔗 Đang liên kết shared resources..."
    cd {{ $newReleaseDir }}

    # Liên kết .env
    ln -sf {{ $sharedDir }}/.env .env

    # Liên kết storage
    rm -rf storage
    ln -sf {{ $sharedDir }}/storage storage

    # Đảm bảo cấu trúc storage tồn tại
    mkdir -p {{ $sharedDir }}/storage/{app/public,framework/{cache/data,sessions,views},logs}
    chmod -R 775 {{ $sharedDir }}/storage
    chown -R ec2-user:nginx {{ $sharedDir }}/storage
@endtask

@task('build')
    echo "🏗️ Đang build assets frontend..."
    cd {{ $newReleaseDir }}
    npm ci --prefer-offline
    npm run build
    rm -rf node_modules
@endtask

@task('optimize')
    echo "⚡ Đang tối ưu Laravel..."
    cd {{ $newReleaseDir }}
    php artisan config:cache
    php artisan route:cache
    php artisan view:cache
    php artisan event:cache
@endtask

@task('migrate')
    echo "🗄️ Đang chạy migrations..."
    cd {{ $newReleaseDir }}
    php artisan migrate --force
@endtask

@task('activate')
    echo "🔄 Đang kích hoạt release mới..."
    ln -sfn {{ $newReleaseDir }} {{ $appDir }}/current
    sudo systemctl reload php-fpm
    cd {{ $appDir }}/current
    php artisan queue:restart
    echo "✅ Release {{ $release }} đã hoạt động!"
@endtask

@task('cleanup')
    echo "🧹 Đang dọn releases cũ..."
    cd {{ $releasesDir }}
    ls -dt */ | tail -n +{{ $keepReleases + 1 }} | xargs -r rm -rf
    echo "Giữ lại {{ $keepReleases }} releases gần nhất."
@endtask

@task('health-check')
    echo "🏥 Đang kiểm tra sức khỏe..."
    sleep 3
    HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/up)
    if [ "$HTTP_STATUS" != "200" ]; then
        echo "❌ Health check thất bại (HTTP $HTTP_STATUS)! Đang rollback..."
        cd {{ $releasesDir }}
        PREVIOUS=$(ls -dt */ | sed -n '2p' | tr -d '/')
        if [ -n "$PREVIOUS" ]; then
            ln -sfn {{ $releasesDir }}/$PREVIOUS {{ $appDir }}/current
            sudo systemctl reload php-fpm
            echo "⏪ Đã rollback về $PREVIOUS"
        fi
        exit 1
    fi
    echo "✅ Health check thành công!"
@endtask

@task('rollback')
    echo "⏪ Đang rollback về release trước..."
    cd {{ $releasesDir }}
    PREVIOUS=$(ls -dt */ | sed -n '2p' | tr -d '/')
    if [ -z "$PREVIOUS" ]; then
        echo "❌ Không tìm thấy release trước đó!"
        exit 1
    fi
    ln -sfn {{ $releasesDir }}/$PREVIOUS {{ $appDir }}/current
    sudo systemctl reload php-fpm
    cd {{ $appDir }}/current
    php artisan queue:restart
    echo "✅ Đã rollback về $PREVIOUS"
@endtask

Chạy Thử Envoy

# Deploy
php vendor/bin/envoy run deploy

# Deploy nhánh cụ thể
php vendor/bin/envoy run deploy --branch=feature/new-feature

# Rollback
php vendor/bin/envoy run rollback

EC2: Chuẩn Bị Cho Envoy

Cho Phép Envoy Reload PHP-FPM

Envoy chạy dưới ec2-user nhưng systemctl reload php-fpm cần sudo:

# SSH vào EC2
sudo visudo

Thêm cuối file:

ec2-user ALL=(ALL) NOPASSWD: /usr/bin/systemctl reload php-fpm
ec2-user ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart php-fpm

GitHub Actions — CI/CD Tự Động

Cấu Hình GitHub Secrets

Vào repository trên GitHub: Settings → Secrets and variables → Actions → New repository secret:

Secret Giá trị Mô tả
SSH_PRIVATE_KEY Nội dung file laravel-production.pem Để Envoy SSH vào EC2
AWS_ACCESS_KEY_ID Access key IAM user Cho CloudFront invalidation
AWS_SECRET_ACCESS_KEY Secret key IAM user Cho CloudFront invalidation
CLOUDFRONT_DISTRIBUTION_ID ID distribution Tìm ở CloudFront Console

Lấy CloudFront Distribution ID Từ Console

  1. CloudFront → Distributions
  2. Cột ID → copy (dạng E1234ABCDEF)

Tạo Workflow File

Tạo .github/workflows/deploy.yml trong repository:

name: Deploy to Production

on:
  push:
    branches: [main]

concurrency:
  group: production-deploy
  cancel-in-progress: false

jobs:
  tests:
    name: Chạy Tests
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: testing
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=3
      redis:
        image: redis:7
        ports:
          - 6379:6379

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          extensions: mbstring, xml, curl, zip, gd, intl, bcmath, pdo_mysql, redis
          coverage: none

      - name: Cài đặt Composer dependencies
        run: composer install --prefer-dist --no-interaction

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Cài đặt NPM dependencies & build
        run: |
          npm ci
          npm run build

      - name: Chuẩn bị Laravel
        run: |
          cp .env.example .env
          php artisan key:generate
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_DATABASE: testing
          DB_USERNAME: root
          DB_PASSWORD: password

      - name: Chạy tests
        run: php artisan test
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_DATABASE: testing
          DB_USERNAME: root
          DB_PASSWORD: password
          REDIS_HOST: 127.0.0.1

  deploy:
    name: Deploy lên Production
    needs: tests
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          extensions: mbstring

      - name: Cài đặt Envoy
        run: composer global require laravel/envoy

      - name: Thiết lập SSH key
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan -H your-domain.com >> ~/.ssh/known_hosts

      - name: Deploy với Envoy
        run: envoy run deploy

      - name: Xóa cache CloudFront
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ap-northeast-1
        run: |
          aws cloudfront create-invalidation \
            --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
            --paths "/build/*" "/"

Luồng Deploy Trực Quan

Developer push lên main
         │
         ▼
GitHub Actions: Chạy Tests
         │ (pass)
         ▼
GitHub Actions: Deploy
         │
         ├── SSH vào EC2
         ├── git clone (thư mục release mới)
         ├── composer install --no-dev
         ├── npm ci && npm run build
         ├── Link shared .env + storage
         ├── php artisan migrate
         ├── php artisan config:cache
         ├── ln -sfn current → release mới  ← ZERO DOWNTIME
         ├── systemctl reload php-fpm
         ├── Health check /up
         │   ├── 200 → ✅ Xong
         │   └── !200 → ⏪ Tự động rollback
         │
         ▼
Xóa cache CloudFront
         │
         ▼
✅ Đang hoạt động!

Monitoring Qua Console: CloudWatch

Phần này hoàn toàn thao tác trên Console.

Bước 1: Tạo SNS Topic (Kênh Thông Báo)

  1. Thanh tìm kiếm → "SNS" → click Simple Notification Service
  2. Menu bên trái → Topics → Create topic
Thiết lập Giá trị
Type Standard
Name laravel-alerts
  1. Create topic
  2. Click vào topic vừa tạo → Create subscription
Thiết lập Giá trị
Protocol Email
Endpoint your-email@example.com
  1. Create subscription
  2. Mở email → click link xác nhận

Bước 2: Tạo CloudWatch Alarm — CPU Cao

  1. Thanh tìm kiếm → "CloudWatch" → click CloudWatch
  2. Menu bên trái → Alarms → All alarms → Create alarm
  3. Click "Select metric"
  4. Chọn: EC2 → Per-Instance Metrics
  5. Tìm instance laravel-web-01 → metric CPUUtilizationSelect metric

Metric Settings

Thiết lập Giá trị
Period 5 minutes
Statistic Average

Conditions

Thiết lập Giá trị
Threshold type Static
Whenever CPUUtilization is... Greater than 80
  1. Click Next

Actions

Thiết lập Giá trị
Alarm state trigger In alarm
SNS topic Select existing → laravel-alerts
  1. Click Next
  2. Alarm name: laravel-ec2-high-cpu
  3. Create alarm

Bước 3: Tạo Alarm — ALB 5xx Errors

  1. CloudWatch → Alarms → Create alarm
  2. Select metricApplicationELB → Per AppELB Metrics
  3. Tìm laravel-alb → metric HTTPCode_Target_5XX_Count
Thiết lập Giá trị
Period 1 minute
Statistic Sum
Threshold Greater than 10
  1. Notification → laravel-alerts
  2. Name: laravel-alb-5xx
  3. Create alarm

Bước 4: Tạo Alarm — RDS Storage Thấp

  1. CloudWatch → Create alarm → Select metric
  2. RDS → Per-Database Metricslaravel-mysqlFreeStorageSpace
Thiết lập Giá trị
Period 5 minutes
Statistic Average
Threshold Less than 2000000000 (2 GB)
  1. Notification → laravel-alerts
  2. Name: laravel-rds-low-storage
  3. Create alarm

Bước 5: Xem Dashboard (Tùy chọn)

Tạo dashboard tổng hợp để xem mọi metrics:

  1. CloudWatch → Dashboards → Create dashboard
  2. Name: laravel-production
  3. Thêm widgets:
    • Line chart: EC2 CPUUtilization
    • Line chart: ALB RequestCount, TargetResponseTime
    • Number: RDS FreeStorageSpace
    • Line chart: ElastiCache CurrConnections, CacheHitRate

Kiểm Tra Health Từ Console

ALB Target Health

  1. EC2 → Target Groups → laravel-tg
  2. Tab Targets
  3. Cột Health status: phải là healthy

Nếu unhealthy:

  • Kiểm tra health check path /up có trả về HTTP 200
  • Kiểm tra security group EC2 cho phép traffic từ ALB
  • Kiểm tra Nginx và PHP-FPM đang chạy

CloudFront Invalidation History

  1. CloudFront → Distribution → Invalidations
  2. Xem lịch sử invalidation từ mỗi lần deploy

Quản Lý Log

Cấu Hình Laravel Log

// config/logging.php
'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => ['daily', 'stderr'],
    ],
    'stderr' => [
        'driver' => 'monolog',
        'handler' => StreamHandler::class,
        'with' => ['stream' => 'php://stderr'],
        'level' => 'error',
    ],
],

CloudWatch Agent (Tùy Chọn)

Đẩy logs từ EC2 lên CloudWatch để xem centralized:

# SSH vào EC2
sudo dnf install -y amazon-cloudwatch-agent
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-config-wizard

Sau khi cấu hình, xem logs tại: CloudWatch → Log groups.

Kiến Trúc Hoàn Chỉnh

┌──────────────────────────────────────────────────────────────┐
│                        GitHub                                 │
│  Push lên main → Actions (test) → Actions (deploy qua SSH)  │
└─────────────────────────────┬────────────────────────────────┘
                              │ SSH + Envoy
                              ▼
┌──────────────────────────────────────────────────────────────┐
│                       AWS Cloud                               │
│                                                               │
│  Route 53 → CloudFront (CDN) → ALB (SSL) → EC2              │
│                                              │                │
│                                    ┌─────────┼──────────┐    │
│                                    │         │          │    │
│                                    ▼         ▼          ▼    │
│                                  RDS     ElastiCache    S3   │
│                                 MySQL      Redis       Files │
│                                                               │
│  CloudWatch: Alarms + Logs + Dashboard                        │
└──────────────────────────────────────────────────────────────┘

Tổng Chi Phí (Hàng Tháng)

Dịch vụ Cấu hình Chi phí
EC2 t3.small ~$19
RDS db.t3.micro, Multi-AZ ~$30
ALB Traffic tối thiểu ~$18
CloudFront 50GB transfer ~$4
ElastiCache cache.t3.micro ~$13
S3 10GB ~$0.25
Route 53 1 zone $0.50
CloudWatch Alarms cơ bản ~$1
Tổng ~$86/tháng

Tham Khảo Nhanh

Lệnh Deploy Hàng Ngày

# Deploy (từ máy local)
php vendor/bin/envoy run deploy

# Deploy nhánh cụ thể
php vendor/bin/envoy run deploy --branch=hotfix/critical

# Rollback
php vendor/bin/envoy run rollback

Kiểm Tra Nhanh Qua Console

Kiểm tra Đường dẫn Console
EC2 status EC2 → Instances → laravel-web-01
ALB health EC2 → Target Groups → laravel-tg → Targets
RDS status RDS → Databases → laravel-mysql
Redis status ElastiCache → Redis caches → laravel-redis
SSL expiry ACM → Certificates (tự động renew)
Billing Billing → Budgets
Alarms CloudWatch → Alarms
Logs CloudWatch → Log groups

Kiểm Tra Qua SSH

ssh laravel-prod
sudo systemctl status php-fpm
sudo systemctl status nginx
sudo supervisorctl status
tail -f /var/www/laravel/shared/storage/logs/laravel.log

Tổng Kết Series

Phần Nội dung Console hay CLI?
Phần 0 Tài khoản, IAM, billing Console
Phần 1 VPC, subnets, security groups, IAM roles Console
Phần 2 EC2 launch (Console) + server setup (SSH) Cả hai
Phần 3 RDS, S3, ElastiCache Console + SSH config
Phần 4 ACM, ALB, Route 53, CloudFront Console
Phần 5 CI/CD, monitoring Code + Console

Từ một giao diện trực quan đến hạ tầng production hoàn chỉnh, zero-downtime trên AWS. Mọi resource tạo và quản lý qua Console — bạn luôn nhìn thấy rõ mình đang làm gì.


Điều hướng Series:

Bình luận