AWS CDK(v2): FARGATE_SPOT AWSBatch環境を構築する

目次

AWSCDK を使って、 FARGATE_SPOT の AWSBatch 環境を構築してみることにします。 AWSBatch 環境を作るには、まず

  • VPC
  • ComputeEnvironment
  • JobQueue

が必要で、さらにそれぞれの実行したいタスクの

  • JobDefinition

が必要になってきます。 あまりこの辺の設定に詳しくなくいろいろ躓いたので、初めて作ったときには 1 日くらいかかりました… まあ次回は 1 時間くらいでできるかな。

事前準備

以下の準備がされているものとします。

Version

  • aws-cdk: 2.20.0

ポイント

いくつかポイントを書き残しておきます。

ComputeEnvironment の種類で書き方が変わる

AWS::Batch::ComputeEnvironment ComputeResources にありますが、 現状 EC2 | FARGATE | FARGATE_SPOT | SPOT の 4 種類あるようです。 これによって、JobQueue のタイプが変わったり、指定可能 Option や指定必須な Option が変わるようです。

今回は FARGATE_SPOT の例になります。

ecsTaskExecutionRole

私の場合はいつの間にか作成されていたのですが、ない場合は作る必要があります。 https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_execution_IAM_role.html などに説明があります。

VPC

新規作成するか既存のを使うか、になります。 両方の書き方を残しておきます。

executionRoleArn と jobRoleArn

  • executionRoleArn は Batch の実行開始まで(Image の Pull 等)に最低限必要な Role のようです。
  • jobRoleArn は 更に実行するコンテナが Role を必要なときに指定するようです

assignPublicIp

これを ENABLED とかにしておかないと、コンテナ Image を Pull できなくてエラーになります。 しかしその時のエラーメッセージはコンテナが docker.io にある場合は以下のようになり、

CannotPullContainerError: inspect image has been retried 5 time(s):
failed to resolve ref "docker.io/library/busybox:latest": failed to do request:
Head https://registry-1.docker.io/v2/library/busybox/manifests/latest: dial tcp 54.85.133.123:443: i/o t...

コンテナが ECR にある場合は以下のようになります。

ResourceInitializationError: unable to pull secrets or registry auth: execution resource retrieval failed:
unable to retrieve ecr registry auth: service call has been retried 3 time(s): RequestError:
send request failed caused by: Post https://api.ecr....

なかなかここから原因がわからなくて解決までかなり時間がかかってしまいました。 ※ IAM などの問題かと思ってしまった。

platformCapabilities

これを指定しないと、 FARGATE_SPOT タイプの ComputeEnvironment で実行できなくて(JobQueue に入れられない)ハマります。

コード全体

// lib/awsbatch-stack.ts
import { aws_batch, Stack, StackProps } from "aws-cdk-lib";
import { IVpc, SecurityGroup, SubnetType, Vpc } from "aws-cdk-lib/aws-ec2";
import { Construct } from "constructs";

// 今回作るStack名
const STACK_BASE_NAME = "SampelAWSBatch";

// 既存のVPCを使う場合は指定しておく
const VPC_ID = "vpc-12345678";

// `ecsTaskExecutionRole` がない場合は、以下の手順で作成しておくと良い。
// https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_execution_IAM_role.html
const DEFAULT_EXEC_ROLE_ARN =
  "arn:aws:iam::<<AWS_ACCOUNT_ID>>:role/ecsTaskExecutionRole";

export class SampleAWSBatchStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // https://docs.aws.amazon.com/cdk/api/v1/docs/aws-batch-readme.html

    ///////////////////////////////////////////////////////////////////
    // Prepare VPC
    ///////////////////////////////////////////////////////////////////
    let vpc: IVpc;

    if (!VPC_ID) {
      // VPCを新規作成する場合
      vpc = new Vpc(this, `${STACK_BASE_NAME}VPC`, {
        cidr: "10.9.0.0/16", // 172.16.0.0/16 など、なんでも良い。
        subnetConfiguration: [
          {
            name: `${STACK_BASE_NAME}Subnet`,
            subnetType: SubnetType.PUBLIC,
            cidrMask: 18,
          },
        ],
      });
    } else {
      // 既存のVPCを使う場合。
      // `Vpc.fromLookup() ` を使うには
      // `bin/awsbatch.ts` の `env` か cdk実行時の環境変数で region と accountId を明示しておく必要があるようだ。
      vpc = Vpc.fromLookup(this, "VPC", {
        vpcId: VPC_ID,
      });
    }

    ///////////////////////////////////////////////////////////////////
    // Security Group in the VPC
    ///////////////////////////////////////////////////////////////////
    const securityGroup = new SecurityGroup(this, `${STACK_BASE_NAME}SG`, {
      vpc: vpc,
    });

    ///////////////////////////////////////////////////////////////////
    // ComputeEnvironment type=FARGATE_SPOT
    ///////////////////////////////////////////////////////////////////
    const fargateSpotEnvironment = new aws_batch.CfnComputeEnvironment(
      this,
      `${STACK_BASE_NAME}ComputeEnvironment`,
      {
        type: "MANAGED",
        computeEnvironmentName: STACK_BASE_NAME,
        computeResources: {
          type: "FARGATE_SPOT",
          maxvCpus: 64,
          subnets: vpc.publicSubnets.map((x) => x.subnetId), // List of SubnetId
          securityGroupIds: [securityGroup.securityGroupId],
        },
      }
    );

    ///////////////////////////////////////////////////////////////////
    // Create JobQueue
    ///////////////////////////////////////////////////////////////////
    const jobQueue = new aws_batch.CfnJobQueue(
      this,
      `${STACK_BASE_NAME}JobQueue`,
      {
        jobQueueName: STACK_BASE_NAME,
        computeEnvironmentOrder: [
          {
            computeEnvironment:
              fargateSpotEnvironment.attrComputeEnvironmentArn,
            order: 1,
          },
        ],
        priority: 1,
      }
    );

    ///////////////////////////////////////////////////////////////////
    // Create JobDefinitions
    ///////////////////////////////////////////////////////////////////
    const jobs: { [key: string]: string } = {}; // repoUri -> JobDefArn
    for (const setting of CONTAINER_JOB_SETTINGS) {
      const jobDef = new aws_batch.CfnJobDefinition(
        this,
        `${setting.jobName}JobDef`,
        {
          type: "container",
          jobDefinitionName: setting.jobName,
          platformCapabilities: ["FARGATE"], // 注意: FARGATEを指定しないと FARGATE環境では実行できない
          containerProperties: {
            image: setting.imageUri,
            executionRoleArn: DEFAULT_EXEC_ROLE_ARN,
            jobRoleArn: setting.jobRoleArn,
            resourceRequirements: [
              { type: "MEMORY", value: String(setting.memory) },
              { type: "VCPU", value: String(setting.vcpu) },
            ],
            networkConfiguration: {
              assignPublicIp: "ENABLED", // 注意: これがないとECRなどにアクセスできない
            },
          },
          retryStrategy: {
            attempts: 1,
          },
        }
      );
      jobs[setting.imageUri] = jobDef.ref;
    }
  }
}

export type ContainerJobSetting = {
  imageUri: string;
  jobName: string;
  jobRoleArn?: string;
  memory: number; // in MB
  vcpu: number;
};

/**
 * JobDefinition の情報
 * Memory と CPU の指定可能な組み合わせは以下に限定されているので注意。
 * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-batch-jobdefinition-resourcerequirement.html
 */
const CONTAINER_JOB_SETTINGS: ContainerJobSetting[] = [
  {
    imageUri: "busybox",
    jobName: "HelloWorld",
    memory: 512,
    vcpu: 0.25,
  },
];

さいごに

一度わかると大したことないのですが、そこまでがなかなか難航しますね。