はじめに
以前、AWS公式の「チュートリアル: Amazon S3 トリガーを使用して Lambda 関数を呼び出す」をGUIで一度行ったのですが、今回はこのチュートリアルをIaCの学習の一環として、Terraformで実践することにしました。
実際に作成していきます
providersの設定
Terraformなので、まずはどのクラウドを使用するかの設定を記述します。
ここでは、AWSを使用してリージョンは東京の「ap-northeast-1」を設定しています。
// providers.tf
provider "aws" {
region = "ap-northeast-1"
}
[IAM] 配下
どこでリソースを作成するか決めたら次は、そのリソースを操作するための、ロールを作成します。
// role.tf
resource "aws_iam_role" "lambda_s3_trigger_role" {
// "作成するリソース" "terraform内でのローカル名"
name = "lambda-s3-trigger-role"
//awsで作成されるリソース名
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Principal = {
Service = "lambda.amazonaws.com"
// Lambdaがこのロールを使えるようにする
},
Action = "sts:AssumeRole"
}
]
})
}
sts:AssumeRoleを使用する理由
- LambdaはIAMユーザーを使えない → ロール必須
LambdaはIAMユーザーを直接使えない
代わりにIAMロールを引き受ける(Assume)ことで権限を得る
そのために sts:AssumeRole が必要 - AssumeRole- AssumeRole によって発行される認証情報は一時的なものだから漏洩のリスクが低い
- ロール単位で「誰が何をしたか」を分離できるため
まずは、許可ポリシーを作成します。
今回は「CloudWatch」と「S3」に対して設定します。
※ ベストプラクティスにもある通り、最小限のアクセス権限の適用を目指します!
// policy.tf
resource "aws_iam_policy" "lambda_logging_s3" {
name = "LambdaLoggingAndS3Access"
description = "Allow Lambda to write logs and read from S3 buckets"
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",//CloudWatchLogsに対するログ出力権限(Lambdaからのログ送信を許可)
Action = [
"logs:PutLogEvents", //CloudWatchにイベント送信
"logs:CreateLogGroup", //logを保存するためのグループが存在しなかったら作成
"logs:CreateLogStream" //ログストリーム(ログの時系列チャネル)を作成する
],
Resource = "arn:aws:logs:ap-northeast-1:[自分のアカウントID]:[リソース]" //[リージョン]:[アカウントID]:[リソース]を記載する
},
{
Effect = "Allow",//S3オブジェクト取得権限(Lambdaでファイルを読むための権限)
Action =[
"s3:GetObject" //オブジェクトを取得
],
Resource = "arn:aws:s3:::作成するS3/*" //取得する対象のS3配下全てを設定
}
]
})
}
実行するロールと許可するポリシーが作成出来たら、アタッチしていきます。
アタッチの際記述はterraformのローカル名ではなく、awsのリソース名を使用します。
// iam_policy_attachment.tf
resource "aws_iam_role_policy_attachment" "lambda_logging_attach" {
role = aws_iam_role.lambda_s3_trigger_role.name
policy_arn = aws_iam_policy.lambda_logging_s3.arn
}
今回、「role.tf」「policy.tf」「iam_policy_attachment.tf」をIAMというフォルダ配下に置いているので、「output.tf」を作成してモジュール化を行い、他でも使えるようにします。
// output.tf
output "lambda_s3_trigger_role_arn" {
value = aws_iam_role.lambda_s3_trigger_role.arn
// 作成したIAMロールのarnを「lambda_s3_trigger_role_arn」という名前で出力
}
[Lambda]
ロールとポリシーの作成完了したので、実際に動かすLambdaの構成と関数の中身を記述していきます。
こちらはチュートリアルにあった、pythonファイルを持ってきました。
# lambda_function.py
import json
import urllib.parse
import boto3
print('Loading function')
s3 = boto3.client('s3')
def lambda_handler(event, context):
#print("Received event: " + json.dumps(event, indent=2))
# Get the object from the event and show its content type
bucket = event['Records'][0]['s3']['bucket']['name']
key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
try:
response = s3.get_object(Bucket=bucket, Key=key)
print("CONTENT TYPE: " + response['ContentType'])
print("S3イベント受信!!")
return response['ContentType']
except Exception as e:
print(e)
print('Error getting object {} from bucket {}. Make sure they exist and your bucket is in the same region as this function.'.format(key, bucket))
raise e
作成したファイルはzip化しないとawsで読み込めないので、下記コマンドでzip化を行う
zip [zip化後の名前] [zip化したいファイル名]
次に、Lambdaを記述していきます
// Lambda.tf
module "iam" {
source = "./IAM"
}
// output.ftで定義したロールを外部モジュールから読み込む
resource "aws_lambda_function" "s3_trigger_lambda" {
function_name = "s3-trigger-lambda" //aws内での名前
role = module.iam.lambda_s3_trigger_role_arn //外部モジュールから読み込んだロール
handler = "lambda_function.lambda_handler" //.pyのファイル名.func名
runtime = "python3.12"
filename = "function.zip" //zip化したファイル名
source_code_hash = filebase64sha256("function.zip")
timeout = 10
// 最大実行時間を10秒に設定
}
[S3]
最後に、オブジェクトを入れるS3を作成します。
「aws_s3_bucket_public_access_block」はデフォルトでtrueですが、terrafromの管理では明記しておきます。
// S3.tf
resource "aws_s3_bucket" "test" {
bucket = "S3バケット名"
}
resource "aws_s3_bucket_public_access_block" "test" {
bucket = aws_s3_bucket.test.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
S3トリガーでLambdaを呼び出すので、呼び出しを明示的に許可するものを作成します。S3から呼び出されることを許可するLambda側の許可です。
principalとsource_arnを絞って指定することで最小限の権限で許可となります。
// lambda_parmisson.tf
resource "aws_lambda_permission" "allow_s3" {
statement_id = "AllowExecutionFromS3" //任意で一意な文字列
action = "lambda:InvokeFunction" //関数の実行
function_name = aws_lambda_function.s3_trigger_lambda.function_name
// terraformのローカル名.function_nameとすることで、Lambda.tfに記述のawsの名前が指定される
principal = "s3.amazonaws.com"// 呼び出し元のサービス(今回はS3)
source_arn = aws_s3_bucket.test.arn//さらに詳しくS3を指定
}
次はS3からLambdaに向けての通知を許可する設定を作成します。
// s3_bucket_notification.tf
resource "aws_s3_bucket_notification" "bucket_notify" {
bucket = aws_s3_bucket.test.id
lambda_function {
lambda_function_arn = aws_lambda_function.s3_trigger_lambda.arn
events = ["s3:ObjectCreated:*"]
}
depends_on = [aws_lambda_permission.allow_s3] //明示的なリソースの依存関係を示す
}
Terraformのファイルの作成とLambdaのpythonファイルzip化が完了したので、terraformをapplyしていきます。
まずは「初期化」をしてセットアップしていきます
terraform init
次に作成したterraformファイルによって何のリソースが作成されるか確認します。
ここで「+ create」でリソースが表示されれば大丈夫です。
terraform plan
確認が完了したら、AWSにリソースを作成します。
terraform apply
「Do you want to perform these actions?」と聞かれるので、「yes」と回答します!
少し待つと
「Apply complete! Resources: 〇 added, 〇 changed, 〇 destroyed.」と表示されるので、これにて完了です。
terraformからroleを作成する際に、既に作成済みのroleと同じ名前を付けてしまっていて、エラーになった
対処法
terraform import aws_iam_role.lambda_s3_trigger_role_test test
// Terraform内識別名: lambda_s3_trigger_role_test
// AWS IAMロール名: test(既存リソース)
上記を行うことによって、被ってしまったrole名のものを新規作成するのではなく、既存のものをterraformで管理するようにした
こうすることでGUIや以前手動で作成したものまでterraformで管理することができる
参考:https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-s3-example.html