Terraform을 사용하여 Lambda 함수와 레이어를 일관되게 배포하기
Source: Dev.to
블로그 게시물에 첨부된 코드는 여기에서 확인할 수 있습니다.
Introduction
Terraform을 사용해 AWS에 Lambda 함수를 배포하는 일은 특히 여러 환경(개발 및 테스트 환경에서만 발생하죠?)에서 배포할 때 상당히 까다로울 수 있습니다.
다음과 같은 문제들을 겪을 수 있습니다:
terraform apply를 실행할 때마다 Lambda 함수가 다시 배포되는 현상- Lambda 함수 파일이 포함된 아카이브 파일이 없다는 오류
- Terraform 상태 파일에서 발생하는 소프트 락
이 글에서는 코드에 변경이 있을 때만 Lambda 함수를 일관되게 배포하는 방법을, 여러 환경에서 배포할 때도 적용할 수 있도록 소개하겠습니다.
히스토리
Terraform을 사용하여 Lambda 함수를 배포할 때 오래된 문제가 있었습니다. 아래는 이러한 문제와 이를 해결하려는 시도에 대한 외부 링크 예시입니다.
예시 구성
Terraform으로 AWS Lambda를 성공적으로 배포했습니다:
resource "aws_lambda_function" "lambda" {
filename = "dist/subscriber-lambda.zip"
function_name = "test_get-code"
role =
handler = "main.handler"
timeout = 14
reserved_concurrent_executions = 50
memory_size = 128
runtime = "python3.6"
tags =
source_code_hash = "${base64sha256(file("../modules/lambda/lambda-code/main.py"))}"
kms_key_arn =
vpc_config {
subnet_ids =
security_group_ids =
}
environment {
variables = {
environment = "dev"
}
}
}
terraform plan을 실행하면 Python 코드베이스를 수정하지 않았음에도 (같은 레포지토리에서 버전 관리되고 있음) source_code_hash가 변경되었기 때문에 Lambda 리소스를 업데이트해야 한다고 표시됩니다:
~ module.app.module.lambda.aws_lambda_function.lambda
last_modified: "2018-10-05T07:10:35.323+0000" =>
source_code_hash: "jd6U44lfe4124vR0VtyGiz45HFzDHCH7+yTBjvr400s=" => "JJIv/AQoPvpGIg01Ze/YRsteErqR0S6JsqKDNShz1w78"
null_resource로 업데이트 트리거하기
다른 리소스를 기준으로 업데이트를 트리거하거나 더 세밀한 제어가 필요할 경우 null_resource를 사용합니다:
resource "null_resource" "lambda_update" {
triggers = {
code_hash = filebase64sha256("my-function.zip")
}
provisioner "local-exec" {
command = "echo 'Code updated, triggering Lambda deployment...'"
}
}
resource "aws_lambda_function" "example" {
# ... other configurations
depends_on = [null_resource.lambda_update]
}
이 예제는 my-function.zip의 해시가 변경될 때마다 업데이트를 트리거합니다.
aws_lambda_layer_version 문제
Hi All,
우리는 Terraform 0.14.6을 사용하고 있으며 다음과 같은 문제를 겪고 있습니다.
aws_lambda_layer_version에source_code_hash를 제공하고 있습니다. Terraform은 이를 받아들이지만 상태 파일에 완전히 다른 값을 기록합니다.플랜에서는
source_code_hash가FyN0P9BvuTm023dkHFaWvAGmyD0rlhujGsPCTqaBGyw=이며, 상태 파일에서는c3forIEso3mJh74PY6HrhFK94GfJvQ4zG9rEIgBCBhw=가 됩니다.AWS CLI에서 레이어를 확인하면
"CodeSha256"가c3forIEso3mJh74PY6HrhFK94GfJvQ4zG9rEIgBCBhw=입니다.이를 통해 어떤
source_code_hash를 제공하든 파일 이름의 해시를 덮어쓸 수 없다는 것을 알 수 있습니다.
Terraform 구성:
resource "aws_lambda_layer_version" "loader" {
layer_name = "loader"
compatible_runtimes = ["python3.8"]
filename = "lambda_layer.zip"
source_code_hash = filebase64sha256("lambda_layer.zip")
}
이 예제들에서 볼 수 있듯이, 코드를 변경했는지 여부를 판단하기 위해 해시가 계산됩니다. 이것 자체는 문제가 아니지만, 문제는 모두 base64‑인코딩된 해시를 사용한다는 점입니다.
Lambda 함수 배포를 환경 간에 친화적으로 만드는 방법
base64 인코딩의 문제는 동일한 데이터에 대해 생성된 해시가 환경(운영 체제, 사용자 설정)마다 다를 수 있다는 점입니다.
다음 글에서 이 문제를 설명하고 있습니다:
The root cause of this is the difference in packaging on different machines and bad documentation. Well, and an asinine design choice on the AWS side.
source_code_hashgets overwritten by AWS‑provided data upon response.
source_code_hash(또는 output_base64sha256, filebase64sha256)에 대한 문서는 오해의 소지가 있습니다:
(String) The base64‑encoded SHA256 checksum of output archive file.
왜 해시를 base64‑인코딩해야 할까요? base64 인코딩의 목적은 바이너리 데이터를 출력 가능한 형태로 만드는 것이지만, SHA‑256 해시는 이미 출력 가능한(16진수) 형태입니다.
실제로 일어나는 과정은 다음과 같습니다:
- 아카이브의 SHA‑256을 계산합니다.
- 결과 바이너리 다이제스트를 그대로 취해, 그 바이트들을 원시 데이터로 간주한 뒤, 그 바이너리 블롭을 base64‑인코딩합니다:
sha256sum lambda.zip | xxd -r -p | base64
문제는 최신 zip 버전이 파일 권한을 저장한다는 점이며, 머신마다 다른 umask 값이 적용되면 권한이 달라져 서로 다른 아카이브가 생성되고, 따라서 해시도 달라집니다.
Windows와 macOS/Linux를 모두 사용하는 팀에서는 파일 시스템(그리고 따라서 아카이브 파일명) 차이가 크게 나기 때문에 추가적인 어려움이 발생합니다.
작동시키기
약간의 시도를 거친 후, 다음과 같은 해결책에 도달했습니다.
(게시물의 나머지 부분은 재현 가능한 패키징 프로세스의 단계별 구현, 결정적 zip을 생성하는 사용자 정의 스크립트, 그리고 결정적 해시를 사용하는 Terraform 구성에 대해 계속됩니다. 여기에서 솔루션을 삽입하세요.)
소스 변경 시 자동 재배포가 가능한 Lambda 함수 배포
Lambda 함수 코드를 디렉터리(필요한 파일 포함)로 제공할 때, archive_file 데이터 소스를 사용해 해당 디렉터리에서 아카이브 파일을 생성합니다.
1. 재배포를 트리거하는 랜덤 UUID 생성
먼저, 소스 디렉터리(및 하위 디렉터리) 내 전체 파일(ZIP 파일 제외)에 대해 랜덤 UUID를 생성합니다. 각 파일에 대해 MD5 해시를 계산하고, 해시 중 하나라도 변경되면 UUID가 바뀌어 aws_lambda_function 리소스의 재배포를 강제합니다.
# Create a random UUID which is used to trigger a redeploy of the function.
# The MD5 hash for each file (except ZIP files) will be calculated and,
# if any of those changes, it will trigger a redeploy of the
# aws_lambda_function resource `lambda_function`.
# We cannot rely on a base64 hash, because the seed for that is environment dependent.
resource "random_uuid" "lambda_function" {
keepers = {
for filename in setunion(
toset([for fn in fileset("${path.root}/lambda_function/", "**") : fn if !endswith(fn, ".zip")])
) :
filename => filemd5("${path.root}/lambda_function/${filename}")
}
}
Note:
MD5는 여기서 변경 감지를 위한 용도이며 암호화 목적이 아닙니다. 필요에 따라SHA256이나SHA512로 교체할 수 있습니다(추가 비용은 보통 무시할 수준입니다).
2. ZIP 아카이브 생성
함수 디렉터리를 .gitignore에 무시되는 위치에 아카이브합니다.
# Create an archive file of the function directory
data "archive_file" "lambda_function" {
type = "zip"
source_dir = "${path.root}/lambda_function"
output_path = "${path.root}/lambda_output/${var.function_name}.zip"
}
3. Lambda 함수 배포
random_uuid 리소스를 Lambda 함수의 교체 트리거로 사용합니다.
또한 filename 속성에 대한 변경은 무시합니다. 이는 아카이브의 절대 경로가 머신마다 다를 수 있기 때문입니다.
# Create the Lambda function
resource "aws_lambda_function" "lambda_function" {
function_name = var.function_name
role = aws_iam_role.lambda_execution_role.arn
handler = "${var.function_name}.${var.handler_name}"
runtime = var.runtime
timeout = var.timeout
architectures = var.architectures
# Use the filename of the archive file as input for the function
filename = data.archive_file.lambda_function.output_path
depends_on = [
aws_iam_role.lambda_execution_role
]
lifecycle {
replace_triggered_by = [
# Trigger a replace of the function when any of the function source files changes.
random_uuid.lambda_function
]
ignore_changes = [
# Ignore the source filename of the object itself, because that can change between
# users/machines/operating systems.
filename
]
}
}
이 구성을 적용한 뒤 동일한 코드를 서로 다른 환경에서 실행해도 예기치 않은 재배포가 발생하지 않습니다.
4. Lambda 레이어 배포 (중간 S3 객체 사용)
같은 패턴을 Lambda 레이어에도 적용할 수 있습니다. 차이점은 레이어 버전이 S3 객체에서 빌드된다는 점이며, 소스 파일이 변경될 때마다 해당 객체가 교체됩니다.
# Random UUID for the layer (change detection)
resource "random_uuid" "lambda_layer" {
keepers = {
for filename in setunion(
toset([for fn in fileset("${path.root}/lambda_layer/", "**") : fn if !endswith(fn, ".zip")])
) :
filename => filemd5("${path.root}/lambda_layer/${filename}")
}
}
# Archive the layer directory
data "archive_file" "lambda_layer" {
type = "zip"
source_dir = "${path.root}/lambda_layer"
output_path = "${path.root}/lambda_output/${var.layer_name}.zip"
}
# Store the archive in S3
resource "aws_s3_object" "this" {
depends_on = [data.archive_file.lambda_layer]
key = join("/", [for x in [var.s3_key, "${var.layer_name}.zip"] : x if x != null && x != ""])
bucket = var.s3_bucket
source = data.archive_file.lambda_layer.output_path
checksum_algorithm = "SHA256"
lifecycle {
replace_triggered_by = [
random_uuid.lambda_layer
]
ignore_changes = [
# Ignore the source of the object itself, because that can change between machines/operating systems
source
]
}
# Create the Lambda layer version
resource "aws_lambda_layer_version" "lambda_layer" {
layer_name = var.layer_name
compatible_runtimes = [var.runtime]
source_code_hash = aws_s3_object.this.checksum_sha256
s3_bucket = aws_s3_object.this.bucket
s3_key = aws_s3_object.this.key
}
5. 왜 중요한가
프로덕션 및 스테이징 환경에서 해야 하는 것처럼 파이프라인을 통해 IaC 변경을 실행하면 기여자들의 머신 간 차이로 인한 잘못된 plan/apply 변경을 없앨 수 있습니다.
인기 있는 Anton Babenko의 Lambda 모듈 은 base64 해시와 아카이브 파일명을 사용합니다. 이는 위에서 설명한 문제를 일으킬 수 있습니다. 여기서 보여준 접근 방식을 사용하면 이러한 문제를 피할 수 있습니다.
PR Update
그 모듈의 Base64 처리를 수정하기 위해 PR 작업을 진행 중입니다.
결론
이 게시물의 목표는 Terraform을 사용하여 Lambda 함수 및/또는 레이어를 배포할 때 마주칠 수 있는 (최소한) 두 가지 가능한 문제를 해결하는 방법을 보여주는 것입니다.
이 내용이 이러한 문제들의 원인에 대한 통찰을 제공하고, 이를 해결하기 위한 현명한 결정을 내리는 데 도움이 되길 바랍니다.