🧩

ECS Fargateを踏み台にポートフォワードしてVPC内のRDSに接続する

2022/11/24に公開

AWSにおいて、RDSなどVPC内のリソースにVPC外からアクセスしたい場合、VPNを利用したり、踏み台サーバーを経由することが一般的です。
本記事では、ECS Fargateを踏み台に、SSM Session Managerのポートフォワードを利用して、VPC内のリソースに接続する方法を紹介します。

SSM Session Managerのポートフォワード機能について

SSM Session ManagerはSSM AgentをインストールしたEC2インスタンスにシェルアクセスを提供する機能です。
docker execのECS版であるECS Execもこの延長上で実装されています。

SSM Session Managerはシェルアクセスだけでなく、インバウンドポートを閉じたままのSCP/SSH接続やポートフォワード機能も提供しています。

このポートフォワード機能を利用すると、SSMエージェントがインストールされているインスタンス内のポートフォワードだけでなく、同インスタンスを踏み台としたリモートホスト・ポートのフォワードにも対応しています。

EC2環境では良く知られた構成をECS Fargateにも適用したのが本記事です。

事前準備

ポートフォワードの踏み台に利用したいタスクに対して、ECS Execができるように設定を済ませておいてください。

特に、ECSタスクロールにSSM周りのポリシーを忘れずに付与してください。

https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html

ECS Fargateタスクに対してSSM Session Managerで接続するには?

EC2に対してSSM Session Managerで接続する場合、インスタンスIDを指定します。

$ aws ssm start-session \
  --target i-123

しかし、ECS FargateのコンテナにはインスタンスIDが割り当てられていません。

代わりに、ecs:クラスター名_タスクID_ランタイムID--target 引数に指定します。

クラスター名とタスクIDはコンソールから確認可能です。
ランタイムIDは、ECS:DescribeTasks APIから確認できます。

$ aws ecs describe-tasks \
  --cluster test \
  --task ae8bd3553e9247308fd488b268de766b

{
    "tasks": [
        {
            ...
            "containers": [
                {
                    "containerArn": "arn:aws:ecs:REGION:123456789012:container/test/ae8bd3553e9247308fd488b268de766b/80afabe0-62bc-4c7a-8fb3-b34eda3d5cb6",
                    "taskArn": "arn:aws:ecs:REGION:123456789012:task/test/ae8bd3553e9247308fd488b268de766b",
                    "name": "fargate-app",
                    "image": "public.ecr.aws/docker/library/httpd:latest",
                    "runtimeId": "ae8bd3553e9247308fd488b268de766b-3681984407",
                    "lastStatus": "RUNNING",
                    ...
                }
            ],
            ...
        }
    ],
    "failures": []
}

containers 内に runtimeId フィールドを確認できます。

あとは

  • クラスター名
  • タスクID
  • ランタイムID

_ でつなげたものを target に渡すだけです。

$ aws ssm start-session \
 --target ecs:test_ae8bd3553e9247308fd488b268de766b_ae8bd3553e9247308fd488b268de766b-3681984407

Starting session with SessionId: xxx-0d8b5abb61b899597
# 

ECS Execは実行時に

  • クラスター名
  • タスクID
  • コンテナ名

を指定しています。

ECS:DescribeTasks API のレスポンスからも、これらの情報があればランタイムIDを一意に特定できることがわかりますね。

SSM Session Managerでポートフォワードするには?

ECS FargateにSSM Session Managerで接続できることを確認できたので、あとはポートフォワード用のパラメーターを追加するだけです。

$ aws ssm start-session \
    --target ecs:クラスター名_タスクID_ランタイムID \
    --document-name AWS-StartPortForwardingSessionToRemoteHost \
    --parameters '{"host":["リモートホスト"],"portNumber":["リモートポート番号"], "localPortNumber":["ローカルポート番号"]}'

--parametershostportNumberに、RDSエンドポイントなどポートフォワードしたいリモートシステムのホスト・ポートを指定します。

よりシンプルに、コンテナ内のポートをフォワードしたい場合、次の様に指定します。

$ aws ssm start-session \
    --target ecs:クラスター名_タスクID_ランタイムID \
    --document-name AWS-StartPortForwardingSession \
    --parameters '{"portNumber":["リモートポート番号"], "localPortNumber":["ローカルポート番号"]}'

ECS FargateタスクのSSMリモートポートフォワード対応は2022年11月から

SSMのリモートホストポートフォワードは2022年5月から対応しています。

一方で、FargateのSSMエージェントが同機能に対応したバージョン(version >= 3.1.1374.0)にアップデートされたのは、約半年後の2022年11月からです。

https://twitter.com/fujiwara/status/1591984494520606720

$ /managed-agents/execute-command/amazon-ssm-agent --version
SSM Agent version: 3.1.1732.0

この機能のおかげで、ECS Fargateの踏み台化が非常にシンプルになりました。

ツールを使って入力の手間を減らす

@fujiwaraさん作成のECSツールを利用すると、シュッとポートフォワードできます。
人生は短いので、お手製のラッパーを書くくらいなら、このようなツールの利用を検討しましょう。

ecspresso を利用

$ ecspresso exec \
  --port-forward \
  --local-port 8080 \
  --port 80 \
  --host github.com 

ecsta を利用

$ ecsta portforward \
  --local-port=8080 \
  --remote-port=80 \
  --remote-host=github.com

補足:ecstaのコードからSession ManagerのECS用ターゲットID生成箇所を確認

ECSのSSMではSession Manager時のターゲットに、インスタンスIDの代わりに、ecs:クラスター名_タスクID_ランタイムID を 指定します。

ecstaのコードでは、次の箇所が該当します

func buildSsmRequestParameters(task types.Task, targetContainer string) (*ssmRequestParameters, error) {
	values := strings.Split(*task.TaskArn, "/")
	clusterName := values[1]
	taskID := values[2]
	var runtimeID string
	for _, c := range task.Containers {
		if *c.Name == targetContainer {
			runtimeID = *c.RuntimeId
		}
	}
	return &ssmRequestParameters{
		Target: fmt.Sprintf("ecs:%s_%s_%s", clusterName, taskID, runtimeID),
	}, nil
}

Note:

English translation of this article : Securely connect to an Amazon RDS using ECS Fargate as a bastion

Discussion