ecspressoを活用してECSのコンテナ経由でRDSに接続する
ECSのデプロイツールとして普段からecspressoを使用している。
ecspressoのサブコマンドにexec
というECS Execを使うための機能があるが、最近exec
コマンドのオプションにポートフォワーディングをする機能が追加された。
これをうまく使えばECSのコンテナ経由でプライベートサブネットに配置されているRDSに対して接続できるのではないか!?と思い試してみた。
実現方法
socat
という通信をリレーするためのコマンド実行するコンテナを用意して、ローカル環境からecspresso exec
でコンテナをポートフォワーディングすることによって、ポートフォワーディングしたコンテナを経由してローカル環境からRDSに接続する。
想定する環境
今回検証する環境はVPC内にパブリックサブネットとプライベートサブネットが構築されており、プライベートサブネットにRDS、パブリックサブネットにsocatコマンドが動作しているコンテナを含むECSタスクを配置されている環境とする。
実運用ではNATゲートウェイを用意して、ECSもRDSもプライベートサブネットに配置となるだろうが、今回は検証のための最小限の環境で構築する。
環境の構築
RDSはMySQLを使用する想定。
ECS Execを使用するためSessionManagerPluginを事前に導入しておく必要がある。
ecspresso管轄外のリソースはTerraformで構築。
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
# VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "main_public_1a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-northeast-1a"
map_public_ip_on_launch = true
tags = {
Name = "main_public_1a"
}
}
resource "aws_subnet" "main_private_1a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "ap-northeast-1a"
tags = {
Name = "main_private_1a"
}
}
resource "aws_route_table_association" "main_public_1a_rta" {
subnet_id = aws_subnet.main_public_1a.id
route_table_id = aws_route_table.public_rt.id
}
resource "aws_route_table_association" "main_private_1a_rta" {
subnet_id = aws_subnet.main_private_1a.id
route_table_id = aws_route_table.private_rt.id
}
resource "aws_subnet" "main_public_1c" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.3.0/24"
availability_zone = "ap-northeast-1c"
map_public_ip_on_launch = true
tags = {
Name = "main_public_1c"
}
}
resource "aws_subnet" "main_private_1c" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.4.0/24"
availability_zone = "ap-northeast-1c"
tags = {
Name = "main_private_1c"
}
}
resource "aws_route_table_association" "main_public_1c_rta" {
subnet_id = aws_subnet.main_public_1c.id
route_table_id = aws_route_table.public_rt.id
}
resource "aws_route_table_association" "main_private_1c_rta" {
subnet_id = aws_subnet.main_private_1c.id
route_table_id = aws_route_table.private_rt.id
}
resource "aws_internet_gateway" "main_igw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "main"
}
}
resource "aws_route_table" "public_rt" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main_igw.id
}
}
resource "aws_route_table" "private_rt" {
vpc_id = aws_vpc.main.id
}
resource "aws_default_security_group" "default" {
vpc_id = aws_vpc.main.id
ingress {
protocol = -1
self = true
from_port = 0
to_port = 0
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "main_db" {
name = "main_db"
vpc_id = aws_vpc.main.id
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_default_security_group.default.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# RDS
resource "aws_db_instance" "default" {
allocated_storage = 10
engine = "mysql"
engine_version = "5.7"
instance_class = "db.t3.micro"
name = "mydb"
username = "test"
password = "password"
parameter_group_name = "default.mysql5.7"
skip_final_snapshot = true
db_subnet_group_name = aws_db_subnet_group.private_subnet_group.name
vpc_security_group_ids = [
aws_security_group.main_db.id
]
}
resource "aws_db_subnet_group" "private_subnet_group" {
subnet_ids = [aws_subnet.main_private_1a.id, aws_subnet.main_private_1c.id]
}
# ECS
resource "aws_ecs_cluster" "relay" {
name = "relay"
}
# IAM
resource "aws_iam_role" "ecs_task_exection_role" {
name = "ECSTaskExectionRole"
assume_role_policy = data.aws_iam_policy_document.ecs_assume_role_policy.json
managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"]
}
resource "aws_iam_role" "ecs_task_role_for_ssm" {
name = "ECSTaskRoleForSSM"
assume_role_policy = data.aws_iam_policy_document.ecs_assume_role_policy.json
inline_policy {
name = "ssmpolicy"
policy = jsonencode(
{
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
]
Resource = "*"
}
]
}
)
}
}
data "aws_iam_policy_document" "ecs_assume_role_policy" {
version = "2012-10-17"
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}
effect = "Allow"
}
}
ecspressoの設定ファイルは以下のように記述に。
pluginでtfstateからのリソース名の読み込みをサポートしているで、それを利用する。
region: ap-northeast-1
cluster: relay
service: relay
service_definition: ecs-service-def.json
task_definition: ecs-task-def.json
timeout: 10m0s
plugins:
- name: tfstate
config:
path: terraform.tfstate
{
"deploymentConfiguration": {
"deploymentCircuitBreaker": {
"enable": false,
"rollback": false
},
"maximumPercent": 200,
"minimumHealthyPercent": 100
},
"desiredCount": 1,
"enableECSManagedTags": true,
"enableExecuteCommand": true,
"launchType": "FARGATE",
"loadBalancers": [],
"networkConfiguration": {
"awsvpcConfiguration": {
"assignPublicIp": "ENABLED",
"securityGroups": ["{{ tfstate `aws_default_security_group.default.id` }}"],
"subnets": [
"{{ tfstate `aws_subnet.main_public_1a.id` }}",
"{{ tfstate `aws_subnet.main_public_1c.id` }}"
]
}
},
"placementConstraints": [],
"placementStrategy": [],
"platformVersion": "LATEST",
"schedulingStrategy": "REPLICA",
"serviceRegistries": [],
"tags": []
}
{
"containerDefinitions": [
{
"command": [
"tcp4-listen:3306,reuseaddr,fork",
"tcp-connect:{{ tfstate `aws_db_instance.default.endpoint` }}"
],
"environment": [],
"essential": true,
"image": "alpine/socat",
"mountPoints": [],
"name": "socat",
"portMappings": [],
"volumesFrom": []
}
],
"cpu": "256",
"executionRoleArn": "{{ tfstate `aws_iam_role.ecs_task_exection_role.arn` }}",
"family": "socat",
"memory": "512",
"networkMode": "awsvpc",
"placementConstraints": [],
"requiresCompatibilities": ["FARGATE"],
"taskRoleArn": "{{ tfstate `aws_iam_role.ecs_task_role_for_ssm.arn` }}",
"volumes": []
}
これらのファイルを同じディレクトリに配置すれば準備完了。
.
├── ecs-service-def.json
├── ecs-task-def.json
├── ecspresso.yml
└── terraform.tf
以下の手順で各種リソースを作成する。
# Terraformでのリソース作成
$ terraform init
$ terraform apply
# ecspressoでのリソース作成
$ ecspresso create
ECSのタスク定義で使用しているコンテナのイメージはalpine/socatを使用している。
command
で実行するコマンドの内容をtcp4-listen:3306,reuseaddr,fork tcp-connect:<RDSのエンドポイント名>
とすることにより、立ち上がったコンテナは3306ポートへの通信をRDSへリレーすることになる。
接続確認
リソースが作成されるとECS上にsocatが動作しているコンテナのタスクが立ち上がるためecspresso exec
でポートフォワーディングを行う。
$ ecspresso exec --port-forward --local-port 3306 --port 3306
対象のコンテナを選んで接続すると「Waiting for connections...」というメッセージが表示されるため、そのShellはそのままで、新しく別のShellを開いて127.0.0.1
に対してmysqlの接続を試みる。
$ mysql -h 127.0.0.1 -u test -ppassword
これでRDSに接続されれれば成功、ecspresso exec
をした方のShellでは「Connection accepted for session」というメッセージが表示されているはず。
リソースの削除
検証が完了したらリソースを削除。
$ ecspresso scale --tasks 0
$ ecspresso delete
$ terraform destroy
Discussion