[AWS CDK] Private Subnetで運用するBatchコンピューティング環境(EC2タイプ)を構築した

# 概要

CDKを用いて，AWS Batchのコンピューティング環境(EC2起動タイプ)を，インターネットとの通信経路を持たないVPCで起動する方法をまとめました．

Batchのコンピューティング環境は，ECSクラスタとして作成されます．なので，ECSクラスタが展開できるVPC環境を準備してあげればOKです．

# なぜそんなことをしたのか？

Batchで高負荷な計算処理（シミュレーション）を行う環境[^1]を構築していたのですが，セキュリティ上の理由から，「インターネット経由の通信が存在しない環境でで」との要件があったためです．

[^1]: 合計で数千vCPUのクラスタを初めて動かしたときは，「今まで数日かかっていた計算が，こんなに早く終わるのか！！」という興奮と，「終わったらちゃんとスケールインされるよね...？？」という心配とで，ドキドキでした．

# 検証環境
- CDK 1.59.0, Typesccript
- 東京リージョン (ap-northeast-1) [^2]

[^2]: 他のリージョンで展開したい場合は，適宜リージョン名を読み替えていただければと思います．


# VPC Endpointを設置する

NAT GatewayやInternet Gateway等との通信経路が一切ないSubnetを選択しても，コンピューティング環境の作成自体は正常に終了します（してしまいます）．

しかし，この状態では，ECSのコントロールプレーンやECRリポジトリとの通信もできず，コンピューティング環境を利用することができません．

そこで，必要なプライベート通信経路を確保するため，VPC Endpointを設置します．

必要なVPC Endpointは，下記のとおりです．

- ECS コントロールプレーンとの通信経路

    3つのInterface endpointが必要です([公式ドキュメントはこちら](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/vpc-endpoints.html))．
    
    - com.amazonaws.ap-northeast-1.ecs-agent
    - com.amazonaws.ap-northeast-1.ecs-telemetry
    - com.amazonaws.ap-northeast-1.ecs

- ECR リポジトリとの通信経路

    2つのInterface endpointとS3 gateway endpointが必要です([公式ドキュメントはこちら](https://docs.aws.amazon.com/AmazonECR/latest/userguide/vpc-endpoints.html))．
    
    - com.amazonaws.ap-northeast-1.ecr.dkr
    - com.amazonaws.ap-northeast-1.ecr.api
    - com.amazonaws.ap-northeast-1.s3

- (用途によって) CloudWatch Logsとの通信経路

    CloudWatch Logsへログを流したい場合には，追加で下記のInterface endpointも設置してください([公式ドキュメントはこちら](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/cloudwatch-logs-and-interface-VPC.html#create-VPC-endpoint-for-CloudWatchLogs))．

    - com.amazonaws.ap-northeast-1.logs

# CDKで実装する

それでは，上述の環境をCDKで構築していきたいと思います．

### Importするモジュール

```ts
import ec2 = require('@aws-cdk/aws-ec2');
import batch = require('@aws-cdk/aws-batch');
```

### VPC

まずは，VPCを作成します．今回は，インターネットとの通信経路を持たないVPCを作成するので，
VPCコンストラクタに，下記のように引数を与えます．

```ts
const vpc = new ec2.Vpc(this, 'vpc', {
        cidr: "10.0.0.0/16", // ここは環境に応じて変更してください
        maxAzs: 3, // EC2を分散配置する際の，最大AZ数を指定する
        subnetConfiguration: [{
            cidrMask: 18, // ここは環境に応じて変更してください
            name: 'for-batch-compute-env',
            subnetType: ec2.SubnetType.ISOLATED, // ISOLATEDを指定する
        }]
    });
```

subnetTypeに`ISOLATED`を指定することがポイントです．こうすると，NAT Gatewayを設置しないVPCを構築できます．

(他に`PRIVATE`や`PUBLIC`を指定できますが，これらはNAT Gatewayを設置する構成となります．）

### Security Group, Subnet Selection

次にEC2用のセキュリティグループを設定します．今回は，同一VPC内のみの通信を許可しました．

```ts
const securityGroup = new ec2.SecurityGroup(
        this, 'batch-compute-env-sg',
        {
            vpc: vpc,
            allowAllOutbound: true,
            description: `For batch compute environment.`,
            securityGroupName: `for-batch-compute-environment`
        });
securityGroup.addIngressRule(
    ec2.Peer.ipv4(vpc.vpcCidrBlock),
    ec2.Port.allTraffic(),
    `Allow inside security group`
);
```

後ほど用いる，サブネット選択の変数も定義しておきます．

```ts
const subnetSelection: ec2.SubnetSelection = {
    subnetType: ec2.SubnetType.ISOLATED, onePerAz: true
};
```

### VPC Endpoint

VPCを作成したら，必要なVPC endpointを設置していきます．CDKでは，専用のコンストラクタがあるので，VPC endpoint設置も簡単です．

S3向けのendpointのみgateway endpointでコンストラクタが違うので注意してください．

```ts
// Create VPC Endpoints for ECS
new ec2.InterfaceVpcEndpoint(
    this, `vpce-ecs-agent`, {
    service: ec2.InterfaceVpcEndpointAwsService.ECS_AGENT,
    open: true,
    privateDnsEnabled: true,
    vpc: vpc,
    subnets: { subnetType: ec2.SubnetType.ISOLATED },
    securityGroups: [securityGroup]
});
new ec2.InterfaceVpcEndpoint(
    this, `vpce-ecs-telemetry`, {
    service: ec2.InterfaceVpcEndpointAwsService.ECS_TELEMETRY,
    open: true,
    privateDnsEnabled: true,
    vpc: vpc,
    subnets: { subnetType: ec2.SubnetType.ISOLATED },
    securityGroups: [securityGroup]
});
new ec2.InterfaceVpcEndpoint(
    this, `vpce-ecs`, {
    service: ec2.InterfaceVpcEndpointAwsService.ECS,
    open: true,
    privateDnsEnabled: true,
    vpc: vpc,
    subnets: { subnetType: ec2.SubnetType.ISOLATED },
    securityGroups: [securityGroup]
});

// Create VPC Endpoints for ECR
new ec2.InterfaceVpcEndpoint(
    this, `vpce-ecr-docker`, {
    service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER,
    open: true,
    privateDnsEnabled: true,
    vpc: vpc,
    subnets: { subnetType: ec2.SubnetType.ISOLATED },
    securityGroups: [securityGroup]
});
new ec2.InterfaceVpcEndpoint(
    this, `vpce-ecr`, {
    service: ec2.InterfaceVpcEndpointAwsService.ECR,
    open: true,
    privateDnsEnabled: true,
    vpc: vpc,
    subnets: { subnetType: ec2.SubnetType.ISOLATED },
    securityGroups: [securityGroup]
});

new ec2.GatewayVpcEndpoint(
    this, `vpce-s3`, {
    service: ec2.GatewayVpcEndpointAwsService.S3,
    vpc: vpc,
    subnets: [subnetSelection],
});
```

### Batch Computing環境

VPCの準備ができたらBatchコンピューティング環境を作成していきます．

```ts

const computeEnv = new batch.ComputeEnvironment(this, 'cluster', {
    computeEnvironmentName: `my-batch-cluster`,
    enabled: true,
    managed: true,
    computeResources: {
        type: batch.ComputeResourceType.ON_DEMAND,
        // spotインスタンスを使いたいときはSPOTを指定
        // type: batch.ComputeResourceType.SPOT,
        instanceTypes: [new ec2.InstanceType("t3small")], // インスタンスタイプ
        vpc: vpc, // 先ほど作成したVPC
        vpcSubnets: subnetSelection, // Isolated subnetを指定
        securityGroups: [securityGroup], // インスタンスのセキュリティグループを指定
        minvCpus: 0, // min VCPU数を指定
        desiredvCpus: 0, // desired VCPU数を指定 (minとmaxの間の値)
        maxvCpus: 4, // max VCPU数を指定
        // 特定のAMIを指定したい場合はAMI IDの変数も与えます．
        // image: new ec2.GenericLinuxImage({ 'ap-northeast-1': "AMI-ID" }),
    }
});
```

# 注意点

- VPCエンドポイントは，利用状況に関わらず立てておくだけで料金が発生します．ECSクラスタ(EC2タイプ)では，最低６つのエンドポイントが必要で，そこそこの金額になります．お試しする際は，終わったら全てのリソースを削除することをお忘れなく！
- 本番で運用する際には，コストに見合うかの判断が大切ですね．状況によっては，セキュリティを担保する他の方法を検討したほうが良さそうです．
- EFSなど他のサービスも使用したい場合は，それぞれに対応したエンドポイント設置などの対応が必要です．

# 参考

- [エンドポイントを使用してプライベートサブネットでECSを使用する | Developers.IO](https://dev.classmethod.jp/articles/privatesubnet_ecs/)

Fargate起動タイプでは，必要なエンドポイントを減らせるようです．詳しくは，こちらのウェブサイトをご参照ください（分かりやすくまとまっており，私も助かりました）．

# ソースコード

[GitHub（準備中）](https://github.com/nasubeee)