Triển Khai Laravel Lên AWS Qua Console (Phần 5): CI/CD Với GitHub Actions & Zero-Downtime Deploy
Ở 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.
Chiến Lược Deploy: Symlink Releases
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ì:
- Code mới deploy vào thư mục
releases/hoàn toàn mới - 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
- Symlink
currentchuyển nguyên tử (ln -sfn) - 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
- CloudFront → Distributions
- 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)
- Thanh tìm kiếm → "SNS" → click Simple Notification Service
- Menu bên trái → Topics → Create topic
| Thiết lập | Giá trị |
|---|---|
| Type | Standard |
| Name | laravel-alerts |
- Create topic
- Click vào topic vừa tạo → Create subscription
| Thiết lập | Giá trị |
|---|---|
| Protocol | |
| Endpoint | your-email@example.com |
- Create subscription
- Mở email → click link xác nhận
Bước 2: Tạo CloudWatch Alarm — CPU Cao
- Thanh tìm kiếm → "CloudWatch" → click CloudWatch
- Menu bên trái → Alarms → All alarms → Create alarm
- Click "Select metric"
- Chọn: EC2 → Per-Instance Metrics
- Tìm instance
laravel-web-01→ metric CPUUtilization → Select 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 |
- Click Next
Actions
| Thiết lập | Giá trị |
|---|---|
| Alarm state trigger | In alarm |
| SNS topic | Select existing → laravel-alerts |
- Click Next
- Alarm name:
laravel-ec2-high-cpu - Create alarm
Bước 3: Tạo Alarm — ALB 5xx Errors
- CloudWatch → Alarms → Create alarm
- Select metric → ApplicationELB → Per AppELB Metrics
- Tìm
laravel-alb→ metric HTTPCode_Target_5XX_Count
| Thiết lập | Giá trị |
|---|---|
| Period | 1 minute |
| Statistic | Sum |
| Threshold | Greater than 10 |
- Notification →
laravel-alerts - Name:
laravel-alb-5xx - Create alarm
Bước 4: Tạo Alarm — RDS Storage Thấp
- CloudWatch → Create alarm → Select metric
- RDS → Per-Database Metrics →
laravel-mysql→ FreeStorageSpace
| Thiết lập | Giá trị |
|---|---|
| Period | 5 minutes |
| Statistic | Average |
| Threshold | Less than 2000000000 (2 GB) |
- Notification →
laravel-alerts - Name:
laravel-rds-low-storage - Create alarm
Bước 5: Xem Dashboard (Tùy chọn)
Tạo dashboard tổng hợp để xem mọi metrics:
- CloudWatch → Dashboards → Create dashboard
- Name:
laravel-production - 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
- EC2 → Target Groups → laravel-tg
- Tab Targets
- Cột Health status: phải là healthy
Nếu unhealthy:
- Kiểm tra health check path
/upcó 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
- CloudFront → Distribution → Invalidations
- 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:
- ← Phần 0: Chuẩn Bị
- ← Phần 1: Kiến Trúc & VPC
- ← Phần 2: EC2 & Amazon Linux 2023
- ← Phần 3: RDS, S3 & ElastiCache
- ← Phần 4: ALB, CloudFront & SSL
- Phần 5: CI/CD & Zero-Downtime Deploy (Bạn đang ở đây)