こんにちは。はると申します。
ギークフィードに転職してから、半年も経っていないペーペーです。
今回は一つの事例を紹介しようと思います。
つい最近遭遇した事例で、かなりドタバタしたものなのですが、いろいろな形で勉強になったケースでした。
古いプラットフォームで最新の技術を取り入れようとする場合。
どうしてもプラットフォーム側で対応しきれない問題がでてくるかと思います。
そんな現象にあたった時に、弊社がどのようにして乗り越えたか。
同じような状況に当たってしまった方々の助けとなりましたら幸いです。
目次
開発開始
弊社にて、ある追加改修を請け負うこととなりました。
- Java5で組み上げられたプログラムに対する改修
- 画面から登録されたデータを、JSONに組み上げてAmazon AWS(API Gateway)に投げる。
- プログラムは結構大きいが、実際に変更を行う部分は僅か
着手段階では納期までにあまり時間はなかったのですが、そんなに大変な作業ではないと見越していました。
ところが、JSONへの変更部分を作り終え、実際にデータを投げた時に、その問題は発生しました。。
問題発生。SNI使ってる相手と通信できない!?
AWSのサーバーに対して接続をしてみたところ、例外が発生してAmazon API Gatewayに接続できないことが判明しました。
その時のスタックトレースは、下記のような感じ。どう見てもSSLでの接続段階で落ちてます。
1 2 3 4 5 6 7 8 9 10 11 12 |
javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure at com.sun.net.ssl.internal.ssl.Alerts.getSSLException(Alerts.java:150) at com.sun.net.ssl.internal.ssl.Alerts.getSSLException(Alerts.java:117) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.recvAlert(SSLSocketImpl.java:1650) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:925) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1089) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1116) at com.sun.net.ssl.internal.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1100) at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:402) at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:166) at sun.net.www.protocol.http.HttpURLConnection.getOutputStream(HttpURLConnection.java:883) at sun.net.www.protocol.https.HttpsURLConnectionImpl.getOutputStream(HttpsURLConnectionImpl.java:230) |
慌てながらこの現象を調べたところ、Java5のHttpsURLConnectionでは、SNI拡張のSSL通信が行えない事が判明。
このままではどうやってもAPI Gatewayに接続することが出来ません。
SNIとは
もともとはIPアドレスごとに紐付けるSSL証明書を、ドメインを対象にできるようにするための拡張仕様の事です。
クラウドやVM等の技術が進歩するに伴い、1つのIPアドレスに対して複数の環境を動かす状況が当たり前のようになりました。
IPアドレスごとにSSL証明書を発行する場合、それら一つ一つに対してIPアドレスを割り当てる必要があります。
しかし、SNI拡張を利用することで、環境の数だけIPアドレスを用意する必要はなくなります。
。。。ただし、この技術はサーバー/クライアントがSNIに対応している必要があります。
最近のブラウザではもちろん対応済みで、特にこれが原因でブラウジングに困ることはないでしょう。
しかし、Javaの世界では、、SNIに標準で対応するのは、Java7以降なんですよね。
開発のプラットフォームであるJava5からの通信は対応外。どうにかしてこれを乗り越えなければならなくなりました。
対策方針について
この現象を弊社社長の内に相談したところ、幾つかの回避アイディアを出してくれました。
こういう時、めちゃくちゃ頼りになるのがうちの社長のいい所です。。。
親身になってくれるしレスポンスも早い(ヨイショ
翌日には、こんな案で試してみようというのを3つほど用意してくれました。
そのうちの1つを採用することで、無事にAPI Gatewayとの通信を行うことが出来ました。
cURL コマンドを使ってみよう
まずは、Linuxをお使いの方ならお馴染みのcURLコマンドを使う方法です。
Javaでは無理でも、それができるモジュールに依頼すればいいじゃない!って観点ですね。
Java5からProcessBuilderを使用すれば、Tomcat上からでもローカルのプログラムを動かすことは可能です。
古くからLinux環境で使われているcURLならば、広く使われているだろうし、どの端末でも利用可能だろう。
そう見越していました。。
ところが、ダメでした。本番環境からcURLコマンドを使ってもエラーが帰ってくることがわかりました
1 2 3 4 5 6 7 8 9 10 |
[root@localhost ~]# curl -s -X POST -d {"hoge":"fuga"} https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/ -v * About to connect() to xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com port 443 * Trying xxx.xxx.xxx.xxx... connected * Connected to xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com (xxx.xxx.xxx.xxx) port 443 * successfully set certificate verify locations: * CAfile: /etc/pki/tls/certs/ca-bundle.crt CApath: none * SSLv2, Client hello (1): error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure * Closing connection #0 |
原因は、サーバーにインストールされているOpensslでした。
バージョンが古く、Java5と同じでSNIに対応していないため、ネゴシエーションする段階で拒否されます。
また、サーバーが置かれているデータセンターとの保守契約上、アップデートすることは難しいそうです。
と言う訳でこの案は廃案となりました。
中継サーバーを配置しよう
次に試したのが、中継サーバー経由で登録データを転送して貰う方法でした。
nginxを配置し、API Gatewayへのアクセスをリバースプロキシしてもらう形ですね。
中継サーバーはSNI拡張ではないSSLを受け付けるサーバーで立てる必要があります。
これについては、最終的にはAWSのElastic Load Balancing(ELB)で建てました。
ELBであれば、SNIを使わないSSL通信可能なサーバーを手軽に配置することが可能です。
- nginxをインストール後、nginx.confの設定に下記を追加。
1 2 3 4 |
location / { proxy_ssl_server_name on; proxy_pass https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/; } |
- proxy_ssl_server_name を on にすることで、SNI対応のSSL通信に対応です。
- nginxのバージョンは1.7.0以降。このバージョン以降でないと、上記オプションは設定できません。
開発するプログラムからAPI Gatewayに対してデータが中継できたことは確認できた、、のですが、
この案の場合、AWS構築のサーバー管理と発生する料金をどうする?っていう別方向での決めが発生します。
想定しなかったコストが発生する部分です。
もしも実際に試されるのであれば、発注者とのコスト負担について、話し合う必要が出てきますね。
Java7 で作成したプログラムに処理を委託しよう。
最終的には、この案でAPI Gatewayに接続を行う形で、ユーザーに了承を得ることが出来ました。
そして無事に開発を完成させることが出来ました。
Java7を採用するに至った理由は、次のとおりです。
- 最初に記載したとおり、Java7以降がSNI拡張に対応していること
- 保守側の環境で、保証されているJavaのバージョンが7までだったこと。
コードについては、最初にJava5で作成したものをそのまま流用です。
環境への影響を最小限に抑えるため、tarを解凍して適当な場所に配置。
そこに絶対パスで呼び出すことで問題なく動作してくれます。
HttpsURLConnectionはJava7だと元気に動いてくれて、SNIなんて意識しない感じでデータをやり取りしてくれます。
そんな感じでJava7で書いたコードをjarに固め、Java5から呼び出すようにしました。
Java5からの呼び出しは、cURLコマンドを実行したときと同じようにProcessBuilderを使用すれば大丈夫です。
実装における注意点
一点だけ詰まったのは、ProcessBuilderのwaitFor()で最初組んだこと。
ProcessBuilderで検索すると、多くのサイトでwaitFor()を使用してますが、呼ぶと応答が帰ってきません。
どうやらwaitFor()は、標準出力(またはエラー出力)バッファが一杯になると処理が固まるという厄介な性質があるみたいです。
回避方法の一つに、redirectErrorStreamを使用する方法があります。
この2つを一つにまとめてしまい、適時データを逃してあげるのです。
今回の事例では、出力より取得するデータは捨ててしまうため、次のように実装をして事なきを得ました。
1 2 3 4 5 6 7 8 9 |
// 引数作成 ArrayList<String> cmds = makeCmds(); // コマンドをセット // コマンド例 ProcessBuilder pb = new ProcessBuilder(cmds.toArray(new String[0])); pb.redirectErrorStream(true); Process p = pb.start(); while (p.getInputStream().read() >= 0);; |
最後に
こんな形でどうにかユーザーの要望に沿った開発を行うことが出来ました。
最初の現象が発生したときには半分発狂しかけたりもしました。
ですが、社長の出してくれたアイディアも適切で無事に乗り切れたことは本当に良かったですね。
ここでは省略した解決候補案もあり、それを含めて勉強になることも多かったです。
悩んでる時に適時アドバイスをいただける環境というのは、すごく貴重だと思います。
いずれは、アドバイスを皆にできる側に回って、みんなの力になりたいですね。
- Simple AWS DeepRacer Reward Function Using Waypoints - 2023-12-19
- Restrict S3 Bucket Access from Specified Resource - 2023-12-16
- Expand Amazon EBS Volume on EC2 Instance without Downtime - 2023-09-28
- Monitor OpenSearch Status On EC2 with CloudWatch Alarm - 2023-07-02
- Tokyo’s Coworking Space Hidden Gem: AWS Startup Loft Tokyo - 2023-05-24