踏み台サーバーをEC2からECSに移行してオンデマンド起動してみた

こんにちはSREチームの宮後(@miya10kei)です。最近、iPad Air(M2)をゲットしたので便利な使い方を模索しています。

みなさんは踏み台サーバをどうやって構築していますか?
今回、EC2で構築していた踏み台サーバーをECSに移行することでセキュリティ向上 x 運用負荷低減 x コスト削減をおこなうことができたので紹介したいと思います。

背景

弊社ではローカルPCからAurora or ElastiCacheに接続する際の踏み台サーバーをEC2で構築していました。最近、システムのセキュリティ向上の活動をおこなっていく中で踏み台サーバーに対して以下の課題感を持つようになりました。

  • EC2をパブリックサブネットに配置していたのでプライベートサブネットに配置することでよりセキュアな構成にしたい
  • EC2のOSアップデートなどのEC2に起因する定期運用を削減したい
  • 踏み台サーバは24時間365日必要になるわけではないので、必要な時だけ起動するようにしたい(同時にコスト削減したい)

構成を検討する中でセキュリティ向上 x 運用負荷低減 x コスト削減に繋げることができそうでしたので、EC2からECSに移行することにしました。

全体構成

弊社では踏み台サーバーを以下の2つのユースケースで使用しています。

  1. 開発者がローカルPCからAurora or ElastiCacheに接続するケース
  2. troccoからAuroraに接続するケース

使用ソフトウェアの制約により構成を分けているのでそれぞれ紹介します。

全体構成

開発者がローカルPCからAurora or ElastiCacheに接続するケース

開発者がローカルPCから接続する場合、なるべくセキュアな接続を行うため、また内部統制の要件で接続履歴を記録する必要があるためSSMを使用しています。また、踏み台ECSタスクは常に必要になるわけではないため、常時起動はおこなわず必要な時にオンデマンドで起動できるようにしています。

troccoからAuroraに接続するケース

データベースのデータを日時でGoogle CloudのBig Queryにtroccoで転送しています。troccoはEC2に対するSSM接続はサポートしていますが、ECSタスクに対するSSM接続はサポートしていません。そこで、GatewayとしてNLBを使用し、SSMを使用せず接続できるような通信経路を用意しました。

構築する上でのポイント

踏み台ECSタスクの起動からSSM接続までをスクリプトで自動化

開発者がローカルPCから接続する場合、以下の手順を踏みます。

  1. 踏み台サーバが起動済みの場合
    1. 既存の踏み台サーバーにSSM接続
  2. 踏み台サーバが未起動の場合
    1. 踏み台サーバーを起動
    2. 起動した踏み台サーバーにSSM接続

この一連の流れをスクリプト化することで開発者が簡単に接続できるようにしています。 スクリプトを全文載せると長くなってしまうので利用したAWS CLI コマンドのみを記載します。

踏み台ECSクラスターで起動中のECSタスクのARNを取得します。

aws ecs list-tasks --cluster ${ECS_CLUSTER} --query "taskArns" --output text

起動中ECSタスクのグループを取得します。取得結果はtrocco用ECS Service内で起動しているECSタスクを除外するために使用します。

aws ecs describe-tasks --cluster ${ECS_CLUSTER} --task ${task_arn} --query "tasks[].group" --output text

ECSタスクを起動する際に使用するECSタスク定義のリビジョンを取得します。常に最新リビジョンを使用したいので都度取得するようにしています。

aws ecs describe-task-definition --task-definition ${ECS_TASK_DEFINITION} --query "taskDefinition.revision"

ECSタスクを起動します。ECSタスクにSSM接続するにはenable-execute-commandを設定して起動する必要があります。

aws ecs run-task \
    --capacity-provider-strategy capacityProvider=FARGATE_SPOT,weight=1 \
    --cluster ${ECS_CLUSTER} \
    --count 1 \
    --enable-execute-command \
    --network-configuration "awsvpcConfiguration={subnets=[${ECS_VPC_SUBNET_ID}],securityGroups=[${ECS_SECURITY_GROUP_ID}],assignPublicIp=DISABLED}" \
    --task-definition ${ECS_TASK_DEFINITION}:${task_definition_revision} \
    --query "tasks[0].taskArn" \
    --output text

起動したECSタスクのステータスを取得します。スクリプト内でポーリングすることで使用可能になったことを検知しています。

aws ecs describe-tasks --cluster ${ECS_CLUSTER} --tasks ${task_arn} --query "tasks[0].lastStatus" --output text

起動したECSタスクのランタイムIDを取得します。ECSタスクにSSM接続するにはランタイムIDが必要になります。

aws ecs describe-tasks --cluster ${ECS_CLUSTER} --tasks ${TASK_ARN} --query "tasks[0].containers[0].runtimeId" --output text

ECSタスクにSSM接続します。 AWS-StartPortForwardingSessionToRemoteHostを指定することでparametersに設定したホストにPortForwardすることができます。

aws ssm start-session \
    --target ${TARGET} \
    --document-name AWS-StartPortForwardingSessionToRemoteHost \
    --parameters '{\"host\": [ \"${HOST_REMOTE}\" ], \"portNumber\": [ \"${PORT_REMOTE}\" ], \"localPortNumber\": [ \"${PORT_LOCAL}\" ]}

Fargate Spotの利用

開発者がローカルPCから接続するための踏み台サーバはFargate Spotを使用することで少しだけコストを抑えるようにしています。オンデマンドでECSタスクが起動するため大きな削減にはなりませんが、コスト削減はチリツモだと思っているので採用しました。
一方、troccoから接続するためのECSタスクはFargate Spotによる急な停止が発生すると、その後リカバリー作業が必要になるためFargateを採用しています。

SSM接続切断後の自動停止

SSM接続が開始されると踏み台サーバ内で ssm-session-workerプロセスが起動します。このプロセスを監視するスクリプトを作成し、コンテナ起動時に呼び出すことで一定時間SSM接続がない場合にコンテナが停止するようにしています。

#!/bin/sh

while true
do
    sleep 120
    count=$(pgrep -f ssm-session-worker | wc -l)
    if [ $count -eq 0 ]; then
        exit 0
    fi
done

socatでのトラフィック転送

troccoからの接続にはSSMを使用しないため、SSMのPort Forward機能を使用することができません。そこで、ECSタスクコンテナ内でsocatを起動しトラフィックを転送することで解決しました。socatは以下のオプションで起動しています。

socat TCP-LISTEN:${listen_port},reuseaddr,fork TCP:${db_host}:${remote_port}

さいごに

今回はECSで踏み台サーバを構築した事例を紹介しました。 ECSに移行したことで冒頭に紹介した3つの課題をうまく解決できたと思います。 同じ課題を感じている方がいらしたら参考にしてみてください!