はじめに
以前の記事ではGolangでLambda Layersを実装してみたが、今回はPythonで作ってみようと思う。
Pythonは、クラスメソッド先生の記事でも触れられている通り、ラインタイムバージョンによって使えるライブラリが異なっている。requestsはまあまあ利用頻度が高いのに、Python3.8のランタイムからは標準では使えなくなってしまっている。
GolangやNode.jsであればコンパイルやトランスパイル・バンドルしてパッケージを込めることができるが、Pythonはそうもいかないため、Lambda Layerで標準ライブラリを使うのがセオリーのようだ。
あまり手数をかけたくないので、terraform apply
一発でライブラリまで含めてデプロイできることを目指す。
構成
以下のようなディレクトリツリーにする。
outputsは、terraformの中で作成する一時ファイルを置く場所なので、.gitignoreでCommit対象にならないようにしておこう。
.
├── .gitignore
├── outputs/
│ ├── layer/
│ ├── venv/
│ └── zipfile/
├── scripts/
│ ├── example.py
│ └── requirements.txt
└── terraform/
└── [もろもろのTerraformの記述].tf
Pythonのスクリプト
Pythonはシンプルにrequestsのライブラリを使って静的コンテンツをGETしてくるというもの。
import requests
def lambda_handler(event, context):
req = requests.get('https://www.google.com/')
try:
req.raise_for_status()
print("Request succeeded")
return req.text
except requests.RequestException as e:
print("Request failed: %s", e)
このあとインストールに使うので、requestsはpipで簡単に入れられるようにしておく。
今回はrequestsしか使っていないので問題ないが、pip freezeでインストールしようとすると、boto3のようにLambdaのランタイムに標準で含まれれるライブラリまで余分にインストールすることになってしまうので、必要なものに絞って記述しよう。これでも依存関係はpipが勝手に解決してくれる。
requests==2.22.0
TerraformのIaC
準備
まずは、Lambda Layerの元にになるrequestsのインストールをしておく。
pip installは、仮想環境でやらないとグローバルでインストールしているすべてのライブラリを対象にしてしまうため、一時ディレクトリに仮想環境を作り、そこで生成されたpipを使ってrequirements.txtを食わせることで、必要最低限に絞ったものが作れる。
Lambda Layersで実行されるライブラリは、/opt配下に展開されるが、Pythonのライブラリは/opt/python配下が読まれるため、outputs/layer/pythonbに展開するようにしておく(後述のHCLでは、layer配下をzipするように設定している)。
また、null_resource
のtriggers
プロパティを使って、requirements.txtに更新があったときに再度展開とzip処理が自動で走るようにしておこう。
resource "null_resource" "example_prepare_layer" {
triggers = {
"requirements_diff" = filebase64("${path.module}/../scripts/requirements.txt")
}
provisioner "local-exec" {
command = <<-EOF
rm -rf ${path.module}/../outputs/layer &&
python3 -m venv ${path.module}/../outputs/venv &&
${path.module}/../outputs/venv/bin/pip3 install -r ${path.module}/../scripts/requirements.txt -t ${path.module}/../outputs/layer/python --no-cache-dir
EOF
on_failure = fail
}
}
Lambda LayerとFunctionの構築
ここから先は難しい部分は特にない。
archive_file.example_layer
は、Pythonの仮想環境のインストールが完了してからでないとzipできないので、depends_on
しておこう(しておかないといずれにしろplan
やapply
時にエラーになる)。
Lambdaに付与するロールは、今回であればlogs:CreateLogGroup
、logs:CreateLogStream
、logs:PutLogEvents
あたりがあれば良い。Layerの利用には特に必要な権限はない。
################################################################################
# Layer #
################################################################################
data "archive_file" "example_layer" {
depends_on = [
null_resource.example_prepare_layer,
]
type = "zip"
source_dir = "../outputs/layer"
output_path = "../outputs/zipfile/example_layer.zip"
}
resource "aws_lambda_layer_version" "example" {
layer_name = local.lambda_layer_name
filename = data.archive_file.example_layer.output_path
source_code_hash = data.archive_file.example_layer.output_base64sha256
compatible_runtimes = ["python3.8"]
}
################################################################################
# Function #
################################################################################
data "archive_file" "example_function" {
type = "zip"
source_dir = "../scripts"
output_path = "../outputs/zipfile/example_function.zip"
}
resource "aws_lambda_function" "example" {
depends_on = [
aws_cloudwatch_log_group.lambda,
]
function_name = local.lambda_function_name
filename = data.archive_file.example_function.output_path
role = aws_iam_role.lambda.arn
handler = "example.lambda_handler"
source_code_hash = data.archive_file.example_function.output_base64sha256
runtime = "python3.8"
memory_size = 128
timeout = 30
layers = [aws_lambda_layer_version.example.arn]
}
これで、aws lambda invoke
でrequestsが機能していることが分かる。
おまけ) destroy
時のゴミ掃除
outputsがゴミなので、destroy時に掃除するようにnull_resource
を仕掛けておこう。
別になくても困らない。
################################################################################
# Null Resources on destroy #
################################################################################
resource "null_resource" "destroy" {
provisioner "local-exec" {
when = destroy
command = "rm -rf ${path.module}/../outputs"
on_failure = continue
}
}