티스토리 뷰
문제점
accept-language에 따라 다른 언어의 html을 보여주어야했습니다. accept-language header를 cloudfront whitelist에 추가하여 origin에 header를 전달하도록 구성했습니다. 하지만 accept-language header의 형태가 다양하여 cloudfront cache의 효율이 좋지 않았고 지원하지 않는 언어가 들어오는 경우 default language 설정이 필요했습니다.
user-agent header를 parsing하여 IE 브라우저로 접속 시 redirect 처리하고 있습니다. user-agent header 또한 cloudfront whitelist에 추가하여 origin으로 header를 전달하고 있었지만 cache 효율이 좋지 않았습니다.
해결방법
client 요청 시 header에 관련된 처리를 하는 lambda@edge
를 생성하여 cache 효율을 높이고자 했습니다.
Lambda@Edge

lambda@edge 함수는 viewer request, viewer response, origin request, origin response 중 선택하여 동작할 수 있습니다. 모든 요청에 대한 cache key를 변경하려면 viewer request를 사용해야 합니다.
Lambda@Edge 제한사항
lambda@edge는 기본 lamdba function을 사용하는 것보다 많은 제약사항이 있습니다.
$LATEST
또는 별칭이 아니라 번호가 매겨진 Lambda 함수 버전을 사용해야합니다.- Lambda 함수는 미국 동부 리전에 있아야 합니다.
- Viewer request와 viewer response의 timetout 제한 5초, Origin request와 origin response timeout 제한 30초 입니다.
- Viewer request와 viewer response의 package size 제한 1MB, Origin request와 origin response의 package size 제한 50MB 제한이 있습니다.
- 환경변수를 사용할 수 없습니다.
구현
header parsing
구현 시 lambda@edge의 1MB 제한사항 때문에 module 선택 시 size를 꼭 확인해야합니다. accept-language header를 accept-language-parser
npm module을 사용하여 정형화 했습니다.
import { pick } from "accept-language-parser";
export class AcceptLanguageParser {
private supportedLanguages: string[];
private defaultLanguage: string;
constructor(supportedLanguages: string[], defaultLanguage: string) {
this.supportedLanguages = supportedLanguages;
this.defaultLanguage = defaultLanguage;
}
public pickLanguage(acceptLanguage: string): string {
const lang = pick(this.supportedLanguages, acceptLanguage, {
loose: true,
});
if (lang) return lang;
return this.defaultLanguage;
}
}
const acceptLanguageParser = new AcceptLanguageParser(["en", "ko"], "ko");
const lang1 = acceptLanguageParser.pickLanguage("en-GB,en;q=0.8"); // en
const lang2 = acceptLanguageParser.pickLanguage("fr-CA', 'fr-FR', 'fr"); // ko
정형화한 값을 cloudfront origin으로 전달할 때는 querystring으로 전달하도록 구성했습니다.
import { parse, stringify } from "querystring";
import * as ua from "useragent";
const acceptLanguageParser = new AcceptLanguageParser(["en", "ko"], "ko");
export function handler(
event: CloudFrontRequestEvent,
context: Context,
callback: Callback
) {
const request = Records[0].cf.request;
let userAgent;
if (
request.headers["accept-language"] &&
request.headers["accept-language"][0]
) {
const acceptLanguage = request.headers["accept-language"][0].value;
delete request.headers["accept-language"]; // cloudfront cache 생성 시 accept-language header에 따라 cache가 생성되는 것을 방지하기 위해 삭제
const lang = acceptLanguageParser.pick(acceptLanguage)
request.querystring = stringify({ lang });
}
if (request.headers["user-agent"] && request.headers["user-agent"][0]) {
userAgent = request.headers["user-agent"][0].value;
delete request.headers["user-agent"]; // cloudfront cache 생성 시 user-agent header에 따라 cache가 생성되는 것을 방지하기 위해 삭제
}
if(ua.is(userAgent).ie) callback(null, {
status: "301",
statusDescription: "Permanently Moved",
headers: {
location: [
{
key: "Location",
value: "{{url}}",
},
],
},
}); // ie 브라우저로 접근 시 redirect
else callback(null, request);
}
배포 스크립트 작성
배포 스크립트는 node.js code를 작성하고 작성한 code를 packaging하여 s3에 upload하는 것까지 작성했습니다. s3에 올라간 package는 terraform으로 lambda@edge, cloudfront 설정을 했습니다. 아래 코드는 s3에 upload하는 shell 스크립트입니다.
npm install && tsc
npm pack --json | jq '.[0].filename' | xargs -I {} mv {} "$OUTPUT_FILENAME"
mkdir "$TMP_FOLDER"
mv "$OUTPUT_FILENAME" "$TMP_FOLDER"/"$OUTPUT_FILENAME"
cd $TMP_FOLDER
tar -xvzf "$OUTPUT_FILENAME"
cd package && zip -r "../$OUTPUT_ZIP" . && cd ..
ENC_METADATA=`openssl dgst -sha256 -binary $OUTPUT_ZIP | openssl enc -base64`
aws s3 cp ./$OUTPUT_ZIP s3://{{s3 upload 위치}} --metadata sha256=$ENC_METADATA
작성 시 어려웠던 점은 node.js application package하는 것과 새로운 package를 s3 upload 시에만 terraform으 변경사항을 감지하고 배포할 수 있도록 구성하는 것이었습니다.
1. node.js application package
lambda@edge의 1MB 제한사항 때문에 devdependency가 들어가지 않도록 해야합니다. 이를 위해 package.json
에 bundleDependencies
를 사용했습니다.
{
...
"dependencies": {
"accept-language-parser": "^1.5.0",
"useragent": "^2.3.0"
},
"bundleDependencies": [
"accept-language-parser",
"useragent"
],
}
bundleDependencies
를 사용하면 npm pack
실행 시 bundleDependencies
에 추가되어 있는 module만 node_modules
에 추가하게 됩니다. 기존에는 yarn을 사용했지만 bundleDependencies
설정이 yarn pack
으로는 제대로 실행되지 않는 것을 확인되어 npm pack
으로 실행하는 스크립트를 작성했습니다. .npmignore
를 사용하여 ts 파일과 log file등 packaging에 필요 없는 file을 제거 했습니다.
npm pack
실행의 결과물은 *.tgz
입니다. tgz를 그대로 s3 upload할 수 없습니다. lambda에서 사용하는 file은 zip 형식이므로 tgz를 zip으로 변환해야합니다.
2. 새로운 package를 s3 upload 시에만 terraform 변경사항 감지
terraform에서는 변경사항이 있을 때만 배포되기 때문에 metadata를 사용하여 aws_lambda_function
의 source_code_hash
에 맞는 방식으로 encode한 hash 값을 설정해야 했습니다. aws_lambda_function
의 source_code_hash
에 잘못된 값을 넣으면 terraform에서 apply 때마다 변경사항이 발생하게 됩니다.
source_code_hash
을 사용하는 방식 이외에도 s3 object를 versioning할 수 있도록 설정하여 version id를 aws_lambda_function
에 설정하는 방식도 있습니다. s3 object를 versioning하는 방식을 사용하지 않고 있어 source_code_hash
를 사용하는 방식으로 구성했습니다.
data "aws_s3_bucket_object" "header_parsing_lambda_edge_artifact" {
bucket = "${node package를 upload한 s3 bucket}"
key = "${node package를 upload한 s3 key}"
}
data "aws_iam_policy_document" "assume_role_policy_doc" {
statement {
sid = "AllowAwsToAssumeRole"
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = [
"edgelambda.amazonaws.com",
"lambda.amazonaws.com",
]
}
}
}
resource "aws_iam_role" "lambda_at_edge" {
name = "lambda-edge-role"
assume_role_policy = data.aws_iam_policy_document.assume_role_policy_doc.json
}
data "aws_iam_policy_document" "lambda_logs_policy_doc" {
statement {
effect = "Allow"
resources = ["*"]
actions = [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:CreateLogGroup",
]
}
}
resource "aws_iam_role_policy" "logs_role_policy" {
name = "log-policy"
role = aws_iam_role.lambda_at_edge.id
policy = data.aws_iam_policy_document.lambda_logs_policy_doc.json
}
resource "aws_lambda_function" "header_parsing_lambda_edge" {
function_name = "lambda-edge"
# Find the file from S3
s3_bucket = "${node package를 upload한 s3 bucket}"
s3_key = "${node package를 upload한 s3 key}"
source_code_hash = chomp(data.aws_s3_bucket_object.header_parsing_lambda_edge_artifact.metadata["Sha256"])
provider = aws.aws_cloudfront
publish = true
handler = "dist/app.handler"
runtime = "nodejs14.x"
role = aws_iam_role.lambda_at_edge.arn
}
resource "aws_cloudfront_distribution" "static_distribution" {
...
ordered_cache_behavior {
lambda_function_association {
event_type = "viewer-request"
include_body = false
lambda_arn = aws_lambda_function.header_parsing_lambda_edge.qualified_arn
}
}
}
결과
cloudfront를 적극적으로 사용하고 있습니다. 하지만 accept-language
, user-agent
와 같이 형태가 다양한 header를 cloudfront origin으로 전달할 경우 cache hit 효율이 떨어집니다. lambda@edge
를 사용하는 방법이 이러한 문제를 해결할 수 있는 방법이 될 수 있습니다. 하지만 lambda@edge
도입에 장벽으로 느껴지는 부분이 있었습니다.
첫번째 장벽은 lambda@edge
의 제약사항이 많다는 것이었습니다. lambda packaging 시 size 제한으로 code 개발 시 항상 사용하는 module을 전부 제거해야만 했습니다. lambda timeout 제한이 있어 외부 요청이나 db 연결 시 timeout이 발생하지 않도록 주의해야 합니다. 이러한 제한 사항으로 설계시 어려움이 많이 느껴졌습니다. 두번째 장벽은 기존에 사용하는 lambda 배포 pipeline을 같이 사용할 수 없다는 것이었습니다. 기존에 serverless framework로 lambda를 관리하고 lambda 이외의 infra는 terraform으로 관리하고 있었습니다. cloudfront를 terraform으로 관리하고 있었고 serverless framework로 배포한 lambda@edge
를 terraform에 연결하는 작업이 어려움이 있다고 판단되었습니다. 기존에 사용하는 serverless framework배포 방식을 선택하지 않고 terraform으로만 관리하는 방법으로 새롭게 구성해야했습니다.
lambda@edge
을 도입하면서 cloudfront origin으로 설정한 application에서 header를 처리하는 로직을 제거할 수 있었습니다. header를 처리하는 로직을 분리하여 application에는 core 로직에만 중집할 수 있게 되었습니다.
Reference
'develop' 카테고리의 다른 글
Elasticsearch 비용 절약하기 (0) | 2024.09.19 |
---|---|
Github Actions 관리하기(Organization secrets, Reusable workflows) (0) | 2022.06.29 |
python 절대경로 / 상대경로 (0) | 2022.01.04 |
DAS / NAS / SAN (0) | 2021.10.29 |
3 계층 architecture (0) | 2021.10.29 |
- Total
- Today
- Yesterday
- nltk
- Github Actions
- conventional commit
- NLP
- aws
- Cognito
- pagination
- mongoDB
- Airflow
- Python
- commit message
- lambda@edge
- sementic version
- typescript
- Develop
- Lifecycle
- shorten
- JavaScript
- mognodb
- Cloudfront
- Neptune
- Clickjacking
- Prisma
- nginx
- slowquery
- AWS community day seoul
- graphql
- Terraform
- inversify
- Elasticsearch
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |