Amazon ECS AWS FargateでポートフォワードしてRDSへ接続する
はじめに
個人開発にてRails(ECS上へデプロイ) x RDS(MySQL)を実装した際にメンテなどでRDSを直接触りたいと思い、せっかくであればモダンな方法で実現したいと思い、調べたところECSで踏み台サーバーのようなことができることを知り、実装してみました。
少し詰まった箇所もあるのでどなたかの助けになれば幸いです。
参考にした記事
IAMロール周りを参考にさせていただきました。
利用するコンテナイメージやssm
コマンドの使い方などを参考にさせていただきました。
そもそもポートフォワードとは
ChatGPTに聞いてみました
ポートフォワード(Port Forwarding)は、特定のネットワークポートへの通信を別のポートやホストに転送する技術です。通常、ルーターやファイアウォールで設定され、内部ネットワーク内のデバイスやサービスに外部からアクセスできるようにするために使われます。
特定のポートとポートを繋ぎ合わせ通信を確立するためのものということでしょうか。
構成に関して
前提として一通りのリソース(ネットワーク周りやECSやRDS)は構築してある状態になります。
大まかな構成図を以下に示します。
今回実装した箇所としてPrivateSubnet内にあるECSになります。
またPubliSubnet内にNATGatewayを配置しインターネットとの通信を可能にしています。
実装したリソースの詳細(Terraform)
IAMリソース
data "aws_iam_policy_document" "ecs_task_role_policy_data" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = [
"ecs-tasks.amazonaws.com"
]
}
}
}
data "aws_iam_policy_document" "ecs_task_policy_data" {
statement {
effect = "Allow"
actions = [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
]
resources = ["*"]
}
statement {
effect = "Allow"
actions = [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
resources = ["*"]
}
statement {
effect = "Allow"
actions = ["iam:PassRole"]
resources = [aws_iam_role.bastion_task_execution_role.arn]
}
}
resource "aws_iam_role" "bastion_task_execution_role" {
name = "bastion-role"
description = "ECS Task Execution"
assume_role_policy = data.aws_iam_policy_document.ecs_task_role_policy_data.json
}
resource "aws_iam_policy" "bastion_task_execution_policy" {
name = "bastion-policy"
policy = data.aws_iam_policy_document.ecs_task_policy_data.json
}
resource "aws_iam_role_policy_attachment" "attach_ecs_task_execution_role" {
role = aws_iam_role.bastion_task_execution_role.name
policy_arn = aws_iam_policy.bastion_task_execution_policy.arn
}
上記今回SSMを用いてECS経由でポートフォワードするためECSの実行ロールとしてssmmessages:
系のポリシーを渡しています。
ECSリソース
Terraform
# SecurityGroup
resource "aws_security_group" "bastion_security_group" {
name = "${var.common_name}-bastion-sg"
description = "Bastion Security Group"
vpc_id = module.ass_vpc_stg.id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# ECSリソース
resource "aws_ecs_cluster" "ecs_cluster" {
name = "${var.common_name}-bastion-${var.environment}"
setting {
name = "containerInsights"
value = "enabled"
}
}
resource "aws_ecs_cluster_capacity_providers" "ecs_cluster_capacity_providers" {
cluster_name = aws_ecs_cluster.ecs_cluster.name
capacity_providers = ["FARGATE"]
default_capacity_provider_strategy {
capacity_provider = "FARGATE"
}
}
resource "aws_ecs_task_definition" "ecs_task_definition" {
family = "${var.common_name}-bastion-taskdef-${var.environment}"
cpu = 256
memory = 512
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
container_definitions = file("${path.module}/bastion_definitions.tpl.json")
execution_role_arn = aws_iam_role.bastion_task_execution_role.arn
task_role_arn = aws_iam_role.bastion_task_execution_role.arn
}
resource "aws_ecs_service" "ecs_service" {
name = "${var.common_name}-bastion-service-${var.environment}"
cluster = aws_ecs_cluster.ecs_cluster.arn
enable_execute_command = true
task_definition = aws_ecs_task_definition.ecs_task_definition.arn
desired_count = 1
launch_type = "FARGATE"
network_configuration {
assign_public_ip = true
security_groups = [aws_security_group.bastion_security_group.id]
subnets = {サブネットのIDをlist形式で入力}
}
lifecycle {
ignore_changes = [task_definition]
}
}
resource "aws_cloudwatch_log_group" "bastion" {
name = "/ecs/bastion"
retention_in_days = 180
}
variable
を利用している箇所はよしなに差し替えてください。
ECS系リソースに加え、ECSサービス用のセキュリティグループとログを出力するためのCloudWatchグループを作成しております。
container_definitions = file("${path.module}/bastion_definitions.tpl.json")
上記にて以下タスク定義のファイルを読み込んでいます。
bastion_definitions.tpl.json
)
タスク定義([
{
"name": "bastion",
"image": "alpine/socat",
"cpu": 256,
"memory": 512,
"essential": true,
"pseudoTerminal": true,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-create-group": "true",
"awslogs-group": "/ecs/bastion",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "/ecs/bastion"
}
},
"portMappings": [],
"command": [
"tcp4-listen:3306,reuseaddr,fork",
"tcp-connect:{RDSのエンドポイント(ex. xxxxx.ap-northeast-1.rds.amazonaws.com)}"
],
"environment": [
{
"name" : "MANAGED_INSTANCE_ROLE_NAME",
"value" : "ass-bastion-role"
}
]
}
]
コストの観点から最小のスペックで構成しております。
コンテナイメージは以下を利用しております。
実際にポートフォワードし接続する
aws ecs describe-tasks
でruntimeId
を入手
$ aws ecs describe-tasks \
--cluster {ECSクラスターの名前} \
--task {ECSタスクのID} \
--query 'tasks[*].containers[*].runtimeId' \
--output text
--query
と--output text
を用いてruntimeId
のみ出力するようにしています。
ssm start-session
実行
$ aws ssm start-session \
--target ecs:{ECSクラスターの名前}_{ECSタスクのID}_{前項で入手したruntimeId} \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"host":["{RDSのエンドポイント}"],"portNumber":["{RDSのポート}(ex, "3306")"], "localPortNumber":["{ローカルPCの任意のポート}"(ex. "1234")]}'
上記を実行し、以下挙動になることを確認します。
Starting session with SessionId: botocore-session-xxxxxx-kagek5goi6glg6dx6fpopea7na.
Port 1234 opened for sessionId botocore-session-xxxxxx-kagek5goi6glg6dx6fpopea7na.
Waiting for connections...
別シェルを開きMySQLへ接続します。
$ mysql -h 127.0.0.1 -P {ssm start-sessionにて設定したローカルPCのポート(ex. 1234)} -u {設定したMySQLのユーザーネーム} -p
MySQLにて設定したパスワードを入力して接続できれば成功です。
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 48
Server version: 8.0.35 Source distribution
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
aws ssm start-session
を実行したシェルは以下のようになっています。
Exiting session with sessionId: botocore-session-xxxxxxx-64gns3gidtgd5kvnmbfogerll4.
まとめ
今回ECSを用いてRDSへ接続する方法を実装してみました。
以前業務ではEC2を用いて踏み台サーバーを用意していましたが、踏み台のEC2自体の管理も行う必要があり大変だと感じており、今回のようにECSを使ってAWSマネージドにすれば管理の工数も削減できてよさそうです。
積極的にマネージドサービスを使い,楽できるところは楽をして機能開発などに集中したいですよね、、
AWSでの踏み台はECSがデファクトスタンダードなのではと思いました。
Discussion