背景
開発期間中にPJメンバーから、DBに投入しているSeedデータではとあるテストを実施する際にカバー出来ない部分があって、ローカルからMysqlClientで値を変えながら実施したい旨の話がありました。
該当PJではFargate+Auroraで構成されており、また常駐的な踏み台は用意せず、必要な時だけそれ用のmiscコンテナを起動させる方針を取っています。
そんな中、丁度今年の5月下旬にタイトルにある機能がセッションマネージャーにリリースされたこともあり、テンポラリーなmiscコンテナをリモートホストポートフォワードの中継役として実施したところ、既に幾つか記事として見かけますが、便利だと言うことで周辺知識を改めて追いながら書いてます。
概要
ポートフォワードのイメージ
そもそもポートフォワードとはなんぞやという話ですが、下図のように例えば自端末の特定ポート(例:13306)への通信に対して、別のホストやアドレスの特定ポート(例:3306)に自動で転送してくれる仕組みを指します。
セッションマネージャーのリリース
Session Manager のポートフォワーディングは、クライアントマシンと Systems Manager が管理するインスタンス間の通信をトンネルするために使用されます。本日より、Systems Manager はクライアントマシンからリモートホストのポートへのフォワーディングコネクションをサポートします。リモートのポートフォワーディングにより、データベースやウェブサーバーなどのリモートホストのアプリケーションポートに、これらのサーバーを外部ネットワークにさらすことなく安全に接続するための「ジャンプホスト」として管理対象インスタンスを使用可能になりました。
リリース文にある通り、セッションマネージャーはそもそもポートフォワード機能を持ってまして、今回追加された機能はリモートホストに対してもポートフォワード出来るという内容になっています。
何が違うのかというと
従来までの機能(ローカルホストポートフォワード)
これまでは例えばEC2にssm-agentとmysqlをインストールしていた場合、ssm-agentを用いてPrivateSubnet内のinstanceにセッションを張って、ポートフォワードによりEC2内のMysqlにアクセス出来るといったものでした。
以下のコマンドをローカルで実行する事でローカルポート13306にアクセスするとEC2内の3306ポートに転送されます。
aws ssm start-session \
--target i-exsampleinstance \
--document-name AWS-StartPortForwardingSession \
--parameters '{"portNumber":["3306"], "localPortNumber":["13306"]}'
セッションが確立されたらmysqlへのログインが可能になります。
mysql -u username -p -h 127.0.0.1 -P 13306
リモートホストポートフォワード
従来の例ですとローカルホスト内のポートに対してアクセスを行うものでしたが、新しい機能では呼んで字の如く、リモートホストのポートに対してアクセス出来るようになりました。ですので、ssm-agentをインストールしたインスタンスだけでなく、PrivateSubnet内のRDSやElastiCacheにローカル端末からのアクセスがセッションマネージャーで可能ということです。踏み台にログインというのも当然不要になります。
以下のコマンドをローカルで実行する事でローカルポート13306にアクセスするとEC2を経由してAuroraの3306ポートに転送されます。※先程とコマンドが若干違うので注意。
aws ssm start-session \
--target i-exsampleinstance \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"host":["xxx.xxx.cluster-xxx.ap-northeast-1.rds.amazonaws.com"],"portNumber":["3306"], "localPortNumber":["13306"]}'
セッションが確立されたらmysqlへのログインが可能になります。
mysql -u username -p -h 127.0.0.1 -P 13306
sshを用いたRDSへのポートフォワード
一応セッションマネージャーの別の機能で、sshを用いてPrivateSubnetのRDSにアクセスすることは、これまでも可能でした。※この場合、認証鍵の管理などが必要という点に注意が必要です。
ssh -i xxxx.pem ec2-user@i-exsampleinstance -L 13306:xxx.xxx.cluster-xxx.ap-northeast-1.rds.amazonaws.com:3306
やってみよう
前提
セッションマネージャーによるリモートホストポートフォワードを実現する為には下記条件があるのでそれぞれ満たす必要があります。
-
セッションマネージャーそのものの前提条件
- os、ssm-agent、SystemsManagerエンドポイントへの経路(NatGateway or VPCEndpoint)、aws-cli、iam権限、アドバンストインスタンス層有効化(Fargate)など
- ssm-agentのバージョンが3.1.1374.0以降
構成とフロー
- SystemsManagerのインスタンス層をスタンダードからアドバンストに変更
- ssm-agent、aws-cli、scriptを入れたコンテナイメージを用意
- ECRにPush
- コンテナ起動/ポートフォワード用scriptを実行
- コンテナが起動
- コンテナ内のscriptが実行
- SystemsManagerにコンテナを管理ノードとして登録
- ノードIDをParameterStoreに登録
- ssm-agentを起動
- コンテナ起動/ポートフォワード用scriptにてノードIDを取得
- 取得したノードIDを用いてセッションの確立
- ポートフォワード実行
インスタンス層をアドバンストに変更
今回はコンソール上から実施。
- SystemsManagerのナビゲーションペインよりフリートマネージャーを選択
- 「アカウント管理」よりインスタンス枠の設定を押下
- 「アカウント設定の変更」を押下
- 「アカウントとリージョン内のすべて〜」にチェックを入れて「設定の変更」を押下
※アドバンスドインスタンスは以下のルールで従量課金が発生するので注意
アドバンスドオンプレミスインスタンスごとに時間あたり 0.00695 USD
無料利用枠なし
コンテナイメージの作成
ファイル構成
docker
├── scripts
│ └── setting.sh
└── ssm
└── Dockerfile
FROM public.ecr.aws/ubuntu/ubuntu:20.04
WORKDIR /
RUN \
--mount=type=cache,target=/var/lib/apt/lists \
--mount=type=cache,target=/var/cache/apt/archives \
apt-get update \
&& apt-get -y install \
curl \
jq \
unzip
RUN curl https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip -o awscliv2.zip \
&& unzip awscliv2.zip \
&& ./aws/install \
&& rm -rf awscliv2.zip ./aws
RUN curl https://s3.ap-northeast-1.amazonaws.com/amazon-ssm-ap-northeast-1/latest/debian_amd64/amazon-ssm-agent.deb -o /tmp/amazon-ssm-agent.deb \
&& dpkg -i /tmp/amazon-ssm-agent.deb \
&& mv /etc/amazon/ssm/amazon-ssm-agent.json.template /etc/amazon/ssm/amazon-ssm-agent.json \
&& rm /tmp/amazon-ssm-agent.deb
COPY ./docker/scripts/setting.sh /setting.sh
RUN chmod 755 /setting.sh
CMD ["bash", "/setting.sh"]
※Scriptのコメントにも記載していますが、該当イメージを指定したタスク定義の環境変数にはENVを指定します。
※また、以下の権限を持たせたIAMRoleを用意します。
- SSMServiceRole
- 「AmazonSSMManagedInstanceCore」のマネージドポリシー
- 「ssm:DeregisterManagedInstance」の許可
#!/bin/bash
set -e
# 事前にIAMRole(SSMServiceRole)を用意する必要あり
# タスク定義の環境変数にENVを指定してください。
SSM_SERVICE_ROLE_NAME="SSMServiceRole"
SSM_PARAMETER_NAME="/${ENV}/portforward/ssminstanceid"
AWS_REGION="ap-northeast-1"
REGISTRATION_FILE="/var/lib/amazon/ssm/registration"
cleanup() {
# コンテナ終了時、マネージドインスタンス登録を解除
echo "Deregister a managed instance..."
aws ssm deregister-managed-instance --instance-id "$(cat "${REGISTRATION_FILE}" | jq -r .ManagedInstanceID)" || true
exit 0
}
# エラー対処
amazon-ssm-agent stop
rm -rf /var/lib/amazon/ssm/ipc
# マネージドインスタンスのアクティベーションを作成
ACTIVATION_PARAMETERS=$(aws ssm create-activation \
--description "Activation Code for Fargate Bastion" \
--default-instance-name bastion \
--iam-role ${SSM_SERVICE_ROLE_NAME} \
--registration-limit 1 \
--tags Key=Type,Value=Bastion \
--region ${AWS_REGION})
# アクティベーションIDとアクティベーションコードの変数化
SSM_ACTIVATION_ID=$(echo ${ACTIVATION_PARAMETERS} | jq -r .ActivationId)
SSM_ACTIVATION_CODE=$(echo ${ACTIVATION_PARAMETERS} | jq -r .ActivationCode)
# アクティベーションIDとアクティベーションコードを使用してマネージインスタンスの登録
result=$(amazon-ssm-agent -register -code "${SSM_ACTIVATION_CODE}" -id "${SSM_ACTIVATION_ID}" -region ${AWS_REGION})
trap "cleanup" EXIT ERR
echo "${result}"
# マネージドインスタンスIDを取得
MANAGED_INSTANCE_ID=$(echo ${result} | grep -Eo "instance-id: .*$" | grep -Eo "[^ ]*$")
echo "Managed instance-id: ${MANAGED_INSTANCE_ID}"
# ParameterStoreにマネージドインスタンスIDを登録
# 該当のParameterStoreが存在しなくても自動的に作成してくれます。
aws ssm put-parameter --name "${SSM_PARAMETER_NAME}" --value "${MANAGED_INSTANCE_ID}" --type String --overwrite
# マネージドインスタンスのアクティベーションの削除
aws ssm delete-activation --activation-id ${SSM_ACTIVATION_ID}
# ssm-agentの起動
amazon-ssm-agent
ECRにイメージをPush
# build
docker build -t portforward ./docker/ssm
# タグ
docker tag portforward:latest xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/portforward:latest
# ecrにログイン
aws ecr get-login-password --region ap-northeast-1 ーーprofile <awscli profile名> | docker login --username AWS --password-stdin xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com
# push
docker push xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/portforward:latest
Script実行
※ECS周り(Cluster、Service、タスク定義など)やAuroraなどは既にあるものとして話を進めますが、TaskRoleは以下の権限を付与します。
- SessionManager用ポリシー
- "ssm:DeleteActivation"の許可
- "ssm:RemoveTagsFromResource"の許可
- "ssm:AddTagsToResource"の許可
- "ssm:CreateActivation"の許可
- "ssm:DeregisterManagedInstance"の許可
- ParameterStore用ポリシー
- "ssm:PutParameter"の許可
- "ssm:GetParameter*"の許可
- "ssm:DescribeParameters"の許可
- SessionManager-PassRole
- "iam:PassRole"の許可
- iam:PassedToService: "ssm.amazonaws.com"の条件付与
- "iam:PassRole"の許可
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"ssm:DescribeParameters",
"ssm:GetParameter*",
"ssm:PutParameter"
],
"Resource": "*",
"Effect": "Allow",
"Sid": "AccesstoParameterStore"
},
{
"Action": [
"ssm:DeleteActivation",
"ssm:RemoveTagsFromResource",
"ssm:AddTagsToResource",
"ssm:CreateActivation",
"ssm:DeregisterManagedInstance"
],
"Resource": "*",
"Effect": "Allow",
"Sid": "SessionManager"
},
{
"Condition": {
"StringEquals": {
"iam:PassedToService": "ssm.amazonaws.com"
}
},
"Action": "iam:PassRole",
"Resource": "*",
"Effect": "Allow",
"Sid": "SessionManagerPassRole"
}
]
}
ローカルから下記Scriptを実行します。
これにより、以下も併せて実行されます。
- コンテナが起動
- コンテナ内のsetting.shが実行
- SystemsManagerにコンテナを管理ノードとして登録
- ノードIDをParameterStoreに登録
- ssm-agentを起動
- コンテナ起動/ポートフォワード用scriptにてノードIDを取得
- 取得したノードIDを用いてセッションの確立
./portforward.sh <awscli-profile>
※ECSやAuroraなど関連リソースの命名は[xxx-stg-xxxx]のように環境値を入れてそれぞれ立てている前提で書いてます。
#!/bin/bash
## 第1引数にAWScliのprofileを指定
PROFILE=$1
cd $(dirname $(readlink $0 || echo $0))
CUR_DIR=$(pwd)
echo "対象環境を数字から選択してください"
select ENV in "prod" "stg" "dev"
do
echo "${ENV}が選択されました。"
ENV=${ENV}
break
done
if [ -z $PROFILE ]; then
echo "aws cli プロファイル名を数字から選択してください"
PROFILES=$(aws configure list-profiles)
select PROFILE in $PROFILES
do
echo "${PROFILE}が選択されました。"
PROFILE=${PROFILE}
break
done
fi
##--------------
## コンテナ起動処理
##--------------
echo "Clusterを数字から選択してください"
CLUSTERS=$(aws ecs list-clusters --profile $PROFILE --output text | sed -e 's/.*cluster\///' | grep $ENV)
select CLUSTER in $CLUSTERS
do
echo "${CLUSTER}が選択されました。"
CLUSTER=${CLUSTER}
break
done
echo "Serviceを数字から選択してください"
SERVICES=$(aws ecs list-services --cluster ${CLUSTER} --profile $PROFILE --output text | sed -e 's/.*'$CLUSTER'\///')
select SERVICE in $SERVICES
do
echo "${SERVICE}が選択されました。"
SERVICE=${SERVICE}
break
done
SUBNETS=$(aws ecs describe-services \
--cluster ${CLUSTER} \
--services ${SERVICE} \
--profile $PROFILE \
--query 'services[].deployments[].networkConfiguration[].awsvpcConfiguration[].subnets[]')
SECURITY_GROUPS=$(aws ecs describe-services \
--cluster ${CLUSTER} \
--services ${SERVICE} \
--profile $PROFILE \
--query 'services[].deployments[].networkConfiguration[].awsvpcConfiguration[].securityGroups[]')
TASK_DEFINITION=$(aws ecs describe-services \
--cluster ${CLUSTER} \
--services ${SERVICE} \
--profile $PROFILE \
--query 'services[].taskDefinition[]' \
--output text | sed -e 's/.*task-definition\///')
NETWORK_CONFIGURATION="awsvpcConfiguration={subnets=${SUBNETS},securityGroups=${SECURITY_GROUPS},assignPublicIp=DISABLED}"
RUN_TASK=$(aws ecs run-task \
--cluster ${CLUSTER} \
--task-definition ${TASK_DEFINITION} \
--network-configuration="${NETWORK_CONFIGURATION}" \
--enable-execute-command \
--platform-version 'LATEST' \
--launch-type=FARGATE \
--profile $PROFILE \
--query 'tasks[].containers[].taskArn' \
--output text)
EXEC_WAITING_SEC=20
echo "コンテナを起動しています。 ${EXEC_WAITING_SEC} 秒お待ち下さい。";
sleep ${EXEC_WAITING_SEC};
##-----------------
## ポートフォワードセッション処理
##-----------------
echo "接続するDatabaseを数字から選択してください"
TARGET_HOSTS=$(aws rds describe-db-clusters \
--profile $PROFILE \
--query 'DBClusters[?contains(Endpoint, `'$ENV'`)].[Endpoint]' \
--output text)
select TARGET_HOST in $TARGET_HOSTS
do
echo "${TARGET_HOST}が選択されました。"
TARGET_HOST=${TARGET_HOST}
break
done
LOCAL_DB_PORT="13306"
TARGET_DB_PORT="3306"
REGION="ap-northeast-1"
APP_NAME="Bastion"
SSM_PARAMETER_NAME="/${ENV}/portforward/ssminstanceid"
TUNNEL_LOG_DIR_PATH="${CUR_DIR}/tunnel_logs"
TUNNEL_LOG_FILE_PATH="${TUNNEL_LOG_DIR_PATH}/$(date +%Y_%m%d_%H%M%S).log"
TARGET=$(aws ssm get-parameters \
--name "${SSM_PARAMETER_NAME}" \
--profile $PROFILE \
--query 'Parameters[].Value' \
--output text)
PARAMETERS="{\"host\":[\"${TARGET_HOST}\"],\"portNumber\":[\"${TARGET_DB_PORT}\"], \"localPortNumber\":[\"${LOCAL_DB_PORT}\"]}"
mkdir -p ${TUNNEL_LOG_DIR_PATH}
touch ${TUNNEL_LOG_FILE_PATH}
aws ssm start-session \
--target ${TARGET} \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters "${PARAMETERS}" \
--region ${REGION} \
--profile $PROFILE > ${TUNNEL_LOG_FILE_PATH} &
# defaultでは20分。SystemsManagerのSessionManager設定で変更できます。
echo "セッションを開始しました。"
echo "20分経つとセッション接続が切れます。"
# 1日経ったログは消す
find ${TUNNEL_LOG_DIR_PATH} -type f -mtime +1 | xargs rm
##-----------------
## 終了処理
##-----------------
TASK_ARN=$RUN_TASK
echo "作業が終わったらコンテナを削除してください。"
function try_exec {
while true; do
echo -n "$* [y/n]: "
read YN
case $YN in
[Yy]*)
return 0
;;
[Nn]*)
return 1
;;
*)
echo "yまたはnを入力してください"
;;
esac
done
}
if try_exec "コンテナを削除しますか?"; then
STOP_CMD=$(aws ecs stop-task --cluster $CLUSTER --task $TASK_ARN --profile $PROFILE)
echo "コンテナ削除を実行しました。Scriptを終了します。"
else
echo "コンテナを削除せずScriptを終了します。"
echo "コンテナの削除を忘れずに実施してください。"
fi
Scriptを実行してコンテナが起動するとフリートマネージャー上ではノードが登録されます。
また、ParameterStore上には上記のノードIDが値として保管されます。
MysqlClientで接続
お好みのClientを使ってlocalhost:13306にアクセスするとリモートホストポートフォワードでAuroraにアクセスすることが出来ます。
Mysql Workbenchで試してみましょう。
接続情報を入力して、「Test Connection」を押下します。
「Successfully made the MySQL connection」が表示されたので問題なく成功していますね。
作業が完了したらターミナルの案内に従って「Y」を入力してエンターを押下し、コンテナを削除してください。
コンテナが削除されると、フリートマネージャーに登録されていたノードも併せて解除されます。
纏め
Clientをインストールした踏み台を用意せずにローカルのClientからRDSにアクセス出来る様になったので、どうしてもという時非常に便利です!また、セッションマネージャーによるアクセスはIAM内で条件付け(IP、MFA必須、ユーザなど)を行うことが可能になるのでよりセキュアに運用できるのも嬉しいポイントですね。
参考
- Scriptなど参考にさせて頂きました
- 権限を絞りたい場合