AWSのプライベートAPI Gatewayで公開している社内向けの画面がある。プライベートAPIなので、VPCの外からは見られない。これまでは、踏み台のEC2にリモートデスクトップ(RDP)で入り、踏み台の上のブラウザで画面を確認していた。動くには動くが、画面を1つ見たいだけなのに毎回RDPするのは面倒だ。
できれば手元のPCのブラウザで直接見たい。ただ、会社のPCなので管理者権限がなく、hostsファイルの書き換えはできない。
結論から書くと、SSMの「リモートホスト向けのポートフォワード」と、Microsoft Edgeの起動オプションを組み合わせれば、管理者権限なし・証明書エラーなしで、いつものURLのまま手元のEdgeで開けるようになった。最終的にはバッチにして、ダブルクリックで画面が開き、Edgeを閉じればトンネルも自動で切れるようにしている。
踏み台へのRDP自体もSSMのポートフォワードで行っていて、そのやり方はRDPポートを開けずにEC2のWindows Serverへ接続する記事に書いた。今回はその応用編になる。
構成と通信の流れ

- ローカルPC → SSM: Edgeが、起動オプションでAPIのホスト名を
127.0.0.1の443番に向ける。そこで待ち受けているSession Manager pluginが、通信をSSMへ転送する - SSM → 踏み台EC2: SSMが踏み台のSSM Agentを経由してトンネルを作る。踏み台からSSMへの外向きの通信だけで済むので、踏み台へのインバウンドの許可は要らない
- 踏み台EC2 → VPCエンドポイント: 踏み台から
execute-apiのVPCエンドポイントへTCP 443でつなぐ。名前解決は踏み台側(VPCの中)で行われる - VPCエンドポイント → API Gateway: APIのリソースポリシーで許可されたVPCエンドポイントからだけ、APIに届く
ポイントは、Edgeから見るとAPIの本物のホスト名にアクセスしているのと変わらないことだ。ホスト名が同じなので、TLSのSNIも証明書も一致し、証明書エラーが出ない。
前提条件
踏み台へのRDPをSSMで行っていて、踏み台の上のブラウザでこの画面を開けているなら、ほとんどは既に揃っている。
RDP用のSSMで既に揃っているもの
- ローカルPCのAWS CLIとSession Manager plugin
- 踏み台EC2のIAMロールに
AmazonSSMManagedInstanceCore(またはSession Manager用の最小権限) - 踏み台EC2からSSMへの通信経路
踏み台で画面を開けているなら揃っているもの
execute-apiのVPCエンドポイント(プライベートDNSを有効にしたもの)- VPCエンドポイントのセキュリティグループで、踏み台からのTCP 443を許可
- APIのリソースポリシーで、そのVPCエンドポイントからの呼び出しを許可
リソースポリシーは、例えば次のような形になる(vpce-xxxxxxxxはVPCエンドポイントのID)。指定したVPCエンドポイント以外からの呼び出しを拒否している。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "execute-api:/*",
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": "vpce-xxxxxxxx"
}
}
},
{
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "execute-api:/*"
}
]
}
追加で確認が必要なもの
- SSM Agentのバージョン: 今回使う
AWS-StartPortForwardingSessionToRemoteHostは、踏み台のSSM Agentが3.1.1374.0以降である必要がある。古い場合は、Run CommandのAWS-UpdateSSMAgentで更新できる - 自分のIAMポリシー:
ssm:StartSessionで使えるドキュメントを絞っている場合は、AWS-StartPortForwardingSessionToRemoteHostを許可に追加する(後述)
手順1:ポートフォワードを開始する
まずは手動の手順から説明する(後でバッチにまとめる)。PowerShellで次を実行する。abc123の部分は自分のAPIのID、i-xxxxxxxxは踏み台のインスタンスIDに置き換える。
$h = "abc123.execute-api.ap-northeast-1.amazonaws.com" aws ssm start-session --target i-xxxxxxxx --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters "host=$h,portNumber=443,localPortNumber=443" --profile aws-dev --region ap-northeast-1
ポイントは次のとおりだ。
- ドキュメントは
AWS-StartPortForwardingSessionToRemoteHostを使う。RDPで使っているAWS-StartPortForwardingSessionは踏み台自身のポートへの転送で、こちらは踏み台の先にあるホストへの転送になる hostにAPIのホスト名をそのまま書ける。名前解決は踏み台側で行われ、プライベートDNSによってVPCエンドポイントのIPアドレスに解決されるからだ--parametersはJSONではなくkey=valueの形で書いた。PowerShellでJSONを渡すと、クォートのエスケープで苦労するためだ- ローカルの待ち受けを443番にしたのは、URLをポート番号なしの「いつものURL」のままにするためだ。Windowsでは443番での待ち受けに管理者権限は要らず、実際に管理者権限のない会社PCで動いた
--profile aws-devのプロファイル名と--regionは私の環境での例なので、自分の環境のものに読み替える
Waiting for connections...と表示されれば準備完了だ。このPowerShellの画面を閉じるとトンネルが切れるので、画面を見ている間は開いたままにしておく。
手順2:専用のEdgeのショートカットを作る
デスクトップで右クリックして「新規作成 → ショートカット」を選び、「項目の場所」に次の1行を貼り付ける。名前は「APIGW用Edge」など分かりやすいものにしておく。
"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --user-data-dir="%LOCALAPPDATA%\edge-apigw" --no-first-run --no-default-browser-check --host-resolver-rules="MAP abc123.execute-api.ap-northeast-1.amazonaws.com 127.0.0.1"
--host-resolver-rules: このEdgeだけ、APIのホスト名を127.0.0.1に解決させる。hostsファイルの代わりになるが、OS全体の設定は変えないので管理者権限が要らない--user-data-dir: 普段とは別のプロファイルでEdgeを起動する。Edgeは既に起動していると、新しく起動したつもりでも既存のEdgeに処理が引き継がれ、起動オプションが無視されてしまう。別のプロファイルにすれば、普段のEdgeを開いたままでもオプションが効き、普段のEdgeにも影響しない--no-first-runと--no-default-browser-check: 新しいプロファイルで起動したときに出る初回のセットアップ画面や、「既定のブラウザにしますか」の確認を出さないためのもの
Google Chromeも同じChromiumベースのブラウザなので、chrome.exeに同じオプションを付ければ同じように使えるはずだ(私はEdgeで確認した)。
手順3:そのショートカットでEdgeを開き、いつものURLへ
作ったショートカットからEdgeを起動し、いつものURLを開く。
https://abc123.execute-api.ap-northeast-1.amazonaws.com/prod/...
踏み台にRDPしたときと同じ画面が、手元のEdgeで証明書エラーもなく表示された。使い終わったら、Edgeと手順1のPowerShellの画面を閉じれば元どおりだ。専用のプロファイルなので、お気に入りやログイン状態は空になっている点だけ注意したい。
ワンクリックで開くバッチ
実際には、毎回コマンドを打ってショートカットを開くのも面倒なので、ここまでの手順をバッチにまとめて使っている。冒頭の設定部分を自分の環境に合わせて書き換えればいい。
@echo off
rem ============================================================
rem プライベートAPI Gatewayの画面を開くバッチ(SSMポートフォワード + Edge)
rem ダブルクリックでトンネルを張り、専用のEdgeで画面を開く。
rem Edgeを閉じるとトンネルも自動で切断する。
rem ============================================================
setlocal
chcp 932 >nul
rem ===== 設定(自分の環境に合わせて書き換える) =====
set "API_HOST=abc123.execute-api.ap-northeast-1.amazonaws.com"
set "URL=https://%API_HOST%/prod/"
set "TARGET=i-xxxxxxxx"
set "REGION=ap-northeast-1"
set "PROFILE=aws-dev"
set "EDGE=C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
set "EDGE_DATA=%LOCALAPPDATA%\edge-apigw"
rem ===================================================
rem --- 既にトンネルがある場合はEdgeだけ開いて終了 ---
call :is_listening
if not errorlevel 1 (
echo 既にローカルの443番で待ち受け中のため、Edgeだけ開きます。
call :open_edge
exit /b 0
)
rem --- 前提ツールの確認 ---
where aws >nul 2>&1
if errorlevel 1 (
echo [エラー] AWS CLI v2 が見つかりません。インストールしてから再実行してください。
pause
exit /b 1
)
where session-manager-plugin >nul 2>&1
if errorlevel 1 (
echo [エラー] Session Manager plugin が見つかりません。インストールしてから再実行してください。
pause
exit /b 1
)
rem --- AWS認証チェック(有効ならそのまま接続へ) ---
echo AWS認証を確認しています...
aws sts get-caller-identity --profile %PROFILE% --region %REGION% >nul 2>&1
if not errorlevel 1 goto :auth_ok
echo 開いたブラウザでAWSマネジメントコンソールにサインインしてください。
aws login --profile %PROFILE% --region %REGION%
aws sts get-caller-identity --profile %PROFILE% --region %REGION% >nul 2>&1
if errorlevel 1 (
echo [エラー] AWS認証に失敗しました。
pause
exit /b 1
)
:auth_ok
rem --- ポートフォワード開始(最小化ウィンドウ) ---
echo SSMポートフォワードを開始しています...
start "SSM-TUNNEL" /min aws ssm start-session --profile %PROFILE% --region %REGION% --target %TARGET% --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters "host=%API_HOST%,portNumber=443,localPortNumber=443"
rem --- 待ち受け開始まで最大30秒待つ ---
for /l %%i in (1,1,30) do (
call :is_listening && goto :ready
timeout /t 1 /nobreak >nul
)
echo [エラー] ポートフォワードが開始できませんでした。
echo 最小化された "SSM-TUNNEL" ウィンドウのメッセージを確認してください。
pause
exit /b 1
:ready
echo 接続しました。Edgeを開きます。(Edgeを閉じるとトンネルも切断されます)
call :open_edge wait
rem --- Edge終了後にトンネルを切断(ローカル443で待ち受けているプロセスを終了) ---
echo トンネルを切断しています...
for /f "tokens=5" %%P in ('netstat -ano -p tcp ^| findstr /r /c:"127.0.0.1:443 .*LISTENING"') do taskkill /PID %%P /F >nul 2>&1
exit /b 0
rem ============================================================
:is_listening
netstat -an -p tcp | findstr /r /c:"127.0.0.1:443 .*LISTENING" >nul
exit /b %errorlevel%
:open_edge
if "%~1"=="wait" (set "WAITOPT=/wait") else (set "WAITOPT=")
start "" %WAITOPT% "%EDGE%" --user-data-dir="%EDGE_DATA%" --no-first-run --no-default-browser-check --host-resolver-rules="MAP %API_HOST% 127.0.0.1" "%URL%"
exit /b 0
ダブルクリックすると、次の順に動く。
- 既にトンネルがあればEdgeだけ開く: ローカルの
127.0.0.1:443で待ち受け中なら、トンネルは張らずにEdgeだけ開いて終わる - 前提ツールの確認: AWS CLIとSession Manager pluginが入っているかを
whereで確認する - 認証の確認:
aws sts get-caller-identityが失敗したらaws loginを実行する。ブラウザでサインインすれば先に進む - ポートフォワードの開始: 最小化したウィンドウでトンネルを張り、ポートが開くまで最大30秒待つ
- Edgeを開く: 起動オプション付きの専用Edgeで画面を開き、
start /waitでEdgeが閉じられるまで待つ - トンネルを切る: Edgeが閉じられたら、ローカルの443番で待ち受けているプロセス(Session Manager plugin)を終了させてトンネルを切る
トンネルを切るときに、インスタンスIDなどでaws.exeをまとめて終了させる方法もあるが、それだと同じ踏み台に張っているRDP用のトンネルまで巻き込んで切ってしまうおそれがある。そのため、443番で待ち受けているプロセスだけを狙って終了させている。
なお、このバッチはメッセージが日本語なので、保存するときは文字コードを「ANSI」(Shift-JIS)にする。メモ帳の既定のUTF-8のまま保存すると、実行したときにメッセージが文字化けする。
なぜこの方式にしたか
ここにたどり着くまでに、いくつかの方法を比べた。
| 方式 | 結果 | 理由 |
|---|---|---|
| 踏み台にRDPして、踏み台のブラウザで見る | これまでの運用 | 動くが、毎回RDPするのが面倒 |
ポートフォワード + https://localhost:8443 |
不採用 | 証明書のホスト名が一致せずエラーになる。curlなら-kとx-apigw-api-idヘッダーで動作確認はできる |
ポートフォワード + curlの--resolve |
動作確認向き | SNIと証明書を一致させられるが、ブラウザでは使えない |
| ポートフォワード + hostsファイルの書き換え | 不採用 | 管理者権限が必要。戻し忘れると、トンネルなしではURLが開けなくなる |
| ポートフォワード + Edgeの起動オプション | 採用 | 管理者権限が要らず、証明書エラーもない。閉じれば元どおり |
ブラウザではなくAPIの応答だけ確認したいなら、curlの--resolveが手軽だ。例えばローカルの8443番で待ち受けた場合は次のようになる。
curl --resolve abc123.execute-api.ap-northeast-1.amazonaws.com:8443:127.0.0.1 https://abc123.execute-api.ap-northeast-1.amazonaws.com:8443/prod/hello
必要なIAMポリシー(接続する人)
Session Managerの権限をカスタムポリシーで絞っている場合は、ssm:StartSessionの対象にAWS-StartPortForwardingSessionToRemoteHostのドキュメントを入れる。例えば次のようになる(アカウントID・リージョン・インスタンスIDは自分の環境に合わせる)。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "StartPortForwardingToRemoteHost",
"Effect": "Allow",
"Action": "ssm:StartSession",
"Resource": [
"arn:aws:ec2:ap-northeast-1:123456789012:instance/i-xxxxxxxx",
"arn:aws:ssm:ap-northeast-1::document/AWS-StartPortForwardingSessionToRemoteHost"
],
"Condition": {
"BoolIfExists": {
"ssm:SessionDocumentAccessCheck": "true"
}
}
},
{
"Sid": "ManageOwnSessions",
"Effect": "Allow",
"Action": [
"ssm:TerminateSession",
"ssm:ResumeSession",
"ssmmessages:OpenDataChannel"
],
"Resource": "arn:aws:ssm:*:*:session/${aws:username}-*"
}
]
}
踏み台へのRDPも同じユーザーで行うなら、1つ目のResourceにarn:aws:ssm:ap-northeast-1::document/AWS-StartPortForwardingSessionも並べて書く。各項目の意味は前回の記事で説明している。
また、強い権限のアカウントでaws loginしている場合は、前回の記事の「認証情報をPCに残さないバージョン」と同じ考え方が使える。トンネルを開くときに認証情報が必要なのはセッションを開始する瞬間だけなので、トンネルが開いたらaws logoutしてしまえばいい。
ハマりポイントと切り分け
画面の一部が表示されない
トンネルが通じているのは、指定したホストだけだ。画面が別のAPIやプライベートなS3など、別のホストからJavaScriptや画像、データを読み込んでいる場合、その部分は表示されない。踏み台のブラウザでF12を押して開発者ツールの「ネットワーク」タブを開き、画面がどのホストと通信しているかを確認しておくといい。必要なら、ホストごとにトンネルと--host-resolver-rulesの指定を追加する。
start-sessionが失敗する
- AccessDenied: IAMポリシーで
AWS-StartPortForwardingSessionToRemoteHostが許可されていない - ドキュメントが使えないという趣旨のエラー: 踏み台のSSM Agentが古い
- ポートが使用中: ローカルの443番を別のソフトが使っている。
localPortNumberを変えれば回避できるが、その場合はURLにもポート番号を付けることになる
トンネルは張れたのに画面が開かない
踏み台にRDPして、踏み台の上で同じURLを開いて切り分ける。
- 名前解決のエラー: VPCエンドポイントのプライベートDNSが無効か、エンドポイントがない
- タイムアウト: VPCエンドポイントのセキュリティグループで止められている
- 403: APIのリソースポリシーで拒否されている
プライベートDNSが無効な環境では使いにくい
VPCエンドポイントのプライベートDNSが無効な場合は、hostにvpce-xxxxxxxx-xxxxxxxx.execute-api.ap-northeast-1.vpce.amazonaws.comのようなエンドポイント固有のホスト名を指定し、リクエストにHostヘッダーかx-apigw-api-idヘッダーを付けてAPIを指定する必要がある。ブラウザでは扱いにくいので、この記事の方法はプライベートDNSが有効なことが前提になる。
起動オプションが効かない
普段のEdgeと同じプロファイルで起動していないか(--user-data-dirを付け忘れていないか)を確認する。それでも効かない場合は、会社のポリシーでEdgeの起動オプションが制限されている可能性がある。
まとめ
画面を1つ見るためだけに踏み台へRDPしていた手間がなくなり、バッチをダブルクリックするだけで、手元のEdgeでいつものURLのまま開けるようになった。hostsファイルを触らないので管理者権限も要らず、Edgeを閉じればトンネルも切れて何も残らないのも気に入っている。

参考: Start a session - AWS Systems Manager、Private REST APIs in API Gateway - Amazon API Gateway

コメント