soratun を改造して AWS Lambda から簡単に SORACOM Arc を使ってみました
「一般消費者が事業者の表示であることを判別することが困難である表示」の運用基準に基づく開示: この記事は記載の日付時点で株式会社ソラコムのエンジニアリングチームに所属する社員が執筆しました。ただし、個人としての投稿であり、株式会社ソラコムとしての正式な発言や見解ではありません。
はじめに
株式会社ソラコム Advent Calendar 2021 16 日目の記事です。今日は 2021 年 6 月に発表された SORACOM Arc (以下 Arc) とそのクライアントエージェント soratun、そして AWS Lambda extension のお話です。
SORACOM Arc とは
SORACOM Arc はソラコムが 2021 年 6 月にリリースした、SIM カードがなくてもソラコムのプラットフォームサービスをセキュアに利用できるサービスです。伝送路の安全性を WireGuard を使って確保しており、WireGuard を使える機器であれば iPhone や Android のようなスマートフォンから、Raspberry Pi を含む Linux、そして M5Stack で利用されている ESP32 でも動作します(参照: WireGuard for ESP32 の実装的なところ—ciniml さんによる SORACOM Advent Calendar 2021 7 日目の記事)。
物理的な SIM カードに代わってバーチャル SIM を発行し、バーチャル SIM に紐付いた WireGuard 認証情報を使って SORACOM プラットフォームと接続します。
soratun とは
soratun (sora-com tun-nel, ソラタン) とは SORACOM Arc を簡単・安全に使っていただくために開発した WireGuard クライアントで、MIT ライセンスで公開しています。
バーチャル SIM は SORACOM ユーザーコンソール や SORACOM CLI から作成できますが、soratun は作成から仮想ネットワークデバイス(tun)を使った WireGuard 接続までを一つのコンパクトなバイナリファイルでまとめていい感じにやってくれます。詳細は 公式ドキュメント や SORACOM Meetup〜Wi-Fi からもソラコムを!はじめよう新サービス SORACOM Arc で発表した資料 クライアントエージェント soratun とは / SORACOM Meetup Arc soratun overview - Speaker Deck をご覧ください。
AWS Lambda Extension とは
AWS Lambda extension は 2021 年 5 月に GA となった比較的新しい機能で、AWS Lambda (以下 Lambda) 関数の実行環境を独自に拡張できます。こちらも詳細は 公式ドキュメント をご覧いただきたいところですが、誤解を恐れずに今回のユースケースに絞って簡単に説明すると、Lambda の実行環境に Lambda 関数とネットワーク経由でコミュニケーションできる独自のプロセスを立てられる機能です。
GA が発表された 記事 ではトレーシング、ロギング、モニタリング、プロファイリングなどの extension が紹介されています。
AWS Lambda Extension と SORACOM Arc
ようやく本題です。Lambda extension と改造版 soratun を組み合わせて以下のようなことをやりました。
soratun
を SORACOM プラットフォームへのプロキシサーバーとして動作するよう改造しました (雑にsoraproxy
と命名)。soraproxy
を Lambda extension として動作するよう AWS のサンプルを元にラッパースクリプトを作りました。- Lambda extension を経由して Lambda 関数からプログラミング言語を問わず簡単に SORACOM プラットフォームへデータを送信できるようになりました。
アウトプットの質と量で尊敬する 1stship さんによる SORACOM Advent Calendar 2021 13 日目の記事 SORACOM Arc を特権なしで使えるツールを開発してみた を直前に拝見してネタが重複してしまったかと焦りましたが、アプローチが異なっていたのでよかったです。そして 1stship さんの記事は AWS やソラコムのスクリーンショット満載で大変親切であります。
以下にそれぞれのステップを説明します。
soratun
の改造
soratun
を SORACOM プラットフォームへのプロキシサーバーとして動作するよう改造します。既存の soratun
コマンドとは別に soraproxy
というコマンドを作っています。
soratun
の WireGuard 実装そのものはユーザー空間で動くとはいえ、稼動には tun デバイスの作成が必須です。実際に試していないのですが、Lambda extension では root 権限(正確には NET_ADMIN
capability)が必要になるネットワークインターフェースの作成ができないような気がしました。
そこで、soratun
が使用している wireguard-go に含まれる tun/netstack
パッケージを利用します。このパッケージはドキュメントがほとんど無いのですが、gVisor の TCP/IP スタックを使った WireGuard トンネルを提供します。すなわちネットワークインターフェースを含め WireGuard 接続を全部ユーザー空間で実装できる(= 特別な権限が不要)ことになります。
soratun | soraproxy | |
---|---|---|
WireGuard | ユーザー空間 (wireguard-go) | ユーザー空間 (wireguard-go) |
ネットワーク | カーネル (tun) | ユーザー空間 (gVisor) |
プロキシサーバーとしての実装はだいぶ適当で、SORACOM プラットフォームの Unified Endpoint に対する HTTP クライアントを作成し、受け取った POST リクエストを転送します。
今回のポイントである tun/netstack
パッケージを使用しているのはこのあたりです。gVisor を使ったトンネルを作成し Go の net/http.Client の Transport
として渡しています。簡単ですね。
AWS Lambda Extension としてパッケージ
AWS の公式サンプル aws-lambda-extensions/extension1.sh
を参考に soraproxy
が Lambda extension として動作するようラッパースクリプトを作成します。
LAMBDA_TASK_ROOT
環境変数を使用し、SORACOM Arc の認証情報 (arc.json
) は Lambda 関数にパッケージされたファイルを参照しています。Lambda 関数単位でバーチャル SIM を切り替えるのも簡単ですね。最後に &
を付けずにブロックしてしまってはまりましたが、厳密には soraproxy
の起動まで待つようにしないと問題が起きそうな概念検証感あふれる実装であります。
/opt/bin/soraproxy up --config ${LAMBDA_TASK_ROOT}/arc.json --address ${SORAPROXY_ADDRESS} --port ${SORAPROXY_PORT} &
soraproxy
のバイナリと上記のスクリプトを aws-lambda-extension/scripts/build.sh
を使って Zip ファイルにまとめ、Lambda レイヤーとして登録します(参考: Lambda レイヤーの作成と共有 - AWS Lambda)。
soraproxy
を使用する AWS Lambda 関数の実装
新しい Lambda 関数を作って上記の Lambda extension をレイヤーとして指定します(Lambda 関数の言語は何でも使えますが、レイヤーは ARN で指定する必要がありました)。
今回は JavaScript でテストしました。関数(index.js
)と SORACOM Arc の認証情報を保存した arc.json
(soratun
で使っているものそのまま)の 2 つのみです。Lambda 関数としてはシンプルで、soraproxy
が待ち受けている localhost:8080
へ POST
しています。これだけで Unified Endpoint へデータが送信できます。
const http = require("http");
exports.handler = async (event) =>
new Promise(async (resolve, reject) => {
const req = http.request(
{ host: "localhost", port: 8080, path: "/", method: "POST" },
(res) => {
let buffer = "";
res.on("data", (chunk) => (buffer += chunk));
res.on("end", () => resolve({ statusCode: 200, body: buffer }));
}
);
req.on("error", (e) =>
reject({ statusCode: 500, body: JSON.stringify(e.message, null, 2) })
);
req.write(JSON.stringify(event));
req.end();
});
テスト実行すると、以下のように Lambda extension (soraproxy
) が WireGuard 接続を初期化し、Unified Endpoint へ POST
している様子が見えます。
Function Logs
15:23:32 setup proxy server for Unified Endpoint
DEBUG: (soraproxy/proxy) 2021/12/12 15:23:32 proxy server for Unified Endpoint started at 0.0.0.0:8080
DEBUG: (soraproxy/proxy/tunnel) 2021/12/12 15:23:32 Routine: handshake worker 1 - started
DEBUG: (soraproxy/proxy/tunnel) 2021/12/12 15:23:32 Routine: encryption worker 1 - started
DEBUG: (soraproxy/proxy/tunnel) 2021/12/12 15:23:32 Routine: decryption worker 1 - started
DEBUG: (soraproxy/proxy/tunnel) 2021/12/12 15:23:32 Routine: encryption worker 2 - started
DEBUG: (soraproxy/proxy/tunnel) 2021/12/12 15:23:32 Routine: receive incoming v4 - started
DEBUG: (soraproxy/proxy/tunnel) 2021/12/12 15:23:32 peer(cx/Q…7vVA) - Routine: sequential sender - started
DEBUG: (soraproxy/proxy/tunnel) 2021/12/12 15:23:32 peer(cx/Q…7vVA) - Routine: sequential receiver - started
DEBUG: (soraproxy/proxy/tunnel) 2021/12/12 15:23:32 Routine: decryption worker 2 - started
DEBUG: (soraproxy/proxy/tunnel) 2021/12/12 15:23:32 Routine: handshake worker 2 - started
DEBUG: (soraproxy/proxy/tunnel) 2021/12/12 15:23:32 Routine: TUN reader - started
DEBUG: (soraproxy/proxy/tunnel) 2021/12/12 15:23:32 Routine: event worker - started
DEBUG: (soraproxy/proxy/tunnel) 2021/12/12 15:23:32 Interface up requested
EXTENSION Name: extension.sh State: Ready Events: [SHUTDOWN,INVOKE]
+ trap - SIGTERM
+ EVENT_DATA='{"eventType":"INVOKE","deadlineMs":1639322915570,"requestId":"xxxxxxxx-68e3-4be4-a682-e362c35837ff","invokedFunctionArn":"arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:soraproxy","tracing":{"type":"X-Amzn-Trace-Id","value":"Root=1-61b613f3-37678b955845097c7d499e07;Parent=033592cd59b3e60b;Sampled=0"}}'
+ [[ {"eventType":"INVOKE","deadlineMs":1639322915570,"requestId":"xxxxxxxx-68e3-4be4-a682-e362c35837ff","invokedFunctionArn":"arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:soraproxy","tracing":{"type":"X-Amzn-Trace-Id","value":"Root=1-61b613f3-37678b955845097c7d499e07;Parent=033592cd59b3e60b;Sampled=0"}} == *\S\H\U\T\D\O\W\N* ]]
+ echo '[extension.sh] Received event: {"eventType":"INVOKE","deadlineMs":1639322915570,"requestId":"xxxxxxxx-68e3-4be4-a682-e362c35837ff","invokedFunctionArn":"arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:soraproxy","tracing":{"type":"X-Amzn-Trace-Id","value":"Root=1-61b613f3-37678b955845097c7d499e07;Parent=033592cd59b3e60b;Sampled=0"}}'
[extension.sh] Received event: {"eventType":"INVOKE","deadlineMs":1639322915570,"requestId":"xxxxxxxx-68e3-4be4-a682-e362c35837ff","invokedFunctionArn":"arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:soraproxy","tracing":{"type":"X-Amzn-Trace-Id","value":"Root=1-61b613f3-37678b955845097c7d499e07;Parent=033592cd59b3e60b;Sampled=0"}}
+ sleep 1
DEBUG: (soraproxy/proxy) 2021/12/12 15:23:32 route POST / to Unified Endpoint
--- Request dump ---------------------------------
POST / HTTP/1.1
Host: 100.127.69.42:80
{"key1":10.2,"key2":0,"key3":5}
--- End of request dump --------------------------
DEBUG: (soraproxy/proxy/tunnel) 2021/12/12 15:23:32 peer(cx/Q…7vVA) - Sending handshake initiation
DEBUG: (soraproxy/proxy/tunnel) 2021/12/12 15:23:32 peer(cx/Q…7vVA) - Received handshake response
--- Response dump --------------------------------
HTTP/1.1 201 Created
Connection: close
Content-Length: 0
Date: Sun, 12 Dec 2021 15:23:32 GMT
--- End of response dump -------------------------
+ true
+ echo '[extension.sh] Waiting for event. Get /next event from http://127.0.0.1:9001/2020-01-01/extension/event/next'
[extension.sh] Waiting for event. Get /next event from http://127.0.0.1:9001/2020-01-01/extension/event/next
+ PID=50
+ forward_sigterm_and_wait
+ trap _term SIGTERM
+ /opt/bin/curl -sS -L -XGET http://127.0.0.1:9001/2020-01-01/extension/event/next --header 'Lambda-Extension-Identifier: xxxxxxxx-6dff-4060-83b5-c3fac699488f'
+ wait 50
END RequestId: xxxxxxxx-68e3-4be4-a682-e362c35837ff
REPORT RequestId: xxxxxxxx-68e3-4be4-a682-e362c35837ff Duration: 1005.43 ms Billed Duration: 1006 ms Memory Size: 512 MB Max Memory Used: 81 MB Init Duration: 275.38 ms
試してみたい方は
実験的な機能であることから soracom/soratun にはマージしていません。soratun のメインのターゲットである Raspberry Pi (の 32-bit 版)を gVisor が 正式にサポートしていない ことが大きな理由です。とはいえ soratun はオープンソースとして公開しており、要件に応じたカスタマイズの土台として利用できます。今回のようにすべてユーザー空間で実装する方式にはいろいろな可能性がありそうに思いますのでぜひお試しください。
Lambda extension レイヤーの Zip ファイルアーカイブを作る手順は以下のとおりです。soratun/CONTRIBUTING.md の Prerequisites が必要です。
$ git clone https://github.com/0x6b/soratun
$ cd soratun
$ git switch soraproxy
$ make lambda-extension
...
Created /path/to/aws-lambda-extension/soraproxy.zip
差分全体は GitHub でご覧ください。
ちなみに、最近の gVisor ではコンパイルに失敗してしまうため少し古めのバージョンを使用しています。コミットログから分かるように soraproxy
は結構前から試作していたのですが、アドベントカレンダーしめきり前に最新パッケージにしようとして失敗して焦りました。詳細は追いかけられていません。
まとめ
wireguard-go と gVisor という超巨人の肩を借りつつ AWS Lambda extension として実装することで、Lambda 関数から簡単にソラコムのプラットフォームサービスを利用できるようになりました。
2021 年 12 月現在、SORACOM Arc は 1 アカウントあたりバーチャル SIM1 契約分の基本使用料(月額)、1GB 分のデータ通信が無料で利用できますのでぜひお試しください。