Lambda LayersをPython＋Terraformで実装してみる

# はじめに
[以前の記事](https://qiita.com/neruneruo/items/6236918c1669cb8337ef)ではGolangでLambda Layersを実装してみたが、今回はPythonで作ってみようと思う。

Pythonは、[クラスメソッド先生の記事](https://dev.classmethod.jp/articles/aws-lambda-python-library-list-with-pkg-resources/)でも触れられている通り、ラインタイムバージョンによって使えるライブラリが異なっている。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してくるというもの。

```example.py
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が勝手に解決してくれる。

```requirements.txt
requests==2.22.0
```

# TerraformのIaC
## 準備
まずは、Lambda Layerの元にになるrequestsのインストールをしておく。

pip installは、仮想環境でやらないとグローバルでインストールしているすべてのライブラリを対象にしてしまうため、一時ディレクトリに仮想環境を作り、そこで生成されたpipを使ってrequirements.txtを食わせることで、必要最低限に絞ったものが作れる。

Lambda Layersで実行されるライブラリは、/opt配下に展開されるが、Pythonのライブラリは/opt/python配下が読まれるため、outputs/layer/pythonｂに展開するようにしておく(後述のHCLでは、layer配下をzipするように設定している)。

また、```null_resource```の```triggers```プロパティを使って、requirements.txtに更新があったときに再度展開とzip処理が自動で走るようにしておこう。

```HCL
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の利用には特に必要な権限はない。

```HCL
################################################################################
# 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```を仕掛けておこう。
別になくても困らない。

```HCL
################################################################################
# Null Resources on destroy                                                    #
################################################################################
resource "null_resource" "destroy" {
  provisioner "local-exec" {
    when       = destroy

    command = "rm -rf ${path.module}/../outputs"

    on_failure = continue
  }
}
```