Obiz Solutions

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 build xuất ra dist/).

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.

  1. S3 Console → Create bucket
  2. 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
  3. 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
  4. Block all public access: giữ nguyên bật (ON) — đúng ý đồ, vì CloudFront sẽ là bên duy nhất được đọc bucket
  5. 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.

  1. Chuyển sang region US East (N. Virginia) trên Console
  2. ACM → Request certificate → Public certificate
  3. Domain names: thêm cả hai
    • YOUR_DOMAIN
    • www.YOUR_DOMAIN
  4. Validation method: DNS validation (khuyến nghị, tự động gia hạn)
  5. 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.
  6. 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 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.

  1. Route 53 Console → Hosted zonesCreate hosted zone
  2. Domain name: YOUR_DOMAIN, Type: Public hosted zone
  3. 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.)
  4. Đă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ý)
  5. 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_DOMAIN bằng CNAME sang CloudFront, còn YOUR_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ản www — 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):

  1. 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
  2. Đợi trạng thái chứng chỉ chuyển từ Pending validationIssued (thường vài phút sau khi DNS đã trỏ đúng)

Bước 5 — Tạo CloudFront distribution

  1. CloudFront Console → Create distribution
  2. Origin domain: chọn bucket S3 YOUR_BUCKET từ danh sách
  3. 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 đó
  4. Viewer protocol policy: Redirect HTTP to HTTPS
  5. Alternate domain name (CNAME): thêm cả
    • YOUR_DOMAIN
    • www.YOUR_DOMAIN
  6. Custom SSL certificate: chọn chứng chỉ ACM vừa Issued ở Bước 4
  7. Default root object: index.html
  8. 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ạng about/index.html, không có object tên about/ hay about, nên S3 trả lỗi (và vì bucket policy chỉ cấp s3:GetObject chứ không có s3:ListBucket, lỗi hiện ra là 403 thay vì 404). Astro (và hầu hết SSG khác) xuất ra đúng cấu trúc path/index.html nà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:

  1. CloudFront Console → FunctionsCreate function, đặt tên vd. rewrite-index-html
  2. 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;
    }
    
  3. SavePublish
  4. Distribution vừa tạo → tab Behaviors → chọn behavior mặc định (*) → EditFunction associationsViewer request → chọn CloudFront Functions → chọn function vừa tạo → Save
  5. Đợ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 → PermissionsBucket 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_DOMAINCreate 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ì?.

  1. IAM → Identity providersAdd 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
  2. IAM → RolesCreate role → Trusted entity type: Web identity → chọn provider vừa tạo, Audience sts.amazonaws.com
  3. 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ạng repo: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, AssumeRoleWithWebIdentity sẽ bị từ chối dù mọi thứ khác đều đúng (lỗi Not authorized to perform sts:AssumeRoleWithWebIdentity).

Thêm bước debug này vào đầu workflow để xem chính xác giá trị sub token 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ị sub in ra (có thể là repo:org@12345/repo@67890:ref:refs/heads/main) dán đè vào trust policy — dùng nguyên StringEquals với giá trị chính xác đó, không dùng wildcard (*). AWS IAM console tự khuyến nghị tránh wildcard trong sub (“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.

  1. 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"
    }
  ]
}
  1. 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/REPOSettingsSecrets and variablesActionsNew repository secret, thêm 4 secret:

SecretGiá trị
AWS_ROLE_ARNRole ARN vừa copy ở Bước 8
AWS_REGIONregion của bucket, vd. ap-southeast-1
AWS_S3_BUCKETYOUR_BUCKET
AWS_CLOUDFRONT_DISTRIBUTION_IDDistribution 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
  1. Xem tab Actions trên GitHub — workflow deploy sẽ chạy: cài dependency, build site, aws s3 sync, cloudfront create-invalidation
  2. 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
  3. 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-DnsName chư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 Denied chỉ ở 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ày
  • 403 Access Denied ngay 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_DOMAIN www.YOUR_DOMAIN trong 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ị sub trong 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ào sub khi 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à secret AWS_ROLE_ARN có 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.