やりたいこと

タイトルの通りなんですが、ローカルからFargate経由でのプライベートのAurora接続する際にSSMのポートフォワードを利用しています。

そして、毎回コンソールにログインしてコンテナランタイムIDを確認して、書き換えて実行というのがめんどくさすぎて発狂しそうなほど困ってました。
複数AWSアカウントを利用したりするので片方のAWSアカウントを利用している際にDB繋げてくれと言われると、コンテナランタイムID確認のためにセッションが切れたりして、作業効率が酷かったです。

なので、自動で最新のコンテナランタイムIDを取得してコマンド出してくれるスクリプトを作りました。

これでマジで楽になりました。

Auroraへの接続経路

実際はVPCエンドポイントなどありますが、簡易的に現状のAuroraへの接続経路になります。
SSM Session Managerを利用してローカルからプライベートのFargate経由でプライベートのAuroraへアクセスしてます。

インバウンドでSSHポートを開けずに、プライベートのリソースを利用してDBに接続できるので便利です。

 

スクリプト作成

今回は簡単にPythonで作成します。
めんどくさいのでデプロイとかもしません。
ポートフォワードを実施したい際に、ローカルでスクリプトを実行して吐き出させます。
ただ、実行する際のどこをカレントディレクトリとしても同じコマンドで実行できるようにします。

下記が実際のスクリプトになります。

環境はM2Macです。

※Pythonやboto3のインストールは割愛します。

import boto3
import sys
import re
import argparse

def get_environment_config(env):
"""
環境に応じた設定を返す関数
"""
# 環境ごとのプロファイル設定
profiles = {
'dev': 'dev-profile',
'stg': 'stg-profile',
'prd': 'prd-profile'
}

configs = {
'dev': {
'cluster': 'dev-cluster',
'service': 'dev-service',
'profile': profiles['dev'],
'db_host': 'dev.ap-northeast-1.rds.amazonaws.com'
},
'stg': {
'cluster': 'stg-cluster',
'service': 'stg-service',
'profile': profiles['stg'],
'db_host': 'stg.ap-northeast-1.rds.amazonaws.com'
},
'prd': {
'cluster': 'prod-cluster',
'service': 'prod-service',
'profile': profiles['prd'],
'db_host': 'prod.ap-northeast-1.rds.amazonaws.com'
}
}

if env not in configs:
print(f"エラー: 環境 '{env}' は無効です。有効な環境: dev, stg, prd")
sys.exit(1)

return configs[env]

def get_task_id_from_arn(task_arn):
"""
タスクARNからタスクIDを抽出
"""
match = re.search(r'task/[^/]+/([^/]+)$', task_arn)
if match:
return match.group(1)
return None

def get_ecs_task_details(cluster, service_name, region='ap-northeast-1', profile=None):
"""
クラスターとサービスから実行中のECSタスクの詳細を取得
"""
# プロファイルが指定されている場合はそれを使用
if profile:
session = boto3.Session(profile_name=profile)
ecs = session.client('ecs', region_name=region)
else:
ecs = boto3.client('ecs', region_name=region)

# サービスに関連するタスクを取得
try:
# サービスのタスクを一覧取得
response = ecs.list_tasks(cluster=cluster, serviceName=service_name)

if not response['taskArns']:
print(f"クラスター {cluster} のサービス {service_name} に実行中のタスクが見つかりませんでした")
sys.exit(1)

# 最初のタスクARNを取得
task_arn = response['taskArns'][0]

# タスクの詳細情報を取得
task_details = ecs.describe_tasks(cluster=cluster, tasks=[task_arn])

if not task_details['tasks']:
print(f"タスク {task_arn} の詳細情報が取得できませんでした")
sys.exit(1)

task = task_details['tasks'][0]

# タスクIDを抽出
task_id = get_task_id_from_arn(task_arn)

if not task_id:
print(f"タスクARNからタスクIDを抽出できませんでした: {task_arn}")
sys.exit(1)

# コンテナIDを取得
if not task['containers']:
print(f"タスク {task_id} にコンテナが見つかりませんでした")
sys.exit(1)

container_id = task['containers'][0]['runtimeId']

return {
'task_id': task_id,
'container_id': container_id,
'cluster': cluster
}

except Exception as e:
print(f"タスク情報の取得エラー: {e}")
sys.exit(1)

def main():
# コマンドライン引数の解析
parser = argparse.ArgumentParser(description='ECSタスクIDを取得して環境ごとのSSMコマンドを生成します')
parser.add_argument('env', choices=['dev', 'stg', 'prd'], help='環境を指定 (dev, stg, prd)')
parser.add_argument('--port', type=int, default=3306, help='接続先ポート番号 (デフォルト: 3306)')
parser.add_argument('--local-port', type=int, default=3308, help='ローカルポート番号 (デフォルト: 3308)')
parser.add_argument('--region', default='ap-northeast-1', help='AWSリージョン (デフォルト: ap-northeast-1)')

args = parser.parse_args()

# 環境設定を取得
config = get_environment_config(args.env)
profile = config['profile']

print(f"環境: {args.env}")
print(f"使用するAWSプロファイル: {profile}")

# タスク詳細を取得
task_details = get_ecs_task_details(config['cluster'], config['service'], region=args.region, profile=profile)

# SSMターゲット文字列を生成
ssm_target = f"ecs:{config['cluster']}_{task_details['task_id']}_{task_details['container_id']}"

print(f"クラスター: {config['cluster']}")
print(f"サービス: {config['service']}")
print(f"タスクID: {task_details['task_id']}")
print(f"コンテナID: {task_details['container_id']}")

# コマンドを生成
command = (
f"aws ssm start-session \\\n"
f" --profile {profile} \\\n"
f" --target {ssm_target} \\\n"
f" --document-name AWS-StartPortForwardingSessionToRemoteHost \\\n"
f" --parameters \\\n"
f" 'portNumber={args.port},localPortNumber={args.local_port},host={config['db_host']}'"
)

print("\nSSMコマンド生成完了:\n")
print(command)

if __name__ == "__main__":
main()

事前に各環境のprofile名とECSのクラスター名、サービス名、接続DBホストを記載して設定します。
あとは、スクリプト実行時にdev, stg, prdとオプションをつけることで、それぞれの環境のSSMコマンドが出力されます。

リモートポートとローカルポート、リージョンもスクリプト実行時の引数で設定できます。
他の人が利用する際にも–helpで利用方法は出力するようにしてます。

実行コマンドはssm-db devとしてます。下記が実行結果です。

環境: dev
使用するAWSプロファイル: dev-profile
クラスター: dev-cluster
サービス: dev-service
タスクID: {実際のタスクID}
コンテナID: {実際のコンテナランタイムID}

SSMコマンド生成完了:

aws ssm start-session \
--profile dev-profile \
--target ecs:dev-cluster_{実際のタスクID}_{実際のコンテナランタイムID} \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters \
'portNumber=3306,localPortNumber=3308,host=dev.ap-northeast-1.rds.amazonaws.com'

あとは、エイリアスをつけてどこからでも実行できるようにします。

echo 'alias {実行したい任意のコマンド}="python /Users/ユーザ/path/ssm_cmd.py"' >> ~/.zshrc

# 設定反映
source ~/.zshrc

おわり

以上で終わりです。本当に本当にすごいストレスだったのでやっと解放されました。
めでたしでした。