- vừa được xem lúc

Terraform Series - Bài 11 - Terraform A/B Testing Deployment

0 0 46

Người đăng: Quân Huỳnh

Theo Viblo Asia

Giới thiệu

Chào các bạn tới với series về Terraform, ở 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 deploy tiếp theo là A/B Testing Deployment với CloudFront và S3.

image.png

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 version cùng một lúc, và người dùng sẽ được redirect tới một version cụ thể dựa vào một flag gì đó 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 browser hoặc là tùy vào location của người dùng ta sẽ redirect họ tới version mà ta chỉ định.

image.png

A/B Testing Deployment with CloudFront and _@.com

Trong bài này chúng ta sẽ dùng CloudFront và _@.com để 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 version 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, và 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 redirect % request của client này ta sẽ dùng _@.com Và ta cần cấu hình CloudFront để nó trigger _@.com, như hình minh họa sau.

Có 4 event mà CloudFront sẽ trigger _@.com là:

  • CloudFront Viewer Request: _@.com sẽ được gọi khi CloudFront nhận request từ client.
  • CloudFront Origin Request: _@.com sẽ được gọi khi CloudFront gửi request tới origin phía sau.
  • CloudFront Origin Response: _@.com sẽ được gọi CloudFront nhận request từ origin.
  • CloudFront Viewer Response: _@.com sẽ được gọi trước khi CloudFront trả về response cho client.

Và trong _@.com ta sẽ sửa lại request của client để nó redirect tới S3 chứa trang SPA mà ta chỉ định.

image.png

Implement

Base structure

Oke giờ ta sẽ bắt tay vào làm nào, 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 file là main.tf, s3.tf, cloudfront-tf.

provider "aws" { region = "us-west-2" profile = "kala"
} output "dns" { value = aws_cloudfront_distribution.s3_distribution.domain_name
}

S3 code.

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
}

CloudFront code.

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ẽ output cho ta domain 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.

image.png

Tiếp theo ta sẽ upload code lên trên S3 bucket pro, các bạn tải code của source SPA ở repo sau https://github.com/hoalongnatsu/terraform-series.git, sau đó mở thư mục bai-11 lên, ta sẽ thấy hai source là s3-pro và s3-pre-pro, ta làm việc với source 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ó output ra một thư mục là build, ta sẽ upload 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 domain của cloudfront https://d2qm7woq264bw9.cloudfront.net/, bạn sẽ thấy được trang SPA của ta.

image.png

Pre pro enviroment

Tiếp theo ta sẽ tạo S3 bucket pre pro, tạo một file 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 file 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 để upload 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.

image.png

Config _@.com and CloudFront Function.

Giờ ta sẽ làm phần quan trọng nhất là cấu hình _@.com để redirect client tới S3 bucket mà ta muốn, ta sẽ làm việc đó bằng cách nhúng một cookies vào trong browser của client, cookie mà ta sẽ nhúng có giá trị là X-Redirect-Flag=Pro hoặc X-Redirect-Flag=Pre-Pro.

Sau đó nếu client gửi request lên trang của ta thì ta sẽ kiểm ta là nếu trong headers của client 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.

image.png

Logic mà ta sẽ thực hiện ở function 1.

  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.
  2. 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à cookie X-Redirect-Flag=Pre-Pro.

Logic mà ta sẽ thực hiện ở function 2.

  1. Kiểm tra nếu request của client có chứa cookie X-Redirect-Flag=Pro thì redirect tới S3 bucket pro.
  2. Kiểm tra nếu request của client có chứa cookie X-Redirect-Flag=Pre-Pro thì redirect tới S3 bucket pre pro.

Logic mà ta sẽ thực hiện ở function 3.

  1. Sau khi ta trả về response cho client, ta sẽ kiểm tra tiếp nếu trong headers có cookie X-Redirect-Flag=Pro hoặc X-Redirect-Flag=Pre-Pro thì ta sẽ set cookie đó cho browser của client đó luôn, để lần sau client 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 file 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 file 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 file 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 _@.com cho CloudFront, tạo một hai file tên là iam_role.tflambda.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 đó vì Lambda funcion yêu cầu file zip để upload code, ta sẽ tạo 3 file zip cho 3 function 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 file zip cho 3 function.

├── 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 file lambda.tf.

...
provider "aws" { region = "us-east-1" alias = "us-east-1" profile = "kala"
} 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ể deploy thành _@.com, 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 region us-east-1.

provider "aws" { region = "us-east-1" alias = "us-east-1" profile = "kala"
}

Tiếp theo, ta sẽ thêm S3 bucket pre pro vào trong CloudFront và deploy _@.com lên trên CloudFront, cập nhật lại file 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 doạ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 deploy Lambda thành _@.com 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 domain của CloudFront và kiểm tra nào.

image.png

Lúc trang ta tải xong, bạn mở qua tab Application kiểm tra cookie sẽ thấy cookie mà ta set cho browser của client. Để test ta có thể nhảy qua trang pre pro được hay không, bạn xóa sửa lại value của cookie lại thành Pre-Pro.

image.png

Oke, 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. Nếu có thắc mắc hoặc cần giải thích rõ thêm chỗ nào thì các bạn có thể hỏi dưới phần comment. Bài tiếp theo ta sẽ tìm hiểu về chủ đề khá thú vị là sử dụng Terraform với Ansible.

Mục tìm kiếm đồng đội

Hiện tại thì công ty bên mình, là Hoàng Phúc International, với hơn 30 năm kinh nghiệm trong lĩnh vực thời trang. Và sở hữu trang thương mại điện tử về thời trang lớn nhất Việt Nam. Team công nghệ của HPI đang tìm kiếm đồng đội cho các vị trí như:

Với mục tiêu trong vòng 5 năm tới về mảng công nghệ là:

  • Sẽ có trang web nằm trong top 10 trang web nhanh nhất VN với 20 triệu lượt truy cập mỗi tháng.
  • 5 triệu loyal customers và có hơn 10 triệu transactions mỗi năm.

Team đang xây dựng một hệ thống rất lớn với rất nhiều vấn đề cần giải quyết, và sẽ có rất nhiều bài toán thú vị cho các bạn. Nếu các bạn có hứng thú trong việc xây dựng một hệ thống lớn, linh hoạt, dễ dàng mở rộng, và performance cao với kiến trúc microservices thì hãy tham gia với tụi mình.

Nếu các bạn quan tâm hãy gửi CV ở trong trang tuyển dụng của Hoàng Phúc International hoặc qua email của mình nha _@.com. Cảm ơn các bạn đã đọc.

Bình luận

Bài viết tương tự

- vừa được xem lúc

Đề thi interview DevOps ở Châu Âu

Well. Chào mọi người, mình là Rice - một DevOps Engineers ở đâu đó tại Châu Âu.

0 0 88

- vừa được xem lúc

In calculus, love also means zero.

Mình nhớ hồi năm 2 đại học, thầy giáo môn calculus, trong một giây phút ngẫu hứng, đã đưa ra cái definition này. Lúc đấy mình cũng không nghĩ gì nhiều.

0 0 65

- vừa được xem lúc

Chuyện thay đổi

Thay đổi là một thứ gì đó luôn luôn đáng sợ. Cách đây vài tháng mình có duyên đi làm cho một banking solution tên là X.

0 0 47

- vừa được xem lúc

Pet vs Cattle - Thú cưng và gia súc

Khái niệm. Pets vs Cattle là một khái niệm cơ bản của DevOps. Bài viết này sẽ nói về sự phát triển của các mô hình dịch vụ từ cốt lõi Pets and Cattle. 1.

0 0 34

- vừa được xem lúc

Git workflow được Google và Facebook sử dụng có gì hay ho

Với developer thì Git hẳn là công cụ rất quen thuộc và không thể thiếu rồi. Thế nhưng có mấy ai thực sự hiểu được Git.

0 0 85

- vừa được xem lúc

Kubernetes - Học cách sử dụng Kubernetes Namespace cơ bản

Namespace trong Kubernetes là gì. Tại sao nên sử dụng namespace.

0 0 113