こしあん
2024-12-24

CodeBuildで作る、LambdaにEFSをマウントしてPyTorchをロード


7{icon} {views}

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:DescribeDhcpOptionsec2: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の中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です