Terraformを使ってS3とAWS Batchを連携するようAWSリソース作成してみた

S3とAWS Batchを連携するAWSリソースを、Terraformを使って作成してみました!
2023.08.04

みなさん、こんにちは!

クルトンです。

今回は、S3バケットの特定のフォルダへファイルをアップロードをした時に、AWS Batchのコンテナを動かしてみます。AWSリソース作成にはTerraformを使っているので、関連するAWSサービスをTerraformで書きたい方には参考になるかと思います。

前提

処理する流れとしては、次のものを想定しています。

  1. S3
    • コンテナ内で使用するファイルを指定フォルダへアップロード
  2. EventBridge
    • アップロードを検知すると、AWS Batchを動かす
  3. AWS Batch
    • S3へアップロードされたファイルを使用してコンテナ内で処理をし、アウトプットとしてインプットで使用したファイルと同じS3バケットへファイルアップロード
  4. S3
    • AWS Batchのコンテナでのアウトプットされたファイルが指定されたフォルダへ格納される

コンテナ

コンテナは次の流れで処理するものを作成済みとします。

  1. S3からファイルをダウンロードする
  2. ダウンロードしたファイルをインプットとして使う機械学習モデルを GPU環境 で動かす
  3. 機械学習モデルのアウトプットしたファイルをS3へアップロード

※インプット・アウトプット両方のファイル形式はCSVファイルとする

「コンテナをどう作成したら良いのか?」については、次のブログをご参考にしてください。

AWS Batch上でGPUを使うコンテナの動かし方については、次のブログをご参考にしてください。

Terraformの環境構築について

リソース作成についてはTerraformを使用しています。Terraformを使ってのリソース作成をした事が無い方は、環境構築から始めてみてください。

参考になるのが次のブログです。

Mac環境お使いの方向け

Windows環境お使いの方向け

MFA設定しているIAMを使いたい方へ

Terraformにおいて、MFAを設定しているIAMを使いたい方は次のブログが参考になります。(リンク先はMac環境で設定していますがaws-vaultはWindowsでも使えます。)

概要

次のように、ファイルを3つ作成します。ファイル名については、他の名前を使っても良いです。

.
├── main.tf
├── secrets.tfvars
└── variables.tf
  • main.tf
    • 各AWSリソース作成について書かれている本体のファイル
  • variables.tf
    • variableで変数定義をしているファイル
  • secrets.tfvars
    • variables.tfファイルで定義している変数の値を定義しているファイル

以上3つのファイルを作成し、terraform init実行後にterraform applyコマンドを実行して、AWSリソースを作成します。

次以降の内容は、AWSリソースを書くのにどのようにtfファイルを書くと良いかを説明していきます。

説明の順番は次のとおりです。

  1. providerの定義
    • 記述するファイル: main.tf
  2. 変数の定義
    • 記述するファイル: main.tf, variables.tf, secrets.tfvars
  3. S3の設定
    • 記述するファイル: main.tf
  4. AWS Batchcの設定
    • 記述するファイル: main.tf
  5. EventBridgeの設定
    • 記述するファイル: main.tf

Terraformの書き方が分からない場合は、公式サイトをご確認ください。

なお、本ブログで使う内容は以下のものですので、全て知っている方は次の内容にお読みいただければ幸いです。

providerの定義

main.tfファイルの冒頭に、以下内容を記載してください。

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.16"
    }
  }
  required_version = ">= 1.2.0"
}

変数定義

ハードコーディングにならないよう主にlocalsブロックを使って、各AWSリソースを作成するときに使用する内容を変数で保持します。(一部variableブロックを使っています。)

main.tfファイル

locals {
  prefix = "お好きな名前をご記入ください" # 自分が作成したAWSリソースを分かりやすくするために先頭文字列として付ける

  # S3
  bucket_name = "${local.prefix}-bucket"

  # AWS Batch
  job_name                            = "${local.prefix}-job"
  job_definition                      = "${local.prefix}-job-definition"
  compute_environment_name            = "${local.prefix}-compute-environment"
  job_queue                           = "${local.prefix}-job-queue"
  S3_for_instance_role                = "${local.prefix}-S3-for-instance-role"
  AmazonEC2ContainerServiceforEC2Role = "${local.prefix}-aws-batch-for-instance-role-AmazonEC2ContainerServiceforEC2Role"
  AWSBatchServiceRole                 = "${local.prefix}-AWSBatchServiceRole"
  aws_batch_service_role              = "${local.prefix}-aws-batch-service_role"
  instance_role_profile               = "${local.prefix}-terraform-iam-role-for-aws-batch-gpu"
  instance_role                       = "${local.prefix}-iam-role-for-aws-batch-gpu"

  # EventBridge
  event_rule               = "${local.prefix}-event-rule"
  eventbridge_service_role = "${local.prefix}_eventbridge_service_role"
  eventbridge_policy       = "${local.prefix}-eventbridge-policy"

  # VPC
  subnet_list = ["${var.subnet1}", "${var.subnet2}", "${var.subnet2}"]
}

また変数とは別ですが、後で使う必要があるものについても一緒に定義してしまいます。

# 現在使用しているアカウントIDを自動取得
data "aws_caller_identity" "current" {}

# 現在使用しているリージョンを自動取得
data "aws_region" "current" {}

variables.tfファイル

すでに作成されているデフォルトVPCを使っているため、そのVPCを使用するための変数宣言しております。 また、ECRにプッシュされているカスタムコンテナを使用するのに必要な変数を宣言しています。

ここでは変数名のみを定義しており、変数で保持する値については secrets.tfvarsファイルで定義します。

# VPC
variable "subnet1" {}
variable "subnet2" {}
variable "subnet3" {}
variable "security_group_ids" {}

# AWS Batch
variable "container_name" {}

secrets.tfvars

variables.tfファイルで変数宣言したものについて、具体的な値を書きます。

# VPC
subnet1            = "subnet-aaaaaaaa"
subnet2            = "subnet-bbbbbbbb"
subnet3            = "subnet-cccccccc"
security_group_ids = "sg-xxxxxxxx"

# AWS Batch
container_name = "コンテナ名を記載"

以上で、3つのファイルにまたがって使用する変数について定義しました。それでは実際に宣言した変数を使いながらAWSリソースを定義してみましょう。

main.tfファイルでAWSリソースの定義

どのような内容を書くのかをAWSサービスごとに記載していきます。

S3の設定

一点注意が必要で、Bucketを作成するだけでなく、EventBridgeとの連携のための設定も必要です。

resource "aws_s3_bucket" "bucket" {
  bucket = local.bucket_name
}
resource "aws_s3_object" "input_folder" {
  bucket = aws_s3_bucket.bucket.id
  key    = "input-file/"
}

# EventBridgeへ通知を送るのに必要
resource "aws_s3_bucket_notification" "bucket_notification" {
  bucket      = aws_s3_bucket.bucket.bucket
  eventbridge = true
}

AWS Batchの設定

Terraformで記載する内容は次のものです。対象のAWSサービスだけでなく、権限の設定としてIAM関連の設定も必要です。

  • aws_batch_job_definition
    • ジョブ定義の設定をします。
  • aws_batch_compute_environment
    • コンピューティング環境の設定をします。
  • aws_batch_job_queue
    • ジョブキューの設定をします。
  • aws_iam_instance_profile
    • コンテナを動かす際に必要となります。
  • aws_iam_policy
    • AWS定義済みのマネージドポリシーを使用するのに使っています。
  • aws_iam_role_policy_attachment
    • 権限を作成した時に、IAMロールへ付与するのに必要です。
  • aws_iam_policy_document
    • "batch.amazonaws.com"を書く必要があります。
  • aws_iam_role
    • インスタンスロールとサービスロールの2つを定義します。
# ジョブ定義
resource "aws_batch_job_definition" "job_definition" {
  name                 = local.job_definition
  type                 = "container"
  container_properties = <<CONTAINER_PROP
  {
    "command":["python","app.py","--input_bucket_name","Ref::input_bucket_name","--input_s3_key","Ref::input_s3_key","--output_bucket_name","Ref::output_bucket_name","--output_s3_key","Ref::output_s3_key"],
    "image":"${data.aws_caller_identity.current.account_id}.dkr.ecr.${data.aws_region.current.name}.amazonaws.com/${var.container_name}:latest",
   "resourceRequirements": [
      {
        "value": "1",
        "type": "VCPU"
      },
      {
        "value": "2048",
        "type": "MEMORY"
      },
      {
        "value": "1",
        "type": "GPU"
      }
    ]
  }
  CONTAINER_PROP
}

# コンピューティング環境
resource "aws_batch_compute_environment" "compute_environment" {
  compute_environment_name = local.compute_environment_name
  compute_resources {
    instance_role      = aws_iam_instance_profile.iam-role-for-aws-batch-gpu.arn
    instance_type      = ["g4dn.xlarge"]
    max_vcpus          = 256
    subnets            = local.subnet_list
    security_group_ids = ["${var.security_group_ids}"]
    type               = "EC2"
    desired_vcpus      = 0
  }
  type         = "MANAGED"
  service_role = aws_iam_role.aws_batch_service_role.arn
  depends_on   = [aws_iam_role_policy_attachment.aws_batch_service_role_AWSBatchServiceRole]
}


# ジョブキュー
resource "aws_batch_job_queue" "job_queue" {
  name                 = local.job_queue
  state                = "ENABLED"
  priority             = 0
  compute_environments = [aws_batch_compute_environment.compute_environment.arn]
}

# インスタンスロール
resource "aws_iam_instance_profile" "iam-role-for-aws-batch-gpu" {
  name = local.instance_role_profile
  role = aws_iam_role.instance_role.name
}
data "aws_iam_policy" "S3_for_instance_role" {
  arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}
data "aws_iam_policy" "aws_batch_for_instance_role_AmazonEC2ContainerServiceforEC2Role" {
  arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
}
resource "aws_iam_policy" "S3_for_instance_role" {
  name   = local.S3_for_instance_role
  policy = data.aws_iam_policy.S3_for_instance_role.policy
}
resource "aws_iam_policy" "aws_batch_for_instance_role_AmazonEC2ContainerServiceforEC2Role" {
  name   = local.AmazonEC2ContainerServiceforEC2Role
  policy = data.aws_iam_policy.aws_batch_for_instance_role_AmazonEC2ContainerServiceforEC2Role.policy
}
resource "aws_iam_role_policy_attachment" "S3_for_instance_role" {
  role       = aws_iam_role.instance_role.name
  policy_arn = aws_iam_policy.S3_for_instance_role.arn
}
resource "aws_iam_role_policy_attachment" "aws_batch_for_instance_role_AmazonEC2ContainerServiceforEC2Role" {
  role       = aws_iam_role.instance_role.name
  policy_arn = aws_iam_policy.aws_batch_for_instance_role_AmazonEC2ContainerServiceforEC2Role.arn
}

data "aws_iam_policy_document" "assume_role_for_instance" {
  statement {
    effect = "Allow"

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

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "instance_role" {
  name               = local.instance_role
  assume_role_policy = data.aws_iam_policy_document.assume_role_for_instance.json
}


# サービスロール用のIAMロール
data "aws_iam_policy" "for_service_role_AWSBatchServiceRole" {
  arn = "arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole"
}
resource "aws_iam_policy" "aws_batch_service_role_AWSBatchServiceRole" {
  name   = local.AWSBatchServiceRole
  policy = data.aws_iam_policy.for_service_role_AWSBatchServiceRole.policy
}
resource "aws_iam_role_policy_attachment" "aws_batch_service_role_AWSBatchServiceRole" {
  role       = aws_iam_role.aws_batch_service_role.name
  policy_arn = aws_iam_policy.aws_batch_service_role_AWSBatchServiceRole.arn
}
data "aws_iam_policy_document" "assume_role_for_service_role" {
  statement {
    effect = "Allow"

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

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "aws_batch_service_role" {
  name               = local.aws_batch_service_role
  assume_role_policy = data.aws_iam_policy_document.assume_role_for_service_role.json
}

EventBridgeの設定

  • aws_cloudwatch_event_rule
    • event_patternという項目で、発火条件を設定する
  • aws_cloudwatch_event_target
    • 発火後にデータを渡すAWSリソースの情報を書く
  • aws_iam_policy_document
    • EventBridgeを動かすために必要なものを定義するので、"events.amazonaws.com"を記載
  • aws_iam_role
    • ターゲット先のAWSリソースの指定をする必要がある
resource "aws_cloudwatch_event_rule" "eventbridge_rule" {
  name        = local.event_rule
  description = "S3の特定フォルダ配下にファイルアップロードしたときに、AWS Batchを起動するルールをIaC化"

  event_pattern = jsonencode({
    "detail-type" : ["Object Created"],
    "source" : ["aws.s3"],
    "detail" : {
      "bucket" : {
        "name" : ["${local.bucket_name}"]
      },
      "object" : {
        "key" : [{
          "prefix" : "input-file/"
        }]
      }
    }
  })
}

resource "aws_cloudwatch_event_target" "eventbridge_target" {
  rule = aws_cloudwatch_event_rule.eventbridge_rule.name

  arn = aws_batch_job_queue.job_queue.arn # ジョブキューのARN
  batch_target {
    job_definition = aws_batch_job_definition.job_definition.arn
    job_name       = local.job_name
  }
  input_transformer {
    input_paths = {
      "input_bucket_name" : "$.detail.bucket.name",
      "input_s3_key" : "$.detail.object.key"
    }
    input_template = <<TEMPLATE
{"Parameters": {"input_bucket_name":"<input_bucket_name>", "input_s3_key":"<input_s3_key>","output_bucket_name":"${local.bucket_name}", "output_s3_key":"output-eventbridge-test/output.csv"}}
    TEMPLATE
  }
  role_arn = aws_iam_role.eventbridge_service_role.arn
}

data "aws_iam_policy_document" "eventbridge_test" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["events.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "eventbridge_service_role" {
  name               = local.eventbridge_service_role
  assume_role_policy = data.aws_iam_policy_document.eventbridge_test.json
  inline_policy {
    name = local.eventbridge_policy
    policy = jsonencode({
      "Version" : "2012-10-17"
      "Statement" : [
        {
          Effect = "Allow"
          Action = [
            "batch:SubmitJob"
          ],
          Resource = [
            "arn:aws:batch:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:job/${local.job_name}",
            "${aws_batch_job_definition.job_definition.arn}",
            "${aws_batch_job_queue.job_queue.arn}"
          ]
        }
      ]
    })
  }
}

動かしてみた

ここまでで設定した内容を使ってAWSリソースの作成を行います。

まず初めにterraform initコマンドを実行してください。その後にterraform applyコマンドを実行します。(tfファイルの作成途中であれば、terraform validateterraform planで書いた内容が問題ないか確認しますが、今回は動作チェックを行なったtfファイルですので飛ばしています。)

CLIで次の表示が出ればAWSリソース作成が完了です。

Apply complete! Resources: 18 added, 0 changed, 18 destroyed.

まずはS3を開いてください。input-fileというフォルダがあるので実際にファイルをアップロードしてみます。

S3_upload_for_booting_AWSBatch

アップロード完了後にAWS Batchの画面を開きます。ダッシュボードを見てみると、ジョブが動いているのを確認できます。

AWSBatch_Job_Running_picture

少し待つと、ジョブが完了している事を確認できました!(一度実行していたので成功数は2の表記になっています。) AWSBatch_Job_Succeeded

そして冒頭にも書いたとおり、ファイルをアップロード出来ている事も確認できました。

S3_output_using_ML_container

AWSリソースの削除方法

今回作成したAWSリソースを削除したい場合は、S3バケットの中身を空にしてterraform destroyコマンドを叩いてください。

終わりに

今回は、S3とAWS Batchの連携のためにEventBridgeを使いました。また、それらAWSリソースをTerraformを使って作成しています。

Terraformを使うと簡単にAWSリソースを作成できるので便利ですね! 前回書いた『EventBridgeの発火条件にしたS3バケットとフォルダ含むファイル名をパラメータとして受け渡ししてみた』についても、IaC化してみようかと思います。

今回はここまで。

それでは、また!