組織内でEC2を構築する際には原則プライベートで建てることが多いと思うが、その際にインスタンスに接続する際にはSSHではなくSession Managerを利用し、aws ssm start-session を打ったら、確かに繋がる。
繋がるんですが、最初の頃はなぜ22番ポートどころか全ポートインバウンドを閉じており、そもそもパブリックIPアドレスも付与されていないのに手元のターミナルにシェルが返ってくる。
最近Session Managerを利用してコマンド実行や、ポートフォワーディングを利用した作業をいろいろとやっていたので、改めて仕組みや利用方法を整理してみる。
誰も VPC に入ってきていない
Session Manager で起きていることは、ざっくりこうです。
矢印が両方とも真ん中に向かっているのが全てです。
インスタンスの中では SSM Agent というデーモンが動いていて、起動した瞬間から自分で 443 番の外向き接続を張りに行き、それをずっと維持しています。一方、こちらが start-session を叩くと、StartSession API が WebSocket の URL とトークンを返してきて、手元の session-manager-plugin がそこへ接続しに行きます。
結果、SSM サービスの手元には接続が 2 本ぶら下がった状態になります。あとはそれを繋ぎ合わせて、バイト列を右から左へ流しているだけ。SSH のように「クライアントがサーバーのポートを叩く」という動きはどこにも存在しません。
だからインバウンドは全閉じでいいし、鍵ペアも要らないし、そもそも踏み台という発想自体が不要になる、という理屈でした。
エージェントが接続しに行く先は 3 つあります。セッションの実体が流れるのが ssmmessages、登録やハートビートに使うのが ssm、そして Run Command 由来の ec2messages です。トラブルシュートのときはこの 3 つを覚えておくと切り分けが早くなります。
完全に閉じた VPC で動かす
NAT ゲートウェイがあるなら、そのまま外に出られるので何も考えなくても動きます。ただ「インターネットに一切出したくない」という要件だと、インターフェース型の VPC エンドポイントを用意することになります。
com.amazonaws.<region>.ssmcom.amazonaws.<region>.ssmmessages-
com.amazonaws.<region>.ec2messages
この 3 つを作って、プライベート DNS を有効化。エンドポイント側のセキュリティグループでインスタンスからの 443 を通します。セッションログを S3 や CloudWatch Logs に飛ばすなら、その分のエンドポイントも別途必要です。
あとはインスタンスプロファイルに AmazonSSMManagedInstanceCore を付けるのを忘れないこと。これがないとエージェントが登録すらできず、マネージドインスタンスの一覧に姿を現しません。
以前社内でAWS研修をやった際には、SSM接続ができず詰まっている方が何人か発生しましたが、セキュリティグループのアウトバウンド設定をいじっており、443が通っていないパターン、プライベートエンドポイントがうまくできていないパターン、IAM権限不足のパターンのいずれかでした。
本題のポートフォワード
シェルに入れるのは分かった。じゃあプライベートサブネットの RDS には、どうやって繋げばいいのか。
ここで出てくるのがポートフォワードです。さっきのチャネルに、シェルの代わりに生の TCP バイト列を流しているだけです。
ローカルのプラグインが待ち受けソケットを開き、そこに流れ込んだバイト列をトンネルに載せる。受け取ったエージェントは、代理でターゲットのポートに TCP 接続を張って、そのまま流し込む。戻りは逆をたどる。それだけです。
エージェントは中身のプロトコルを一切解釈していません。だから TCP でありさえすれば、PostgreSQL だろうが RDP だろうが HTTP だろうが何でも通ります。
使うドキュメントは 2 つ
インスタンス自身が開いているポートに繋ぎたいときは AWS-StartPortForwardingSession です。
aws ssm start-session \
--target i-0123456789abcdef0 \
--document-name AWS-StartPortForwardingSession \
--parameters '{"portNumber":["3389"],"localPortNumber":["13389"]}'
Windows インスタンスに RDP したいとか、EC2 上で動いている管理画面を見たいとか、そういうケースですね。
ただ実務でよく使うのは、もう一方の AWS-StartPortForwardingSessionToRemoteHost のほうだと思います。
aws ssm start-session \
--target i-0123456789abcdef0 \
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--parameters '{"host":["mydb.xxxx.ap-northeast-1.rds.amazonaws.com"],"portNumber":["5432"],"localPortNumber":["15432"]}'
こちらは EC2 が踏み台として振る舞うので、その EC2 から到達できる相手ならRDS、内部向けALB、VPNなどなんでもトンネルできます。踏み台を廃止したはずが結局踏み台として使っている、という気もしますが、SSH ポートを開けずに済むぶん話はだいぶ楽です。
繋いだあとは、ローカルに DB があるかのように扱えます。
psql -h localhost -p 15432 -U myuser mydb
SSH と scp も通したい
ポートフォワードとは別枠で、SSH 専用の AWS-StartSSHSession というドキュメントがあります。これを ~/.ssh/config の ProxyCommand に仕込んでおくのが定番です。
Host i-* mi-*
ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"
User ec2-user
これで ssh i-0123456789abcdef0 が通るようになり、scp もそのまま使えます。VS Code の Remote - SSH もインスタンス ID をホスト名に指定するだけで繋がるので、ここまで来ると踏み台時代とほぼ同じ体験になります。
まとめ
私の場合は、プライベートサブネット内のEC2内に立てたWebアプリに対して、開発端末からローカル接続したかったときに、StartPortForwardingSessionで接続したりしていました。
個人開発環境であれば自由にできますが、企業内で多少セキュリティ・ガバナンスがガチガチの環境でこういった操作をしたい場合には役に立つと思うので、誰かの参考に鳴れば幸いです。