概要
先日リリースされたSystems Manager セッションマネージャーのリモートホストへのポートフォワード機能を使って、ローカルから直接プライベートサブネットのRDSなどへトンネリングする環境を、ECS on Fargateで構築してみました。
具体的には、プライベートサブネットにあるMySQLやPostgreSQLに、ローカルPCのターミナル等から直接アクセスできるようになります。
目次
リモートホストへのポートフォワード
Systems Manager(SSM)のセッションマネージャーのリモートホストへのポートフォワード機能ですが、以下の記事に説明を記載しています。
システム構成
処理のフローとしては、以下の流れになります。
ECS on Fargate
クライアント(ローカルPC)
- SSMパラメータストアに登録したインスタンスIDを取得
- 取得したインスタンスIDに対し、SSMセッションマネージャーでリモートホストへのポートフォワードを張る
- AWS-StartPortForwardingSessionToRemoteHost
- リモートホスト(MySQL, PostgreSQLなど)のポートへ通信できるようになる
※補足(現在はこの構成でなくても出来ます!)
SSMセッションマネージャーのリモートホストのポートフォワードの機能が出た当初は、ECS Fargate内にデフォルトで入っているSSMエージェントのバージョンが古くそのままでは使えませんでした。
そのため本記事では、「SSMエージェントをわざわざDockerfile内でインストール」して、かつ「コンテナ内からSSMマネージドインスタンス(ノード)の登録」をして使用しています。
現在はリモートホストのポートフォワード対応済みのバージョンがFargate内に入っているためこれらは必要がなく、ECSを立ち上げた後に以下のようなコマンドでポートフォワードが可能になっています。
また、記事に出てくるSSMパラメータストアなども必要ありません。
※targetにマネージドインスタンスを指定するのではなく、ecs:(クラスター)_(タスクID)_(コンテナID)
を指定するだけでOK
TARGET="ecs:${CLUSTER}_${TASK_ID}_${CONTAINER_ID}" PARAMETERS="{\"host\":[\"${DB_HOST}\"],\"portNumber\":[\"${REMOTE_DB_PORT}\"], \"localPortNumber\":[\"${LOCAL_DB_PORT}\"]}" aws ssm start-session \ --target ${TARGET} \ --document-name AWS-StartPortForwardingSessionToRemoteHost \ --parameters "${PARAMETERS}"
またDockerfileも以下のようなシンプルなものでOKです。
FROM arm64v8/ubuntu:20.04 WORKDIR / ENTRYPOINT ["tail", "-F", "/dev/null"]
前提
プライベートサブネットのFargateからSSMやECRにアクセスするためにはNAT GatewayやVPCエンドポイントが必要になりますが、本記事ではすでにあることを前提として省略しています。
構築
CloudFormationで構築します。
デプロイ用のシェルファイルを叩くだけで、AWS環境にECS on Fargate + Systems Managerによるトンネリング環境が構築されるようになっています。
また実際にローカル環境からトンネリングする際も、1コマンドでいけるようスクリプトを提供し、パスを通すところまでも用意しています。
※上記前提の通りNAT GatewayやVPCエンドポイント、またVPCなどのリソースも構築済みを前提としているため、本CloudFormationテンプレートには含まれていません。 (それをテンプレートのパラメータとして渡すようにしています。)
※全コードは以下GitHubに載せています。詳しい使い方はREADMEにあります。
スタック・スクリプト構成
デプロイスクリプト・CloudFormation
以下のようなデプロイスクリプト・CloudFormationスタック構成になっています。
- 01_deploy_preparation.sh
- preparation.yaml
- ECR
- SSMパラメータストア
- preparation.yaml
- 02_build_ecs.sh
- Dockerfileのビルド(CloudFormationスタックなし)
- 03_deploy_ecs.sh
ローカル実行スクリプト
また、ローカルPCで実行するスクリプトは以下になります。
詳細解説(デプロイ編)
重要なところを抜粋して解説します。
01_deploy_preparation.sh
01_deploy_preparation.sh 全文(折りたたみ)
#!/bin/bash
set -eu
cd $(dirname $0) && cd ../../
trap 'echo Error Occurred!!! Exit...' ERR
CFN_TEMPLATE="./deploy_scripts/resources/preparation.yaml"
SESSION_MANAGER_RUN_SHELL_CONTENT_PATH="./deploy_scripts/sessionManagerRunShell.json"
DEPLOYMODE="on"
PROFILE=""
APPNAME="Bastion"
REGION="ap-northeast-1"
while getopts p:d: OPT; do
case $OPT in
p)
PROFILE="$OPTARG"
;;
d)
DEPLOYMODE="$OPTARG"
;;
esac
done
if [ "${DEPLOYMODE}" != "on" -a "${DEPLOYMODE}" != "off" ]; then
echo "required DEPLOYMODE"
echo "[-p (profile)](option): aws profile name"
echo "[-d on|off](option): deploy mode (off=change set mode)"
exit 0
fi
function deployPreparation {
local profileOption=""
if [ -n "${1:-}" ]; then
profileOption="--profile ${profile}"
fi
local repositoryName=$(echo "${APPNAME}-ECR" | tr '[:upper:]' '[:lower:]')
local stackName="${APPNAME}-Preparation"
local changesetOption="--no-execute-changeset"
if [ "${DEPLOYMODE}" == "on" ]; then
echo "deploy mode"
changesetOption=""
fi
aws cloudformation deploy \
--stack-name ${stackName} \
--region ${REGION} \
--template-file ${CFN_TEMPLATE} \
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \
--no-fail-on-empty-changeset \
${changesetOption} \
--parameter-overrides \
AppName=${APPNAME} \
RepositoryName=${repositoryName} \
${profileOption}
if [ "${DEPLOYMODE}" == "on" ]; then
local accountId=$(aws sts get-caller-identity --query "Account" --output text ${profileOption})
local settingId="arn:aws:ssm:${REGION}:${accountId}:servicesetting/ssm/managed-instance/activation-tier"
local settingValue=$(aws ssm get-service-setting --setting-id ${settingId} ${profileOption} | jq -r .ServiceSetting.settingValue)
if [ "${settingValue}" == "standard" ]; then
aws ssm update-service-setting \
--setting-id ${settingId} \
--setting-value advanced \
${profileOption}
fi
local documentCount=$( \
aws ssm list-documents \
--filters Key=Name,Values=SSM-SessionManagerRunShell \
${profileOption} \
| jq '.DocumentIdentifiers|length' \
)
if [ ${documentCount} -eq 0 ]; then
aws ssm create-document \
--name "SSM-SessionManagerRunShell" \
--content "file://${SESSION_MANAGER_RUN_SHELL_CONTENT_PATH}" \
--document-type "Session" \
${profileOption} > /dev/null
else
local currentSessionManagerRunShellDocument=$( \
aws ssm get-document \
--name "SSM-SessionManagerRunShell" \
--document-version \$LATEST \
${profileOption} \
| jq -r .Content \
| jq -c . \
)
local newSessionManagerRunShellDocument=$( \
cat "${SESSION_MANAGER_RUN_SHELL_CONTENT_PATH}" \
| jq -c '.' \
)
if [ "${currentSessionManagerRunShellDocument}" != "${newSessionManagerRunShellDocument}" ]; then
aws ssm update-document \
--name "SSM-SessionManagerRunShell" \
--content "file://${SESSION_MANAGER_RUN_SHELL_CONTENT_PATH}" \
--document-version "\$LATEST" \
${profileOption} > /dev/null
fi
fi
fi
}
deployPreparation "${PROFILE:-}"
preparation.yaml 全文(折りたたみ)
AWSTemplateFormatVersion: '2010-09-09'
Description: ECR and SSM Parameter Store
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------#
Metadata:
"AWS::CloudFormation::Interface":
ParameterGroups:
- Label:
default: "AppName"
Parameters:
- AppName
- Label:
default: "RepositoryName"
Parameters:
- RepositoryName
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
AppName:
Type: String
RepositoryName:
Type: String
# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#
Resources:
ECR:
Type: AWS::ECR::Repository
Properties:
RepositoryName: !Ref RepositoryName
ImageTagMutability: IMMUTABLE
ImageScanningConfiguration:
ScanOnPush: "true"
LifecyclePolicy:
LifecyclePolicyText: |
{
"rules": [
{
"rulePriority": 1,
"description": "Delete more than 20 images",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 20
},
"action": {
"type": "expire"
}
}
]
}
Tags:
- Key: Name
Value: !Sub "${AppName}-ECR"
ManagedInstanceIDParameter:
Type: AWS::SSM::Parameter
Properties:
Name: !Sub "/ManagedInstanceIDParameter/${AppName}"
Type: "String"
Value: "ManagedInstanceIDParameter"
Description: "Managed Instance ID Parameter"
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------#
Outputs:
#ECRRepositoryUri:
# Export:
# Name: !Sub "${AppName}-ECR-RepositoryUri"
# Description: "ECR RepositoryUri"
# Value: !GetAtt ECR.RepositoryUri #Not currently supported by AWS CloudFormation.
ECRArn:
Export:
Name: !Sub "${AppName}-ECR-Arn"
Description: "ECR ARN"
Value: !GetAtt ECR.Arn
ManagedInstanceIDParameterName:
Export:
Name: !Sub "${AppName}-SSM-ManagedInstanceIDParameterName"
Description: "Managed Instance ID Parameter Name"
Value: !Ref ManagedInstanceIDParameter
アドバンスドインスタンスティア
FargateでSSMエージェントを起動し、マネージドインスタンスとして登録するためには、Systems Managerのインスタンスティアの設定をアドバンスドインスタンスティアに変更する必要があります。
local accountId=$(aws sts get-caller-identity --query "Account" --output text ${profileOption}) local settingId="arn:aws:ssm:${REGION}:${accountId}:servicesetting/ssm/managed-instance/activation-tier" local settingValue=$(aws ssm get-service-setting --setting-id ${settingId} ${profileOption} | jq -r .ServiceSetting.settingValue) if [ "${settingValue}" == "standard" ]; then aws ssm update-service-setting \ --setting-id ${settingId} \ --setting-value advanced \ ${profileOption} fi
General preferences
また、SSMセッションマネージャーの接続設定(General preferences)も自由に変更できるようにしました。(すでに設定があれば更新、なければ作成)
local documentCount=$( \ aws ssm list-documents \ --filters Key=Name,Values=SSM-SessionManagerRunShell \ ${profileOption} \ | jq '.DocumentIdentifiers|length' \ ) if [ ${documentCount} -eq 0 ]; then aws ssm create-document \ --name "SSM-SessionManagerRunShell" \ --content "file://${SESSION_MANAGER_RUN_SHELL_CONTENT_PATH}" \ --document-type "Session" \ ${profileOption} > /dev/null else local currentSessionManagerRunShellDocument=$( \ aws ssm get-document \ --name "SSM-SessionManagerRunShell" \ --document-version \$LATEST \ ${profileOption} \ | jq -r .Content \ | jq -c . \ ) local newSessionManagerRunShellDocument=$( \ cat "${SESSION_MANAGER_RUN_SHELL_CONTENT_PATH}" \ | jq -c '.' \ ) if [ "${currentSessionManagerRunShellDocument}" != "${newSessionManagerRunShellDocument}" ]; then aws ssm update-document \ --name "SSM-SessionManagerRunShell" \ --content "file://${SESSION_MANAGER_RUN_SHELL_CONTENT_PATH}" \ --document-version "\$LATEST" \ ${profileOption} > /dev/null fi fi
sessionManagerRunShell.json
この設定ファイルでタイムアウト設定などができます。
{ "schemaVersion": "1.0", "description": "Document to hold regional settings for Session Manager", "sessionType": "Standard_Stream", "inputs": { "s3BucketName": "", "s3KeyPrefix": "", "s3EncryptionEnabled": true, "cloudWatchLogGroupName": "", "cloudWatchEncryptionEnabled": true, "idleSessionTimeout": "30", "maxSessionDuration": "30", "cloudWatchStreamingEnabled": true, "kmsKeyId": "", "runAsEnabled": false, "runAsDefaultUser": "", "shellProfile": { "windows": "", "linux": "" } } }
02_build_ecs.sh
Dockerfile(後述)をビルドし、ECRにプッシュします。
02_build_ecs.sh 全文(折りたたみ)
#!/bin/bash
set -eu
cd $(dirname $0) && cd ../../
trap 'echo Error Occurred!!! Exit...' ERR
PROFILE=""
APPNAME="Bastion"
REGION="ap-northeast-1"
while getopts p: OPT; do
case $OPT in
p)
PROFILE="$OPTARG"
;;
esac
done
function buildECS {
local profileOption=""
if [ -n "${1:-}" ]; then
profileOption="--profile ${profile}"
fi
#### docker build & push
local repositoryName=$(echo "${APPNAME}-ECR" | tr '[:upper:]' '[:lower:]')
local accountId=$(aws sts get-caller-identity --query "Account" --output text ${profileOption})
local repositoryEnddpoint="${accountId}.dkr.ecr.ap-northeast-1.amazonaws.com"
local repositoryUri="${repositoryEnddpoint}/${repositoryName}"
local ecrTag="$(git rev-parse HEAD)"
local ecrTagPrevious=$(aws ecr describe-images --repository-name ${repositoryName} \
--query "reverse(sort_by(imageDetails[*], &imagePushedAt))[0].imageTags[0]" \
${profileOption} |
sed -e 's/"//g')
docker build \
--cache-from ${ecrTagPrevious} \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-t ${repositoryName} \
.
docker tag ${repositoryName}:latest ${repositoryUri}:${ecrTag}
### Dockle
local dockleVersion=$(
curl --silent "https://api.github.com/repos/goodwithtech/dockle/releases/latest" | \
grep '"tag_name":' | \
sed -E 's/.*"v([^"]+)".*/\1/' \
)
docker run \
--rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $(pwd)/.dockleignore:/.dockleignore \
-e AWS_DEFAULT_REGION=${REGION} \
goodwithtech/dockle:v${dockleVersion} \
--exit-code 1 \
--exit-level "FATAL" \
${repositoryUri}:${ecrTag}
aws ecr get-login-password --region ${REGION} ${profileOption} |
docker login --username AWS --password-stdin ${repositoryEnddpoint}
docker push ${repositoryUri}:${ecrTag}
}
buildECS "${PROFILE:-}"
Dockle
本題と関係ないのですが、dockerの脆弱性検査ツールである「Dockle」によるセキュリティ検査を挟んでいます。
### Dockle local dockleVersion=$( curl --silent "https://api.github.com/repos/goodwithtech/dockle/releases/latest" | \ grep '"tag_name":' | \ sed -E 's/.*"v([^"]+)".*/\1/' \ ) docker run \ --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v $(pwd)/.dockleignore:/.dockleignore \ -e AWS_DEFAULT_REGION=${REGION} \ goodwithtech/dockle:v${dockleVersion} \ --exit-code 1 \ --exit-level "FATAL" \ ${repositoryUri}:${ecrTag}
無視するエラー・ワーニングもファイルとして定義できます。
# Clear apt-get caches ->BUILDKIT使っていてクリアしないため DKL-DI-0005
Dockerfile
肝心なFargateに載せるコンテナ部分です。
まずDockerfileですが、イメージはubuntuで、BuildKitを使用しています。SSMエージェントをインストールし、後述するシェルスクリプトを起動します。
Dockerfile
# syntax=docker/dockerfile:1 FROM 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 \ && mv /etc/amazon/ssm/seelog.xml.template /etc/amazon/ssm/seelog.xml \ && rm /tmp/amazon-ssm-agent.deb COPY deploy_scripts/run.sh /run.sh CMD ["bash", "/run.sh"]
run.sh
次にコンテナ内で実行するシェルスクリプトです。
具体的にはSSMエージェントを起動したり、マネージドインスタンス登録する処理になります。
run.sh 全文(折りたたみ)
#!/bin/bash
set -e
SSM_SERVICE_ROLE_NAME="${APP_NAME}-SSMServiceRole"
SSM_PARAMETER_NAME="/ManagedInstanceIDParameter/${APP_NAME}"
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})
SSM_ACTIVATION_ID=$(echo ${ACTIVATION_PARAMETERS} | jq -r .ActivationId)
SSM_ACTIVATION_CODE=$(echo ${ACTIVATION_PARAMETERS} | jq -r .ActivationCode)
result=$(amazon-ssm-agent -register -code "${SSM_ACTIVATION_CODE}" -id "${SSM_ACTIVATION_ID}" -region ${AWS_REGION})
trap "cleanup" EXIT ERR
echo "${result}"
MANAGED_INSTANCE_ID=$(echo ${result} | grep -Eo "instance-id: .*$" | grep -Eo "[^ ]*$")
echo "Managed instance-id: ${MANAGED_INSTANCE_ID}"
aws ssm put-parameter --name "${SSM_PARAMETER_NAME}" --value "${MANAGED_INSTANCE_ID}" --type String --overwrite
aws ssm delete-activation --activation-id ${SSM_ACTIVATION_ID}
amazon-ssm-agent
エラー対処
いきなりエラー対処とありますが、これがないとエラーが出たため、最初に削除しています。
# エラー対処 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}) SSM_ACTIVATION_ID=$(echo ${ACTIVATION_PARAMETERS} | jq -r .ActivationId) SSM_ACTIVATION_CODE=$(echo ${ACTIVATION_PARAMETERS} | jq -r .ActivationCode) result=$(amazon-ssm-agent -register -code "${SSM_ACTIVATION_CODE}" -id "${SSM_ACTIVATION_ID}" -region ${AWS_REGION})
SSMパラメータストアにインスタンスID登録
ログからインスタンスIDを抽出し、それをSSMパラメータストア(事前にCloudFormationにて作成済み)に登録します。
MANAGED_INSTANCE_ID=$(echo ${result} | grep -Eo "instance-id: .*$" | grep -Eo "[^ ]*$") echo "Managed instance-id: ${MANAGED_INSTANCE_ID}" aws ssm put-parameter --name "${SSM_PARAMETER_NAME}" --value "${MANAGED_INSTANCE_ID}" --type String --overwrite
アクティベーション削除・SSMエージェント起動
先ほど作成したアクティベーションを削除し、最後にSSMエージェントを起動します。
aws ssm delete-activation --activation-id ${SSM_ACTIVATION_ID} amazon-ssm-agent
終了時の後処理
また、trapでコンテナ終了時にマネージドインスタンスの登録を解除するようにします。
cleanup() { # コンテナ終了時、マネージドインスタンス登録を解除 echo "Deregister a managed instance..." aws ssm deregister-managed-instance --instance-id "$(cat "${REGISTRATION_FILE}" | jq -r .ManagedInstanceID)" || true exit 0 } ... ... ... trap "cleanup" EXIT ERR
03_deploy_ecs.sh
最後にECSを構築・デプロイするところです。
03_deploy_ecs.sh 全文(折りたたみ)
#!/bin/bash
set -eu
cd $(dirname $0) && cd ../../
trap 'echo Error Occurred!!! Exit...' ERR
CFN_TEMPLATE="./deploy_scripts/resources/ecs.yaml"
CONFIG_FILE_NAME="./deploy_scripts/ecs.config"
DEPLOYMODE="on"
PROFILE=""
APPNAME="Bastion"
REGION="ap-northeast-1"
VPC_ID="vpc-*****************"
SUBNET_ID1="subnet-*****************"
SUBNET_ID2="subnet-*****************"
while getopts p:d: OPT; do
case $OPT in
p)
PROFILE="$OPTARG"
;;
d)
DEPLOYMODE="$OPTARG"
;;
esac
done
if [ "${DEPLOYMODE}" != "on" -a "${DEPLOYMODE}" != "off" ]; then
echo "required DEPLOYMODE"
echo "[-p (profile)](option): aws profile name"
echo "[-d on|off](option): deploy mode (off=change set mode)"
exit 0
fi
function deployECS {
local profileOption=""
if [ -n "${1:-}" ]; then
profileOption="--profile ${profile}"
fi
local stackName="${APPNAME}-ECS"
if ! [ -f "${CONFIG_FILE_NAME}" ]; then
echo "====================================="
echo "[${CONFIG_FILE_NAME}]ファイルがありません"
echo "====================================="
return 1
fi
source "${CONFIG_FILE_NAME}"
if [ -z "${ECSTaskCPUUnit:-}" ] \
|| [ -z "${ECSTaskMemory:-}" ] \
|| [ -z "${ECSRestMemory:-}" ] \
|| [ -z "${ECSTaskDesiredCount:-}" ] \
|| [ -z "${TaskMinContainerCount:-}" ] \
|| [ -z "${TaskMaxContainerCount:-}" ] \
|| [ -z "${TaskMinContainerCountDuringOffPeakTime:-}" ] \
|| [ -z "${TaskMaxContainerCountDuringOffPeakTime:-}" ] \
|| [ -z "${OffPeakStartTimeCron:-}" ] \
|| [ -z "${OffPeakEndTimeCron:-}" ] \
|| [ -z "${ECSDeploymentMaximumPercent:-}" ] \
|| [ -z "${ECSDeploymentMinimumHealthyPercent:-}" ] \
|| [ -z "${ServiceScaleEvaluationPeriods:-}" ] \
|| [ -z "${ServiceCpuScaleOutThreshold:-}" ] \
|| [ -z "${ServiceCpuScaleInThreshold:-}" ]; then
echo "コンフィグファイルに設定漏れがあります"
echo "ECSTaskCPUUnit: ${ECSTaskCPUUnit:-}"
echo "ECSTaskMemory: ${ECSTaskMemory:-}"
echo "ECSRestMemory: ${ECSRestMemory:-}"
echo "ECSTaskDesiredCount: ${ECSTaskDesiredCount:-}"
echo "TaskMinContainerCount: ${TaskMinContainerCount:-}"
echo "TaskMaxContainerCount: ${TaskMaxContainerCount:-}"
echo "TaskMinContainerCountDuringOffPeakTime: ${TaskMinContainerCountDuringOffPeakTime:-}"
echo "TaskMaxContainerCountDuringOffPeakTime: ${TaskMaxContainerCountDuringOffPeakTime:-}"
echo "OffPeakStartTimeCron: ${OffPeakStartTimeCron:-}"
echo "OffPeakEndTimeCron: ${OffPeakEndTimeCron:-}"
echo "ECSDeploymentMaximumPercent: ${ECSDeploymentMaximumPercent:-}"
echo "ECSDeploymentMinimumHealthyPercent: ${ECSDeploymentMinimumHealthyPercent:-}"
echo "ServiceScaleEvaluationPeriods: ${ServiceScaleEvaluationPeriods:-}"
echo "ServiceCpuScaleOutThreshold: ${ServiceCpuScaleOutThreshold:-}"
echo "ServiceCpuScaleInThreshold: ${ServiceCpuScaleInThreshold:-}"
return 1
fi
#### docker build & push
local repositoryName=$(echo "${APPNAME}-ECR" | tr '[:upper:]' '[:lower:]')
local accountId=$(aws sts get-caller-identity --query "Account" --output text ${profileOption})
local repositoryEnddpoint="${accountId}.dkr.ecr.ap-northeast-1.amazonaws.com"
local repositoryUri="${repositoryEnddpoint}/${repositoryName}"
local ecrTag="$(git rev-parse HEAD)"
#### parameters for ecs
local ecsImageName="${repositoryUri}:${ecrTag}"
local ecsAppTaskMemoryReservation=$(expr ${ECSTaskMemory} - ${ECSRestMemory})
local s3BucketNameForECSExecLogs=$(echo "ecs-exec-logs-${APPNAME}-${accountId}" | tr '[:upper:]' '[:lower:]')
local changesetOption="--no-execute-changeset"
if [ "${DEPLOYMODE}" == "on" ]; then
echo "deploy mode"
changesetOption=""
fi
aws cloudformation deploy \
--stack-name ${stackName} \
--region ${REGION} \
--template-file ${CFN_TEMPLATE} \
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \
--no-fail-on-empty-changeset \
${changesetOption} \
--parameter-overrides \
AppName=${APPNAME} \
VpcID="${VPC_ID}" \
SubnetID1="${SUBNET_ID1}" \
SubnetID2="${SUBNET_ID2}" \
ECSTaskCPUUnit=${ECSTaskCPUUnit} \
ECSTaskMemory=${ECSTaskMemory} \
ECSAppTaskMemoryReservation=${ecsAppTaskMemoryReservation} \
ECSImageName=${ecsImageName} \
ECSTaskDesiredCount=${ECSTaskDesiredCount} \
ECSDeploymentMaximumPercent=${ECSDeploymentMaximumPercent} \
ECSDeploymentMinimumHealthyPercent=${ECSDeploymentMinimumHealthyPercent} \
ServiceScaleEvaluationPeriods=${ServiceScaleEvaluationPeriods} \
ServiceCpuScaleOutThreshold=${ServiceCpuScaleOutThreshold} \
ServiceCpuScaleInThreshold=${ServiceCpuScaleInThreshold} \
TaskMinContainerCount=${TaskMinContainerCount} \
TaskMaxContainerCount=${TaskMaxContainerCount} \
TaskMinContainerCountDuringOffPeakTime=${TaskMinContainerCountDuringOffPeakTime} \
TaskMaxContainerCountDuringOffPeakTime=${TaskMaxContainerCountDuringOffPeakTime} \
OffPeakStartTimeCron="${OffPeakStartTimeCron}" \
OffPeakEndTimeCron="${OffPeakEndTimeCron}" \
S3BucketNameForECSExecLogs=${s3BucketNameForECSExecLogs} \
${profileOption}
}
deployECS "${PROFILE:-}"
ecs.yaml 全文(折りたたみ)
AWSTemplateFormatVersion: 2010-09-09
Description: ECS
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------#
Metadata:
"AWS::CloudFormation::Interface":
ParameterGroups:
- Label:
default: "AppName"
Parameters:
- AppName
- Label:
default: "VpcID"
Parameters:
- VpcID
- Label:
default: "SubnetID1"
Parameters:
- SubnetID1
- Label:
default: "SubnetID2"
Parameters:
- SubnetID2
- Label:
default: "S3BucketNameForECSExecLogs"
Parameters:
- S3BucketNameForECSExecLogs
- Label:
default: "Fargate for ECS Configuration"
Parameters:
- ECSImageName
- ECSTaskCPUUnit
- ECSTaskMemory
- ECSAppTaskMemoryReservation
- ECSTaskDesiredCount
- ECSDeploymentMaximumPercent
- ECSDeploymentMinimumHealthyPercent
- Label:
default: "Scaling Configuration"
Parameters:
- ServiceScaleEvaluationPeriods
- ServiceCpuScaleOutThreshold
- ServiceCpuScaleInThreshold
- TaskMinContainerCount
- TaskMaxContainerCount
- TaskMinContainerCountDuringOffPeakTime
- TaskMaxContainerCountDuringOffPeakTime
- OffPeakStartTimeCron
- OffPeakEndTimeCron
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
AppName:
Type: String
VpcID:
Type: String
SubnetID1:
Type: String
SubnetID2:
Type: String
S3BucketNameForECSExecLogs:
Type: String
ECSImageName:
Type: String
ECSTaskCPUUnit:
AllowedValues: [256, 512, 1024, 2048, 4096]
Type: String
Default: 256
ECSTaskMemory:
AllowedValues: [256, 512, 1024, 2048, 4096]
Type: String
Default: 512
ECSAppTaskMemoryReservation:
Type: Number
Default: 64
ECSTaskDesiredCount:
Type: Number
Default: 1
ECSDeploymentMaximumPercent:
Type: Number
Default: 200
ECSDeploymentMinimumHealthyPercent:
Type: Number
Default: 100
ServiceScaleEvaluationPeriods:
Type: Number
Default: 2
MinValue: 2
ServiceCpuScaleOutThreshold:
Type: Number
Description: Average CPU value to trigger auto scaling out
Default: 50
MinValue: 0
MaxValue: 100
ConstraintDescription: Value must be between 0 and 100
ServiceCpuScaleInThreshold:
Type: Number
Description: Average CPU value to trigger auto scaling in
Default: 25
MinValue: 0
MaxValue: 100
ConstraintDescription: Value must be between 0 and 100
TaskMinContainerCount:
Type: Number
Description: Minimum number of containers to run for the service
Default: 1
MinValue: 0
ConstraintDescription: Value must be >= 0
TaskMaxContainerCount:
Type: Number
Description: Maximum number of containers to run for the service when auto scaling out
Default: 2
MinValue: 0
ConstraintDescription: Value must be >= 0
TaskMinContainerCountDuringOffPeakTime:
Type: Number
Description: Minimum number of containers to run for the service during OffPeak time
Default: 1
MinValue: 0
ConstraintDescription: Value must be >= 0
TaskMaxContainerCountDuringOffPeakTime:
Type: Number
Description: Maximum number of containers to run for the service during OffPeak time
Default: 1
MinValue: 0
ConstraintDescription: Value must be >= 0
OffPeakStartTimeCron:
Type: String
OffPeakEndTimeCron:
Type: String
# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#
Resources:
# ------------------------------------------------------------#
# Security Group
# ------------------------------------------------------------#
ECSServiceSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
VpcId: !Ref VpcID
GroupName: !Sub "${AppName}-ECS-sg"
GroupDescription: "ECS security group"
Tags:
- Key: "Name"
Value: !Sub "${AppName}-ECS-sg"
# ------------------------------------------------------------#
# ECS Cluster
# ------------------------------------------------------------#
ECSCluster:
Type: "AWS::ECS::Cluster"
Properties:
ClusterName: !Sub "${AppName}-Cluster"
Configuration:
ExecuteCommandConfiguration:
LogConfiguration:
CloudWatchLogGroupName: !Ref EcsExecLogGroup
S3BucketName: !Ref S3BucketNameForECSExecLogs
Logging: OVERRIDE
ClusterSettings:
- Name: containerInsights
Value: disabled
# ------------------------------------------------------------#
# ECS LogGroup
# ------------------------------------------------------------#
ECSLogGroup:
Type: "AWS::Logs::LogGroup"
Properties:
LogGroupName: !Sub "/ecs/logs/${AppName}"
# ------------------------------------------------------------#
# ECS Exec LogGroup
# ------------------------------------------------------------#
EcsExecLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub "/ecs-exec/logs/${AppName}"
# ------------------------------------------------------------#
# ECS Exec Log Bucket
# ------------------------------------------------------------#
EcsExecLogBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref S3BucketNameForECSExecLogs
AccessControl: Private
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
# ------------------------------------------------------------#
# SSM Service Role
# ------------------------------------------------------------#
SSMServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${AppName}-SSMServiceRole"
Path: /
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: ssm.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
Policies:
- PolicyName: !Sub "${AppName}-SSMServiceRole-DeregisterManagedInstance-Policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "ssm:DeregisterManagedInstance"
Resource: "*"
# ------------------------------------------------------------#
# ECS Task Execution Role
# ------------------------------------------------------------#
ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${AppName}-ECSTaskExecutionRole"
Path: /
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
# ------------------------------------------------------------#
# ECS Task Role
# ------------------------------------------------------------#
ECSTaskRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${AppName}-ECSTaskRole"
Path: /
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: !Sub "${AppName}-SessionManager-SSM-Policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "ssm:DeleteActivation"
- "ssm:RemoveTagsFromResource"
- "ssm:AddTagsToResource"
- "ssm:CreateActivation"
- "ssm:DeregisterManagedInstance"
Resource: "*"
- PolicyName: !Sub "${AppName}-ParameterStore-SSM-Policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "ssm:PutParameter"
- "ssm:GetParameter*"
- "ssm:DescribeParameters"
Resource: "*"
- PolicyName: !Sub "${AppName}-SessionManager-PassRole-Policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "iam:PassRole"
Resource: "*"
Condition:
StringEquals:
iam:PassedToService: "ssm.amazonaws.com"
- PolicyName: !Sub "${AppName}-ECSExec-SSM-Policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "ssmmessages:CreateControlChannel"
- "ssmmessages:CreateDataChannel"
- "ssmmessages:OpenControlChannel"
- "ssmmessages:OpenDataChannel"
Resource: "*"
- PolicyName: !Sub "${AppName}-ECSExec-Logging-Policy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "logs:DescribeLogGroups"
Resource: "*"
- Effect: Allow
Action:
- "logs:DescribeLogStreams"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource: !GetAtt EcsExecLogGroup.Arn
- Effect: Allow
Action:
- "s3:PutObject"
Resource: !Sub 'arn:aws:s3:::${EcsExecLogBucket}/*'
- Effect: Allow
Action:
- "s3:GetBucketLocation"
Resource: "*"
- Effect: Allow
Action:
- "s3:GetEncryptionConfiguration"
Resource: !Sub 'arn:aws:s3:::${EcsExecLogBucket}'
# ------------------------------------------------------------#
# ECS TaskDefinition
# ------------------------------------------------------------#
ECSTaskDefinition:
Type: "AWS::ECS::TaskDefinition"
Properties:
Cpu: !Ref ECSTaskCPUUnit
ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
TaskRoleArn: !GetAtt ECSTaskRole.Arn
Family: !Sub "${AppName}-Task"
Memory: !Ref ECSTaskMemory
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ContainerDefinitions:
- Name: !Sub "${AppName}-Container"
Image: !Ref ECSImageName
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref ECSLogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: !Sub "${AppName}"
MemoryReservation: !Ref ECSAppTaskMemoryReservation
Environment:
- Name: APP_NAME
Value: !Ref AppName
# ------------------------------------------------------------#
# ECS Service
# ------------------------------------------------------------#
ECSService:
Type: AWS::ECS::Service
Properties:
Cluster: !Ref ECSCluster
DesiredCount: !Ref ECSTaskDesiredCount
DeploymentConfiguration:
DeploymentCircuitBreaker:
Enable: true
Rollback: true
MaximumPercent: !Ref ECSDeploymentMaximumPercent
MinimumHealthyPercent: !Ref ECSDeploymentMinimumHealthyPercent
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
SecurityGroups:
- !Ref ECSServiceSecurityGroup
Subnets:
- !Ref SubnetID1
- !Ref SubnetID2
ServiceName: !Sub "${AppName}-Service"
TaskDefinition: !Ref ECSTaskDefinition
EnableExecuteCommand: true
# ------------------------------------------------------------#
# Auto Scaling Service
# ------------------------------------------------------------#
ServiceAutoScalingRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: application-autoscaling.amazonaws.com
Action: sts:AssumeRole
Path: /
Policies:
- PolicyName: !Sub "${AppName}-Container-autoscaling"
PolicyDocument:
Statement:
- Effect: Allow
Action:
- application-autoscaling:*
- cloudwatch:DescribeAlarms
- cloudwatch:PutMetricAlarm
- ecs:DescribeServices
- ecs:UpdateService
Resource: "*"
ServiceScalingTarget:
Type: AWS::ApplicationAutoScaling::ScalableTarget
DependsOn:
- ECSService
Properties:
MinCapacity: !Ref TaskMinContainerCount
MaxCapacity: !Ref TaskMaxContainerCount
ResourceId: !Sub
- service/${EcsClusterName}/${EcsDefaultServiceName}
- EcsClusterName: !Ref ECSCluster
EcsDefaultServiceName: !Sub "${AppName}-Service"
RoleARN: !GetAtt ServiceAutoScalingRole.Arn
ScalableDimension: ecs:service:DesiredCount
ServiceNamespace: ecs
ScheduledActions:
- ScheduledActionName: OffPeakStartTime
Schedule: !Ref OffPeakStartTimeCron
Timezone: "Asia/Tokyo"
ScalableTargetAction:
MinCapacity: !Ref TaskMinContainerCountDuringOffPeakTime
MaxCapacity: !Ref TaskMaxContainerCountDuringOffPeakTime
- ScheduledActionName: OffPeakEndTime
Schedule: !Ref OffPeakEndTimeCron
Timezone: "Asia/Tokyo"
ScalableTargetAction:
MinCapacity: !Ref TaskMinContainerCount
MaxCapacity: !Ref TaskMaxContainerCount
ServiceScaleOutPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
Properties:
PolicyName: !Sub "${AppName}-Service-ScaleOutPolicy"
PolicyType: StepScaling
ScalingTargetId: !Ref ServiceScalingTarget
StepScalingPolicyConfiguration:
AdjustmentType: ChangeInCapacity
Cooldown: 60
MetricAggregationType: Average
StepAdjustments:
- ScalingAdjustment: 1
MetricIntervalLowerBound: 0
ServiceScaleInPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
Properties:
PolicyName: !Sub "${AppName}-Service-ScaleInPolicy"
PolicyType: StepScaling
ScalingTargetId: !Ref ServiceScalingTarget
StepScalingPolicyConfiguration:
AdjustmentType: ChangeInCapacity
Cooldown: 300
MetricAggregationType: Average
StepAdjustments:
- ScalingAdjustment: -1
MetricIntervalUpperBound: 0
ServiceScaleOutAlarm:
Type: AWS::CloudWatch::Alarm
DependsOn:
- ECSService
Properties:
AlarmName: !Sub "${AppName}-Service-ScaleOutAlarm"
EvaluationPeriods: !Ref ServiceScaleEvaluationPeriods
Statistic: Average
TreatMissingData: notBreaching
Threshold: !Ref ServiceCpuScaleOutThreshold
AlarmDescription: Alarm to add capacity if CPU is high
Period: 60
AlarmActions:
- !Ref ServiceScaleOutPolicy
Namespace: AWS/ECS
Dimensions:
- Name: ClusterName
Value: !Ref ECSCluster
- Name: ServiceName
Value: !Sub "${AppName}-Service"
ComparisonOperator: GreaterThanThreshold
MetricName: CPUUtilization
ServiceScaleInAlarm:
Type: AWS::CloudWatch::Alarm
DependsOn:
- ECSService
Properties:
AlarmName: !Sub "${AppName}-Service-ScaleInAlarm"
EvaluationPeriods: !Ref ServiceScaleEvaluationPeriods
Statistic: Average
TreatMissingData: notBreaching
Threshold: !Ref ServiceCpuScaleInThreshold
AlarmDescription: Alarm to reduce capacity if container CPU is low
Period: 300
AlarmActions:
- !Ref ServiceScaleInPolicy
Namespace: AWS/ECS
Dimensions:
- Name: ClusterName
Value: !Ref ECSCluster
- Name: ServiceName
Value: !Sub "${AppName}-Service"
ComparisonOperator: LessThanThreshold
MetricName: CPUUtilization
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------#
Outputs:
ECSServiceArn:
Export:
Name: !Sub "${AppName}-ECSService-Arn"
Value: !Ref ECSService
ECSClusterArn:
Export:
Name: !Sub "${AppName}-ECSCluster-Arn"
Value: !GetAtt ECSCluster.Arn
VPC設定
こちらは各自のVPC設定を入れてください。
VPC_ID="vpc-*****************" SUBNET_ID1="subnet-*****************" SUBNET_ID2="subnet-*****************"
ecs.config
細かいのですが、こちらでECSの細かい設定やスペックをコンフィグファイルとしていじれるようにしています。
ecs.config 全文(折りたたみ)
#### spec for ecs
ECSTaskCPUUnit="256" #[ 256, 512, 1024, 2048, 4096 ]
ECSTaskMemory="512" #[ 256, 512, 1024, 2048, 4096 ]
ECSRestMemory="64" # -> ECSAppTaskMemoryReservation=$(expr ${ECSTaskMemory} - ${ECSRestMemory})
#### autoscailing for ecs
ECSDeploymentMaximumPercent="200"
ECSDeploymentMinimumHealthyPercent="100"
ServiceScaleEvaluationPeriods="2"
ServiceCpuScaleOutThreshold="75"
ServiceCpuScaleInThreshold="25"
ECSTaskDesiredCount="1"
TaskMinContainerCount="1"
TaskMaxContainerCount="1"
TaskMinContainerCountDuringOffPeakTime="0"
TaskMaxContainerCountDuringOffPeakTime="0"
OffPeakStartTimeCron="cron(30 23 * * ? *)" # Timezone = "Asia/Tokyo"
OffPeakEndTimeCron="cron(30 9 * * ? *)" # Timezone = "Asia/Tokyo"
以下パラメータによって、夜間停止を実現しています。
具体的には、23:30~09:30はFargateを停止する設定になります。
停止したくない場合は、TaskMinContainerCountDuringOffPeakTime,TaskMaxContainerCountDuringOffPeakTimeを1に設定してください。
TaskMinContainerCountDuringOffPeakTime="0" TaskMaxContainerCountDuringOffPeakTime="0" OffPeakStartTimeCron="cron(30 23 * * ? *)" # Timezone = "Asia/Tokyo" OffPeakEndTimeCron="cron(30 9 * * ? *)" # Timezone = "Asia/Tokyo"
詳細解説(ローカル編)
tunnel.sh
こちらがローカルPCで、実際にSSMセッションマネージャーによってポートフォワードを張るときに叩くスクリプトになります。
tunnel.sh 全文(折りたたみ)
#!/bin/bash
set -eu
cd $(dirname $(readlink $0 || echo $0))
CUR_DIR=$(pwd)
LOCAL_DB_PORT="13306"
TARGET_DB_PORT="3306"
TARGET_HOST="abcde.cluster-1234567890.ap-northeast-1.rds.amazonaws.com"
REGION="ap-northeast-1"
PROFILE=""
APP_NAME="Bastion"
SSM_PARAMETER_NAME="/ManagedInstanceIDParameter/${APP_NAME}"
TUNNEL_LOG_DIR_PATH="${CUR_DIR}/tunnel_logs"
TUNNEL_LOG_FILE_PATH="${TUNNEL_LOG_DIR_PATH}/$(date +%Y_%m%d_%H%M%S).log"
while getopts p:l:t:h:r: OPT; do
case $OPT in
p)
PROFILE="$OPTARG"
;;
l)
LOCAL_DB_PORT="$OPTARG"
;;
t)
TARGET_DB_PORT="$OPTARG"
;;
h)
TARGET_HOST="$OPTARG"
;;
r)
REGION="$OPTARG"
;;
esac
done
if [ -z ${LOCAL_DB_PORT} ] || [ -z ${TARGET_DB_PORT} ] || [ -z ${TARGET_HOST} ] || [ -z ${REGION} ]; then
echo "ex) tunnel [-p profile] [-l 13306] [-t 3306] [-h host] [-r ap-northeast-1]"
echo "-p : AWSプロファイル(デフォルト : 空)"
echo "-l : ローカルポート(デフォルト : 13306)"
echo "-t : ターゲットポート(リモートポート)(デフォルト : 3306)"
echo "-h : ターゲットホスト(デフォルト : abcde.cluster-1234567890.ap-northeast-1.rds.amazonaws.com)"
echo "-r : ECSを構築したAWSリージョン(デフォルト : ap-northeast-1)"
exit 0
fi
profileOption=""
if [ -n "${PROFILE}" ]; then
profileOption="--profile ${profile}"
fi
target=$(aws ssm get-parameters \
--name "${SSM_PARAMETER_NAME}" \
${profileOption} \
| jq -r .Parameters[0].Value \
)
parameters="{\"host\":[\"${TARGET_HOST}\"],\"portNumber\":[\"${TARGET_DB_PORT}\"], \"localPortNumber\":[\"${LOCAL_DB_PORT}\"]}"
mkdir -p ${TUNNEL_LOG_DIR_PATH}
touch ${TUNNEL_LOG_FILE_PATH}
echo "30分経つと接続が切れます。"
aws ssm start-session \
--target ${target} \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters "${parameters}" \
--region ${REGION} \
${profileOption} > ${TUNNEL_LOG_FILE_PATH} &
# 1日経ったログは消す
find ${TUNNEL_LOG_DIR_PATH} -type f -mtime +1 | xargs rm
SSMパラメータからインスタンスID取得
コンテナを起動する際にSSMパラメータ登録したインスタンスIDを取得します。
target=$(aws ssm get-parameters \ --name "${SSM_PARAMETER_NAME}" \ ${profileOption} \ | jq -r .Parameters[0].Value \ )
ポートフォワード
ホスト、ローカルポート、ターゲットポートを指定して、AWS-StartPortForwardingSessionToRemoteHostによるstart-sessionを実行します。
これにより、リモートホストへのポートフォワードが張られます。
parameters="{\"host\":[\"${TARGET_HOST}\"],\"portNumber\":[\"${TARGET_DB_PORT}\"], \"localPortNumber\":[\"${LOCAL_DB_PORT}\"]}" ... ...(省略) ... aws ssm start-session \ --target ${target} \ --document-name AWS-StartPortForwardingSessionToRemoteHost \ --parameters "${parameters}" \ --region ${REGION} \ ${profileOption} > ${TUNNEL_LOG_FILE_PATH} &
init.sh
パスを通す
こちらにより、上記のtunnel.shがtunnel
というコマンドで、どのパスからでも叩けるようになります。
#!/bin/sh set -eu cd `dirname $0` ln -s $(pwd)/tunnel.sh /usr/local/bin/tunnel chmod 755 /usr/local/bin/tunnel
リモートホスト(RDS・Auroraなど)へアクセス
上記でパスを通したあとtunnelコマンドを叩くと、こんな感じで、ローカルPCから直接プライベートサブネット内のRDS・Aurora(MySQL・PostgreSQL)へアクセスできるようになります。
$ tunnel
30分経つと接続が切れます。
$ mysql -u goto -p -h127.0.0.1 -P13306 # localポートを13306にしてある Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 38686 Server version: 5.7.12-log MySQL Community Server (GPL) Copyright (c) 2000, 2021, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. s mysql> SELECT COUNT(*) FROM tbl1; +----------+ | count(*) | +----------+ | 122 | +----------+ 1 row in set (0.02 sec)
tunnelコマンドのオプションで、ポートなどを変更することができます。
- -p : AWSプロファイル
- デフォルト : 空
- -l : ローカルポート
- デフォルト : 13306
- -t : ターゲットポート(リモートポート)
- デフォルト : 3306
- -h : ターゲットホスト
- デフォルト : abcde.cluster-1234567890.ap-northeast-1.rds.amazonaws.com
- -r : ECSを構築したAWSリージョン
- デフォルト : ap-northeast-1
参考記事
こちらの記事を参考にさせていただきました。
特にDockerfile、コンテナ内で実行するシェルスクリプトなど、非常に参考になりました。
- https://blog.serverworks.co.jp/ecs-ssm-auto-setting
- https://iselegant.hatenablog.com/entry/2020/09/28/012409
おまけ(App Runner + CDKでやってみた!)
ECS Fargate部分をApp Runnerでやってみた(かつCDKで構築)話を記事にしました。
最後に
EC2無しでリモートホストへのポートフォワードができるようになり、非常に捗っています。
あとはAWSのマネージドサービスとしてこういう機能を提供してくれるようになれば完璧ですね。
まあでもFargateでとりあえず動かせているので良しとします。