让我们在本地环境中使用Terraform创建一个从SNS到Lambda的Pub/Sub的LocalStack环境
总览
我們的目標是在Mac(M2芯片)環境下使用Terraform + LocalStack(免費版)來建立以下環境。
你好,凯塔。 (Nǐ , tǎ.)
尽管我想要学习AWS,但个人无法投入那么多金钱,但我还是想要学习…
就在这时,我遇到了LocalStack,于是我决定一起学习Terraform。
为了整理自己的思路,我这次首次写文章。
感谢词
由于笔者对Terraform和LocalStack的经验较浅,因此详细的解释将被省略,请谅解。
因此,这篇文章重视”使用LocalStack和Terraform可以做到这样的事情!”的体验。
我曾经认为如果不做这件事,就无法向前进…
我相信大家都知道,但据说使用M2芯片并在LocalStack上构建的Lambda函数在执行时可能存在无响应的错误(?)。
尝试再次复现,但是没有成功…
也许方法不对,但是我会记下解决问题的详细过程。
版本信息-验证环境
检验环境如下所示。
% sw_vers
ProductName: macOS
ProductVersion: 13.3
BuildVersion: 22E252
% aws --version
aws-cli/2.13.0 Python/3.11.4 Darwin/22.4.0 exe/x86_64 prompt/off
% tfenv --version
tfenv 3.0.0
% terraform -version
Terraform v1.5.4
on darwin_arm64
% docker -v
Docker version 24.0.2, build cb74dfc
% docker compose version
Docker Compose version v2.18.1
[in dokcer]# bin/localstack -v
2.2.0
GitHub: GitHub是一个面向开发者的在线代码托管平台。
本文中提到的资料在这里公开发表了。
让我们试试吧
搭建LocalStack的Docker容器
我准备了一个包含docker-compose.yaml文件的示例,请使用它。
version: "3.8"
services:
localstack:
container_name: localstack
image: localstack/localstack:2.2.0-arm64
ports:
- "127.0.0.1:4566:4566"
- "127.0.0.1:4510-4559:4510-4559"
environment:
- DOCKER_HOST=unix:///var/run/docker.sock
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
% docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
localstack localstack/localstack:2.2.0-arm64 "docker-entrypoint.sh" localstack 50 minutes ago Up 50 minutes (healthy) 127.0.0.1:4510-4559->4510-4559/tcp, 127.0.0.1:4566->4566/tcp, 5678/tcp
AWS CLI的安装和初始设置
请查看官方网站以了解安装信息。
此外,我们将在假设以下设置的基础上继续解释。
% cat ~/.aws/config
[profile localstack]
region = us-east-1
output = json
% cat ~/.aws/credentials
[localstack]
aws_access_key_id = dummy
aws_secret_access_key = dummy
安装Terraform
如果使用Mac电脑,可以通过brew进行安装。
还有一种名为tfenv的工具,就像nodenv一样可以进行版本切换,所以我建议你使用这个~
$ brew install tfenv
(バージョン選択などは省略)
$ terraform -v
Terraform v1.5.4
on darwin_arm64
用Terraform编写基础设施配置。
构成和脚本
在验证时的配置如下。
% tree
.
├── lambda_ts
│ ├── dist
│ │ ├── index.js
│ │ └── index.zip
│ ├── index.ts
│ ├── node_modules
│ └── package.json
└── tf
├── main.tf
└── resources.tf
{
"name": "lambdas",
"version": "1.0.0",
"description": "aws terraform samples",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@types/aws-lambda": "^8.10.119",
"esbuild": "^0.18.20"
},
"scripts": {
"prebuild": "rm -rf dist",
"build": "esbuild index.ts --bundle --minify --sourcemap --platform=node --target=es2020 --outfile=dist/index.js",
"postbuild": "cd dist && zip -r index.zip index.js*",
"pretf_init": "cd ../tf",
"tf_init": "rm -rf .terraform/ .terraform* terraform.tfstate",
"posttf_init": "cd ../tf && terraform init",
"tf_apply": "cd ../tf && terraform apply -auto-approve"
}
}
将以下脚本进行转译并压缩为zip文件后部署到Lambda。
export const handler = async (event, context) => {
event.Records.forEach((record) => {
const { body } = record;
console.log("body from sns-sqs.");
console.log(body);
const data = JSON.parse(body);
console.log(`data.Message: ${data.Message}`);
});
return {
statusCode: 200,
body: JSON.stringify({
message: "hello world",
}),
};
};
由于在package.json中准备了命令,可以使用其中的命令来进行构建。
$ yarn build
主要.tf
这是主要的设置。
endpoints的URL和端口是LocalStack Docker的URL。
我认为基本上不会发生变化,但请根据您实际搭建的环境进行更改。
provider "aws" {
region = "us-east-1"
access_key = "dummy"
secret_key = "dummy"
skip_requesting_account_id = true
skip_credentials_validation = true
endpoints {
lambda = "http://localhost:4566"
sns = "http://localhost:4566"
sqs = "http://localhost:4566"
iam = "http://localhost:4566"
}
}
社交网络服务(SNS)、简单队列服务(SQS)、Lambda函数
每个资源的设置。
# SNS
resource "aws_sns_topic" "test-sns" {
name = "test-sns"
}
output "test-sns-arn" {
value = aws_sns_topic.test-sns.arn
}
# SQS
resource "aws_sqs_queue" "test-sqs" {
name = "test-sqs"
}
resource "aws_sns_topic_subscription" "test" {
topic_arn = aws_sns_topic.test-sns.arn
protocol = "sqs"
endpoint = aws_sqs_queue.test-sqs.arn
}
resource "aws_iam_role" "lambda_sqs" {
name = "lambda_sqs"
max_session_duration = 3600
description = "None"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"sqs:*"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
EOF
}
# Lambda
data "archive_file" "example_zip" {
type = "zip"
source_dir = "${path.cwd}/../lambda_ts/dist"
output_path = "${path.cwd}/../lambda_ts/dist/index.zip"
}
resource "aws_lambda_function" "test-lambda" {
function_name = "test-lambda"
description = "Test Lambda"
role = aws_iam_role.lambda_sqs.arn
handler = "index.handler"
runtime = "nodejs18.x"
timeout = 3
filename = data.archive_file.example_zip.output_path
source_code_hash = filebase64sha256(data.archive_file.example_zip.output_path)
}
resource "aws_lambda_event_source_mapping" "test-lambda-trigger" {
event_source_arn = aws_sqs_queue.test-sqs.arn
function_name = "test-lambda"
batch_size = 10
}
你注意到出现了一个名为 ${path.cwd} 的陌生变量了吧。
...
source_dir = "${path.cwd}/../lambda_ts/dist"
output_path = "${path.cwd}/../lambda_ts/dist/index.zip"
...
这是一个返回资源目录路径的变量。
在这里会被转化为tf并展开如下。
tf/../lambda_ts/dist/index.zip
让我们试着部署
进行terraform初始化
首先,执行terraform init。
% yarn tf_init
Initializing the backend...
Initializing provider plugins...
- Finding latest version of hashicorp/aws...
- Finding latest version of hashicorp/archive...
...
Terraform has been successfully initialized!
...
一旦成功,将会生成上下文文件等。
...
tf
├ .terraform
├ .terraform.lock.hcl
└ terraform.tfstate
执行 terraform apply
当执行terraform apply后,将显示正在创建的配置的详细信息。
同时,还将通过交互方式确认是否最终部署。
% yarn tf_apply
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_iam_role.lambda_sqs will be created
+ resource "aws_iam_role" "lambda_sqs" {
...
}
# aws_lambda_event_source_mapping.test-lambda-trigger will be created
+ resource "aws_lambda_event_source_mapping" "test-lambda-trigger" {
...
}
# aws_lambda_function.test-lambda will be created
+ resource "aws_lambda_function" "test-lambda" {
...
}
# aws_sns_topic.test-sns will be created
+ resource "aws_sns_topic" "test-sns" {
...
}
# aws_sns_topic_subscription.test will be created
+ resource "aws_sns_topic_subscription" "test" {
...
}
# aws_sqs_queue.test-sqs will be created
+ resource "aws_sqs_queue" "test-sqs" {
...
}
Plan: 6 to add, 0 to change, 0 to destroy.
...
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value:
你想执行这些操作吗?
Terraform将执行上述所描述的操作。
只有“是”会被接受作为批准。
输入一个值:
如果在这里输入”yes”并执行,将进行部署。
aws_iam_role.lambda_sqs: Creating...
aws_sqs_queue.test-sqs: Creating...
aws_sns_topic.test-sns: Creating...
aws_iam_role.lambda_sqs: Creation complete after 0s [id=lambda_sqs]
aws_sns_topic.test-sns: Creation complete after 0s [id=arn:aws:sns:us-east-1:000000000000:test-sns]
aws_lambda_function.test-lambda: Creating...
aws_lambda_function.test-lambda: Creation complete after 5s [id=test-lambda]
aws_sqs_queue.test-sqs: Still creating... [10s elapsed]
aws_sqs_queue.test-sqs: Still creating... [20s elapsed]
aws_sqs_queue.test-sqs: Creation complete after 25s [id=http://localhost:4566/000000000000/test-sqs]
aws_sns_topic_subscription.test: Creating...
aws_lambda_event_source_mapping.test-lambda-trigger: Creating...
aws_lambda_event_source_mapping.test-lambda-trigger: Creation complete after 0s [id=fe53c2a8-443d-4a1a-8626-dca76f1f9296]
aws_sns_topic_subscription.test: Creation complete after 0s [id=arn:aws:sns:us-east-1:000000000000:test-sns:9056dac2-0385-4e7d-a85b-e84271e2d626]
...
Apply complete! Resources: 6 added, 0 changed, 0 destroyed.
...
如果显示“应用完成”,则表示部署已完成。
让我们试着动起来
因为在使用 LocalStack,所以无法在 AWS 的控制台上进行操作。让我们使用 aws-cli 来进行确认吧!
发布社交网络
由于本次示例采用了将消息发布到SNS并驱动Lambda的架构,因此让我们向SNS发送一条消息。
% aws sns publish \
--profile=localstack \
--endpoint-url http://localhost:4566 \
--topic-arn "arn:aws:sns:us-east-1:000000000000:test-sns" \
--message 'Hello AWS SNS!! check tail LOG!'
如果返回了MessageId,那就表示成功了。
{
"MessageId": "0d04b3e2-aa0b-4f5c-ab3a-86f94261b60a"
}
如果使用日志,就可以确认结果。
日志描述了日志组。
首先,我们要检查日志组。
$ aws logs describe-log-groups \
--profile=localstack \
--endpoint-url http://localhost:4566
{
"logGroups": [
{
"logGroupName": "/aws/lambda/test-lambda",
"creationTime": 1698473196925,
"metricFilterCount": 0,
"arn": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/test-lambda:*",
"storedBytes": 1073
},
{
"logGroupName": "sns/us-east-1/000000000000/test-sns",
"creationTime": 1698473194764,
"metricFilterCount": 0,
"arn": "arn:aws:logs:us-east-1:000000000000:log-group:sns/us-east-1/000000000000/test-sns:*",
"storedBytes": 397
}
]
}
接下来,让我们一起来查看Lambda的日志流。
$ aws logs describe-log-streams \
--profile=localstack \
--endpoint-url http://localhost:4566 \
--log-group-name "/aws/lambda/test-lambda"
--order-by "LastEventTime"
{
"logStreams": [
{
"logStreamName": "2023/10/28/[$LATEST]c13452ba6c7e2b1fc5361590b9de84f0",
"creationTime": 1698473196936,
"firstEventTimestamp": 1698473196914,
"lastEventTimestamp": 1698473196930,
"lastIngestionTime": 1698473196940,
"uploadSequenceToken": "1",
"arn": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/test-lambda:log-stream:2023/10/28/[$LATEST]c13452ba6c7e2b1fc5361590b9de84f0",
"storedBytes": 1073
}
]
}
在确认日志流名称(= logStreamName)之后,让我们实际查看日志。
获取日志事件
% aws logs get-log-events \
--profile=localstack \
--endpoint-url http://localhost:4566 \
--log-group-name "/aws/lambda/test-lambda" \
--log-stream-name '2023/10/28/[$LATEST]c13452ba6c7e2b1fc5361590b9de84f0'
{
"events": [
{
"timestamp": 1698473196914,
"message": "START RequestId: 17cc5309-deaa-42ad-8c99-b6b13ae342bc Version: $LATEST",
"ingestionTime": 1698473196940
},
{
"timestamp": 1698473196917,
"message": "2023-10-28T06:06:36.902Z\t17cc5309-deaa-42ad-8c99-b6b13ae342bc\tINFO\tbody from sns-sqs.",
"ingestionTime": 1698473196940
},
{
"timestamp": 1698473196920,
"message": "2023-10-28T06:06:36.905Z\t17cc5309-deaa-42ad-8c99-b6b13ae342bc\tINFO\t{\"Type\": \"Notification\", \"MessageId\"
: \"0d04b3e2-aa0b-4f5c-ab3a-86f94261b60a\", \"TopicArn\": \"arn:aws:sns:us-east-1:000000000000:test-sns\", \"Message\": \"Hello AWS SNS!
! check tail LOG!\", \"Timestamp\": \"2023-10-28T06:06:34.734Z\", \"SignatureVersion\": \"1\", \"Signature\": \"EXAMPLEpH+..\", \"Signin
gCertURL\": \"https://sns.us-east-1.amazonaws.com/SimpleNotificationService-0000000000000000000000.pem\", \"UnsubscribeURL\": \"http://l
ocalhost:4566/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:000000000000:test-sns:995fff09-56ab-44fc-a92f-00722801199b\"}",
"ingestionTime": 1698473196940
},
{
"timestamp": 1698473196923,
"message": "2023-10-28T06:06:36.905Z\t17cc5309-deaa-42ad-8c99-b6b13ae342bc\tINFO\tdata.Message: Hello AWS SNS!! check tail L
OG!",
"ingestionTime": 1698473196940
},
{
"timestamp": 1698473196927,
"message": "END RequestId: 17cc5309-deaa-42ad-8c99-b6b13ae342bc",
"ingestionTime": 1698473196940
},
{
"timestamp": 1698473196930,
"message": "REPORT RequestId: 17cc5309-deaa-42ad-8c99-b6b13ae342bc\tDuration: 22.53 ms\tBilled Duration: 23 ms\tMemory Size:
128 MB\tMax Memory Used: 128 MB\t",
"ingestionTime": 1698473196940
}
],
"nextForwardToken": "f/00000000000000000000000000000000000000000000000000000005",
"nextBackwardToken": "b/00000000000000000000000000000000000000000000000000000000"
}
虽然有些难以理解,但是如果查看消息内容,就会发现输入了发布的值。
控制台日志也已经被输出了!
...
"message": "2023-10-28T06:06:36.902Z\t17cc5309-deaa-42ad-8c99-b6b13ae342bc\tINFO\tbody from sns-sqs.",
...
"message": "2023-10-28T06:06:36.905Z\t17cc5309-deaa-42ad-8c99-b6b13ae342bc\tINFO\t{\"Type\": \"Notification\", \"MessageId\"
: \"0d04b3e2-aa0b-4f5c-ab3a-86f94261b60a\", \"TopicArn\": \"arn:aws:sns:us-east-1:000000000000:test-sns\", \"Message\": \"Hello AWS SNS!
! check tail LOG!\", \"Timestamp\": \"2023-10-28T06:06:34.734Z\", \"SignatureVersion\": \"1\", \"Signature\": \"EXAMPLEpH+..\", \"Signin
gCertURL\": \"https://sns.us-east-1.amazonaws.com/SimpleNotificationService-0000000000000000000000.pem\", \"UnsubscribeURL\": \"http://l
ocalhost:4566/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:000000000000:test-sns:995fff09-56ab-44fc-a92f-00722801199b\"}",
...
"message": "2023-10-28T06:06:36.905Z\t17cc5309-deaa-42ad-8c99-b6b13ae342bc\tINFO\tdata.Message: Hello AWS SNS!! check tail L
...
故障排除
如果出版成功了但没有输出日志,
我作为作者也经历过几次,我认为有几个可能的原因。1. Lambda中部署的zip文件内容是否正确?
2. 处理程序的名称是否正确?
3. 尝试使用aws-cli直接部署到Lambda
4. (如果以上无法解决)尝试重新创建Docker容器
对于“1”,您可以尝试在本地解压缩zip文件并检查内容。
index.zip
└ index
├ index.js
└ index.js.map
对于“2”,您可能会在错误日志中注意到处理程序名称错误的粗心错误。
resource.tf
…
handler = “index.handler” // 声明要在index文件中执行的处理程序函数名
…
index.ts
export const handler = async (event, context) => { // 确认文件名是否正确
…
对于“3”,这是一种通过简单的将js文件打包成zip并使用aws-cli直接部署到Lambda进行测试的方法。
/*
* 删除(删除已部署的函数)
*/
$ aws lambda delete-function \
–profile=localstack \
–endpoint-url http://localhost:4566 \
–function-name “test-lambda”
/*
* 部署(zip-file选项的路径是zip文件的完整路径)
*/
$ aws lambda create-function \
–profile=localstack \
–endpoint-url=http://localhost:4566 \
–function-name test-lambda \
–zip-file fileb:///Users/path…/to…/index.zip \
–handler index.handler \
–runtime nodejs18.x \
–role arn:aws:iam::123456789012:role/lambda-execute
/*
* 调用(返回值将输出到本地的response.json文件)
*/
$ aws lambda invoke \
–profile=localstack \
–endpoint-url http://localhost:4566 \
–function-name “test-lambda” \
response.json
如果仍然无法解决问题,请尝试重新构建Docker(包括删除所有镜像)并重新尝试。
(还可以尝试更改为稍旧的版本再次尝试)
尾声
最近,包括基础设施环境在内的源代码使用Git进行版本控制的机会增加了。在DevOps的视角下,避免个人化,使每个人都能够管理资源已成为常识。
当中间件进行版本升级或发生重大更改时,如果依赖关系未可视化,人们就会因为害怕而无法动手,导致长时间被搁置不理…(亲身经历)
虽然如此,这次的基础设施即代码化只是一种方法,所以我们也必须考虑运维方面的问题。
烹饪(有趣的)
我尝试用最早从家庭菜园里种出来的小松菜和牛蒡,将其卷入肉中(售价50日元)。
我用酱油和糖调制成照烧风味,做成了便当的配菜。