ECS コンテナに VSCode Remote SSH で接続する
VSCode (および派生エディタ) では Remote - SSH という拡張機能が利用できます。これを利用すると、SSH で接続可能なホスト (たとえば EC2 インスタンス) に対して、そのホスト内で VSCode を起動して作業しているような開発体験が得られます。ECS で起動したサービスまたはタスクに対してもこの体験を得たいと考え、方法を検討しました。
要素技術 #
- EC2 インスタンスに対して SSH する方法として Session Manager を利用する方法 があります。
- この方法を利用すると、ACL, SG の解放などをすることなしに非公開の EC2 インスタンスに対して SSH セッションを確立することができます。
- 具体的な利用方法として、aws ssm start-session コマンドを ProxyCommand に設定します。
# ~/.ssh/config Host i-123456 ProxyCommand sh -c "aws ssm start-session --document-name AWS-StartSSHSession --target i-123456" User ec2-user ...- この場合、
ssh i-123456コマンドを実行することで EC2 インスタンス i-123456 に対して SSH 接続ができます。 - VSCode Remote SSH もこの構成を利用してセッションを確立できます。
- この場合、
- Session Manager は ECS コンテナに対してもセッションを確立することができます。
- 適切に権限を構成し、ECS Exec を有効化したコンテナに対して
aws ssm start-session --target ecs:<CLUSTER_NAME>_<CONTAINER_ID>_<CONTAINER_RUNTIME_ID>として接続ができます。- ここで、CONTAINER_ID, CONTAINER_RUNTIME_ID は describe-tasks で得ることができます。
- ただし、通常 Docker イメージには SSH サーバーをインストールおよび構成していないため、そのまま
AWS-StartSSHSessionドキュメントのセッションを確立することはできません。
- 適切に権限を構成し、ECS Exec を有効化したコンテナに対して
- Session Manager を利用して、疑似ターミナルセッションや SSH セッションを確立する他に、ホスト上で所定のコマンドを実行する操作も可能です。
- ドキュメント AWS-StartInteractiveCommand を利用する例:
aws ssm start-session \ --target TARGET \ --document-name AWS-StartInteractiveCommand \ --parameters '{"command":["/bin/bash -c \"echo hello world\"]}'
- ドキュメント AWS-StartInteractiveCommand を利用する例:
提案手法 #
- Task definition において init process を有効化しておきます。これは SSH サーバーをバックグラウンドプロセスとして稼働させるためです。
resource "aws_ecs_task_definition" "this" { container_definitions = jsonencode([ { linuxParameters = { initProcessEnabled = true } ... } ]) ... } - ECS Exec を有効化したタスクを起動しておきます。
aws ecs run-task \ --cluster YOUR_CLUSTER \ --task-definition YOUR_TASK_DEFINITION \ --enable-execute-command \ ... - SSH config において ProxyCommand に後述するシェルスクリプトを指定した構成を追加します。Note. StrictHostKeyChecking を no に設定することはセキュリティ上のリスクがあり、利用するかはよく検討してください。特別に工夫しない限りタスクを起動し直すごとに host key は変化してしまうため、この設定を取り除くと接続ごとに警告がでるようになります。
# ~/.ssh/config Host ecs-* StrictHostKeyChecking no UserKnownHostsFile /dev/null Host ecs-example IdentityFile ~/.ssh/YOUR_SSH_KEY User root ProxyCommand zsh ~/ecs-ssh-proxy.sh YOUR_CLUSTER YOUR_TASK_FAMILY YOUR_CONTAINER_NAME ~/.ssh/YOUR_SSH_KEY.pub # OR, profile, region を指定する場合 Host ecs-example IdentityFile ~/.ssh/YOUR_SSH_KEY User root ProxyCommand zsh -c "export AWS_PROFILE=YOUR_PROFILE; export AWS_REGION=YOUR_REGION; zsh ~/ecs-ssh-proxy.sh YOUR_CLUSTER YOUR_TASK_FAMILY YOUR_CONTAINER_NAME ~/.ssh/YOUR_SSH_KEY.pub" - 設定したホストに対して
sshコマンドで接続します。また、このホストに対して VSCode Remote SSH セッションを確立することができます。ssh ecs-example
~/ecs-ssh-proxy.sh は次のような手続きを行います。
#!/bin/zsh
set -euo pipefail
CLUSTER_NAME="$1"
TASK_FAMILY="$2"
CONTAINER_NAME="$3"
PUBLIC_KEY_FILE="$4"
# get task by family
TASK_ARN=$(aws ecs list-tasks \
--cluster "$CLUSTER_NAME" \
--family "$TASK_FAMILY" \
--desired-status RUNNING \
--query 'taskArns[0]' \
--output text | head -1)
if [ -z "$TASK_ARN" ] || [ "$TASK_ARN" = "None" ]; then
echo "Task ARN not found. cluster_name=${CLUSTER_NAME}, task_family=${TASK_FAMILY}" >&2
exit 1
fi
echo "Task ARN: $TASK_ARN" >&2
# get container runtime ID
CONTAINER_RUNTIME_ID=$(aws ecs describe-tasks \
--cluster "$CLUSTER_NAME" \
--tasks "$TASK_ARN" \
--query "tasks[0].containers[?name=='$CONTAINER_NAME'].runtimeId | [0]" \
--output text | head -1)
if [ -z "$CONTAINER_RUNTIME_ID" ] || [ "$CONTAINER_RUNTIME_ID" = "None" ]; then
echo "Container runtime ID not found. container_name=${CONTAINER_NAME}" >&2
exit 1
fi
echo "Container runtime ID: $CONTAINER_RUNTIME_ID" >&2
TASK_ID=$(echo "$TASK_ARN" | cut -d '/' -f 3)
TARGET="ecs:${CLUSTER_NAME}_${TASK_ID}_${CONTAINER_RUNTIME_ID}"
echo "Target: $TARGET" >&2
# configure ssh server
COMMAND="/bin/bash -c \"set -exuo pipefail
if [ ! -f /usr/sbin/sshd ]; then
apt-get update
apt-get install -y --no-install-recommends curl ca-certificates openssh-server
echo 'PasswordAuthentication no' >> /etc/ssh/sshd_config
echo 'ListenAddress 127.0.0.1' >> /etc/ssh/sshd_config
fi
if [ ! -f ~/.ssh/authorized_keys ]; then
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo '$(cat "$PUBLIC_KEY_FILE")' >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
fi
if [ ! -f /var/run/sshd.pid ]; then
mkdir -p /run/sshd
nohup /usr/sbin/sshd -e -D < /dev/null > /var/log/sshd.log 2>&1 &
fi
\""
PARAMETERS=$(jq -nr --arg command "$COMMAND" '{command: [$command]}')
/usr/bin/script -q /dev/null \
aws ssm start-session \
--target "$TARGET" \
--document-name AWS-StartNonInteractiveCommand \
--parameters "$PARAMETERS" \
< /dev/null >&2
# start SSH proxy
aws ssm start-session \
--target "$TARGET" \
--document-name AWS-StartSSHSession
- 入力パラメータに基づいて接続する対象となる ECS タスク, コンテナを検索し start-session の target の値を特定します。
- ドキュメント AWS-StartNonInteractiveCommand を利用し、コンテナ内でコマンドを実行します。
- このステップでは、SSH サーバーのインストールおよび起動、公開鍵の配置を行います。
- Note. script コマンドを介して実行しているのは、ProxyCommand として動作する際に stdin/stdout を奪わないためです。これがないと SSH 通信に干渉してエラーになります。
- ドキュメント AWS-StartSSHSession を利用して SSH proxy session を開始します。
(参考) ECS task definition の構成例 (Terraform)
data "aws_iam_policy_document" "ecs_tasks_assume_role" {
statement {
actions = ["sts:AssumeRole"]
effect = "Allow"
principals {
identifiers = ["ecs-tasks.amazonaws.com"]
type = "Service"
}
}
}
resource "aws_iam_role" "task_execution" {
assume_role_policy = data.aws_iam_policy_document.ecs_tasks_assume_role.json
name = "my-example-task-execution"
}
resource "aws_iam_role_policy_attachment" "task_execution" {
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
role = aws_iam_role.task_execution.name
}
resource "aws_iam_role" "task" {
assume_role_policy = data.aws_iam_policy_document.ecs_tasks_assume_role.json
name = "my-example-task"
}
data "aws_iam_policy_document" "task" {
statement {
actions = [
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:CreateControlChannel"
]
effect = "Allow"
resources = ["*"]
}
}
resource "aws_iam_policy" "task" {
name = "my-example-task"
policy = data.aws_iam_policy_document.task.json
}
resource "aws_iam_role_policy_attachment" "task" {
policy_arn = aws_iam_policy.task.arn
role = aws_iam_role.task.name
}
resource "aws_ecs_cluster" "this" {
name = "my-example-cluster"
}
resource "aws_ecs_cluster_capacity_providers" "this" {
capacity_providers = ["FARGATE_SPOT"]
cluster_name = aws_ecs_cluster.this.name
}
resource "aws_ecs_task_definition" "this" {
family = "example"
cpu = "1024"
memory = "2048"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
execution_role_arn = aws_iam_role.task_execution.arn
task_role_arn = aws_iam_role.task.arn
runtime_platform {
operating_system_family = "LINUX"
cpu_architecture = "X86_64"
}
container_definitions = jsonencode([
{
name = "main"
image = "ubuntu:24.04"
command = ["tail", "-f", "/dev/null"]
linuxParameters = {
initProcessEnabled = true
}
}
])
}
まとめ #
このページでは、ECS コンテナに対して Session Manager を利用して SSH 接続を確立する手法について整理しました。
- 提案した手法では、次のような利点があります。
- EC2 インスタンス等に対する VSCode Remote SSH 機能と同等の開発体験が得られます。
- Session Manager など、標準的なツール・手法の組み合わせで実現され、特殊なサードパーティーツールに依存しません。
- ProxyCommand の段階において Session Manager を経由して SSH サーバーをセットアップするため、Docker イメージをこの手法のためにカスタマイズする必要がありません。
- ただし、示したスクリプトは概念を示すための最小実装であり、改善の余地があります。たとえば、Debian 以外のベースイメージへの対応、SSH サーバー構成の最適化、セットアップをよりロバストにする、などです。
- 他方で、実務上は次のような観点に注意が必要です。
- Docker に SSH サーバーをインストールすることは一般的なプラクティスではありません。
- ECS タスクに接続したセッションからは、その task role の範囲で AWS リソースに対して操作が可能です。ECS Exec とあわせて、権限を適切に制限すること、適切な監査ログを構成すること、などが推薦されます。