AWS CDK で Public Subnet に Fargate をデプロイする調査

AWS CDK で Public Subnet に Fargate をデプロイするという内容をなるべく楽にやりたいので調査した。

動機

aws-ecs-patterns の ApplicationLoadBalancedFargateService はそのままでは Private Subnet にデプロイしてしまい、高額な NAT Gateway を使わざるを得ないので、Public Subnet にデプロイしたかった。
あと個人開発なので冗長構成もいらない。

そもそも Public Subnet にデプロイしていいのかという点については

という考え。

作りたいもの

現状の結論

下記のコードは実際にデプロイして動作を確認したわけではない概念コードなので参考にして失敗しても責任は負いません。筆者はインフラ知識が非常に乏しいです。

また TaskDefinition に紐づけている内容は適当なので、仮に本当に MySQL を立てるなら必要と思われるボリューム永続化のための設定などもしていません。

import * as cdk from "aws-cdk-lib";
import {
  Peer,
  Port,
  SecurityGroup,
  SubnetType,
  Vpc,
} from "aws-cdk-lib/aws-ec2";
import {
  Cluster,
  ContainerImage,
  FargatePlatformVersion,
  FargateTaskDefinition,
} from "aws-cdk-lib/aws-ecs";
import { ApplicationLoadBalancedFargateService } from "aws-cdk-lib/aws-ecs-patterns";
import {
  ApplicationLoadBalancer,
  ApplicationProtocol,
  ApplicationTargetGroup,
  ListenerAction,
  TargetType,
} from "aws-cdk-lib/aws-elasticloadbalancingv2";
import { Construct } from "constructs";

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

    const vpc = new Vpc(this, "MyVpc", {
      maxAzs: 1,
      subnetConfiguration: [
        {
          name: "MyPublicSubnet",
          subnetType: SubnetType.PUBLIC,
        },
      ],
    });

    const cluster = new Cluster(this, "MyEcsCluster", {
      vpc,
    });

    const taskDefinition = new FargateTaskDefinition(this, "taskDef", {});

    const goContainer = taskDefinition.addContainer("GoContainer", {
      image: ContainerImage.fromRegistry("golang/golang"),
      portMappings: [
        {
          containerPort: 8080,
        },
      ],
    });
    const redisContainer = taskDefinition.addContainer("RedisContainer", {
      image: ContainerImage.fromRegistry("redis/redis"),
      portMappings: [
        {
          containerPort: 6379,
        },
      ],
    });
    const dbContainer = taskDefinition.addContainer("DBContainer", {
      image: ContainerImage.fromRegistry("mysql/mysql:8.0"),
      portMappings: [
        {
          containerPort: 3306,
        },
      ],
    });
    // defaultContainer を指定しておかないと一番上に書いたものが defaultContainer になる
    taskDefinition.defaultContainer = goContainer;

    const albSg = new SecurityGroup(this, "SecurityGroup", {
      vpc,
      allowAllOutbound: true,
    });
    // 特定IPからのアクセスを許可(記載のIPは例示用IP)
    albSg.addIngressRule(Peer.ipv4("192.0.2.0/24"), Port.tcp(80));
    const alb = new ApplicationLoadBalancer(this, "ApplicationLoadBalancer", {
      vpc: vpc,
      internetFacing: true,
      securityGroup: albSg,
    });
    const targetGroup = new ApplicationTargetGroup(this, "ListenerTarget", {
      protocol: ApplicationProtocol.HTTP,
      port: 8080,
      vpc,
      targetType: TargetType.IP,
    });
    const listener = alb.addListener("Listener", {
      protocol: ApplicationProtocol.HTTP,
      port: 80,
      // open: false を指定しないとポート80に対してフルオープンなIngressRuleが付与される
      open: false,
      defaultAction: ListenerAction.forward([targetGroup]),
    });

    const service = new ApplicationLoadBalancedFargateService(
      this,
      "MyService",
      {
        cluster,
        taskDefinition: taskDefinition,
        taskSubnets: vpc.selectSubnets({
          subnetType: SubnetType.PUBLIC,
        }),
        loadBalancer: alb,
        // openListener: false を指定しないとポート80に対してフルオープンなIngressRuleが付与される
        openListener: false,
        platformVersion: FargatePlatformVersion.VERSION1_4,
      }
    );
  }
}

作ったものの説明

まず冗長構成なし&Public Subnet しか持たない VPC は ApplicationLoadBalancedFargateService のオプションをどう捏ねてもできないと思われるので、自分で作る必要がある。

また ApplicationLoadBalancedFargateService に複数コンテナから成るタスクを渡すため、TaskDefinition も作成する必要がある。

ALB については後学のために独自のルールを適用する方法を知りたかったので作ってみた。
リスナーを設定するときに open: false をつけた上で、更に ApplicationLoadBalancedFargateService でも openListener: false をつけないとフルオープンなルールがくっつく。ここが一番罠だった。

以上