Tự Động Deploy Next.js Static Site với Jenkins, S3 và CloudFront (OAC) Chuyên mục Devops 2025-05-15 1 Lượt xem 0 Lượt thích 0 Bình luận
Trong bài viết này, mình sẽ chia sẻ chi tiết quy trình tự động hóa deploy một ứng dụng Next.js Static Export lên S3 + CloudFront thông qua Jenkins Pipeline. Bài viết sẽ giải thích rõ từng bước và highlight những điểm cực kỳ quan trọng mà bạn nên lưu ý.
Ở bài trước đó mình cũng đã có 1 bài về Hướng Dẫn Triển Khai Hệ Thống Phân Phối Nội Dung Với S3 + CloudFront + ACM + OAC + Route 53 Nếu bạn nào chưa biết thì đọc qua bài này trước đọc bài viết này để hiểu rõ hơn nhé!
1. Tổng Quan Kiến Trúc
-
Next.js app build bằng output: "export" → thư mục out/
-
Jenkins Pipeline tự động hóa:
-
Phân tích mã với SonarQube
-
Build + deploy thư mục out/ lên S3
-
Invalidate cache trên CloudFront
-
-
Không bật Static Website Hosting của S3
-
Sử dụng CloudFront OAC (Origin Access Control) để bảo mật bucket
-
Dùng CloudFront Function để handle đường dẫn clean URLs
2. Cấu hình next.config.js
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: false,
devIndicators: false,
output: "export",
images: {
unoptimized: true, // bắt buộc với static export
},
trailingSlash: false, // quan trọng, xem giải thích bên dưới
};
export default nextConfig;
🔍 trailingSlash: false vs true
Option | Cấu trúc thư mục khi build (out/) | URL cần truy cập |
---|---|---|
trailingSlash: true | out/signin/index.html | /signin/ |
trailingSlash: false | out/signin.html | /signin |
Nếu bạn để true thì khi truy cập /signin sẽ 404, vì CloudFront tìm signin thay vì signin/index.html. Ngược lại, nếu để false thì /signin/ cũng sẽ 404, vì signin/ không tồn tại trong S3.
👉 Lời khuyên: Sử dụng trailingSlash: false và kết hợp CloudFront Function để rewrite đường dẫn chính xác.
3. CloudFront Function: Rewrite URL chuẩn SEO
✅ Mục tiêu:
-
Rewrite /signin → /signin.html
-
Rewrite /setting-user → /setting-user.html
-
Redirect /signin/ → /signin
✅ Function Code:
function handler(event) {
var request = event.request;
var uri = request.uri;
// Nếu URL có slash cuối → redirect về không slash
if (uri !== '/' && uri.endsWith('/')) {
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: {
location: { value: uri.slice(0, -1) }
}
};
}
// Nếu không có phần mở rộng → thêm .html
if (!uri.includes('.') && !uri.endsWith('/')) {
request.uri += '.html';
}
return request;
}
🧠 Tại sao cần function này?
CloudFront không hiểu rewrite như Next.js server. Khi static export, nó chỉ thấy file .html. Nếu bạn truy cập /signin, nó không tự động map thành signin.html. Function này sẽ giúp giải quyết triệt để vấn đề.
⚠️ Đừng quên: Gắn function vào Viewer Request của CloudFront Distribution.
4. Jenkins Pipeline đầy đủ
pipeline {
agent any
environment {
AWS_REGION = "ap-northeast-1"
TEAMS_WEBHOOK_URL = ''
SONARSERVER = 'sonarserver'
SONARSCANNER = 'sonarscanner'
registryCredential = ''
S3_BUCKET_NAME = ''
CLOUDFRONT_ID = ''
}
stages {
stage('Checkout Code') {
steps {
checkout scm
}
}
stage('SonarQube Analysis') {
environment {
scannerHome = tool "${SONARSCANNER}"
}
steps {
withSonarQubeEnv("${SONARSERVER}") {
script {
env.SONAR_SERVER_URL = env.SONAR_HOST_URL
env.SONAR_TOKEN = env.SONAR_AUTH_TOKEN
}
sh '''${scannerHome}/bin/sonar-scanner \
-Dsonar.projectKey=liveapp_cms \
-Dsonar.projectName=LiveAppCMS \
-Dsonar.projectVersion=1.0 \
-Dsonar.sources=. \
-Dsonar.exclusions="**/migrations/**, **/node_modules/**, **/test/**" \
-Dsonar.working.directory=.sonar'''
}
}
}
stage('Wait for SonarQube') {
steps {
timeout(time: 15, unit: 'MINUTES') {
script {
def qualityGate = waitForQualityGate()
if (qualityGate.status != 'OK') {
sendTeamsNotification("Quality Gate failed: ${qualityGate.status}")
error "Quality Gate failed: ${qualityGate.status}"
}
}
}
}
}
stage('Install Dependencies') {
tools {
nodejs 'nodejs_v20'
}
steps {
sh 'npm install'
}
}
stage('Build Next.js Static Site') {
steps {
sh 'npm run build'
}
}
stage('Deploy to S3') {
steps {
withAWS(credentials: 'awscreds', region: "${AWS_REGION}") {
sh "aws s3 sync out/ s3://${S3_BUCKET_NAME} --delete"
}
}
}
stage('Invalidate CloudFront Cache') {
steps {
withAWS(credentials: 'awscreds', region: "${AWS_REGION}") {
sh '''
aws cloudfront create-invalidation --distribution-id ${CLOUDFRONT_ID} --paths "/*"
'''
}
}
}
}
post {
success {
sendTeamsNotification("✅ Deployment successful! 🎉")
}
}
}
def sendTeamsNotification(String message) {
def sanitizedMessage = message
.replaceAll("\\\\", "\\\\\\\\")
.replaceAll("\"", "\\\\\"")
.replaceAll("\n", "\\\\n")
def payload = """
{
"@type": "MessageCard",
"@context": "https://schema.org/extensions",
"themeColor": "ff0000",
"summary": "Jenkins Build Notification",
"sections": [{
"activityTitle": "CMS Jenkins Pipeline Notification",
"text": "${sanitizedMessage}"
}]
}
"""
sh "curl -H 'Content-Type: application/json' -d '${payload}' '${TEAMS_WEBHOOK_URL}'"
}
5. Xử lý lỗi 403/404 trên CloudFront
Vì bạn không bật Static Website Hosting, nên CloudFront khi gặp lỗi sẽ trả về XML thô rất xấu. Bạn nên cấu hình:
-
Custom Error Page cho 403/404 → trỏ về /404.html trong S3
-
Trong CloudFront → chọn Customize Error Response → Response Page Path: /404.html, Response Code: 200
6. Lưu ý quan trọng
-
Invalidate CloudFront Cache sau mỗi deploy để người dùng không thấy file cũ
-
Không để images.optimize = true trong next.config.js khi dùng output: export
-
Dữ liệu dynamic hoặc API cần build thành static trước khi export hoặc dùng các giải pháp SSR khác
✅ Kết luận
Việc kết hợp Next.js Static Export, S3, CloudFront OAC và Jenkins cho CI/CD không hề đơn giản nếu bạn không hiểu rõ cách hoạt động của URL mapping và cấu trúc file sau build. Việc sử dụng CloudFront Function chính là chìa khóa quan trọng để giữ cho hệ thống hoạt động mượt mà, chuẩn SEO và bảo mật tốt hơn.
Nếu bạn muốn source demo hoặc hỗ trợ setup CI/CD tương tự, hãy để lại bình luận hoặc liên hệ qua blog nhé!
Bình luận (0)