CodeBuildで作る、LambdaにEFSをマウントしてPyTorchをロード
Lambda上でPyTorchなどの大容量ライブラリを扱うため、EFSを活用して依存関係を配置する方法を解説します。CodeBuildを使ったライブラリのインストール自動化やTerraformでの構成管理により、Dockerを使わずに対応する事例を紹介します。
目次
はじめに
- Lambdaからライブラリを参照するときは普通レイヤーを使いますが、レイヤーは250MBまでしか対応していません
- 依存関係まるまるインストールしたら250MBを超えるライブラリ(例:PyTorch)はこれまではDockerを使うのがセオリーでした
- しかし、最近LambdaのSnapStartにPythonが対応され、Dockerが未対応なのを見ると、特にコールドスタートの理由から、大きなライブラリに対してはDocker以外も検討する必要がありそうでした
- 今回はライブラリの置き場として、EFS(Elastic File System)を使ってみました。EFSは通常複数のEC2に対して使うユースケースが多いのですが、Lambdaに対しても適用可能です。
- 初回のみEFSにライブラリ(例:PyTorch)をインストールする必要があるため、この自動化をCodeBuildに行わせています
- CodeBuildは主にCI/CDで使われることが多いサービスですが、今回はトリガーは自動化しないで、手動でボタンを押したら実行するようにします。
- EFSの初期化がうまくいったかを確かめるために、EC2を用意しました(これは将来的には不要)
- 全体のアーキテクチャーは以下の通り
補足:後で残念なことがわかり、EFSの場合はSnapStartが非対応でした。
SnapStart does not support provisioned concurrency, Amazon Elastic File System (Amazon EFS), or ephemeral storage greater than 512 MB.
ディレクトリ構造
以下の複数のTerraformのファイルからなります。全体のソースは最後に示します。
├── buildspec.yml.tpl # ビルド仕様テンプレート
├── codebuild.tf # CodeBuild の Terraform 設定
├── efs.tf # EFS の Terraform 設定
├── lambda_function.py # Lambda 関数のコード
├── lambda.tf # Lambda の Terraform 設定
├── main.tf # メインの Terraform 設定
└── test_ec2.tf # テスト用 EC2 の Terraform 設定
なぜEFSの初期化をEC2で行わないのか?
実は最初はEFSの初期化をEC2で行おうとしましたが、実はこれはうまくいきませんでした。CodeBuildを使ったのはこれが理由です。理由は以下の通りです。
- Lambdaの実行環境をエミュレートするには、Amazon Linux (2023)のEC2を使う必要がある
- Lambdaの実行環境を揃えるために、PythonバージョンをLambdaにあわせる必要がある
- しかし、Amazon Linux 2023のデフォルトのPythonは3.9で、LambdaのPython3.12に合わせようとすると、Pythonのアップデートが必要
- Ubuntuなどのディストーションと異なり、Amazon LinuxはデフォルトでインストールされているPythonバージョンに依存したOS構造をしており、デフォルトのPythonのバージョンをアップデートしてしまうと、OSの動作がおかしくなってしまう(コマンド類が全て通らなくなる)
- したがって、EC2のユーザーデータで初期化のタイミングで指定したPythonをインストールして、EFSをマウント→指定したライブラリをインストールするということが結構面倒
- これを解決するにはDocker化なのだが、これをやるのだったら最初からCodeBuildを使ったほうが楽
別の案として初期化用のLambdaを定義してしまうというのもありですが、実際は初期化用と本番用のLambdaが混在するのは誤爆が怖いので、CodeBuildにオフロードしてしまうのはまあいいかなと思います。
CodeBuild
Terraformと組み合わせてbuildspecを動的にする
CodeBuildでは、ビルド設定にbuildspec.yaml
を定義しますが、Terraformと組み合わせてこれにパラメーターを代入することが可能です(CodeBuild本来の機能ではなく、Terraformとの組み合わせで実現できる機能です)。想定ユースケースとしては以下の通りです。
- ベースのPythonバージョンを変更したい
- ライブラリ(PyTorch)のバージョンを変更したい
発想としてはStep Functionsの定義にLambdaのARNを動的に代入するものとほぼ一緒です。以下のようなテンプレートのyamlを定義し、
version: 0.2
phases:
install:
runtime-versions:
python: ${python_version}
commands:
- echo "Pipのアップグレード"
- pip install --upgrade pip
build:
commands:
- echo "PyTorchをEFSにインストール"
- pip install torch==${pytorch_version} torchvision==${torchvision_version} --target /mnt/efs/python --index-url https://download.pytorch.org/whl/cpu
post_build:
commands:
- echo "ビルド完了"
artifacts:
files:
- '**/*'
discard-paths: yes
CodeBuildのTerraformは以下のように書きます。
# CodeBuildプロジェクトの作成(ソースなし)
resource "aws_codebuild_project" "codebuild_pytorch" {
name = "pytorch-installation-project"
service_role = aws_iam_role.codebuild_role.arn
# ソース設定: ソースなし
source {
type = "NO_SOURCE"
buildspec = templatefile("${path.module}/buildspec.yml.tpl", {
python_version = "3.12"
pytorch_version = "2.5.1+cpu"
torchvision_version = "0.20.1+cpu"
})
}
# ビルド環境の設定
environment {
compute_type = "BUILD_GENERAL1_MEDIUM" # 必要に応じて変更
image = "aws/codebuild/amazonlinux-x86_64-standard:5.0"
type = "LINUX_CONTAINER"
privileged_mode = true # Docker daemonをDockerコンテナの中で動かすか。trueが必須
}
# VPC設定: EFSにアクセスするため
vpc_config {
vpc_id = var.vpc_id
subnets = var.subnet_ids
security_group_ids = [aws_security_group.codebuild_sg.id]
}
# EFSのマウント設定
file_system_locations {
identifier = "my-efs"
location = "${aws_efs_file_system.efs.dns_name}:/"
mount_point = "/mnt/efs"
type = "EFS"
}
# アーティファクト設定
artifacts {
type = "NO_ARTIFACTS"
}
}
buildspecにはCodeCommitやS3のファイルを普通書くのですが、NO_SOURCE
とすることでアドホックなyamlを割り当てることが可能です。さらにTerraformのtemplatefile
関数と組み合わせることで、PythonのバージョンやPyTorchのバージョンなどの引数を代入しつつbuildspecを定義できます。
ビルド環境、VPC、EFSの設定
ビルド環境は適当にコンピューティングタイプを選びます。Armタイプも選べます。privileged_mode = true
を選択する必要があります。
VPCの設定は、プライベートサブネットに紐づけます。ここで、プライベートサブネットはNATゲートウェイやNATインスタンス(fck-natなど)でインターネット方向に通信できる必要があります(このコードの管理外)。これはPyTorchのインストールに必要なためです。
file_system_locations
にEFSの設定の設定を入れます。EFSのDNS名を指定し、/mnt/efs
として指定します。このDNSを指定しないとローカルホストとしてマウントされるのでエラーが出ました。
ポリシー
CodeBuildで難しいのは、CodeBuildにわたすIAMポリシーです。ec2:DescribeDhcpOptions
やec2:CreateNetworkInterfacePermission
などのあまりドキュメントに書かれていないポリシーを許可する必要がありました。ここはVPCの構成によっても変わるかもしれません。
権限エラーの際は、CloudTrailのイベント履歴が便利でした。CodeBuildの実行時に何らかのAPIを呼び出しているので、それで必要な権限がわかります。
# IAMポリシー: CodeBuildに必要な権限を付与
resource "aws_iam_role_policy" "codebuild_policy" {
name = "CodeBuildPolicy"
role = aws_iam_role.codebuild_role.id
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"elasticfilesystem:ClientMount",
"elasticfilesystem:ClientWrite",
"elasticfilesystem:DescribeMountTargets"
],
Resource = aws_efs_file_system.efs.arn
},
{
Effect = "Allow",
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
Resource = "*"
},
{
Effect = "Allow",
Action = [
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:DescribeVpcs",
"ec2:DescribeDhcpOptions", # ポリシーの追加が必要
"ec2:CreateNetworkInterface",
"ec2:CreateNetworkInterfacePermission", # ポリシーの追加が必要
"ec2:DeleteNetworkInterface",
"ec2:DescribeNetworkInterfaces"
],
Resource = "*"
}
]
})
}
EFS
EFSは特に難しいところはなくて、教科書的なセキュリティグループにおけるTCP=2049
を許可する部分です。今回はLambdaだのEC2だの区別するのが面倒だったので、プライベートサブネットのCIDR全体からの2049を許可しました。
# EFSのセキュリティグループの作成(Lambda/EC2→EFS)
resource "aws_security_group" "efs_sg" {
name = "efs-sg"
description = "Allow NFS access"
vpc_id = var.vpc_id
ingress {
from_port = 2049
to_port = 2049
protocol = "tcp"
cidr_blocks = [for id, subnet in data.aws_subnet.selected : subnet.cidr_block] # サブネットからの通信を全許可する
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
}
あとはサブネット単位のマウントポイントの作成です。これはLambdaの作成で依存関係の問題になり、全てのサブネットへのマウントポイントの作成が完了してからLambdaをデプロイしないとエラーになります。
# EFSのマウントポイントの作成(サブネット単位)
resource "aws_efs_mount_target" "efs_mt" {
for_each = data.aws_subnet.selected
file_system_id = aws_efs_file_system.efs.id
subnet_id = each.key
security_groups = [aws_security_group.efs_sg.id]
}
Lambda
LambdaのEFSのアクセスポイントを作り、それをLambdaの作成時に読ませる方法を取ります。
EFSのアクセスポイント
LambdaのEFSのアクセスポイントを作ります。posix_user
を適当に指定します。このパス関係がややこしいですが、path=/
でも悪くはないかなと思いますが、多分ベストプラクティスではありません。これはもっといいやり方があるはずです。
# EFSアクセスポイント
resource "aws_efs_access_point" "lambda_access_point" {
file_system_id = aws_efs_file_system.efs.id
posix_user {
uid = 1000
gid = 1000
}
root_directory {
path = "/"
creation_info {
owner_uid = 1000
owner_gid = 1000
permissions = "755"
}
}
tags = {
Name = "lambda-access-point"
}
}
Lambdaの作成
TerraformでのLambdaの作成はVPCの設定と、EFSのマウントですが、依存関係に気をつける必要があります。Lambdaのアクセスポイントと、EFSのマウントポイントの作成が完了してからLambdaをデプロイしないとエラーになります(したがって、depends_on
で明示します)。
# Lambda関数の作成
resource "aws_lambda_function" "efs_validator" {
function_name = "pytorch-efs-validator"
role = aws_iam_role.lambda_role.arn
handler = "lambda_function.lambda_handler"
runtime = "python3.12"
filename = data.archive_file.lambda_zip.output_path
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
memory_size = 4000
timeout = 120
# VPC設定
vpc_config {
subnet_ids = var.subnet_ids
security_group_ids = [aws_security_group.lambda_sg.id]
}
# EFSマウント設定
# 必ず Access Point ARN を指定し、ローカルでのマウントパスを指定します
file_system_config {
arn = aws_efs_access_point.lambda_access_point.arn
local_mount_path = "/mnt/efs"
}
# マウントターゲットが作成完了してからLambdaが作成されるように明示
depends_on = [
aws_efs_access_point.lambda_access_point,
aws_efs_mount_target.efs_mt
]
}
ポリシー系も注意が必要ですが、あまりCodeBuildと変わらないのでここでは一旦省略します。
Lambdaのコードは以下の通り。環境変数PYTHONPATH
をいじっている例もあるけど、副作用はまあまああるので、アプリケーション側でsys.path.append
をすればいいだけなので、こっちでもいいかなという気はする。
import sys
sys.path.append("/mnt/efs/python")
import torch
import torchvision
def lambda_handler(event, context):
return {
"pytorch_version": torch.__version__,
"torchvision_version": torchvision.__version__
}
テスト用のEC2
Ubuntuを使います。以下のユーザーデータでインスタンス作成時にマウントができます。Session Managerのポリシー(AmazonSSMManagedInstanceCore
)を付与しています。接続時は、マネジメントコンソールの「Session Manager」からで十分です。
Lambdaと同様にマウントの依存関係に注意します。
# EC2インスタンス (Ubuntu)
resource "aws_instance" "ubuntu_ssm" {
ami = data.aws_ami.ubuntu_ami.id # 下のdataで取得
instance_type = "t3.micro"
subnet_id = var.subnet_ids[0]
vpc_security_group_ids = [aws_security_group.ec2_ssm_sg.id]
iam_instance_profile = aws_iam_instance_profile.ec2_ssm_instance_profile.name
associate_public_ip_address = false
# EFSをマウント
user_data = <<-EOF
#!/bin/bash
apt-get update
apt-get install -y nfs-common
mkdir -p /mnt/efs
mount -t nfs4 -o nfsvers=4.1 ${aws_efs_file_system.efs.dns_name}:/ /mnt/efs
EOF
tags = {
Name = "ubuntu-ssm-instance"
}
# サブネットへのマウントが終わってからEC2をデプロイ
depends_on = [
aws_efs_mount_target.efs_mt
]
}
結果
CodeBuild
CodeBuild→ビルドプロジェクト→pytorch-installation-projectから、「ビルドを開始」
このようにインストールが進行するはず。DockerデーモンとEFSの間の接続が遅いのか少しビルドに時間がかかるが、気長に待つ。GitHub Actionsが使えれば別にそれでもいいかと思われます。
EC2
テスト用のインスタンスに接続してみます。Session Managerで接続。df -h
を実行し、/mnt/efs
があればOK。しかし最大8エクサバイトはすごい。
$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/root 6.8G 1.9G 4.9G 29% /
tmpfs 458M 0 458M 0% /dev/shm
tmpfs 183M 876K 182M 1% /run
tmpfs 5.0M 0 5.0M 0% /run/lock
efivarfs 128K 3.6K 120K 3% /sys/firmware/efi/efivars
/dev/nvme0n1p16 881M 76M 744M 10% /boot
/dev/nvme0n1p15 105M 6.1M 99M 6% /boot/efi
fs-1234567890abcdef.efs.ap-northeast-1.amazonaws.com:/ 8.0E 0 8.0E 0% /mnt/efs
ls /mnt/efs/python
でインストールされたディレクトリが確認できます。
$ ls /mnt/efs/python
Jinja2-3.1.3.dist-info filelock markupsafe numpy.libs sympy torchvision.libs
MarkupSafe-2.1.5.dist-info filelock-3.13.1.dist-info mpmath pillow-10.2.0.dist-info sympy-1.13.1.dist-info typing_extensions-4.9.0.dist-info
PIL fsspec mpmath-1.3.0.dist-info pillow.libs torch typing_extensions.py
__pycache__ fsspec-2024.2.0.dist-info networkx pkg_resources torch-2.5.1+cpu.dist-info
_distutils_hack functorch networkx-3.2.1.dist-info setuptools torchgen
bin isympy.py numpy setuptools-70.0.0.dist-info torchvision
distutils-precedence.pth jinja2 numpy-1.26.3.dist-info share torchvision-0.20.1+cpu.dist-info
Lambda
pytorch-efs-validator
を実行してみる。結果は以下の通り。
Response:
{
"pytorch_version": "2.5.1+cpu",
"torchvision_version": "0.20.1+cpu"
}
Function Logs:
INIT_REPORT Init Duration: 10008.39 ms Phase: init Status: timeout
START RequestId: ca71683b-f69f-46a7-8705-566165d40f3b Version: $LATEST
END RequestId: ca71683b-f69f-46a7-8705-566165d40f3b
REPORT RequestId: ca71683b-f69f-46a7-8705-566165d40f3b Duration: 20869.77 ms Billed Duration: 20870 ms Memory Size: 4000 MB Max Memory Used: 236 MB
確かに正常にバージョンは取れているが、コールドスタートおっそ笑 (ちなみに2回目以降は速い)
おそらくEFSのスループットモードがバーストになっているから。AWSの推奨はElasticなのに、Terraformのデフォルトはバーストらしい。
参考:Amazon EFS の Elastic Throughput モードと Bursting Throughput モードでファイルの読み書きしてスループットを確認してみた
CloudWatch MetricsでPermittedThroughputをみると105MBytes/Secondしか出ていない。EFSが1TBぐらいになっているので、ちょっとこれは遅いなあ。
EFSをElastic Throughputにてみる
実はEFSの設定をこうするだけでElastic Throughputにできる。
# EFSの作成
resource "aws_efs_file_system" "efs" {
creation_token = "example-lambda-efs"
throughput_mode = "elastic"
tags = {
Name = "example-lambda-efs"
}
}
このへんはガチャ要素があるのでなんともいえないが、コールドスタートは若干短縮されていた。
tatus: Succeeded
Test Event Name: (unsaved) test event
Response:
{
"pytorch_version": "2.5.1+cpu",
"torchvision_version": "0.20.1+cpu"
}
Function Logs:
INIT_REPORT Init Duration: 10006.34 ms Phase: init Status: timeout
START RequestId: 5d9a9b57-2fe2-43b2-8bca-c1a065594a87 Version: $LATEST
END RequestId: 5d9a9b57-2fe2-43b2-8bca-c1a065594a87
REPORT RequestId: 5d9a9b57-2fe2-43b2-8bca-c1a065594a87 Duration: 13835.76 ms Billed Duration: 13836 ms Memory Size: 4000 MB Max Memory Used: 231 MB
Request ID: 5d9a9b57-2fe2-43b2-8bca-c1a065594a87
2回目以降は爆速
Duration: 1.43 ms Billed Duration: 2 ms Memory Size: 4000 MB Max Memory Used: 232 MB
PermittedThroughputはこんなもの。明らかにElasticのほうが多い
おわりに
- EFSできたけど、いろいろちょっと大変なところはあったんで、コールドスタートに困ってなきゃDockerでいいんじゃない説はある
- EFSの明確な有利点は見つけていきたい。ダメな点は同時実行数多くなったときにEFS側でスロットルかかるなどいろいろ思いつく。
全体コード
main.tf
# 入力変数の定義
variable "vpc_id" {
description = "VPC ID"
type = string
}
variable "subnet_ids" {
description = "Subnet IDs"
type = list(string)
}
data "aws_region" "current" {}
data "aws_caller_identity" "current" {}
buildspec.yml.tpl
version: 0.2
phases:
install:
runtime-versions:
python: ${python_version}
commands:
- echo "Pipのアップグレード"
- pip install --upgrade pip
build:
commands:
- echo "PyTorchをEFSにインストール"
- pip install torch==${pytorch_version} torchvision==${torchvision_version} --target /mnt/efs/python --index-url https://download.pytorch.org/whl/cpu
post_build:
commands:
- echo "ビルド完了"
artifacts:
files:
- '**/*'
discard-paths: yes
codebuild.tf
# CodeBuild用セキュリティグループの作成
resource "aws_security_group" "codebuild_sg" {
name = "codebuild-sg"
description = "Security group for CodeBuild to access EFS"
vpc_id = var.vpc_id
ingress {
from_port = 2049
to_port = 2049
protocol = "tcp"
security_groups = [aws_security_group.efs_sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
}
# IAMロール: CodeBuild用
resource "aws_iam_role" "codebuild_role" {
name = "codebuild-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Principal = {
Service = "codebuild.amazonaws.com"
},
Action = "sts:AssumeRole"
}
]
})
}
# IAMポリシー: CodeBuildに必要な権限を付与
resource "aws_iam_role_policy" "codebuild_policy" {
name = "CodeBuildPolicy"
role = aws_iam_role.codebuild_role.id
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"elasticfilesystem:ClientMount",
"elasticfilesystem:ClientWrite",
"elasticfilesystem:DescribeMountTargets"
],
Resource = aws_efs_file_system.efs.arn
},
{
Effect = "Allow",
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
Resource = "*"
},
{
Effect = "Allow",
Action = [
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:DescribeVpcs",
"ec2:DescribeDhcpOptions", # ポリシーの追加が必要
"ec2:CreateNetworkInterface",
"ec2:CreateNetworkInterfacePermission", # ポリシーの追加が必要
"ec2:DeleteNetworkInterface",
"ec2:DescribeNetworkInterfaces"
],
Resource = "*"
}
]
})
}
# CodeBuildプロジェクトの作成(ソースなし)
resource "aws_codebuild_project" "codebuild_pytorch" {
name = "pytorch-installation-project"
service_role = aws_iam_role.codebuild_role.arn
# ソース設定: ソースなし
source {
type = "NO_SOURCE"
buildspec = templatefile("${path.module}/buildspec.yml.tpl", {
python_version = "3.12"
pytorch_version = "2.5.1+cpu"
torchvision_version = "0.20.1+cpu"
})
}
# ビルド環境の設定
environment {
compute_type = "BUILD_GENERAL1_MEDIUM" # 必要に応じて変更
image = "aws/codebuild/amazonlinux-x86_64-standard:5.0"
type = "LINUX_CONTAINER"
privileged_mode = true # Docker daemonをDockerコンテナの中で動かすか。trueが必須
}
# VPC設定: EFSにアクセスするため
vpc_config {
vpc_id = var.vpc_id
subnets = var.subnet_ids
security_group_ids = [aws_security_group.codebuild_sg.id]
}
# EFSのマウント設定
file_system_locations {
identifier = "my-efs"
location = "${aws_efs_file_system.efs.dns_name}:/"
mount_point = "/mnt/efs"
type = "EFS"
}
# アーティファクト設定
artifacts {
type = "NO_ARTIFACTS"
}
}
efs.tf
# EFSの作成
resource "aws_efs_file_system" "efs" {
creation_token = "example-lambda-efs"
throughput_mode = "elastic"
tags = {
Name = "example-lambda-efs"
}
}
# サブネットの情報を取得
data "aws_subnet" "selected" {
for_each = toset(var.subnet_ids)
id = each.value
}
# EFSのマウントポイントの作成(サブネット単位)
resource "aws_efs_mount_target" "efs_mt" {
for_each = data.aws_subnet.selected
file_system_id = aws_efs_file_system.efs.id
subnet_id = each.key
security_groups = [aws_security_group.efs_sg.id]
}
# EFSのセキュリティグループの作成(Lambda/EC2→EFS)
resource "aws_security_group" "efs_sg" {
name = "efs-sg"
description = "Allow NFS access"
vpc_id = var.vpc_id
ingress {
from_port = 2049
to_port = 2049
protocol = "tcp"
cidr_blocks = [for id, subnet in data.aws_subnet.selected : subnet.cidr_block] # サブネットからの通信を全許可する
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
}
lambda_function.py
import sys
sys.path.append("/mnt/efs/python")
import torch
import torchvision
def lambda_handler(event, context):
return {
"pytorch_version": torch.__version__,
"torchvision_version": torchvision.__version__
}
lambda.tf
# EFSアクセスポイント
resource "aws_efs_access_point" "lambda_access_point" {
file_system_id = aws_efs_file_system.efs.id
posix_user {
uid = 1000
gid = 1000
}
root_directory {
path = "/"
creation_info {
owner_uid = 1000
owner_gid = 1000
permissions = "755"
}
}
tags = {
Name = "lambda-access-point"
}
}
# Lambda用 IAMロール
resource "aws_iam_role" "lambda_role" {
name = "pytorch-lambda-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Principal = {
Service = "lambda.amazonaws.com"
},
Action = "sts:AssumeRole"
}
]
})
}
# Lambdaのログ出力等に必要なポリシーを付与 (CloudWatch Logs 等)
resource "aws_iam_role_policy_attachment" "lambda_basic_logging" {
role = aws_iam_role.lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
# LambdaからEFSへアクセスするためのポリシー付与
resource "aws_iam_role_policy" "lambda_efs_policy" {
name = "lambda-efs-policy"
role = aws_iam_role.lambda_role.id
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"elasticfilesystem:ClientMount",
"elasticfilesystem:ClientWrite",
"elasticfilesystem:DescribeMountTargets"
],
Resource = "*"
},
{
Effect = "Allow",
Action = [
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:DescribeVpcs",
"ec2:DescribeDhcpOptions",
"ec2:CreateNetworkInterface",
"ec2:DeleteNetworkInterface",
"ec2:DescribeNetworkInterfaces"
],
Resource = "*"
}
]
})
}
# Lambda用セキュリティグループ
resource "aws_security_group" "lambda_sg" {
name = "lambda-sg"
description = "Security group for Lambda to access EFS"
vpc_id = var.vpc_id
ingress {
description = "Allow Lambda to EFS"
from_port = 2049
to_port = 2049
protocol = "tcp"
security_groups = [aws_security_group.efs_sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
}
# Lambda関数のコードをZIP化
data "archive_file" "lambda_zip" {
type = "zip"
source_file = "lambda_function.py"
output_path = ".cache/lambda_function.zip"
}
# Lambda関数の作成
resource "aws_lambda_function" "efs_validator" {
function_name = "pytorch-efs-validator"
role = aws_iam_role.lambda_role.arn
handler = "lambda_function.lambda_handler"
runtime = "python3.12"
filename = data.archive_file.lambda_zip.output_path
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
memory_size = 4000
timeout = 120
# VPC設定
vpc_config {
subnet_ids = var.subnet_ids
security_group_ids = [aws_security_group.lambda_sg.id]
}
# EFSマウント設定
# 必ず Access Point ARN を指定し、ローカルでのマウントパスを指定します
file_system_config {
arn = aws_efs_access_point.lambda_access_point.arn
local_mount_path = "/mnt/efs"
}
# マウントターゲットが作成完了してからLambdaが作成されるように明示
depends_on = [
aws_efs_access_point.lambda_access_point,
aws_efs_mount_target.efs_mt
]
}
test_ec2.tf
# IAMロールとインスタンスプロファイル
resource "aws_iam_role" "ec2_ssm_role" {
name = "ec2-ssm-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Principal = {
Service = "ec2.amazonaws.com"
},
Action = "sts:AssumeRole"
}
]
})
}
# SSM に必要なポリシーをロールに付与
resource "aws_iam_role_policy_attachment" "ec2_ssm_attach" {
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
role = aws_iam_role.ec2_ssm_role.name
}
# EC2インスタンスが使うインスタンスプロファイル
resource "aws_iam_instance_profile" "ec2_ssm_instance_profile" {
name = "ec2-ssm-instance-profile"
role = aws_iam_role.ec2_ssm_role.name
}
# EC2セキュリティグループ
resource "aws_security_group" "ec2_ssm_sg" {
name = "ec2-ssm-sg"
description = "Security Group for EC2 accessible via Session Manager"
vpc_id = var.vpc_id
# Session Managerのみ使う場合、SSH(22)は不要
ingress {
from_port = 2049
to_port = 2049
protocol = "tcp"
security_groups = [aws_security_group.efs_sg.id]
description = "Allow NFS access from EFS security group"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
tags = {
Name = "ec2-ssm-sg"
}
}
# EC2インスタンス (Ubuntu)
resource "aws_instance" "ubuntu_ssm" {
ami = data.aws_ami.ubuntu_ami.id # 下のdataで取得
instance_type = "t3.micro"
subnet_id = var.subnet_ids[0]
vpc_security_group_ids = [aws_security_group.ec2_ssm_sg.id]
iam_instance_profile = aws_iam_instance_profile.ec2_ssm_instance_profile.name
associate_public_ip_address = false
# EFSをマウント
user_data = <<-EOF
#!/bin/bash
apt-get update
apt-get install -y nfs-common
mkdir -p /mnt/efs
mount -t nfs4 -o nfsvers=4.1 ${aws_efs_file_system.efs.dns_name}:/ /mnt/efs
EOF
tags = {
Name = "ubuntu-ssm-instance"
}
# サブネットへのマウントが終わってからEC2をデプロイ
depends_on = [
aws_efs_mount_target.efs_mt
]
}
data "aws_ami" "ubuntu_ami" {
most_recent = true
owners = ["099720109477"] # CanonicalのAMI所有者ID
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー