Giới thiệu
Ở bài trước chúng ta đã tìm hiểu về Blue/Green Deployment. Ở bài này chúng ta sẽ tìm hiểu về một phương pháp triển khai tiếp theo là A/B Testing Deployment với CloudFront và S3.
A/B Testing Deployment
Đây là phương pháp triển khai mà cho phép ứng dụng của ta sẽ có nhiều phiên bản cùng một lúc, và người dùng sẽ được điều hướng tới một phiên bản cụ thể mà ta chỉ định. Có thể là dựa vào một biến nào đó mà ta cấu hình ở Cookies của trình duyệt hoặc là tùy vào vị trí của người dùng ta sẽ điều hướng họ tới phiên bản mà ta chỉ định.
CloudFront và Lambda@Edge
Trong bài này chúng ta sẽ dùng CloudFront và Lambda@Edge để thực hiện A/B Testing Deployment cho một trang Single Page Application.
Trang SPA của ta sẽ được Hosting trên S3 Bucket và được Cache bằng DNS CloudFront, bây giờ ta sẽ Hosting thêm một phiên bản mới của trang SPA lên trên một S3 Bucket khác. Ta gọi S3 cũ là Pro và S3 mới là Pre Pro. Cấu hình 60% Request sẽ tới S3 Pro và 40% Request sẽ tới S3 Pre Pro.
Để thực hiện được việc điều hướng % Request của Client ta sẽ dùng Lambda@Edge.
Có 4 sự kiện mà CloudFront sẽ thực thi Lambda@Edge là:
- CloudFront Viewer Request: Lambda@Edge sẽ được gọi khi CloudFront nhận Request từ Client
- CloudFront Origin Request: Lambda@Edge sẽ được gọi khi CloudFront gửi Request tới Origin phía sau
- CloudFront Origin Response: Lambda@Edge sẽ được gọi CloudFront nhận Request từ Origin
- CloudFront Viewer Response: Lambda@Edge sẽ được gọi trước khi CloudFront trả về response cho Client
Và trong Lambda@Edge ta sẽ sửa lại Request của Client để nó điều hướng tới S3 mà ta chỉ định.
Thực thi
Base Structure
Hệ thống của ta sẽ làm như sau.
Mình sẽ giải thích kĩ từng phần, đầu tiên ta sẽ tạo CloudFront và S3 Bucket Pro trước, tạo 3 tệp tin là main.tf
, s3.tf
, cloudfront-tf
.
provider "aws" {
region = "us-west-2"
}
output "dns" {
value = aws_cloudfront_distribution.s3_distribution.domain_name
}
Code của S3.
resource "aws_s3_bucket" "s3_pro" {
bucket = "terraform-serries-s3-pro"
force_destroy = true
}
resource "aws_s3_bucket_acl" "s3_pro" {
bucket = aws_s3_bucket.s3_pro.id
acl = "private"
}
resource "aws_s3_bucket_website_configuration" "s3_pro" {
bucket = aws_s3_bucket.s3_pro.bucket
index_document {
suffix = "index.html"
}
error_document {
key = "error.html"
}
}
data "aws_iam_policy_document" "s3_pro" {
statement {
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.s3_pro.arn}/*"]
principals {
type = "AWS"
identifiers = [
aws_cloudfront_origin_access_identity.origin_access_identity.iam_arn
]
}
}
}
resource "aws_s3_bucket_policy" "s3_pro" {
bucket = aws_s3_bucket.s3_pro.id
policy = data.aws_iam_policy_document.s3_pro.json
}
Code của CloudFront.
locals {
s3_origin_id = "access-identity-s3-pro"
s3_origin_staging_id = "access-identity-s3-pre-pro"
}
resource "aws_cloudfront_origin_access_identity" "origin_access_identity" {
comment = local.s3_origin_id
}
resource "aws_cloudfront_distribution" "s3_distribution" {
origin {
domain_name = aws_s3_bucket.s3_pro.bucket_regional_domain_name
origin_id = local.s3_origin_id
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.origin_access_identity.cloudfront_access_identity_path
}
}
enabled = true
is_ipv6_enabled = true
default_root_object = "index.html"
default_cache_behavior {
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = local.s3_origin_id
forwarded_values {
query_string = true
query_string_cache_keys = ["index"]
cookies {
forward = "all" // none or all
}
}
viewer_protocol_policy = "allow-all"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
}
Sau đó ta chạy câu lệnh apply
để tạo Resource.
$ terraform apply -auto-approve
...
Plan: 6 to add, 0 to change, 0 to destroy
...
Apply complete! Resources: 6 added, 0 changed, 0 destroyed.
Outputs:
dns = "d2qm7woq264bw9.cloudfront.net"
Sau khi Terraform chạy xong nó sẽ hiển thị cho ta URL của CloudFront, nếu trong quá trình chạy có lỗi do tên của S3 Bucket trùng thì các bạn đổi tên S3 Bucket lại nhé. Hệ thống của ta sau khi chạy Terraform.
Tiếp theo ta sẽ up code lên trên S3 Bucket Pro, các bạn tải code của SPA ở Repo sau Terraform Series, sau đó mở thư mục bai-11
lên, ta sẽ thấy hai thư mục là s3-pro
và s3-pre-pro
, ta làm việc với thư mục s3-pro
trước.
Nhảy vào thư mục s3-pro
và chạy những câu lệnh sau.
npm install
npm run build
Sau khi ta chạy câu lệnh build
xong thì sẽ thấy nó xuất ra một thư mục là build
, ta sẽ up thư mục này lên trên S3 Pro.
aws s3 cp build s3://terraform-serries-s3-pro/ --recursive
Giờ thi ta truy cập vào URL của Cloudfront https://d2qm7woq264bw9.cloudfront.net/
.
Môi trường Pre Pro
Tiếp theo ta sẽ tạo S3 Bucket Pre Pro, tạo một tệp tin tên là s3_pre_pro.tf
.
resource "aws_s3_bucket" "s3_pre_pro" {
bucket = "terraform-serries-s3-pre-pro"
force_destroy = true
}
resource "aws_s3_bucket_acl" "s3_pre_pro" {
bucket = aws_s3_bucket.s3_pre_pro.id
acl = "private"
}
resource "aws_s3_bucket_website_configuration" "s3_pre_pro" {
bucket = aws_s3_bucket.s3_pre_pro.bucket
index_document {
suffix = "index.html"
}
error_document {
key = "error.html"
}
}
data "aws_iam_policy_document" "s3_pre_pro" {
statement {
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.s3_pre_pro.arn}/*"]
principals {
type = "AWS"
identifiers = [
aws_cloudfront_origin_access_identity.origin_access_identity.iam_arn
]
}
}
}
resource "aws_s3_bucket_policy" "s3_pre_pro" {
bucket = aws_s3_bucket.s3_pre_pro.id
policy = data.aws_iam_policy_document.s3_pre_pro.json
}
Và cập nhật lại tệp tin main.tf
thêm vào output
sau.
...
output "s3" {
value = {
pro = aws_s3_bucket.s3_pro.bucket_domain_name
pre_pro = aws_s3_bucket.s3_pre_pro.bucket_domain_name
}
}
Chạy câu lệnh apply
để tạo Resource mới.
$ terraform apply -auto-approve
...
Plan: 4 to add, 0 to change, 0 to destroy.
...
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
dns = "d2qm7woq264bw9.cloudfront.net"
s3 = {
"pre_pro" = "terraform-serries-s3-pre-pro.s3.amazonaws.com"
"pro" = "terraform-serries-s3-pro.s3.amazonaws.com"
}
Sau đó ta mở thư mục s3-pre-pro
lên, làm tương tự khi nãy để up code lên S3.
npm install & npm run build
aws s3 cp build s3://terraform-serries-s3-pre-pro/ --recursive
Hệ thống của ta lúc này.
Cấu hình Lambda@Edge
Giờ ta sẽ làm phần quan trọng nhất là cấu hình Lambda@Edge để điều hướng người dùng tới S3 Bucket mà ta muốn. Ta làm việc đó bằng cách nhúng một Cookies vào trong trình duyệt của người dùng. Cookie mà ta nhúng có giá trị là X-Redirect-Flag=Pro
hoặc X-Redirect-Flag=Pre-Pro
.
Sau đó ta sẽ kiểm tra nếu trong Request của người dùng có Cookie với giá trị là X-Redirect-Flag=Pro
thì ta sẽ chuyển nó qua S3 Pro hoặc ngược lại.
Logic mà ta sẽ thực hiện ở hàm 1:
- Kiểm tra trong Headers có Cookies mà ta cần hay chưa, nếu có thì ta sẽ cho Request đi tiếp bình thường
- Nếu trong Headers không có Cookies mà ta cần, thì ta sẽ random để nhúng vào Headers của 60% Request là Cookie
X-Redirect-Flag=Pro
, 40% Request là CookieX-Redirect-Flag=Pre-Pro
Logic mà ta sẽ thực hiện ở hàm 2:
- Kiểm tra nếu Request của người dùng có chứa Cookie
X-Redirect-Flag=Pro
thì điều hướng tới S3 Bucket Pro - Kiểm tra nếu Request của người dùng có chứa Cookie
X-Redirect-Flag=Pre-Pro
thì điều hướng tới S3 Bucket Pre Pro
Logic mà ta sẽ thực hiện ở hàm 3:
- Sau khi ta trả về Response cho người dùng, ta sẽ kiểm tra tiếp nếu trong Headers có Cookie
X-Redirect-Flag=Pro
hoặcX-Redirect-Flag=Pre-Pro
thì ta sẽ cấu hình Cookie đó cho trình duyệt của người dùng đó luôn, để lần sau người gửi Request lên sẽ có Cookie đó
Ở thư mục terraform
ta một thêm một thư mục nữa là function
, sau đó ta tạo thêm 3 tệp tên là viewer-request.js
, origin-request.js
, origin-response.js
├── cloudfront.tf
├── function
│ ├── origin_request.js
│ ├── origin_response.js
│ └── viewer_request.js
├── main.tf
├── s3.tf
├── s3_pre_pro.tf
└── terraform.tfstate
Code của tệp tin viewer-request.js
.
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
// Look for cookie
if (headers.cookie) {
for (let i = 0; i < headers.cookie.length; i++) {
if (headers.cookie[i].value.indexOf("X-Redirect-Flag") >= 0) {
console.log("Source cookie found. Forwarding request as-is");
// Forward request as-is
callback(null, request);
return;
}
}
}
// Add Source cookie
const cookie = Math.random() < 0.6 ? "X-Redirect-Flag=Pro" : "X-Redirect-Flag=Pre-Pro";
headers.cookie = headers.cookie || [];
headers.cookie.push({ key: "Cookie", value: cookie });
// Forwarding request
callback(null, request);
};
Code của file origin-request.js
.
exports.handler = async (event, context, callback) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
if (headers.cookie) {
for (let i = 0; i < headers.cookie.length; i++) {
if (headers.cookie[i].value.indexOf("X-Redirect-Flag=Pro") >= 0) {
request.origin = {
s3: {
authMethod: "origin-access-identity",
domainName: "terraform-serries-s3-pro.s3.amazonaws.com",
region: "us-west-2",
path: "",
},
};
headers["host"] = [
{
key: "host",
value: "terraform-serries-s3-pro.s3.amazonaws.com",
},
];
break;
}
if (headers.cookie[i].value.indexOf("X-Redirect-Flag=Pre-Pro") >= 0) {
request.origin = {
s3: {
authMethod: "origin-access-identity",
domainName: "terraform-serries-s3-pre-pro.s3.amazonaws.com",
region: "us-west-2",
path: "",
},
};
headers["host"] = [
{
key: "host",
value: "terraform-serries-s3-pre-pro.s3.amazonaws.com",
},
];
break;
}
}
}
callback(null, request);
};
Code của tệp tin origin-response.js
.
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const requestHeaders = request.headers;
const response = event.Records[0].cf.response;
// Look for cookie
if (requestHeaders.cookie) {
for (let i = 0; i < requestHeaders.cookie.length; i++) {
if (requestHeaders.cookie[i].value.indexOf("X-Redirect-Flag=Pro") >= 0) {
response.headers["set-cookie"] = [{ key: "Set-Cookie", value: `X-Redirect-Flag=Pro; Path=/` }];
callback(null, response);
return;
}
if (requestHeaders.cookie[i].value.indexOf("X-Redirect-Flag=Pre-Pro") >= 0) {
response.headers["set-cookie"] = [{ key: "Set-Cookie", value: `X-Redirect-Flag=Pre-Pro; Path=/` }];
callback(null, response);
return;
}
}
}
// If request contains no Source cookie, do nothing and forward the response as-is
callback(null, response);
};
Giờ ta sẽ dùng Terraform để tạo Lambda Function và cấu hình Lambda@Edge cho CloudFront, tạo hai tệp tin tên là iam_role.tf
và lambda.tf
.
resource "aws_iam_role" "lambda_edge" {
name = "AWSLambdaEdgeRole"
path = "/service-role/"
assume_role_policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Principal" : {
"Service" : [
"edgelambda.amazonaws.com",
"lambda.amazonaws.com",
]
},
"Action" : "sts:AssumeRole",
}
]
})
inline_policy {
name = "AWSLambdaEdgeInlinePolicy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect : "Allow",
Action : [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
Resource : [
"arn:aws:logs:*:*:*"
]
}
]
})
}
}
Ta sẽ tạo IAM Role cho Lambda để nó có quyền ghi logs vào CloudWatch. Sau đó ta sẽ tạo 3 tệp tin Zip cho 3 hàm của ta ở trên.
data "archive_file" "zip_file_for_lambda_viewer_request" {
type = "zip"
source_file = "function/viewer-request.js"
output_path = "function/viewer-request.zip"
}
data "archive_file" "zip_file_for_lambda_origin_request" {
type = "zip"
source_file = "function/origin-request.js"
output_path = "function/origin-request.zip"
}
data "archive_file" "zip_file_for_lambda_origin_response" {
type = "zip"
source_file = "function/origin-response.js"
output_path = "function/origin-response.zip"
}
Chạy lại câu lệnh init
vì ta thêm một provider
mới là archive_file
.
terraform init
Sau đó ta chạy câu lệnh apply
.
$ terraform apply -auto-approve
...
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
dns = "d2qm7woq264bw9.cloudfront.net"
s3 = {
"pre_pro" = "terraform-serries-s3-pre-pro.s3.amazonaws.com"
"pro" = "terraform-serries-s3-pro.s3.amazonaws.com"
}
Lúc này ta sẽ thấy có 3 tệp tin Zip cho 3 hàm.
├── cloudfront.tf
├── function
│ ├── origin-request.js
│ ├── origin-request.zip
│ ├── origin-response.js
│ ├── origin-response.zip
│ ├── viewer-request.js
│ └── viewer-request.zip
├── iam_role.tf
├── lambda.tf
├── main.tf
├── s3.tf
├── s3_pre_pro.tf
└── terraform.tfstate
Tiếp theo ta tạo Lambda Function, cập nhật lại tệp tin lambda.tf
.
...
provider "aws" {
region = "us-east-1"
alias = "us-east-1"
}
resource "aws_lambda_function" "viewer_request_function" {
function_name = "viewer-request-ab-testing"
role = aws_iam_role.lambda_edge.arn
publish = true
handler = "viewer-request.handler"
runtime = "nodejs14.x"
filename = "function/viewer-request.zip"
source_code_hash = filebase64sha256("function/viewer-request.zip")
provider = aws.us-east-1
}
resource "aws_lambda_function" "origin_request_function" {
function_name = "origin-request-ab-testing"
role = aws_iam_role.lambda_edge.arn
publish = true
handler = "origin-request.handler"
runtime = "nodejs14.x"
filename = "function/origin-request.zip"
source_code_hash = filebase64sha256("function/origin-request.zip")
provider = aws.us-east-1
}
resource "aws_lambda_function" "origin_response_function" {
function_name = "origin-response-ab-testing"
role = aws_iam_role.lambda_edge.arn
publish = true
handler = "origin-response.handler"
runtime = "nodejs14.x"
filename = "function/origin-response.zip"
source_code_hash = filebase64sha256("function/origin-response.zip")
provider = aws.us-east-1
}
Hiện tại AWS chỉ hỗ trợ các Lambda nào tạo ở Region us-east-1
mới có thể triển khai thành Lambda@Edge, nên ta phải tạo Lambda ở Region us-east-1
. Ở trong Terraform nếu ta muốn tạo Resource ở nhiều Region khác nhau, ta phải thêm vào Resource đó trường provider
, với provider
được cấu hình với trường alias
đi kèm, ví dụ ở trên ta khai báo AWS Provider cho us-east-1
.
provider "aws" {
region = "us-east-1"
alias = "us-east-1"
}
Tiếp theo ta thêm S3 Bucket Pre Pro vào trong CloudFront và triển khai Lambda@Edge lên trên CloudFront, cập nhật lại tệp tin cloudfront.tf
.
resource "aws_cloudfront_distribution" "s3_distribution" {
origin {
domain_name = aws_s3_bucket.s3_pro.bucket_regional_domain_name
origin_id = local.s3_origin_id
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.origin_access_identity.cloudfront_access_identity_path
}
}
origin {
domain_name = aws_s3_bucket.s3_pre_pro.bucket_regional_domain_name
origin_id = local.s3_origin_staging_id
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.origin_access_identity.cloudfront_access_identity_path
}
}
enabled = true
is_ipv6_enabled = true
default_root_object = "index.html"
default_cache_behavior {
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = local.s3_origin_id
forwarded_values {
query_string = true
query_string_cache_keys = ["index"]
cookies {
forward = "all" // none or all
}
}
lambda_function_association {
event_type = "viewer-request"
lambda_arn = aws_lambda_function.viewer_request_function.qualified_arn
include_body = false
}
lambda_function_association {
event_type = "origin-request"
lambda_arn = aws_lambda_function.origin_request_function.qualified_arn
include_body = false
}
lambda_function_association {
event_type = "origin-response"
lambda_arn = aws_lambda_function.origin_response_function.qualified_arn
include_body = false
}
viewer_protocol_policy = "allow-all"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
}
Ta thêm hai đoạn code.
origin {
domain_name = aws_s3_bucket.s3_pre_pro.bucket_regional_domain_name
origin_id = local.s3_origin_staging_id
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.origin_access_identity.cloudfront_access_identity_path
}
}
Chỗ này ta thêm origin
là S3 Bucket Pre Pro cho Cloudfront, và đoạn:
default_cache_behavior {
...
lambda_function_association {
event_type = "viewer-request"
lambda_arn = aws_lambda_function.viewer_request_function.qualified_arn
include_body = false
}
lambda_function_association {
event_type = "origin-request"
lambda_arn = aws_lambda_function.origin_request_function.qualified_arn
include_body = false
}
lambda_function_association {
event_type = "origin-response"
lambda_arn = aws_lambda_function.origin_response_function.qualified_arn
include_body = false
}
...
}
Chỗ này ta triển khai Lambda thành Lambda@Edge cho CloudFront, ta chạy câu lệnh apply
để tạo Lambda.
$ terraform apply -auto-approve
...
Apply complete! Resources: 3 added, 1 changed, 0 destroyed.
Outputs:
dns = "d2qm7woq264bw9.cloudfront.net"
s3 = {
"pre_pro" = "terraform-serries-s3-pre-pro.s3.amazonaws.com"
"pro" = "terraform-serries-s3-pro.s3.amazonaws.com"
}
Sau khi Terraform chạy xong thì ta truy cập vào URL của CloudFront và kiểm tra.
Lúc trang ta tải xong bạn mở qua phần Application kiểm tra Cookie sẽ thấy Cookie mà ta cấu hình cho trình duyệt của người dùng. Để kiểm tra ta có thể nhảy qua trang Pre Pro được hay không, bạn sửa giá trị của Cookie lại thành Pre-Pro
.
Ta đã triển khai A/B Testing Deployment thành công 😁.
Kết luận
Vậy là ta đã tìm hiểu xong cách triển khai A/B Testing Deployment bằng Terraform. Trong bài này thì ta cũng có code một chút, mà đối với Role DevOps của chúng ta thì cũng không cần phải hiểu quá rõ về code làm gì, ta chỉ cần biết những cú pháp cơ bản và đọc hiểu code đơn giản là được.
Tác giả Quân Huỳnh
Nếu bài viết có gì sai hoặc cần cập nhật thì liên hệ Admin.
Tham gia nhóm chat của DevOps VN tại Telegram.
Kém tiếng Anh và cần nâng cao trình độ giao tiếp: Tại sao bạn học không hiệu quả?