terraform-aws-modules/lambda/aws を利用してuv管理のPython Lambdaのデプロイ

TerraformでLambdaをデプロイするときに、[AWS Lambda Terraform module](https://registry.terraform.io/modules/terraform-aws-modules/lambda/aws/latest)を使っていて、[uv](https://github.com/astral-sh/uv)で管理しているPythonコードをLambdaでデプロイしたい人向けです。

現時点のバージョンである `7.20.1` を前提として書きます。


## 最初に
以下のIssueが対応されれば、公式で機能提供されるようになると思いますので、必要な人はウォッチしておくと良いかもです。

https://github.com/terraform-aws-modules/terraform-aws-lambda/issues/654


## 対応方針
現時点では対応されていないので `commands` を上手く使って実装することにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116068/e7033a85-71fe-d2ae-0eba-f355462ec70b.png)


それに伴い、裏の仕様も確認する必要あったのでコードを見ていたのですが、以下の `package.py` ファイルを読みながら検討すると `commands` に何を渡せば良いかわかりやすいです。

https://github.com/terraform-aws-modules/terraform-aws-lambda/blob/v7.20.1/package.py

## 対応してみる

module以下のようなディレクトリ構成を想定しています。

### ディレクトリ構成
uvを利用したシンプルな構成


```sh
module/lambda/
├── code
│   ├── .python-version
│   ├── pyproject.toml
│   ├── src
│   │   └── main.py
│   └── uv.lock
└── main.tf
```





### Pythonコード
シンプルに `requests` を利用するコードです。

```src/main.py
import requests


def handler(event: dict, context: dict) -> None:
    resp = requests.get("http://checkip.amazonaws.com/")
    print(f"{resp.text=}")
```

```pyproject.toml
[project]
name = "code"
version = "0.1.0"
readme = "README.md"
requires-python = ">=3.11"
dependencies = ["requests>=2.32.3"]

[dependency-groups]
dev = ["types-requests>=2.32.0.20241016"]
```


### Terraform


```tf
variable "s3_bucket" {
  type = string
}
variable "s3_lambda_package_prefix" {
  type = string
}

locals {
  function_name = "lambda-python-uv"
  role_name     = "lambda-python-uv"
  runtime       = "python3.11"
}

data "aws_iam_policy_document" "this" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "this" {
  name               = local.role_name
  assume_role_policy = data.aws_iam_policy_document.this.json
}

# Lambda のベーシック実行ポリシー(AWS 管理ポリシー)をアタッチ
resource "aws_iam_role_policy_attachment" "this" {
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
  role       = aws_iam_role.this.name
}

# コードをLambda にデプロイするためのパッケージを作成
module "package" {
  source  = "terraform-aws-modules/lambda/aws"
  version = "7.20.1"


  runtime                  = local.runtime
  create_role              = false
  create_function          = false
  create_package           = true
  recreate_missing_package = false
  store_on_s3              = true
  s3_bucket                = var.s3_bucket


  source_path = [
    {
      path = "${path.module}/code"
      commands = [
        "mkdir -p ./dist/",
        "cp -a ./src/ ./dist/",
        "uv export --no-dev --format requirements-txt > ./dist/requirements.txt",
        "uv run pip install --no-compile --no-deps --prefix= --target=./dist --requirement=./dist/requirements.txt",
        ":zip ./dist/",
        "rm -fR ./dist/"
      ]
    }
  ]
}

# Lambda 関数を作成
module "lambda" {
  source  = "terraform-aws-modules/lambda/aws"
  version = "7.20.1"

  function_name            = local.function_name
  handler                  = "src.main.handler"
  runtime                  = local.runtime
  lambda_role              = aws_iam_role.this.arn
  create_role              = false
  create_function          = true
  create_package           = false
  s3_existing_package      = module.package.s3_object
  recreate_missing_package = false
  timeout                  = 60
  memory_size              = 256
}
```


ポイントとなっているのはこの部分です。

書いてあるとおりなのですが、`dist` というディレクトリを作成して、そこにソースコード入れて `pip` でモジュールも一緒に入れ込んでいます。ちなみにこの方法は ↑に書いた `package.py` の中の方法を参考にして、`uv` と組み合わせています。 

```terraform
      commands = [
        "mkdir -p ./dist/",
        "cp -a ./src/ ./dist/",
        "uv export --no-dev --format requirements-txt > ./dist/requirements.txt",
        "uv run pip install --no-compile --no-deps --prefix= --target=./dist --requirement=./dist/requirements.txt",
        ":zip ./dist/",
        "rm -fR ./dist/"
      ]
```



### デプロイしてみる
[AWS Lambda Terraform module](https://registry.terraform.io/modules/terraform-aws-modules/lambda/aws/latest)を使って、`pip` や `poetry` 使った時と同じようにPythonパッケージがフラットに入った状態でデプロイできています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116068/6d3a43a2-3191-f32e-b49f-a9836629f9b6.png)


実行すると正常終了して、`requests` モジュールでGlobal IPが取得できています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116068/23552b91-2f91-4a63-5056-476f6b549ae1.png)


コードの構成によっては調整必要な場合もあるかもしれませんが、この方法を参考に `package.py` を参考に構成考えれば解決できると思います。