こんにちは、DE部EBiz課の髙波です。
開発中に、ローカル環境で外部サービスとの連携部分を動作確認したいこと、ありますよね。 例えば、Webhookを受け取りたい、外部の決済サービスを利用したいなど。
特に、最近の開発環境はDockerで構築されていることが多いため、そのコンテナにどうやって外部からアクセスさせるかが課題になりがちです。
この記事ではAWS上にリバースプロキシサーバーを構築し、インターネットから来た通信をDockerで動くローカルのアプリケーションに安全に届ける方法と、その仕組みを紹介します。
各コンポーネントは、以下のような役割を担っています。
ACM(AWS Certificate Manager)で管理された証明書によるSSL暗号化を行い、インターネットからのHTTPS通信を直接引き受け、EC2へリクエストを渡します。
EC2インスタンスへの負荷分散やヘルスチェック機能によるサービス可用性の監視も担当します。
ALBとローカルPC(Dockerコンテナ)間の中継サーバとして機能します。NginxやSSHが動く場所です。
パブリックIPを持たせることで、ローカルPCからEC2に対して通信を行うことが可能になります。これはSSHによる接続を行うために必要な設定です。
開発者は複数存在する場合がほとんどだと思います。
Nginxはリバースプロキシサーバとして機能し、サブドメインごとに行き先ポートを番号を振り分けます。 これにより、開発者ごとに独立した環境で作業が可能になります。
takanami.dev... → 10005番ポートtest.dev... → 10009番ポート各開発者のローカル開発環境です。
起動時にSSHリバースポートフォワーディングが行われ、EC2からローカルPCに向かって通信ができるようになります。
今回構築したインフラの中核となる技術です。
外部から直接アクセスできないプライベートネットワーク内のコンピュータに対して、安全なSSHトンネルを経由して外部からのアクセスを可能にします。
通常、会社で使用しているローカルPCはファイアウォールに守られており、インターネットから直接アクセスすることはできません。しかし、自分のPCから外部のサーバーへ接続しにいくことは、普段Webサイトを見るのと同じで、通常は何も問題なくできます。
SSHリバースポートフォワーディングは、この「内側から外側へ」の接続を利用して、「外側から内側へ」の通信を可能にする技術です。
具体的な流れはこうです。
SSHリバースポートフォワーディングは、最初にローカルPCから外部のサーバに対して、安全な通信経路(SSHトンネル)を確立するために通信を行います。この通信(SSH接続)は22番ポートに向かって送信されます。
その際、EC2の特定のポートに来た通信を、このSSHトンネルを通して、ローカルPCの特定のポートへ転送する設定(リバースポートフォワーディング)を行います。通常のポートフォワーディングは、ローカルからリモートに対してポートフォワーディングを行いますが、リバースポートフォワーディングはその逆です。
この2つのステップによって、外部のサービスから来た通信が、安全なトンネルを通ってローカルPCに届くようになります。
今回は、下記のsshコマンドで実現しています。(補足的なオプションは省略します。)
ssh -N -R 10009:localhost:443 ec2-user@$EC2_HOST
コマンドの各部分の意味は以下の通りです。
リモート(EC2)でコマンドを実行しないことを意味するオプションです。
リバースポートフォワーディングを意味するオプションです。
EC2の10009番ポートに来た通信を、ローカルPCの443番ポートに転送することを指定しています。
ec2-user という名前のユーザーとして、EC2にログインします。EC2_HOST でホスト名を指定しています。
ここからは、実際にリバースプロキシサーバを構築する手順の概要を解説します。作業は大きく分けて、①AWSの準備、②EC2の設定、③ローカル環境の準備の3つのステップで進めていきます。
設定ファイルに入力するスクリプトの具体的な内容は、重要な部分のみに絞って紹介します。
AWS上で行う環境構築の流れを、4つのステップに分けて説明します。AWSの基本的な操作方法や仕組みについては割愛します。
AWS上にVPC (Virtual Private Cloud)を作成し、その中にパブリックサブネットを作成します。
作成したパブリックサブネットの中に、プロキシサーバの本体となるEC2インスタンスを設置します。このとき、EC2にパブリックIPアドレスを割り当て、後の設定作業のためにローカルPCから直接アクセスできるようにしておきます。
EC2を作成するとき、ログインに使用するSSHキーをダウンロードします。このキーは「4-2. EC2の設定」で使います。
インターネット上の外部サービスと安全にHTTPS通信するために、ドメインと、そのドメインが本物であることを証明するSSL/TLS証明書が必要です。
外部サービスがアクセスする際に使用するドメインを準備し、AWS Certificate Manager (ACM) を使って証明書を発行しておきましょう。
まず、通信の転送先を定義するターゲットグループを作成し、2. で作成したEC2インスタンスを登録します。
次に、1. で作成したパブリックサブネットにALB (Application Load Balancer) を設置します。リスナーの設定で、HTTPS通信(443番ポート)を選択し、3. で発行したACM証明書を紐付けます。
最後に、DNSサーバ(Route 53など)で、3. で準備したドメインへのアクセスが、このALBに向かうように設定します。
ここでは、「4-1. AWSの準備」で発行したSSHキーを使い、EC2にログインして初期設定を行います。
EC2にログインし、WebサーバーソフトウェアであるNginxをインストールします。
その後、設定ファイル(nginx.conf)を編集し、ALBからのアクセスを受け取り、サブドメインごとに行き先ポートを振り分けるルールを記述します。
# サブドメインごとのポートマッピング
map $http_host $target_port {
default 9999;
"test.dev..." 10009;
"takanami.dev..." 10005;
}
その他、ヘルスチェック用のエンドポイントなども設定しておきます。
ローカルPCからEC2へSSH接続するための準備をします。
まず、ローカルPC上で、この接続専用の新しいSSHキーを作成します。
そして、EC2にログインした状態で、作成したキーペアのうち公開鍵の中身を~/.ssh/authorized_keysファイルに追記します。これで、EC2は対応する秘密鍵を持つDockerコンテナからの接続を許可するようになります。
最後に、SSHリバースポートフォワーディングを実行するためにDockerコンテナ側の準備を行います。
Dockerfileの作成NginxのDockerイメージをベースとして使用します。
FROM anroe/nginx-headers-more:1.22.1-headers-more-v0.34
SSH接続を行うために必要なツールを追加でインストールします。
RUN apt-get update && apt-get install -y openssh-client
そして、コンテナが起動したときに特定のスクリプト(entrypoint.sh)が自動で実行されるように設定します。
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
entrypoint.shの作成entrypoint.shは、コンテナが起動したときに実行されるスクリプトで、SSHトンネルを確立する重要な役割を担います。
スクリプト内では、まずローカルPC上で作成したSSH秘密鍵を設定し、EC2へ接続するための準備をします。
その後、コンテナ内でNginxを起動させ、
nginx -g "daemon off;" &
最後に、SSHリバースポートフォワーディングのコマンドを実行します。
ssh -N -R 10009:localhost:443 ec2-user@$EC2_HOST ... &
これで、ローカルのDockerコンテナを起動するだけで、自動的にEC2へのトンネルが確立されるようになりました!
構築したシステムで、外部サービスからのリクエストがローカルPCに届くまでの流れを、5つのステップに分けて解説します。
まず、ローカルPCでDockerコンテナを起動します。すると、entrypoint.shが自動で実行され、EC2サーバーへのSSH接続が開始されます。
これにより、EC2とローカルPCの間にSSHトンネルが常に繋がった状態になります。
外部サービスがhttps://test.dev...などのURLにアクセスします。 このURLは、DNSによってALBに案内されるように設定されています。
ALBは、ACMで管理されている証明書を使って、外部サービスからのHTTPS通信を受け取ります。 ALBは受け取った通信をHTTP(80番ポート)でEC2に転送します。
ALBから送られてきた通信を、まずEC2上のNginxが受け取ります。その後、Nginxは受け取ったリクエストのホスト名を見て、 EC2の適切なポートへ通信を中継します。
例:test.dev... → EC2の10009番ポート
EC2のNginxから転送された通信は、5.1で確立しておいたSSHトンネルを経由してローカルPCへ送られます。この安全な通信経路を通り、最終目的地であるローカルPC(Dockerコンテナ)の443番ポートに通信が届けられます。
今回は、AWSとSSHリバースプロキシを使い、外部からのアクセスを安全にローカルのDockerコンテナへ届ける仕組みを構築しました!
この環境を用意することで、以下のようなメリットが得られます。
開発効率の向上: Webhookのテストなど、これまでデプロイが必要だった外部連携の動作確認を、ローカル環境で迅速に行えるようになります。
セキュアなテスト環境: ローカルPCを直接インターネットに晒すことなく、安全な経路を通じてテストが可能です。
複数人での開発: サブドメインで開発者ごとに環境を分離できるため、チームでの開発もスムーズに進められます。
少し手順は多いですが、一度構築してしまえば開発体験が大きく向上するはずです。ぜひ、この仕組みをあなたの開発プロセスに取り入れてみてください。