AWS LambdaをTerraformで管理する
こんにちは!
SREチームの宮原(@TakashiMiyahara)です🙋
本日は、AWS LambdaをTerraformを利用して管理するよ!というお話しです。
💡 きっかけ
クラッソーネのAWS上のリソースは、Terraformを利用してコード化されています。
しかしながら、既存のLambda関数は、AWS サーバーレスアプリケーションモデル (SAM、Serverless Application Model)を利用して、開発・運用されています。
新しいLambda関数を作成するときに、SAMを利用するか、Terraformを利用するかで悩みました。
Terraformを利用して、Lambda関数を開発・運用する知見を得たので、紹介してみたいと思います。
Lambda関数で実行する関数の作成
今回は、Pythonを利用したいと思います。
Pipfileの作成
Pipenvを利用して関数を作っていきます。
Pipenv
を利用することで、プロジェクトや関数ごとの依存関係を、仮想環境に閉じ込めることができ、ローカルの開発環境を汚さずに開発が行え、とても開発体験が良いです✨
Pipfile
に、pytestなど利用するパッケージや、テストやフォーマット用のスクリプトを記述します。
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
[dev-packages]
pytest = "*"
pytest-cov = "*"
pytest-mock = "*"
moto = "*"
autopep8 = "*"
[requires]
python_version = "3.10"
[scripts]
test = "bash -c \"PYTHONPATH=src python -m pytest --cov --cov-branch --cov-report=html -v\""
format = "autopep8 -ivr ."
下記コマンドを利用して、必要なパッケージをインストールし、Pipfile.lock
ファイルを作成します。
pipenv install -d
Lambda関数で実行したい関数の作成
次に lambda_handler
関数を作成します。
今回は、sample
という文字列を返すだけにしてみます。
def lambda_handler():
return 'sample'
次にテストコードを作成します。
import importlib
import pytest
@pytest.fixture
def app():
return importlib.import_module('src.app')
def test_lambda_handler(app):
ret = app.lambda_handler()
assert ret == 'sample'
runコマンドからテストを実行します。
pipenv run test
最終的なディレクトリ構造は、以下のようになります。
.
├── Pipfile
├── Pipfile.lock
├── src
│ └── app.py
└── tests
└── test_app.py
AWSのリソースの作成
次に、Lambda関数を実行するために必要なリソースを定義していきましょう。
ロググループの作成
Lambda関数を実行したときのログを残せるように、ロググループを作成しましょう。
今回は、14日間分のログを維持するようにしています。
こちらは、それぞれのプロジェクトやチームのルールに合わせて設定してください。
resource "aws_cloudwatch_log_group" "sample_app" {
name = "/aws/lambda/sample-app"
retention_in_days = 14
}
IAM Policy と IAM Roleの作成
Lambda関数を実行したときに、ログストリームへログを出力する権限を設定します。
resource "aws_iam_policy" "sample_app" {
name = "sample-app-logging-policy"
path = "/"
description = "IAM policy for logging from a sample_app"
policy = jsonencode(
{
"Statement" : [
{
"Action" : "logs:CreateLogGroup",
"Effect" : "Allow",
"Resource" : "arn:aws:logs:ap-northeast-1:AWSアカウントのID:*"
},
{
"Action" : [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Effect" : "Allow",
"Resource" : [
"arn:aws:logs:ap-northeast-1:AWSアカウントのID:log-group:/aws/lambda/sample-app:*"
]
}
],
"Version" : "2012-10-17"
}
)
}
作成した aws_iam_policy
のARNを、aws_iam_role
の managed_policy_arns
に設定します。
こちらのRoleをLambda関数の実行ロールとして設定しましょう。
resource "aws_iam_role" "sample_app" {
name = "sample-app-execution-role"
assume_role_policy = jsonencode(
{
"Version" : "2012-10-17",
"Statement" : [
{
"Action" : "sts:AssumeRole",
"Principal" : {
"Service" : "lambda.amazonaws.com"
},
"Effect" : "Allow",
"Sid" : ""
}
]
}
)
managed_policy_arns = [
aws_iam_policy.sample_app.arn,
]
}
Lambda関数の作成
lambda_functions/sample_app/src/app.py
に、Lambda関数で実行したい処理を実装しています。
このファイルをLambda関数で実行したいので、データリソースのarchive_file
を利用します。
terraform plan
実行時に、指定したディレクトリのZipファイルを作成してくれます。
data "archive_file" "sample_app" {
type = "zip"
source_dir = "lambda_functions/sample_app/src"
output_path = "lambda_functions/sample_app/src/app.zip"
}
aws_lambda_function
を定義していきます。
function_name
は、プロジェクトやチームの命名ルールに合わせてみてくださいね。
role
には、先ほど作成したIAM RoleのARNを渡します。
handler
には、app.py
のlambda_handler()
を呼び出したいので、app.lambda_handler
と設定します。
source_code_hash
には、データリソースの出力ファイルをBase64でエンコードした際のSHA256チェックサムを渡します。
source_code_hash
は、Lambda関数の更新のトリガーに使用できます。app.pyの変更をトリガーに、Lambda関数をTerraformからデプロイできるようになります。
resource "aws_lambda_function" "sample_app" {
filename = data.archive_file.sample_app.output_path
function_name = "sample-app-function"
role = aws_iam_role.sample_app.arn
handler = "app.lambda_handler"
runtime = "python3.9"
source_code_hash = data.archive_file.sample_app.output_base64sha256
}
すでにLambda関数がデプロイ済みでlambda_functions/sample_app/src/app.py
を編集し、terraform plan
を実行すると以下のような出力を得られます。
このことから、app.py
の変更によりLambda関数自体にも、変更が入ることがわかります。
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# aws_lambda_function.sample_app will be updated in-place
~ resource "aws_lambda_function" "sample_app" {
id = "sample-app-function"
~ last_modified = "2023-01-06T00:00:00.000+0000" -> (known after apply)
~ source_code_hash = "****************************=" -> ""****************************=""
tags = {}
# (19 unchanged attributes hidden)
# (2 unchanged blocks hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
source_code_hashの値は、マスキングしています。
EventBridgeのルールとトリガーを作成する
作成したLambda関数を、指定したスケジュールで定期的に呼び出したいですよね。
EventBridgeのルールとトリガーを作成し、Lambda関数を紐づけてあげることで、設定が行えます。
以下は、毎時0分に実行するルールです。
resource "aws_cloudwatch_event_rule" "sample_app" {
name = "sample-app-function-rule"
description = "Rule to run sample_app hourly."
schedule_expression = "cron(0 * * * ? *)"
is_enabled = true
}
resource "aws_cloudwatch_event_target" "sample_app" {
rule = aws_cloudwatch_event_rule.sample_app.name
target_id = aws_cloudwatch_event_rule.sample_app.name
arn = aws_lambda_function.sample_app.arn
}
aws_lambda_permission
を利用して、aws_cloudwatch_event_rule
とaws_lambda_function
を紐づけます。
resource "aws_lambda_permission" "sample_app" {
statement_id = "AllowExecutionFromCloudWatch"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.sample_app.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.sample_app.arn
}
最終的な構成
ディレクトリ構造
pythonのソースコードやTerraformのリソース定義を、1つのRepositoryに閉じ込める構成にしてみました。
必要なリソースやLambda関数で実行したい処理だけでを、新規に作成したり、改修したりすることが行いやすくなったかと思います。
.
├── README.md
├── backend.tf
├── cloudwatch_event_rule.tf
├── cloudwatch_event_target.tf
├── cloudwatch_log_group.tf
├── iam_policy.tf
├── iam_role.tf
├── lambda_functions
│ ├── sample_app
│ │ ├── Pipfile
│ │ ├── Pipfile.lock
│ │ ├── README.md
│ │ ├── src
│ │ │ └── app.py
│ │ └── tests
│ │ └── test_app.py
├── lambda.tf
├── lambda_permission.tf
├── provider.tf
└── versions.tf
デプロイの流れ
Pull RequestにApproveが付きマージされると、Terraform Cloudを利用してAWS環境へデプロイされていきます。
Terraformに不慣れなメンバーが多い場合は、Auto applyは許可せず、Manual applyに制限し、詳しいメンバーが最終的な反映をするようにしておくと安全です。
終わりに
今回は、Terraformを利用してLambda関数を作成する構成について紹介してみました。
実行する関数の中身や、関数に必要なリソースをひとまとめにして管理し、デプロイまで行えることがわかりました。
SAMも非常に便利なツールだと思うのですが、他のリソースをTerraformで管理している場合は、Terraformに寄せてしまう方が使い方で悩むことが少なくなるのかなと思いました。
クラッソーネでは、プロダクトとチームの双方をより良く改善していけるエンジニアを募集中です!
ご興味ある方はぜひ採用サイトをご覧ください!