👣

Fargate+Session ManagerポートフォワードでRDSに接続する踏み台サーバをAWS CDKで構築する

2023/12/11に公開

はじめに

アルファドライブにて、エンジニアインターンをしております。toshi-bpと申します。

今回はFargate+Session Manager(ポートフォワード機能を使用)でRDS(Aurora Postgres)に接続する踏み台サーバをAWS CDKで実装しました。

導入背景・メリット

背景

以下の背景からFargate + Session Managerを用いた踏み台サーバの構築を行いました。

  • 従来はClient VPNを用いた接続手法を採用していた(以下のような問題点が…)
    • 初回セットアップ手順が煩雑
    • 証明書を持っている人なら誰でも接続可能→チームを抜けた人も接続できる可能性がある

メリット

  • AWS CLI・Session Managerプラグインをインストールすれば接続可能に→セットアップ手順の単純化
  • IAMを用いた権限の管理→チームを抜けた人がRDSに接続できるリスクの解消
  • Client VPNを廃止したことによるAWS費用の削減(月10万円!!!)

踏み台サーバとは

プライベートサブネット上に存在するサーバにアクセスするための中継地点の役割を担うサーバです。

今回はプライベートサブネット上に存在するRDSに接続するための踏み台サーバを実装します。

前提

  • CDKプロジェクトは既に作成されているものとします
  • 使用言語はTypeScriptです
  • 簡単のため、環境指定の細かい部分についてはこの記事では割愛します
  • VPC、RDS(Aurora Postgres)は既存のものを使用することを想定しております
    • 存在しない場合は新たに作成するコードに置き換えてください
  • RDSへの接続はMakefileにて定義したコマンドを用いて行うものとします
  • Session Managerプラグインのインストール及びAWSプロファイルの設定は完了しているものとします

結論

ディレクトリ構造

ディレクトリ構造は以下となります。

参考:https://qiita.com/matsunao722/items/f18114c37f1667d171c4

cdkProject
├── bin/
│   └── fargate-bastion.ts
├── config/
│   └── index.ts
├── lib/
│   └── fargate-bastion-stack.ts
├── docker/
│   └── Dockerfile
├── Makefile
└── package.jsonなどの依存関係について記されたファイル群

全体的なコード

全体的なコードは以下です。

  • bin/fargate-bastion.ts

    #!/usr/bin/env node
    import * as cdk from 'aws-cdk-lib';
    import 'source-map-support/register';
    import { FargateBastionStack } from '../lib/fargate-bastion-stack';
    import { devConfig } from "../config"
    
    const app = new cdk.App();
    
    const appEnv = app.node.tryGetContext("appEnv") as string; // "dev"
    
    new FargateBastionStack(
    	app,
    	devConfig,
    	`${appEnv}-fargate-bastion-stack`
    );
    
  • config/index.ts

    export type Config = {
      vpc: {
        id: string;
        vpcEndPointSecurityGroup: vpcEndPointSecurityGroup;
        vpcEndPointSubnets: vpcEndPointSubnet[];
      };
      fargateSubnets: subnetConfig[];
      rdsSecurityGroups: rdsSecurityGroup[];
    };
    
    export type subnetConfig = {
      cidrBlock: string;
      availabiltyZone: string;
    };
    
    export type vpcEndPointSubnet = {
      subnetId: string;
      availavilityZone: string;
      routeTableId: string;
    };
    
    export type vpcEndPointSecurityGroup = {
      securityGroupId: string;
    };
    
    export type rdsSecurityGroup = {
      instanceName: string;
      securityGroupId: string;
    };
    
    export const devConfig: Config = {
      vpc: {
        id: "AWS上で確認してください",
        vpcEndPointSecurityGroup: {
          securityGroupId: "AWS上で確認してください",
        },
        vpcEndPointSubnets: [
          {
            subnetId: "AWS上で確認してください",
            availavilityZone: "ap-northeast-1a",
            routeTableId: "AWS上で確認してください",
          },
          {
            subnetId: "AWS上で確認してください",
            availavilityZone: "ap-northeast-1c",
            routeTableId: "AWS上で確認してください",
          },
        ],
      },
      fargateSubnets: [
        {
          cidrBlock: "任意のIPアドレス群",
          availabiltyZone: "ap-northeast-1a",
        },
        {
          cidrBlock: "任意のIPアドレス群",
          availabiltyZone: "ap-northeast-1c",
        },
      ],
      rdsSecurityGroups: [
        {
          instanceName: "dev-db-cluster",
          securityGroupId: "AWS上で確認してください",
        },
      ],
    };
    
  • lib/fargate-bastion-stack.ts

    import * as cdk from "aws-cdk-lib";
    import {
      aws_ec2 as ec2,
      aws_ecr as ecr,
      aws_ecs as ecs,
      aws_iam as iam,
      aws_logs as cwl,
    } from "aws-cdk-lib";
    import {
      Destination,
      DockerImageDeployment,
      Source,
    } from "cdk-docker-image-deployment";
    import { Construct } from "constructs";
    import { join } from "path";
    import { Config } from "../config";
    
    export type FargateBastionStackProps = cdk.StackProps &
      Config & { appEnv: "dev" };
    
    export class FargateBastionStack extends cdk.Stack {
      constructor(
        scope: Construct,
        id: string,
        props: FargateBastionStackProps
      ) {
        super(scope, id, props);
    
        const { appEnv } = props;
    
        const vpc = ec2.Vpc.fromLookup(this, `Vpc`, {
          vpcId: props.vpc?.id,
        });
    
        const cluster = new ecs.Cluster(this, `FargateBastionCluster`, {
          vpc,
          clusterName: `${appEnv}-fargate-bastion-cluster`,
        });
    
        const taskRole = new iam.Role(this, `FargateBastionTaskRole`, {
          assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
          roleName: `${appEnv}-fargate-bastion-task-role`,
        });
        taskRole.addToPolicy(
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: [
              "ssmmessages:CreateControlChannel",
              "ssmmessages:CreateDataChannel",
              "ssmmessages:OpenControlChannel",
              "ssmmessages:OpenDataChannel",
              "logs:DescribeLogGroups",
              "logs:CreateLogStream",
              "logs:DescribeLogStreams",
              "logs:PutLogEvents",
            ],
            resources: ["*"],
          })
        );
    
        const taskExecutionRole = new iam.Role(
          this,
          `FargateBastionTaskExecutionRole`,
          {
            assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
            roleName: `${appEnv}-fargate-bastion-task-execution-role`,
            managedPolicies: [
              iam.ManagedPolicy.fromAwsManagedPolicyName(
                "service-role/AmazonECSTaskExecutionRolePolicy"
              ),
            ],
          }
        );
        taskExecutionRole.addToPolicy(
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: ["ecs:ExecutionCommand"],
            resources: [cluster.clusterArn],
          })
        );
    
        // create Subnets for proxy-to-rds
        const fargateSubnets: ec2.Subnet[] | undefined = props.fargateSubnets?.map(
          (subnet) => {
            return new ec2.Subnet(
              this,
              `FargateBastionSubnet${subnet.availabiltyZone}`,
              {
                availabilityZone: subnet.availabiltyZone,
                vpcId: props.vpc?.id!,
                cidrBlock: subnet.cidrBlock,
                mapPublicIpOnLaunch: false,
              }
            );
          }
        );
    
        const fargateTaskDef = new ecs.FargateTaskDefinition(
          this,
          `FargateBastionTaskDefinition`,
          {
            taskRole: taskRole,
            executionRole: taskExecutionRole,
            family: `${appEnv}-fargate-bastion-task-definition`,
            cpu: 256,
            memoryLimitMiB: 512,
            runtimePlatform: {
              operatingSystemFamily: ecs.OperatingSystemFamily.LINUX,
              cpuArchitecture: ecs.CpuArchitecture.ARM64,
            },
          }
        );
    
        const fargateTaskLogGroup = new cwl.LogGroup(
          this,
          `FargateBastionLogGroup`,
          {
            logGroupName: `${appEnv}-fargate-bastion-log-group`,
            retention: cwl.RetentionDays.ONE_DAY,
            removalPolicy: cdk.RemovalPolicy.DESTROY,
          }
        );
    
        const repository = new ecr.Repository(this, `FargateBastionRepository`, {
          repositoryName: `${appEnv}/fargate-bastion`,
          removalPolicy: cdk.RemovalPolicy.DESTROY,
          autoDeleteImages: true,
        });
    
        const imageDeploy = new DockerImageDeployment(
          this,
          `FargateBastionDockerImageDeployment`,
          {
            source: Source.directory(join(__dirname, "../docker")),
            destination: Destination.ecr(repository, {
              tag: "latest",
            }),
          }
        );
    
        fargateTaskDef.node.addDependency(imageDeploy);
    
        // Can't set readonlyRootFilesystem to true because using ECS Exec
        fargateTaskDef.addContainer(`FargateBastionContainer`, {
          containerName: `${appEnv}-fargate-bastion-container`,
          image: ecs.ContainerImage.fromEcrRepository(repository),
          logging: ecs.LogDrivers.awsLogs({
            streamPrefix: `${appEnv}-fargate-bastion-container`,
            logGroup: fargateTaskLogGroup,
          }),
        });
    
        const fargateSecurityGroup = new ec2.SecurityGroup(
          this,
          `FargateBastionSecurityGroup`,
          {
            vpc,
            allowAllOutbound: true,
            securityGroupName: `${appEnv}-fargate-bastion-security-group`,
            description: "Security group of fargate bastion server",
          }
        );
    
        const vpcEndPointSecurityGroup = ec2.SecurityGroup.fromSecurityGroupId(
          this,
          `VpcEndPointSecurityGroup`,
          props.vpc?.vpcEndPointSecurityGroup.securityGroupId!
        );
        vpcEndPointSecurityGroup.addIngressRule(
          fargateSecurityGroup,
          ec2.Port.tcp(443)
        );
    
        const vpcEndpointSubnets = vpc.selectSubnets({
          subnets: props.vpc?.vpcEndPointSubnets.map((vpcEndPointSubnet) => {
            return ec2.Subnet.fromSubnetAttributes(
              this,
              `VpcEndPointSubnets${vpcEndPointSubnet.availavilityZone}`,
              {
                subnetId: vpcEndPointSubnet.subnetId,
                availabilityZone: vpcEndPointSubnet.availavilityZone,
                routeTableId: vpcEndPointSubnet.routeTableId,
              }
            );
          }),
        });
    
        // ECRからコンテナを取得するために必要なVPCエンドポイント
        vpc.addInterfaceEndpoint(`ECREndpoint`, {
          service: ec2.InterfaceVpcEndpointAwsService.ECR,
          subnets: vpcEndpointSubnets,
          securityGroups: [vpcEndPointSecurityGroup],
          privateDnsEnabled: true,
        });
        vpc.addInterfaceEndpoint(`ECRDockerEndpoint`, {
          service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER,
          subnets: vpcEndpointSubnets,
          securityGroups: [vpcEndPointSecurityGroup],
          privateDnsEnabled: true,
        });
        vpc.addInterfaceEndpoint(`SSMEndpoint`, {
          service: ec2.InterfaceVpcEndpointAwsService.SSM,
          subnets: vpcEndpointSubnets,
          securityGroups: [vpcEndPointSecurityGroup],
          privateDnsEnabled: true,
        });
        vpc.addInterfaceEndpoint(`SSMMessagesEndpoint`, {
          service: ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES,
          subnets: vpcEndpointSubnets,
          securityGroups: [vpcEndPointSecurityGroup],
          privateDnsEnabled: true,
        });
        vpc.addGatewayEndpoint(`S3Endpoint`, {
          service: ec2.GatewayVpcEndpointAwsService.S3,
          subnets: [
            {
              subnets: fargateSubnets,
            },
          ],
        });
    
        const fargateService = new ecs.FargateService(
          this,
          `FargateBastionService`,
          {
            cluster,
            serviceName: `${appEnv}-fargate-bastion-service`,
            vpcSubnets: {
              subnets: fargateSubnets,
            },
            taskDefinition: fargateTaskDef,
            securityGroups: [fargateSecurityGroup],
            enableExecuteCommand: true,
            desiredCount: 1,
          }
        );
    
        props.rdsSecurityGroups?.map((rdsSecurityGroup) => {
          const securityGroup = ec2.SecurityGroup.fromSecurityGroupId(
            this,
            `${rdsSecurityGroup.instanceName}SecurityGroup`,
            rdsSecurityGroup.securityGroupId
          );
          securityGroup.addIngressRule(
            fargateSecurityGroup,
            ec2.Port.tcp(5432),
            `inbound from ${fargateService.serviceName}`
          );
        });
      }
    }
    
  • docker/Dockerfile

    # 任意のコンテナを指定(この例ではubuntu 22.04)
    FROM --platform=arm64 public.ecr.aws/ubuntu/ubuntu:22.04
    
    WORKDIR /
    
    ENTRYPOINT ["tail", "-F", "/dev/null"]
    
  • Makefile

    .DEFAULT_GOAL	:= help
    
    APP_ENV:=dev
    AWS_REGION:=ap-northeast-1
    MODE:=RO
    
    ifeq ($(APP_ENV), dev)
    	AWS_PROFILE:=hoge_profile
    	CLUSTER_NAME:=dev-fargate-bastion-cluster
    	CONTAINER_NAME:=dev-fargate-bastion-container
    	DB_NAME:=db_name
    	DB_URL:=db_url.region.rds.amazonaws.com
    	ifeq ($(MODE), RW)
    		DB_URL:=rw_mode_db_url.region.rds.amazonaws.com
    	endif
    
    DB_PORT:=5432
    DB_USER:=db_user
    DB_PW=$(shell aws rds generate-db-auth-token --hostname "$(DB_URL)" --username $(DB_USER) --port $(DB_PORT) --profile $(AWS_PROFILE))
    LOCAL_PORT:=55431
    
    # get fargate task id
    .PHONY: get-task-id
    get-task-id:
    	$(eval TASK_ID := $(shell aws ecs list-tasks \
    		--region ${AWS_REGION} \
    		--profile ${AWS_PROFILE} \
    		--cluster ${CLUSTER_NAME} |\
    		jq -r '.taskArns[]' | awk -F'/' '{print $$3}'\
    	))
    	@echo TASK_ID: ${TASK_ID}
    
    # get fargate runtime id
    .PHONY: get-runtime-id
    get-runtime-id: get-task-id
    	$(eval RUNTIME_ID := $(shell aws ecs describe-tasks \
    		--region ${AWS_REGION} \
    		--profile ${AWS_PROFILE} \
    		--cluster ${CLUSTER_NAME} \
    		--task ${TASK_ID} |\
    		jq -r '.tasks[].containers[].runtimeId' \
    	))
    	@echo RUNTIME_ID: ${RUNTIME_ID}
    
    # connect to proxy server
    .PHONY: connect-proxy-server
    connect-proxy-server: get-runtime-id
    	@aws ssm start-session --target ecs:${CLUSTER_NAME}_${TASK_ID}_${RUNTIME_ID} \
    		--document-name AWS-StartPortForwardingSessionToRemoteHost \
    		--parameters "{\"host\": [\"${DB_URL}\"], \"portNumber\":[\"${DB_PORT}\"], \"localPortNumber\":[\"${LOCAL_PORT}\"]}" \
    		--region ${AWS_REGION} \
    		--profile ${AWS_PROFILE}
    
    # connect to rds via proxy server
    .PHONY: connect-to-rds
    connect-to-rds:
    	@PGPASSWORD="${DB_PW}" psql -h localhost -p ${LOCAL_PORT} -U ${DB_USER} -d ${DB_NAME}
    

実行コマンド

Session Managerのポートフォワード機能を用いてローカルからRDSに接続します。
https://zenn.dev/quiver/articles/1458e453118254

(ターミナルのタブを2つ使用します)

  1. 踏み台サーバへの接続

    make connect-proxy-server
    
  2. RDSへの接続

    make connect-to-rds
    

手順及びそこに該当するコード

lib/fargate-bastion-stack.tsについて、手順毎に該当するコードを示します。

全体的な手順

今回定義したStackでは、以下の処理が動くように実装を進めました。

(一部まとめたり、手順を入れ替えたりすることでもっと簡潔になるかもしれません。。)

  1. 既存VPCの参照
  2. 新たなECSクラスターの作成
  3. タスクロールの設定
  4. タスク実行ロールの設定
  5. 踏み台サーバ用のVPC サブネットの作成
  6. Fargateタスクの定義
  7. Fargateタスクのロググループ設定
  8. ECRリポジトリの作成、コンテナデプロイの設定
  9. Fargateタスクにコンテナを追加
  10. Fargate用のSecurityGroupを追加
  11. 既存エンドポイントのセキュリティグループ参照し・入力ルール追加
  12. 既存VPCサブネットを参照し、VPCエンドポイントを追加
  13. 新たなFargateサービスの作成
  14. 既存のRDSセキュリティグループとFargateサービスのSecurityGroupの関連付け

1. 既存VPCの参照

const vpc = ec2.Vpc.fromLookup(this, `Vpc`, {
  vpcId: props.vpc?.id,
});

2. 新たなECSクラスターの作成

const cluster = new ecs.Cluster(this, `FargateBastionCluster`, {
  vpc,
  clusterName: `${appEnv}-fargate-bastion-cluster`,
});

3. タスクロールの設定

以下ドキュメントを参考にタスクロールの設定をしました。
https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/getting-started-create-iam-instance-profile.html

ECS Execのログはタスクロールで行われるため、こちらにログの権限を付与しておきます。
https://dev.classmethod.jp/articles/ecs-exec-use-task-role-for-logging/

const taskRole = new iam.Role(this, `FargateBastionTaskRole`, {
  assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
  roleName: `${appEnv}-fargate-bastion-task-role`,
});
taskRole.addToPolicy(
  new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    actions: [
      "ssmmessages:CreateControlChannel",
      "ssmmessages:CreateDataChannel",
      "ssmmessages:OpenControlChannel",
      "ssmmessages:OpenDataChannel",
      "logs:DescribeLogGroups",
      "logs:CreateLogStream",
      "logs:DescribeLogStreams",
      "logs:PutLogEvents",
    ],
    resources: ["*"],
  })
);

4. タスク実行ロールの設定

コマンド上でFargateタスクが実行できるように権限を付与します。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_execution_IAM_role.html

const taskExecutionRole = new iam.Role(
  this,
  `FargateBastionTaskExecutionRole`,
  {
    assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
    roleName: `${appEnv}-fargate-bastion-task-execution-role`,
    managedPolicies: [
      iam.ManagedPolicy.fromAwsManagedPolicyName(
        "service-role/AmazonECSTaskExecutionRolePolicy"
      ),
    ],
  }
);
taskExecutionRole.addToPolicy(
  new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    actions: ["ecs:ExecutionCommand"],
    resources: [cluster.clusterArn],
  })
);

ちなみに、タスクロールとタスク実行ロールの違いは以下のイメージで考えれば良いみたいです。

以下記事より引用
https://zenn.dev/sugay0519/articles/88f13ca589fcba

タスク実行ロール
タスク実行時にアクセスしたいAWSリソースの権限を管理
タスクロール
タスク実行して起動したコンテナがアクセスしたいAWSリソースの権限を管理

5. 踏み台サーバ用のVPCサブネットの作成

// create Subnets for proxy-to-rds
const fargateSubnets: ec2.Subnet[] | undefined = props.fargateSubnets?.map(
  (subnet) => {
    return new ec2.Subnet(
      this,
      `FargateBastionSubnet${subnet.availabiltyZone}`,
      {
        availabilityZone: subnet.availabiltyZone,
        vpcId: props.vpc?.id!,
        cidrBlock: subnet.cidrBlock,
        mapPublicIpOnLaunch: false,
      }
    );
  }
);

6. Fargateタスクの定義・7. Fargateタスクのロググループ設定

const fargateTaskDef = new ecs.FargateTaskDefinition(
  this,
  `FargateBastionTaskDefinition`,
  {
    taskRole: taskRole,
    executionRole: taskExecutionRole,
    family: `${appEnv}-fargate-bastion-task-definition`,
    cpu: 256,
    memoryLimitMiB: 512,
    runtimePlatform: {
      operatingSystemFamily: ecs.OperatingSystemFamily.LINUX,
      cpuArchitecture: ecs.CpuArchitecture.ARM64,
    },
  }
);

const fargateTaskLogGroup = new cwl.LogGroup(
  this,
  `FargateBastionLogGroup`,
  {
    logGroupName: `${appEnv}-fargate-bastion-log-group`,
    retention: cwl.RetentionDays.ONE_DAY,
    removalPolicy: cdk.RemovalPolicy.DESTROY,
  }
);

8. ECRリポジトリの作成、コンテナデプロイの設定

const repository = new ecr.Repository(this, `FargateBastionRepository`, {
  repositoryName: `${appEnv}/fargate-bastion`,
  removalPolicy: cdk.RemovalPolicy.DESTROY,
  autoDeleteImages: true,
});

const imageDeploy = new DockerImageDeployment(
  this,
  `FargateBastionDockerImageDeployment`,
  {
    source: Source.directory(join(__dirname, "../docker")),
    destination: Destination.ecr(repository, {
      tag: "latest",
    }),
  }
);

fargateTaskDef.node.addDependency(imageDeploy);

9. Fargateタスクにコンテナを追加

コメント文にも記述していますが、ECS Execをするため、読み取り専用の設定はFalseとしています。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/ecs-exec.html

// Can't set readonlyRootFilesystem to true because using ECS Exec
fargateTaskDef.addContainer(`FargateBastionContainer`, {
  containerName: `${appEnv}-fargate-bastion-container`,
  image: ecs.ContainerImage.fromEcrRepository(repository),
  logging: ecs.LogDrivers.awsLogs({
    streamPrefix: `${appEnv}-fargate-bastion-container`,
    logGroup: fargateTaskLogGroup,
  }),
});

10. Fargate用のSecurityGroupを追加

const fargateSecurityGroup = new ec2.SecurityGroup(
  this,
  `FargateBastionSecurityGroup`,
  {
    vpc,
    allowAllOutbound: true,
    securityGroupName: `${appEnv}-fargate-bastion-security-group`,
    description: "Security group of fargate bastion server",
  }
);

11. VPCエンドポイントに関連づけるセキュリティグループの参照・入力ルール追加

VPCエンドポイントと関連付けを行うセキュリティグループを参照し、そのセキュリティグループにaddIngressRuleで踏み台サーバからの接続を許可

const vpcEndPointSecurityGroup = ec2.SecurityGroup.fromSecurityGroupId(
  this,
  `VpcEndPointSecurityGroup`,
  props.vpc?.vpcEndPointSecurityGroup.securityGroupId!
);
vpcEndPointSecurityGroup.addIngressRule(
  fargateSecurityGroup,
  ec2.Port.tcp(443)
);

12. 既存VPCサブネットを参照し、VPCエンドポイントを追加

以下2つの目的を達成するためにVPCエンドポイントを追加

  • プライベートネットワークにてECR上のコンテナの取得

    https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/vpc-endpoints.html

    • com.amazonaws.*region*.ecr.dkr
    • com.amazonaws.*region*.ecr.api
    • S3のゲートウェイエンドポイント
      • ルートテーブルの指定が困難であること
      • ゲートウェイエンドポイントを増やしても追加料金がないこと

プライベートネットワーク上でSession Managerを利用可能にする

https://dev.classmethod.jp/articles/privatesubnet_ecs/

  • プライベートネットワーク上でSession Managerを利用可能にする(必要なVPCエンドポイントは以下)
    • ssm.*region*.amazonaws.com
    • ssmmessages.*region*.amazonaws.com
const vpcEndpointSubnets = vpc.selectSubnets({
  subnets: props.vpc?.vpcEndPointSubnets.map((vpcEndPointSubnet) => {
    return ec2.Subnet.fromSubnetAttributes(
      this,
      `VpcEndPointSubnets${vpcEndPointSubnet.availavilityZone}`,
      {
        subnetId: vpcEndPointSubnet.subnetId,
        availabilityZone: vpcEndPointSubnet.availavilityZone,
        routeTableId: vpcEndPointSubnet.routeTableId,
      }
    );
  }),
});

// ECRからコンテナを取得するために必要なVPCエンドポイント
vpc.addInterfaceEndpoint(`ECREndpoint`, {
	service: ec2.InterfaceVpcEndpointAwsService.ECR,
  subnets: vpcEndpointSubnets,
  securityGroups: [vpcEndPointSecurityGroup],
  privateDnsEnabled: true,
});
vpc.addInterfaceEndpoint(`ECRDockerEndpoint`, {
	service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER,
  subnets: vpcEndpointSubnets,
  securityGroups: [vpcEndPointSecurityGroup],
  privateDnsEnabled: true,
});
vpc.addInterfaceEndpoint(`SSMEndpoint`, {
  service: ec2.InterfaceVpcEndpointAwsService.SSM,
  subnets: vpcEndpointSubnets,
  securityGroups: [vpcEndPointSecurityGroup],
  privateDnsEnabled: true,
});
vpc.addInterfaceEndpoint(`SSMMessagesEndpoint`, {
  service: ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES,
  subnets: vpcEndpointSubnets,
  securityGroups: [vpcEndPointSecurityGroup],
  privateDnsEnabled: true,
});
vpc.addGatewayEndpoint(`S3Endpoint`, {
  service: ec2.GatewayVpcEndpointAwsService.S3,
  subnets: [
    {
      subnets: fargateSubnets,
    },
  ],
});

13. 新たなFargateサービスの作成

Fargate上で動作するコンテナにSession Managerでアクセスしたいので、enableExecuteCommandをtrueにする。

const fargateService = new ecs.FargateService(
  this,
  `FargateBastionService`,
  {
    cluster,
    serviceName: `${appEnv}-fargate-bastion-service`,
    vpcSubnets: {
      subnets: fargateSubnets,
    },
    taskDefinition: fargateTaskDef,
    securityGroups: [fargateSecurityGroup],
    enableExecuteCommand: true,
    desiredCount: 1,
  }
);

14. 既存のRDSセキュリティグループとFargateサービスのSecurityGroupの関連付け

踏み台サーバからRDSに接続する許可を追加

props.rdsSecurityGroups?.map((rdsSecurityGroup) => {
  const securityGroup = ec2.SecurityGroup.fromSecurityGroupId(
    this,
    `${rdsSecurityGroup.instanceName}SecurityGroup`,
    rdsSecurityGroup.securityGroupId
  );
  securityGroup.addIngressRule(
    fargateSecurityGroup,
    ec2.Port.tcp(5432),
    `inbound from ${fargateService.serviceName}`
  );
});

最後に

Session Managerのポートフォワード機能がFargateでも使用可能になったことで、比較的容易に踏み台サーバを構築できるようになりました。

踏み台サーバの構築により、セキュリティ的なリスクを低減しつつ、データベースに接続するためのセットアップ手順も簡単にでき、良いことづくめでした。

踏み台サーバ構築の際にこの記事が参考になれば幸いです。

参考文献

https://zenn.dev/quiver/articles/1458e453118254
https://go-to-k.hatenablog.com/entry/ecs-fargate-ssm-remote-port-forward
https://zenn.dev/pirosikick/articles/70732e8c751354
https://blog.dcs.co.jp/aws/20221124-serverless-bastion.html
https://amzn.asia/d/glyMUos
https://zenn.dev/5t111111/articles/use-cdk-docker-image-deployment

Discussion