Setup và deploy: từ AWS trống đến domain của bạn
Bài này đi hết một lượt từ một tài khoản AWS trống đến website chạy trên
domain riêng của bạn, tự động deploy mỗi khi push code. Các bước áp dụng
cho bất kỳ site static nào (Astro, Next.js static export, Hugo…) deploy
theo mô hình S3 + CloudFront + GitHub Actions — không riêng gì repo này.
Ví dụ minh hoạ dùng domain obiz-solution.com, bạn thay bằng domain thật
của mình ở mọi chỗ có YOUR_DOMAIN. Nếu chưa quen các khái niệm
IAM/Role/OIDC, đọc AWS IAM là gì? trước.
Kiến trúc đích
Route 53 (DNS) → CloudFront (CDN + SSL) → S3 (origin, private)
↑
ACM (chứng chỉ SSL)
GitHub Actions --(OIDC, không lưu key)--> IAM Role --> sync S3 + invalidate CloudFront
Domain sẽ được chuyển nameserver sang Route 53 để có thể trỏ apex
domain (YOUR_DOMAIN, không phải subdomain) thẳng vào CloudFront bằng
ALIAS record — cách chuẩn và đáng tin cậy nhất khi làm việc với CloudFront.
Bạn vẫn giữ quyền sở hữu domain ở nhà đăng ký cũ (Namecheap, GoDaddy…),
chỉ đổi nameserver.
Việc cần chuẩn bị
- Quyền truy cập AWS Console (IAM user có quyền admin tạm thời để setup — sẽ không dùng quyền này cho deploy về sau).
- Quyền truy cập nhà đăng ký domain (Namecheap hoặc tương đương) để đổi nameserver.
- Domain đã mua, ví dụ
YOUR_DOMAIN(minh hoạ:obiz-solution.com). - Repo GitHub
GITHUB_ORG/REPO(minh hoạ:tranvinhvu/obiz-solution) đã có sẵn code và workflow build (npm run buildxuất radist/).
Bước 1 — Tạo S3 bucket (origin, private)
Bucket này không bật Static website hosting và không public — vì CloudFront sẽ đọc trực tiếp qua Origin Access Control (OAC), nên bucket có thể giữ private hoàn toàn, an toàn hơn.
- S3 Console → Create bucket
- Tên bucket:
YOUR_BUCKET— không bắt buộc trùng domain, nhưng đặt tên giống domain (vd.obiz-solution.com) cho dễ nhận diện - Region: chọn region bạn quen dùng (vd.
ap-southeast-1) — không ảnh hưởng tốc độ vì CloudFront sẽ cache toàn cầu - Block all public access: giữ nguyên bật (ON) — đúng ý đồ, vì CloudFront sẽ là bên duy nhất được đọc bucket
- Bỏ qua Static website hosting — không cần bật
aws s3api create-bucket \
--bucket YOUR_BUCKET \
--region YOUR_REGION \
--create-bucket-configuration LocationConstraint=YOUR_REGION
Bước 2 — Xin chứng chỉ SSL trong ACM
CloudFront chỉ chấp nhận chứng chỉ ACM nằm ở region us-east-1 (N. Virginia), bất kể bucket hay CloudFront distribution nằm ở region nào.
- Chuyển sang region US East (N. Virginia) trên Console
- ACM → Request certificate → Public certificate
- Domain names: thêm cả hai
YOUR_DOMAINwww.YOUR_DOMAIN
- Validation method: DNS validation (khuyến nghị, tự động gia hạn)
- Sau khi tạo, ACM cung cấp một bản ghi CNAME (name + value) cho mỗi domain — cần thêm bản ghi này vào DNS thật để chứng minh bạn sở hữu domain. Việc này sẽ làm ở Bước 4, sau khi chuyển DNS sang Route 53.
- Chứng chỉ ở trạng thái Pending validation cho đến khi bản ghi CNAME được phát hiện — có thể mất vài phút đến vài giờ.
Bước 3 — Tạo Route 53 Hosted Zone và đổi nameserver
Trước khi đổi nameserver, backup lại toàn bộ record đang có ở Namecheap. Hosted zone mới tạo trong Route 53 hoàn toàn trống — mọi record cũ (MX cho email, TXT xác minh Google Workspace, CNAME xác thực SSL, record cho subdomain khác…) sẽ ngừng có hiệu lực ngay khi nameserver đổi, vì các resolver DNS không còn hỏi Namecheap nữa. Vào Namecheap → Advanced DNS → chụp lại màn hình (hoặc copy ra file) toàn bộ bảng Host Records và tab Mail Settings (MX thường được Namecheap quản lý riêng ở đây, không hiện trong Host Records) trước khi làm bước 4 bên dưới. Thiếu bước này là nguyên nhân phổ biến nhất khiến domain ngừng nhận được email ngay sau khi chuyển DNS sang AWS.
- Route 53 Console → Hosted zones → Create hosted zone
- Domain name:
YOUR_DOMAIN, Type: Public hosted zone - Route 53 tự sinh ra 4 bản ghi NS (name server) — copy lại 4 giá trị
này (dạng
ns-xxx.awsdns-xx.com, v.v.) - Đăng nhập tài khoản đăng ký domain (Namecheap…) → tìm
YOUR_DOMAIN→ phần quản lý Nameservers → chọn Custom DNS → dán 4 nameserver của Route 53 vào (bỏ nameserver mặc định của nhà đăng ký) - Lưu lại. DNS propagate có thể mất từ vài phút đến 24-48 giờ (thường nhanh hơn nhiều trong thực tế).
Kiểm tra đã trỏ đúng chưa:
Linux / macOS:
dig NS YOUR_DOMAIN +short
# phải thấy 4 dòng ns-xxxx.awsdns-xx.{com,net,org,co.uk}
Windows (PowerShell) — dig không có sẵn trên Windows, dùng:
Resolve-DnsName -Name YOUR_DOMAIN -Type NS
# cột NameHost phải hiện 4 dòng ns-xxxx.awsdns-xx.{com,net,org,co.uk}
Không muốn chuyển DNS sang Route 53? Hầu hết nhà đăng ký domain (kể cả Namecheap) không hỗ trợ CNAME/ALIAS thật ở apex domain, nên bạn chỉ có thể trỏ
www.YOUR_DOMAINbằng CNAME sang CloudFront, cònYOUR_DOMAIN(không www) phải dùng tính năng “URL Redirect” của nhà đăng ký để chuyển hướng sang bảnwww— kém tin cậy hơn (không giữ nguyên HTTPS mượt mà, phụ thuộc dịch vụ redirect riêng của họ). Route 53 ALIAS ở Bước 6 là cách được AWS khuyến nghị chính thức và là cách bài này dùng.
Bước 4 — Thêm bản ghi validate SSL vào Route 53
Sau khi hosted zone đã hoạt động (nameserver đã trỏ đúng):
- Quay lại ACM → chứng chỉ vừa tạo → mỗi domain sẽ có nút Create records in Route 53 → bấm để ACM tự thêm bản ghi CNAME validate vào đúng hosted zone
- Đợi trạng thái chứng chỉ chuyển từ Pending validation → Issued (thường vài phút sau khi DNS đã trỏ đúng)
Bước 5 — Tạo CloudFront distribution
- CloudFront Console → Create distribution
- Origin domain: chọn bucket S3
YOUR_BUCKETtừ danh sách - Origin access: chọn Origin access control settings (recommended) → tạo mới một OAC → CloudFront sẽ hiện sẵn một bucket policy mẫu, copy policy đó
- Viewer protocol policy: Redirect HTTP to HTTPS
- Alternate domain name (CNAME): thêm cả
YOUR_DOMAINwww.YOUR_DOMAIN
- Custom SSL certificate: chọn chứng chỉ ACM vừa Issued ở Bước 4
- Default root object:
index.html - Tạo distribution — quá trình deploy toàn cầu mất khoảng 5–15 phút (trạng thái chuyển từ “Deploying” sang “Enabled”)
Bắt buộc, nếu không mọi trang ngoài
/sẽ bị403 Access Denied. “Default root object” ở trên chỉ áp dụng cho đúng URL gốc/— mọi path khác (/about/,/vi/,/blog/hello-world/…) bị forward nguyên văn sang S3, mà bucket chỉ có object dạngabout/index.html, không có object tênabout/hayabout, nên S3 trả lỗi (và vì bucket policy chỉ cấps3:GetObjectchứ không cós3:ListBucket, lỗi hiện ra là403thay vì404). Astro (và hầu hết SSG khác) xuất ra đúng cấu trúcpath/index.htmlnày cho mọi trang, nên site chắc chắn dính lỗi này nếu bỏ qua bước sau:
- CloudFront Console → Functions → Create function, đặt tên vd.
rewrite-index-html- Dán code sau vào tab Build:
function handler(event) { var request = event.request; var uri = request.uri; if (uri.endsWith('/')) { request.uri += 'index.html'; } else if (!uri.includes('.')) { request.uri += '/index.html'; } return request; }- Save → Publish
- Distribution vừa tạo → tab Behaviors → chọn behavior mặc định (
*) → Edit → Function associations → Viewer request → chọn CloudFront Functions → chọn function vừa tạo → Save- Đợi distribution deploy lại (vài phút) trước khi test các route khác ngoài
/
Bước 6 — Gắn bucket policy cho phép CloudFront đọc
Vào S3 bucket → Permissions → Bucket policy → dán policy mà CloudFront gợi ý ở Bước 5 (đã điền sẵn ARN của bucket và của distribution):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::YOUR_BUCKET/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
}
}
}
]
}
Đây là cơ chế OAC: chỉ đúng CloudFront distribution này mới đọc được bucket, không ai khác — kể cả biết URL S3 trực tiếp cũng không truy cập được.
Bước 7 — Trỏ domain về CloudFront (Route 53 ALIAS)
Vào Route 53 → hosted zone YOUR_DOMAIN → Create record:
Record 1 — apex domain:
- Record name: để trống (nghĩa là
YOUR_DOMAIN) - Record type: A
- Alias: bật ON
- Route traffic to: Alias to CloudFront distribution → chọn distribution vừa tạo
Record 2 — www:
- Record name:
www - Record type: A
- Alias: bật ON, cũng trỏ về cùng CloudFront distribution
ALIAS khác CNAME ở chỗ nó dùng được ở apex domain (root, không tiền tố) và không tính phí truy vấn — đây là lý do phải dùng Route 53 thay vì chỉ dùng CNAME của nhà đăng ký domain.
Bước 8 — Tạo OIDC provider + IAM Role cho GitHub Actions
Phần này để GitHub Actions deploy được mà không cần lưu access key AWS — xem giải thích cơ chế ở AWS IAM là gì?.
- IAM → Identity providers → Add provider (bỏ qua nếu tài khoản
đã có sẵn provider cho GitHub):
- Provider type: OpenID Connect
- Provider URL:
https://token.actions.githubusercontent.com - Audience:
sts.amazonaws.com
- IAM → Roles → Create role → Trusted entity type: Web
identity → chọn provider vừa tạo, Audience
sts.amazonaws.com - Sau khi tạo role, sửa lại Trust policy để giới hạn đúng repo và
nhánh
main. Bắt đầu với format đơn giản này (rồi làm tiếp Bước 3b ngay dưới đây — gần như chắc chắn bạn sẽ cần sửa lại):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:GITHUB_ORG/REPO:ref:refs/heads/main"
}
}
}
]
}
Bước 3b — gần như chắc chắn cần làm. GitHub tự thêm ID cố định của owner/repo vào claim
sub(dạngrepo:org@OWNER_ID/repo@REPO_ID:ref:...) như một lớp chống giả mạo khi account hoặc repo từng bị đổi tên — nếu tài khoản bạn rơi vào trường hợp này, giá trị thật không khớp với format đơn giản ở trên,AssumeRoleWithWebIdentitysẽ bị từ chối dù mọi thứ khác đều đúng (lỗiNot authorized to perform sts:AssumeRoleWithWebIdentity).Thêm bước debug này vào đầu workflow để xem chính xác giá trị
subtoken thật sự gửi lên (không cần đợi build xong):- name: Debug OIDC token claims run: | JWT=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r '.value') PAYLOAD=$(echo -n "$JWT" | cut -d '.' -f2 | tr '_-' '/+') case $(( ${#PAYLOAD} % 4 )) in 2) PAYLOAD="${PAYLOAD}==";; 3) PAYLOAD="${PAYLOAD}=";; esac echo "$PAYLOAD" | base64 -d | jq '{iss, aud, sub}'Push, xem log step này trong tab Actions, rồi copy đúng giá trị
subin ra (có thể làrepo:org@12345/repo@67890:ref:refs/heads/main) dán đè vào trust policy — dùng nguyênStringEqualsvới giá trị chính xác đó, không dùng wildcard (*). AWS IAM console tự khuyến nghị tránh wildcard trongsub(“Specific GitHub Repo And Branch Recommended”) — vì chỉ có đúng 1 repo dùng role này, biết chính xác giá trị thì nên khoá tuyệt đối, vừa an toàn nhất vừa hết mọi cảnh báo của console.
- Gắn Permissions policy cho role, giới hạn đúng bucket và
distribution vừa tạo (thay
ACCOUNT_ID,YOUR_BUCKET,DISTRIBUTION_ID):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::YOUR_BUCKET",
"arn:aws:s3:::YOUR_BUCKET/*"
]
},
{
"Effect": "Allow",
"Action": "cloudfront:CreateInvalidation",
"Resource": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
}
]
}
- Copy lại Role ARN (dạng
arn:aws:iam::ACCOUNT_ID:role/ten-role) — cần cho bước tiếp theo.
Bước 9 — Thêm GitHub Secrets
Repo GitHub GITHUB_ORG/REPO → Settings → Secrets and variables →
Actions → New repository secret, thêm 4 secret:
| Secret | Giá trị |
|---|---|
AWS_ROLE_ARN | Role ARN vừa copy ở Bước 8 |
AWS_REGION | region của bucket, vd. ap-southeast-1 |
AWS_S3_BUCKET | YOUR_BUCKET |
AWS_CLOUDFRONT_DISTRIBUTION_ID | Distribution ID (xem trong CloudFront Console) |
Workflow deploy (.github/workflows/deploy.yml) dùng đúng 4 secret này để
build, aws s3 sync, rồi cloudfront create-invalidation — không cần sửa
gì thêm nếu workflow đã có sẵn theo mẫu đó.
Bước 10 — Deploy và kiểm tra
git push origin main
- Xem tab Actions trên GitHub — workflow deploy sẽ chạy: cài
dependency, build site,
aws s3 sync,cloudfront create-invalidation - Sau khi workflow xanh, mở https://YOUR_DOMAIN — kiểm tra ổ khoá
HTTPS (chứng chỉ hợp lệ từ ACM), thử cả
https://www.YOUR_DOMAIN - Nếu thấy trang chưa cập nhật ngay, đợi thêm 1–2 phút — CloudFront invalidation cần thời gian lan ra các edge location
Xử lý sự cố thường gặp
dig NS/Resolve-DnsNamechưa ra kết quả Route 53 — DNS chưa propagate, đợi thêm hoặc kiểm tra lại nameserver đã lưu đúng ở nhà đăng ký domain- Chứng chỉ ACM mãi “Pending validation” — kiểm tra bản ghi CNAME validate đã có trong Route 53 hosted zone chưa (Bước 4)
403 Access Deniedchỉ ở trang chủ/là chạy, mọi trang khác đều lỗi — thiếu CloudFront Function rewrite URI ở Bước 5, đây là nguyên nhân phổ biến nhất cho kiểu lỗi này403 Access Deniedngay cả ở trang chủ — bucket policy chưa đúng ARN distribution, hoặc OAC chưa được gắn đúng origin (Bước 6)- Tạo record ALIAS cho
wwwở Bước 7 nhưng dropdown báo “no record found”, không thấy CloudFront distribution để chọn — Route 53 chỉ cho chọn một distribution làm alias target nếu domain đó đã được khai trong Alternate domain name (CNAME) của distribution. Quay lại CloudFront → kiểm tra distribution có đủ cảYOUR_DOMAINvàwww.YOUR_DOMAINtrong Alternate domain name chưa, thêm nếu thiếu, đợi status quay lại “Enabled” rồi thử lại - CloudFront vẫn “Deploying” — bình thường, có thể mất tới 15 phút sau khi tạo hoặc sau khi đổi cấu hình
- GitHub Actions báo lỗi
Not authorized to perform sts:AssumeRoleWithWebIdentity— nguyên nhân phổ biến nhất là giá trịsubtrong trust policy không khớp: sai tên repo, sai nhánh, hoặc quên rằng GitHub tự thêm ID số vàosubkhi account/repo từng đổi tên (xem Bước 3b ở Bước 8). Dùng step debug OIDC token để xem chính xác giá trị token đang gửi, rồi dán đúng giá trị đó vào trust policy. Cũng nên kiểm tra OIDC Identity Provider có tồn tại đúng account chưa, và secretAWS_ROLE_ARNcó trỏ đúng role không
Ví dụ minh hoạ: site này (Obiz Solutions) triển khai đúng theo các bước
trên với YOUR_DOMAIN = obiz-solution.com, YOUR_BUCKET = obiz-solution.com,
GITHUB_ORG/REPO = tranvinhvu/obiz-solution — xem thêm ở bài
Obiz Solutions — chính website này.