AWS Fargateでポートフォワーディング

この記事は[AWS Containers Advent Calendar 2021](https://qiita.com/advent-calendar/2021/aws-containers)の７日目です。

---

業務でAWS Fargateを使ってシステムを構築しているんですが、手元から直接DBにアクセスしたい作業などがあるのでポートフォワーディングをなんとかできないかと考えていました。

[ECS Exec](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/ecs-exec.html)がマネージドなSSMエージェントで動いているようだったので、なんとなくポートフォワーディングできそうな雰囲気を感じていたのですが、マネージドなSSMエージェントを使う具体的なやり方は不明。

- [New – Amazon ECS Exec による AWS Fargate, Amazon EC2 上のコンテナへのアクセス | Amazon Web Services ブログ](https://aws.amazon.com/jp/blogs/news/new-using-amazon-ecs-exec-access-your-containers-fargate-ec2/)
- [デバッグ用にAmazon ECS Exec を使用 - Amazon ECS](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/ecs-exec.html)
− [AWS Fargateで動いているコンテナにログインしたくて Systems Manager の Session Manager を使ってみた話 - SMARTCAMP Engineer Blog](https://tech.smartcamp.co.jp/entry/fargate-with-session-manager)
- [踏み台EC2を廃止してSession Manager接続に置き換えました | by Daichi Harada | Eureka Engineering | Medium](https://medium.com/eureka-engineering/started-to-use-ssm-session-manager-49dcd1e867a6)

多分、`aws ssm start-session`の`--target`に何かを渡せば良さそうですが、何を渡せばいいのかわからず。`aws ecs execute-command`実行時にsession-manager-pluginを実行しているようだったので、とりあえずpsコマンドを打ってみたところ

> sugawara         80091   0.1  0.1  5441620  17444 s002  S+    4:39PM   0:00.11 session-manager-plugin {"sessionId": "ecs-execute-command-0acebb01a1ed41142", "streamUrl": "wss://ssmmessages.ap-northeast-1.amazonaws.com/v1/data-channel/ecs-execute-command-0acebb01a1ed41142?role=publish_subscribe", "tokenValue": "(省略)"} ap-northeast-1 StartSession  {"Target": "ecs:hello_7412261320c54ef9a1ae606175e9bec1_7412261320c54ef9a1ae606175e9bec1-3811061257"} https://ecs.ap-northeast-1.amazonaws.com

`"Target": "ecs:hello_7412261320c54ef9a1ae606175e9bec1_7412261320c54ef9a1ae606175e9bec1-3811061257"`という謎の文字列。

ドキュメントを探しても見当たらなかったので、AWS CLIのソースコードを読みました。

https://github.com/aws/aws-cli/blob/c0edee0a7427b6e7b654df0696015e96105497a3/awscli/customizations/ecs/executecommand.py#L61-L73

```py
def build_ssm_request_paramaters(response, client):
    cluster_name = response['clusterArn'].split('/')[-1]
    task_id = response['taskArn'].split('/')[-1]
    container_name = response['containerName']
    # in order to get container run-time id
    # we need to make a call to describe-tasks
    container_runtime_id = \
        get_container_runtime_id(client, container_name,
                                 task_id, cluster_name)
    target = "ecs:{}_{}_{}".format(cluster_name, task_id,
                                   container_runtime_id)
    ssm_request_params = {"Target": target}
    return ssm_request_params
```

クラスタ名・タスクID・コンテナIDでTergetの文字列を構築している模様。クラスタ名・タスクID・コンテナ名がわかっているなら、以下のように直接`aws ssm start-session`を実行できます。

```sh
CLUSTER=hello
TASK_ID=7412261320c54ef9a1ae606175e9bec1
CONTAINER_NAME=busybox
CONTAINER_ID=$(aws ecs describe-tasks \
  --cluster $CLUSTER \
  --task $TASK_ID \
  | jq -r --arg N=$CONTAINER_NAME \
      '.tasks[0].containers[] | select(.name == $N).runtimeId')
```
```sh
$ aws ssm start-session --target ecs:${CLUSTER}_${TASK_ID}_${CONTAINER_ID}

Starting session with SessionId: root-06ad4aa7ce3831373
# 
```

SSMドキュメントを指定すればポートフォワーディングも可能です。

```sh
$ aws ssm start-session \
    --target ecs:${CLUSTER}_${TASK_ID}_${CONTAINER_ID} \
    --document-name AWS-StartPortForwardingSession \
    --parameters '{"portNumber":["8080"],"localPortNumber":["18080"]}'

Starting session with SessionId: root-08212759680ea948b
Port 18080 opened for sessionId root-08212759680ea948b.
Waiting for connections...

Connection accepted for session [root-08212759680ea948b]
```
```sh
$ curl localhost:18080
Hello World!
```

これでコンテナに[stone](https://www.gcd.org/sengoku/stone/Welcome.ja.html)でも入れておけば、Fargateのタスクを踏み台にしてDBにアクセスできるようになりました。

※ちなみに一連のTarget文字列生成と`aws ssm start-session`をまとめて行う[ecs-exec-pf](https://github.com/winebarrel/ecs-exec-pf)というコマンドも作成しました。