はじめに
SORACOM Funkが2019年7月にリリースされてから、ちゃんと試せていなかったので試してみて記事を書きました。色々な使い方が考えられますが、今回はよくあるユースケースの割に意外と大変な「自回線の通信量を取得する」ことを目標にします。
SORACOM Funkを使う構成のメリット
SORACOMでは各回線の通信量がコンソール上から簡単に見え、さらにAPIでより詳細に取得できるのですが、デバイスから自回線の通信量を取得しようとすると結構大変です。
SORACOMにはSORACOM Air メタデータサービスという便利な機能があり、SIMにつけた名前やタグなど自回線に関わる情報は、
http://metadata.soracom.io/v1/subscriber
というURLにアクセスすると、認証情報なしで取得できます。そのため、デバイスに認証情報を持たせる必要がありません。このURLは
https://api.soracom.io/v1/subscribers/{IMSI}
というWebAPIのURLにマッピングされており、このURL以下のAPIであれば全て使用することができます。
ところが通信量は通信量取得APIにて取得するのですが、このURLは
https://api.soracom.io/v1/stats/air/subscribers/{IMSI}
であり、subscribers系のAPIでは無く、2019/12/29現在ではSORACOM Airメタデータサービスにはマッピングされていません。
従って、SORACOMの認証情報を使って認証して通常のWebAPIを使う必要があります。
普通に実装するとデバイス側にSAM(SORACOM Access Management)ユーザーの認証情報を保存してそれを使うところなのですが、共通の認証情報だと1つのデバイスで認証情報が漏れると全てのデバイスに影響してしまいますし、デバイスごとに認証情報を作成するとなると、アカウントあたりのユーザー上限数:50(2019/12/29現在)にすぐ引っかかってしまいます。あと単純にユーザーの作成とデバイスへの書き込みが大変です。
以前まではこのような場合はSORACOM Beamの署名ヘッダ機能を使ってどこかのHTTPSエンドポイントに対してアクセスし、エンドポイントは署名ヘッダを用いてデバイスを認証し、認証できたらそのエンドポイントが持つSAMユーザー情報を使って通信量を取得してデバイスに返す、という形を取っていたかと思います。
このようにすると、署名ヘッダにはIMSI(SIMを識別するためのID)が入っており、事前共有鍵を用いた署名ヘッダによって正当なデバイスからのアクセスであることは検証できるため、デバイスに認証情報を持たせること無くHTTPSエンドポイントはデバイスを認証(正当なデバイスであることが検証でき、かつどのデバイスからのアクセスかを識別)することができます。素晴らしい。
ただこの構成にも大きく分けて2つの厄介なところがあります。
- HTTPSエンドポイントを用意し、運用しなければならない
- SORACOM Beamからのアクセスを受け付けるため、HTTPSエンドポイントをインターネットに晒す必要がある
1に関しては、Amazon API Gateway と AWS Lambdaの組み合わせのサーバレス構成を取ることにより、運用負担はかなり軽減できます。ですが、構成要素が増えると構成管理や構築、運用の手間がかかることには変わりません。
2に関してはインターネットからアクセスされてしまうことは許容して、Beamの署名機能にてセキュリティを確保する、ということになるでしょう。
そこで2019/7のSORACOM Discoveryで発表されたSORACOM Funkならどうでしょう。Funkを使うと以下のようになります。
まず、デバイス側に認証情報を持たせなくてもよい、という点では先ほどと同じです。
先ほどの1に関しては、デバイス以外はSORACOMやAWSのサービスであり、自身でサーバーなどを運用する必要はなく構築、運用の手間は小さくなります。また、Amazon API Gatewayを使う場合と比較すると、Amazon API Gatewayを通さない分コストを抑えることができます。(Lambdaの起動に比べるとAPI Gatewayはそこそこコストがかかります。あとAPI Gatewayは設定の仕方が独特で、初見だと結構設定が大変です)
2.に関しては、AWS IAMやAWS Lambdaはインターネットからアクセス可能なサービスですが、AWSの強固な認証方法を用いているため、より安全に通信されます。(ここはSORACOM - AWS間の問題なので、こちら側で関知しなくともよいですし)
このように、AWS Lambdaを利用するユースケースでは、SORACOM Funkを利用すると運用コストを抑え、かつセキュリティを高めることが出来そうですね。
それでは実際やってみましょう。
構築
用意するものは以下の6つです。
- SORACOM APIを呼び出すためのSAMユーザー
- Lambdaの関数設定
- Lambdaを呼び出すためのIAMユーザー
- SORACOMの認証情報
- SORACOM Funkの設定
- Lambdaの関数のコード
まず設定系の1-5を準備します。
- 以下の手順でSAMユーザーを作成します。
できるだけ権限は狭くしておいた方がよいでしょう。最低限必要なAPI設定は以下になります。
{
"statements": [
{
"api": [
"Stats:getAirStats"
],
"effect": "allow"
}
]
}
SAMユーザーの認証キーを作成したら、2.でLambda関数の環境変数に入力するため、記録しておきます。
2.Lambdaの関数設定
Lambdaは「サーバーのことを考えずにコードを実行する」か。。AWSもだいぶカジュアルな説明しますね。
作成したLambda関数のARNは3.と5.で使用するため、記録しておきます。
Lambda関数作成後、先ほど作成したSAMユーザーの認証情報を環境変数として設定します。
これでコード以外のLambdaの設定は終了です。ランタイムは好きなものを選びます。余談ですが僕は今のところ以下のような感じでプログラムを使い分けるので、LambdaはRubyになることが多いです。
- 単一の処理を実行するサーバーサイドのプログラム:Ruby
- 複数の処理を実行するサーバーサイドのプログラム:C#
- クライアントサイド(デバイスもPCも)のプログラム:Go
サーバーサイドが分かれるのは、Rubyの方がパッと書きやすいけど、ある程度規模が大きくなってくると静的型付き言語じゃないとコーディングや修正が大変になってくるためです。
クライアントサイドがGoなのは、様々な環境で動作するプログラムをユーザーに実行させるのに、実行環境を用意させるのが一番大変なのですが、Goであれば各環境でのシングルバイナリで配布できるためです。
サーバーサイドはこちらの都合で環境が作れるため開発の都合だけで決められますが、クライアントサイドは環境構築はユーザー任せになってそこが一番の問題になるので、一番問題が小さい方法を取ることになるんですね。どのOSでも最初からDockerが動いてくれていれば良いのに。。
3.Lambdaを呼び出すためのIAMユーザー
アタッチするポリシーは以下のようになります。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "GetSoracomTrafficPolicy",
"Effect": "Allow",
"Action": "lambda:InvokeFunction",
"Resource": "2.で作成したLambda関数のARN"
}
]
}
続いてIAMユーザーを作成します。
作成したIAMユーザーのアクセスIDとアクセスキーは4. で使用します。
4.SORACOMの認証情報
以下の画面で認証情報を作成します
5.SORACOM Funkの設定
SIMグループから対象SIMのグループを選択し、以下のようにFunkの設定をします。
以上で設定は終わります。
Lambda関数のコード
Lambda関数がやることは、
- SORACOM Funkからの入力の受け取り
- SORACOM APIからの通信量の取得
- SORACOM Funkへの応答
の3つになります。
入力の受け取りは、Funkのドキュメントによると以下の形式で送られます。
AWS Lambda に渡されるデータ
event : ユーザーが指定したデータ形式でリクエストBodyの値が設定されます。
context : 以下のデータフォーマットが設定されます。(以下はSORACOM LTE-M Button for Enterprise からのリクエスト例となります)
{
"custom": {
"srn": "srn:soracom:OP00XXXXXXXX:jp:Subscriber:44052XXXXXXXXXXX",
"operatorId": "OP00XXXXXXXX",
"sourceProtocol": "udp",
"resourceId": "44052XXXXXXXXXXX",
"resourceType": "Subscriber",
"imsi": "44052XXXXXXXXXXX",
"imei": "35956XXXXXXXXXX"
}
}
Funkの送信データ形式を「JSON」としたので、eventにはJSONで渡されたパラメータがHashクラスとして渡されます。具体的には、通信量のperiod(dayもしくはmonth)を
{"period":"month"}
として送信すると、lambda上のRubyのコードでは、
event['period']
として参照できます。また、SIMを識別する情報であるIMSIはcontextの中に入っており、
context.client_context['imsi']
として参照できます。
SORACOM Funkへの応答は、ハンドラー(lambda_handler)の返り値がそのままFunkへの応答になります。
SORACOM APIに対して認証し、通信量を取得する部分は普通の方法であればAPIリファレンスを見ながら実装することになるのですが、SORACOMでは全てのAPIを網羅しているSORACOM CLIが提供されています。認証やアクセスの面倒な部分はこのCLIがやってくれているので、できるだけ利用したいものです。
Lambdaでそんなことできるの?という疑問はありますが、できます。Lambdaはデプロイパッケージをカレントディレクトリとして実行されるため、その中にSORACOM CLIのバイナリを入れておけば、パス指定で実行できるのです。(SORACOM CLIがシングルバイナリで提供されており、依存する実行環境やライブラリなどがほぼ無いためにこのような対応がしやすいです。そういう意味でもGo言語は使いやすいですね。ただし、Lambdaの実行環境は64bit Linux OSである、という前提で動作しているので、その部分が変更されると環境に合わせたバイナリに変更する必要はあります)
SORACOM CLIのリリースページから、最新の64bit Linux OS対応のバイナリ(soracom_X.Y.Z_linux_amd64.tar.gzという名前のファイル
)をダウンロードしておきましょう。
そして以下のようなディレクトリ構成を用意します。「soracom」ファイルはSORACOM CLIのバイナリファイルです。
user$ ls -l
total 27176
-rw-r--r-- 1 user user 2352 12 29 14:54 lambda_function.rb
-rwxr-xr-x@ 1 user user 13909290 12 23 15:22 soracom
require 'fileutils'
require 'json'
require 'date'
require 'securerandom'
def extract_imsi(context)
return nil if context.client_context.class != Hash
return nil if !context.client_context.key?('imsi')
context.client_context['imsi']
end
def create_config_file
soracom_config = { sandbox: false, coverageType: 'jp', authKeyId: ENV['SORACOM_AUTH_KEY_ID'], authKey: ENV['SORACOM_AUTH_KEY'], registerPaymentMethod: false }
tmp_dir_path = File.join('/tmp', 'get_soracom_traffic', SecureRandom.uuid)
config_file_path = File.join(tmp_dir_path, 'default.json')
FileUtils.mkdir_p(tmp_dir_path)
File.write(config_file_path, JSON.generate(soracom_config))
File.chmod(0600, config_file_path)
ENV['SORACOM_PROFILE_DIR'] = tmp_dir_path
config_file_path
end
def delete_config_file(config_file_path)
FileUtils.rm_rf(config_file_path)
end
def get_utc_timestamps(period)
timezone_now = ENV['TZ']
ENV['TZ'] = 'UTC'
if period == 'day'
from = Date.today.to_time.to_i
to = Date.today.next_day.to_time.to_i
elsif period == 'month'
from = (Date.today - Date.today.mday + 1).to_time.to_i
to = (Date.today.next_month - Date.today.next_month.mday + 1).to_time.to_i
end
ENV['TZ'] = timezone_now
{
'from' => from,
'to' => to
}
end
def summarize(traffic_stats)
total = 0
download = 0
upload = 0
traffic_stats.each do |stat|
stat['dataTrafficStatsMap'].each do |key, value|
total += value['downloadByteSizeTotal']
total += value['uploadByteSizeTotal']
download += value['downloadByteSizeTotal']
upload += value['uploadByteSizeTotal']
end
end
{ 'total' => total, 'download' => download, 'upload' => upload }
end
def lambda_handler(event:, context:)
period = event['period'] || 'month'
return { 'error' => "period must be 'month' or 'day'"} if !['month', 'day'].include?(period)
timestamps = get_utc_timestamps(period)
imsi = extract_imsi(context)
return { 'error' => 'IMSI must not be empty' } if imsi.nil?
begin
config_file = create_config_file
ret_text = `./soracom stats air get --imsi #{imsi} --period #{period} --from #{timestamps['from']} --to #{timestamps['to']}`
traffics = JSON.parse(ret_text.strip)
rescue => e
p e
return { 'error' => e.message }
ensure
delete_config_file(config_file)
end
summarize(traffics)
end
SORACOM CLIは環境変数SORACOM_PROFILE_DIRで指定したディレクトリにあるdefault.jsonファイルにある情報をもとにSORACOM APIにアクセスします。従って、環境変数で設定されたSORACOM_AUTH_KEY_IDとSORACOM_AUTH_KEYをもとにLambda実行環境内の/tmpディレクトリ内にランダムなパスのディレクトリを作り、その中にdefault.jsonを都度作成しています。すごいんだかすごくないんだかわからないですね。。当然CLI使用後に情報は削除します。(一時的にでもファイルが出来てしまうのはセキュリティ的にいまいちです。できればSORACOM CLIで環境変数かパラメータでSORACOM_AUTH_KEY_IDとSORACOM_AUTH_KEYを直接指定したいところ)
通信量はJSON配列の文字列で取得されるので、それをJSONでパースし、集計を取ってFunkに返しています。
プログラムができたら、ディレクトリ内の全ファイルをまとめてZIPにします。
user$ zip get_soracom_traffic.zip *
注意点としては、ディレクトリをZIPアーカイブするのではなく、ディレクトリの中でディレクトリ内の全ファイルをZIPアーカイブするということです。ディレクトリをZIPアーカイブするとLambdaの環境で展開した際に、トップがそのディレクトリになってしまい、ハンドラが見つからなくなってしまうためです。
最後に作成したZIPをLambdaにアップロードします。
これでコードの準備は終了です。実行してみましょう。
実行
Raspberry Pi3B+にて実行しました。
period = dayを指定すると、当日の通信量を取得できます。
pi@raspberrypi:~ $ echo -n '{"period":"day"}' | nc funk.soracom.io 23080
200 {"total":205332,"download":124384,"upload":80948}
取得できました。SORACOMでは時間の扱いがUTCなので、1日の区切りがUTCの0時(日本時間の9時)であることには注意しましょう。
period = monthを指定すると、当月の通信量を取得できます。
pi@raspberrypi:~ $ echo -n '{"period":"month"}' | nc funk.soracom.io 23080
200 {"total":206636581,"download":195877314,"upload":10759267}
問題なく動作してそうですね。デバイスに認証情報がなくてもSORACOM APIを利用した処理ができること、回線に紐付いたIMSIが特定できること、入力に対して処理が変えられること、同期的に応答があることが確認できました。
まとめ
SORACOM Funkを使って簡単・安全にクラウドに処理が委譲できることが確認できました。AzureやGCPとの組み合わせはやっていないので分かりませんが、Lambdaとの組み合わせに関する限りは簡単に連携できましたね。クラウドになにかさせる時にはBeam -> API Gateway -> Lambdaではなく、基本Funkで考えた方が運用、コスト、セキュリティといった面でよさそうです。
クラウドならではの高負荷の処理、ビッグデータが必要になる処理がユースケースとして上げられていますが、今回のようにデバイスには置いておけない認証情報が必要な処理をSIM認証と組み合わせてクラウドに任せるユースケースもありかなと思いました。
気になったというか、現状の厳しい点はFunkの設定が1つしかできないことですね。便利なので色々使いたいけど1個しかないので、現状ではパラメータで分けられるようにしておいて、1つめのLambdaは処理の切り替え用と割り切って、そこから別のLambdaを呼び出すというような構成になりそうです。
以上です。