Tech Racho エンジニアの「?」を「!」に。
  • インフラ

AWS ECSで踏み台サーバーを手軽に立ち上げる

概要

たまにVPC内のリソース(RDSやElastiCache)に手元のクライアントだったり、シェル経由でアクセスしたいことがあります。
そんな時に踏み台サーバーを用意するかと思うのですが、ECSのタスクを使うと素早く起動できて便利です。

やり方

事前にセッションマネージャープラグインをインストールして、タスクロールに必要な権限を当ててECS Execが使える状態にしておきます。

デバッグ用にAmazon ECS Exec を使用 - Amazon ECS

まずタスク定義bastionを用意します。
踏み台サーバーに使いたいイメージを指定します。
ここではイメージにubuntu:latestを、コマンドはsleep 1hを指定しています。
(自分の場合、踏み台サーバーでの作業はそれほど長くないので1時間で終了するようにしています)

あとはsession manager経由で入れるように --enable-execute-command オプションを指定してタスクを起動するだけです。
(節約したい場合はFargate Spotを使用した方が良いかと思います)

export CLUSTER=example-cluster
export SUBNETS=subnet-xxxxxxxf,subnet-xxxxxxxx,subnet-xxxxxxxx
export SECURITY_GROUPS=sg-xxxxxxxxxxxxxxxxx
export TASK_ARN=$(aws ecs run-task --cluster $CLUSTER --count 1 --launch-type FARGATE \
        --enable-execute-command \
        --network-configuration "awsvpcConfiguration={subnets=[$SUBNETS],securityGroups=[$SECURITY_GROUPS],assignPublicIp=ENABLED}" \
        --task-definition bastion \
        --output json \
        | jq -r .tasks[0].containers[0].taskArn)

execute-commandでシェルを取ったり

aws ecs execute-command \
    --cluster $CLUSTER \
    --task $TASK_ARN \
    --interactive \
    --command "/bin/bash"

port forwarding もできます。

export TASK_ID=${TASK_ARN##*/}
export RUNTIME_ID=$(aws ecs describe-tasks --cluster $CLUSTER --task $TASK_ID | jq -r .tasks[0].containers[0].runtimeId)
aws ssm start-session \
    --target ecs:${CLUSTER}_${TASK_ID}_${RUNTIME_ID} \
    --document-name AWS-StartPortForwardingSession \
    --parameters '{"portNumber":["80"], "localPortNumber":["8080"]}'

リモートホストへのfowarding は SSM agentのバージョンが SSM Agent version: 3.1.1260.0 だったのでまだ使えなかったですが、しばらくすれば使えるようになるかと思います。
それまではnetcat等で転送すると良いかと思います。

# ローカルの6379番ポートをElastiCacheに転送
apt install netcat
mkfifo fifo
nc -k -l -p 6379 < fifo | nc hoge-redis.xxxxxx.ng.0001.apne1.cache.amazonaws.com 6379 > fifo

関数定義

オプションが多くてコマンドが複雑なので、自分の場合は以下のような関数をbashrcに書いています。

export CLUSTER=example-cluster
export SUBNETS=subnet-xxxxxxxf,subnet-xxxxxxxx,subnet-xxxxxxxx
export SECURITY_GROUPS=sg-xxxxxxxxxxxxxxxxx
export TASK_DEFINITION=bastion
export ECS_BASTION_ENV_FILE=~/ecs-bastion.sh
function ecs-bastion-create {
    local TASK_ARN=$(aws ecs run-task --cluster $CLUSTER --count 1 --launch-type FARGATE \
            --enable-execute-command \
            --network-configuration "awsvpcConfiguration={subnets=[$SUBNETS],securityGroups=[$SECURITY_GROUPS],assignPublicIp=ENABLED}" \
            --task-definition $TASK_DEFINITION \
            --output json \
            | jq -r .tasks[0].containers[0].taskArn)
    echo "wait for task running"
    until [ "$(aws ecs describe-tasks --cluster $CLUSTER --tasks $TASK_ARN | jq -r .tasks[0].containers[0].lastStatus)" = "RUNNING" ]
    do
        echo -n "."
        sleep 30
    done
    sleep 30
    echo -e "\ncreate task $TASK_ARN"
    echo "export TASK_ARN=$TASK_ARN" > $ECS_BASTION_ENV_FILE
}
function ecs-bastion-get-env {
    if [ ! -r $ECS_BASTION_ENV_FILE ];then
        echo "$ECS_BASTION_ENV_FILE not found"
        return 1
    else
        source $ECS_BASTION_ENV_FILE
    fi
}
function ecs-bastion-shell {
    ecs-bastion-get-env || return 1
    aws ecs execute-command \
        --cluster $CLUSTER \
        --task $TASK_ARN \
        --interactive \
        --command "/bin/bash"
}
function ecs-bastion-port-forwarding {
    ecs-bastion-get-env || return 1
    local TASK_ID=${TASK_ARN##*/}
    local RUNTIME_ID=$(aws ecs describe-tasks --cluster $CLUSTER --task $TASK_ID | jq -r .tasks[0].containers[0].runtimeId)
    local OPT
    OPTIND=1
    while getopts "hp:l:" OPT
    do
        case $OPT in
        "h" ) 
            echo "usage: ecs-bastion-port-fowarding -p ecsPort -l localPort"
            return
            ;;
        "l" )
            local localport=$OPTARG
            ;;
        "p" )
            local ecsport=$OPTARG
            ;;
        esac
    done
    echo "localPort=$localport"
    echo "ecsPort=$ecsport"
    aws ssm start-session \
        --target ecs:${CLUSTER}_${TASK_ID}_${RUNTIME_ID} \
        --document-name AWS-StartPortForwardingSession \
        --parameters "{\"portNumber\":[\"$ecsport\"], \"localPortNumber\":[\"$localport\"]}"
}
function ecs-bastion-stop {
    ecs-bastion-get-env || return 1
    aws ecs stop-task --cluster $CLUSTER --task $TASK_ARN
    rm $ECS_BASTION_ENV_FILE
}

使用例

以下はbastionタスクを立ち上げて、port forwarding => rsyncで踏み台サーバーの/etc配下をコピーするという例です。

# bastionタスク作成
$ ecs-bastion-create

# shellで入る
$ ecs-bastion-shell

# 踏み台サーバーでrsync立ち上げ
apt update && apt install -y rsync
cat <<EOF > /etc/rsyncd.conf
uid         = root
gid         = root
pid file = /tmp/rsyncd.pid
log file = /tmp/rsyncd.log
read only = yes
max connections = 10
[root]
    comment = rsyncd server / directory
    path = /
    numeric ids = yes
EOF
rsync --daemon

# 別ターミナルでport fowarding
$ ecs-bastion-port-forwarding -l 8730 -p 873

# 別ターミナルでetc配下をコピー
$ rsync -av rsync://127.0.0.1:8730/root/etc/ /tmp/etc/

# 停止(忘れても1時間で消える)
$ ecs-bastion-stop

関連記事

AWS CodeBuildからECSへexecute-commandする


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。