大停電から見えてきたモノのデータの有用性

IoTチームの川畑です。9月6日 北海道胆振東部地震により被災された皆さまに謹んでお見舞い申し上げます。

去る9月6日の北海道胆振東部地震において、北海道全土に亘って大停電が発生したことは皆様ご存知の事だと思います。
弊社は札幌の北へ約10kmに位置する石狩市にデータセンターを構えているのですが、データセンター事業以外にも、石狩市様とIoT技術を軸とした防災や地域活性化に向けての包括協定*1を結び、取り組んでいます。
今回はその取り組みの中で地震・大停電が発生し、それによって得られた モノからの情報 が非常に興味深かった為、本ブログにて紹介しようと思います。

取り組みについて

事前情報として、弊社と石狩市様で取り組んでいる内容を簡単にご紹介します。
sakura.ioのLPWA版であるLoRaモジュールと超音波測距センサ(以後、水位計測器)を利用し、河川の水位データを集積・可視化する事で、大量降雨時の増水検出・避難所早期開設の役に立てるものです。
ひとまず取り組みは河川水位のみとなっていますが、やれることはセンサの数だけ存在し、温度・湿度など様々なデータを収集可能です。
水位計測器から送出されたLoRa電波は、LoRaゲートウェイ装置で受信され、更に上流のLTE回線を通じて弊社のプラットフォームに蓄積されます。

河川に設置している実際の写真です。センサー付きの子機は2分に1回測距し、LoRaで送信します。電力はリチウム1次電池を利用しており、電池入れ替え無しで1年程の連続稼働を見込んでいます。実際に設置したのは3月ですが、今のところは無停止で稼働中です。詳しい話をすると今回の本筋から外れてしまうため、この取り組みについては改めて別の記事でご紹介させて下さい。

求められる復電情報

例外無く弊社の石狩データセンターでも停電が発生し、非常用発電機による給電を行っていました。停電直後の発表によれば復旧には1週間以上を要するという絶望的な報道もさることながら、お客様はもちろんのこと社内からも復電情報を求める声が多数挙がりました。
世耕経産大臣がTwitterで発電所の再稼働や北電の復電情報を積極的に発信している姿は印象的で、たくさんの人がこれらのツイートに救われたと思います。


ですが、復電情報は地域レベルで見ると粒度が大きく、上記ツイートのようにおおよそは市単位であり、加えて更にその市町村の一部であるなど詳細な情報がありません。復電しても分単位でリアルタイムに情報を得る手段が無く、どうにか石狩の電力事情を知ることはできないかと考えていると、ふとLPWAの取り組みを思い出しました。

モノの声と携帯電話インフラの生命力

ここからは、実際のデータに基づく推察を交えてお話します。
LPWAの水位センサは、下図の通り石狩市北部に位置する浜益区において運用されており、6つの川にそれぞれ水位計測器とゲートウェイ装置が1台ずつ稼働しています。

川名 ゲートウェイ設置場所 給電方式
床丹川 屋外 鉛蓄電池(バッテリー)
幌川 室内 商用電源(100V)
茂生川 室内 商用電源(100V/非常用発電設備あり)
於札内川 室内 商用電源(100V)
毘砂別川 室内 商用電源(100V)
送毛川 室内 商用電源(100V)

表は地理的に北側から順に南下するように並べました。床丹川については鉛蓄電池による給電、それ以外は全て商用電源による給電です。床丹川も商用電源化する計画があるのですが、今回はむしろバッテリ運用によることでしか得られなかったデータもありました。
計測された水位データはsakura.ioプラットフォームを通じてインターネット上にあるサーバへ蓄積されたのち、Webアプリにより可視化されます。

まずは、地震発生直後のグラフをご覧ください。(水位計測器の筐体内温度グラフ)

複数のグラフが一斉に途切れているところが地震発生である3時7分を表しているのですが、1本だけ切れていない青色のグラフがあります。地震発生と同時に商用電源から給電しているゲートウェイ装置は全て止まってしまいましたが、青線が示すゲートウェイ装置は蓄電池から給電されており、その後も何時間か元気に動いていることが分かります。

少しだけ時間軸を進めたグラフがこちら。

地震発生から約8時間ほど時間を進めた所で、伸びていた青いグラフ(床丹川/バッテリー給電)が切れてしまいました。これは推察になってしまいますが、 最寄りの携帯電話基地局も地震直後に商用電源供給が停止したものの、附属の蓄電池設備で8時間は基地局の電力を賄っていたお陰で、ゲートウェイは通信できていたのではないか と考えています。
携帯電話設備やPHS設備は、電気通信事業法における事業電気通信設備規則(昭和60年郵政省令第30号)の第3条[適用範囲]及び第11条[停電対策]で自家発電設備や蓄電池など、これに類する停電対策を行うように定められているため、この基地局も対策がされていたのでしょう。
残念ながら他の河川についてはゲートウェイ装置が全て商用電源給電であったことから、復電まで通信が回復することはありませんでした。
水位計測器・ゲートウェイ装置ともにバッテリーで運用できていたからこそ、普段はひっそりと息を潜めている携帯電話インフラの、その生命力を垣間見ることができたのです。(そう考えると、「停電なう」とTwitterで呟けることも凄い事です)

復電の兆し

9/6 AM3:06に停電してから約16時間後、浜益南部の送毛川で動きが見られます。

驚く事に、9/6 PM19:10 突然送毛が復電しました。グラフにも描画されている通り、翌9/7のAM04:41に再度停電しましたが約10時間に亘って電力供給が行われたことが、グラフからも見て取れます。
北海道電力の電力系統図(110kV以下)によれば浜益地区は北江別系統に属するようです。系統図を見ると送毛変電所の奥に浜益変電所が構えており、この 送毛が一時的に復旧した という情報により浜益地区電力復旧の希望が見えてきます。

電力回復

ついに9/8 AM1:21頃、復電の合図かの如く浜益に設置している水位センサから同時刻で一斉にデータが届きます。商用電源から給電しているゲートウェイからデータが中継されており、当該地域において復電したことが伺えます。

同日9/8 PM14:05、当社のデータセンターも特別高圧の送電が復旧し、約60時間と長きにわたる非常用発電機の運転を終え、完全商用電源による電源供給を行うことができました。

振り返り

今までのイベント情報を時系列に並べて見てみましょう。

何時何分にデータを受信したか否か だけでここまでの情報を得ることができました。これこそ モノのつぶやき と言うに相応しく、LoRaやLTE等の無線通信、バッテリー&商用電源という電力供給方法、適度な地理的分散、更には他社が運用する設備など、複数の要素をいくつもの組み合わせで利用したことが功を奏し、本来の目的である防災だけではなくIoTが持つ潜在的な力が発揮されたのではないかと思います。
言ってしまえば、これらのゲートウェイをばら撒いて機器の電源状況を可視化すれば、設置場所が多ければ多いほど高精度・ピンポイントで停電情報が把握できるということを示しています。当社のsakura.ioは通信部分に専用モデムとLTE回線を提供しているため、通信状況の確認(基地局のダウン等)も同時に活用できるでしょう。今回は副次的効果として停電情報を得ることができましたが、このように実際に取得しているデータとは「異なる情報」が取得できる可能性は、他にもありそうです。
世界的に見ても飛び抜けて停電回数が少ない日本。IoTのチカラを利用して電力・通信事情を監視することだけが今回の解ではありませんが、高性能・高価なセンサーデバイスをいくつか用意して運用するより、安価で各々の性能は高くないけれども、それらを大量に設置し、データを収集・解析をすることで、前者より高精度かつ有用性の高い情報を得られることは間違いありません。

sakura.ioは、そんな方々も支えられるプラットフォームを目指しています。
長文にお付き合い頂き、ありがとうございました。

*1 北海道石狩市とさくらインターネット、IoTなどの情報技術を活用した地域活性化に関する包括連携協定を締結(弊社)
 石狩市とさくらインターネット株式会社による情報技術を活用した地域活性化に関する包括連携協定の締結について(石狩市様)

Continue Reading

【セキュアモバイルコネクト】SIMルート機能をリリースしました

IoTチームの川畑です。セキュアモバイルコネクトにて新機能をリリースしましたので、その概要から使い方・利用例をご紹介します。

SIMルートで実現できること

SIMルート機能では、SIMカードのICCID(固有識別番号)をNext-Hopとした静的IPルーティングができるようになりました。利用感覚はルータに静的IPルートを設定する事と同じように、CIDR表記の宛先ネットワークアドレス(e.g. 192.168.0.0/24)と、そのNext-HopになるSIMカードをコントロールパネル上から投入するだけで簡単に利用可能です。

使い所

昨今のLTEデバイスでは、EthernetやWiFi(IEEE802.11)の機能も持つような製品がリリースされ、携帯電話のみならず様々な場面でLTEネットワークを活用できるようになっています。

所謂M2MルータやIoTゲートウェイと呼ばれるこれらの製品は、特徴としてEthernet/LTE間のL3ルーティング機能が挙げられ、Ethernetポートに設定されたネットワークと、その先のLTEネットワークをNAPTを噛まずに直接接続可能です。一般的なポケットWiFiや携帯電話のテザリング機能でも、LTEデバイス自身の配下の端末をモバイルネットワークに接続することができますが、その多くはNAPTで実現されています。ここで問題なのは、そのLTEデバイス配下の端末にLTE外の通常ネットワークから直接アクセスできないことが挙げられるでしょう。

通常のインターネット専用MVNOと違い、我々のサービスはクラウドとモバイル端末を完全に閉域で相互接続できることにあります。インターネット専用MVNOではセキュリティ上懸念されていた LTEデバイスがPort LISTENしてサーバとして動作する ような構成が安全に取れるようになるのはもちろんの事、今回のSIMルート機能では更にその配下の端末がサーバとして動作しても安全に接続できるようになりました。

モバイルネットワークの利用形態

MNOやMVNOとの契約によって、利用可能な構成が全く異なります。弊社のセキュアモバイルコネクトを使った場合も含め、SIMが入っているデバイスへのアクセスを大きく4パータンにまとめてみました。(クリックで拡大)

パターン1: 通常のMNO/MVNO

一般的に契約可能なMNOやMVNOのLTEネットワークの構成です。LTEデバイスにはプライベートIPアドレスが割り当てられ、端末からインターネットにアクセスしようとすると2段でNAPTがかかります。通常のブラウジングのような動作には全く問題無い構成ですが、LTEデバイスやその配下の端末に直接アクセスしたい場合には不向きな構成です。

パターン2: グローバルIPオプションのついたMNO/MVNO

オプション料金を支払えば、グローバルIPアドレスがLTEデバイスに割り当てられるサービスです。直接グローバルIPアドレスが付くので、LTEデバイスがPort ForwardingやDMZに対応しているものであれば、配下の端末へのアクセスはある程度可能そうです。ですが、インターネットに直接晒されているため、直接DoS攻撃に遭うなど安全性に欠ける面が多く見られます。(DoSに遭ったとき、そのまま下り通信として計算された時には大変なことに…)

パターン3: 閉域網接続可能なMNO/MVNO

コンシューマ向けではなく、法人契約をターゲットにしたプランを打ち出しているMNOやMNVOが存在します。モバイル端末をお客様のネットワーク(データセンタやオフィス)まで直接閉域網で安全にデータ通信ができる仕組みで、インターネットからのリーチや他のお客様からの攻撃を受けることはありません。この場合ですと、 NAPTだけではなくL3 Routingできるデバイスでも使えるのでは? と思いがちですが、LTE網にデバイスをアタッチしたときに割り当てられるプライベートIPアドレスは、基本的に動的です。加えて、通常はパケットの宛先がLTEデバイスに付与したIPアドレスのときのみに通信が成立するようになっており、それ以外(ここではデバイス配下のネットワーク)の宛先のパケットは捨てられてしまいます。

パターン4: セキュアモバイルコネクト


弊社のサービスですが、上記3パターンの問題を解消しています。モバイルゲートウェイ(お客様のLTEデバイスを収容するアプライアンス)をキャリアと同じ感覚でお客様に操作して頂けるように作っており、端末に割り当てるIPアドレスをSIMカード毎に好きな値に固定できますし、インターネット接続も任意(デフォルトはOFF)に可能です。 今回リリースしたSIMルート機能についても、IPアドレスではなくICCIDをNext-Hopとすることで、端末(SIM)に割り当てるIPアドレスが変更されても、必ずそのSIMカードの先にStatic Routeが設定された状態が担保されます。

まとめ

SIMへのアクセスを観点に、簡単に表にまとめてみました

パターン メリット デメリット
パターン1 安価でキャリア選択肢が豊富 キャリア側でNAPTされる
SSH Reverse TunnelもしくはIPsecが必要
パターン2 安価でキャリア選択肢が豊富
デバイスに直接アクセス可能
インターネットからの攻撃に巻き込まれやすい
IPアドレスが変動する可能性が高い
パターン3 インターネットからの攻撃に巻き込まれない
デバイスに直接アクセス可能
IPアドレスが変動する可能性が高い
パターン4 IMSIとIPアドレスの1:1対応が可能
デバイスに直接アクセス可能
デバイスを超えてのルーティング設定が可能
特になし

最後に

今回リリースしたSIMルート機能の概要と、その使い方やモバイルネットワークの特徴・利用例をお伝えしました。SIMルートについては、その独自性について特許出願中です!本機能の具体的な設定方法については公式ドキュメントに掲載しておりますので、SIMルートドキュメントを御覧ください。今後とも、sakura.ioならびにセキュアモバイルコネクトを宜しくおねがいします!

Continue Reading

さくらがIINとPLMNを取得した話

さくらインターネットがIINとPLMNを取得した話

これはさくらインターネットAdvent Calendar 2017 20日目の記事です

まえがき

 IoTチームの川畑です。この度さくらインターネットは総務省より IIN(898104)PLMN(44006) の割り当てを受けました。これにより、キャリアに紐付かない独自の番号が入ったSIMカードを製造する事や、sakura.ioで展開している端末設備にユニークな識別番号を付与することが可能になりました。お気づきかと思いますが、この番号は主にモバイルネットワークで利用されます。

 今回割り当てを受けるにあたって、始終総務省との全ての交渉を担当する非常に貴重な経験をさせて頂きました。何せ探せど探せどどこにも情報がありませんし、通常は各キャリアの中の人がやる業務です。インターネット上で番号取得の実例が公開されるのは、もしかしたらこれが史上初かもしれません。

これらの番号取得の最たる例は、携帯電話事業者による取得・運用です。皆さんが普段利用している携帯電話と番号の関係、取得までの道のりをお届けしたいと思います。

IINとは

 IINとは、Issuer Indentify Numberの略で、ICカードの固有番号(ICCID)を発行する先頭最大7桁のプレフィックスです。世界中で利用するため、この番号はITU-T(国際電気通信連合)が管理をしており、電気通信事業用途で利用する場合は総務省を通して番号の申請を行う必要があります。

割り当てられた桁にはそれぞれ以下のような意味があります。
– 89 : 電気通信用途
– 81 : 国番号(日本)。最大3桁
– 04 : 事業者コード

SIMカードはもちろんのこと、クレジットカードなどのICチップにはICCIDが付与されます。弊社におけるICCIDの番号構成は以下の通りです。
– ①ICCIDの先頭6桁にIINを利用する
– ②残りの12桁は事業者が任意の番号を付与する
– ③最後の1桁はチェックディジット

PLMNとは

 PLMNとは、Public Land Mobile Networkの略で、携帯電話事業者の国際的な識別番号です。5桁で構成され、先頭3桁はMCC(Mobile Country Code)で国ごとに番号が変わり、日本には 440441 が割り当てられています。下2桁はMNC(Mobile Network Code)で、各国内の事業者コードですね。こちらも世界中で利用される一意の番号となるため、管轄は同じくITU-Tです。

 PLMNの用途は大きく2つあり、MNOが設置する携帯電話基地局の事業者識別に用いられることと、SIMカードにICCIDとは別に付与される、IMSI(International Mobile Subscriber Identity)のプレフィックスで利用されます。

PLMNの用途1:ビーコン

 各社の携帯電話基地局が自身のPLMNを電波(ビーコン)として報知することで、利用者の端末が契約された事業者へ自動的に接続するために利用されます。docomoですと44010、auだと44050/44051、SoftBankだと44020を基地局から報知しています。自動的に、と書きましたが手動で事業者を選択することも可能です。この図はiPhone7において周辺で報知されている事業者コードをサーチした時のスクリーンショットです。

PLMNの用途2:IMSI

 PLMNの2つ目の用途として、IMSI番号の生成で利用されます。IMSIは、モバイルネットワークにおける加入者の識別番号で、ネットワーク内でのアクセス制御のキーとしても利用されます。ICCIDはICカード固有の番号ですので変わることはありませんが、IMSIはOTA(Over The Air:遠隔でカード内の情報を書き換える技術)により書き換わる可能性があります。

 日本において、IMSIは総務省により 電気通信番号 として割り当てられます。皆さんよくご存知の電話番号も、電気通信番号の1つです(モバイルの世界ではMSISDNと呼ばれています)。IINもPLMNも申請自体は総務省に行うのですが、番号の管轄はITU-Tなので、番号割り当ての審査基準を満たせば裏で代理申請を行ってくれるという形になります。次の章では、実際の割り当てに係る手続きを見ていきましょう。

番号申請

 電気通信番号の管理は、総務本省の番号企画室が担当しています。(実は番号の申請を行おうにも全くアテが無い中、ふだん電気通信事業者の関係でお世話になっている担当官に紹介頂きました)

 ひとまず部内数名でお伺いし、概要をお伝えするところから始まりました。お渡しした資料を精査頂くのですが、不明点は逐一電話等で連絡が入ります。お伺いしてから約1~2週間で番号申請と審査に入りました。IINはITU-Tの規則に基づく審査、PLMNは電気通信番号規則に基づく審査ですので、それぞれ別々の申請用紙に記入を行います。

IIN

 ITUの基準に沿って総務省が代理審査を行います。通常IINは直接ITU申請なのですが、電気通信用途に限っては各国の省庁(日本は総務省)を通して申請を行う必要があります。申請書は以下の2枚です。

  • 1.ITU Registration Form
  • 2.IIN割り当て依頼文書

1の書式は公開文書ですので、こちらから参照して下さい。蛇足ですが、IINの申請にはITUへ手数料として80スイスフラン支払う必要があります。

2は総務省独自の文書ですが、内容はISOで定められたIIN割り当ての承認・拒否基準についての日本語回答です。基準は公開されており、こちらの4.3.1(承認基準)と4.3.2(拒絶基準)に回答します。

IINの申請は書類作成に約3週間、割り当てまで約2ヶ月かかりました。

PLMN

 電気通信番号規則に基づき、電気通信番号申請書への記入を行います。事項書の内容は以下の通りです。

  • 1.電気通信番号を必要とする理由
  • 2.必要とする電気通信番号の数及びその根拠となる需要の見込み
  • 3.必要とする電気通信番号の数に係る電気通信役務の提供の計画
  • 4.電気通信番号を管理する方法
  • 5.ネットワーク構成図
  • 6.別表第2に規定する要件を確認できる事項
  • 7.別表第3に規定する要件を確認できる事項
  • 8.その他電気通信番号の指定のために特に必要な事項

書式を見たい方は電気通信番号申請書で誰でも申請書を閲覧することが可能です。

補足:電気通信番号の概要と割り当てフローはこちらを参照。

ここからひたすらトライアンドエラーです。番号の申請内容に付随して、電気通信番号を利用する端末が、技術基準に適合している旨の証明、責任分界点の細かな設定・説明が必要です。このように、他の規則(端末設備等規則)や法令(電波法)に係る情報も求められます。
申請内容へのツッコミ・他の法令に係る確認等で、PLMN取得にまるまる3ヶ月要しました。

番号の割り当て

往訪から取得まで約3ヶ月の道のりでしたが、無事に番号を取得することができました。

最後に

番号の概要と、その取得の道のりについて書いてきました。興味のある人にとって、普段見られない事業者の裏側が垣間見れる記事は非常に新鮮でわくわくするものがあると思うのですが、いかがでしょうか。ITUによって取得内容は公示されますし、あえて隠す必要も無いので、せっかくですからIoTチーム活動の軌跡の1つとして書かせてもらいました。 また、自分自身が感じた 全く情報が無くて困った 事もあり、同じく番号取得をお考えの他の事業者の参考にもなれば良いなという気持ちです。

さくらインターネットは、今後はこの番号を活用し、更なるサービス価値の向上を目指します。

番号取得は、まだまだ序章に過ぎません。

(追記:PLMNとIINの取得にあたり、ITUより申請者の情報が公開されるのですが、まさかこのような形で自分の名前がインターネットに残るとは思ってもみませんでした。)

加筆・修正

2018/6/15 読者にご指摘を頂き、 IINの下二桁=MNC という誤った表現を削除しました。

Continue Reading

babelプラグインの作り方・活用方法

さくらインターネット Advent Calendar 2017の13日目の記事です。

さくらインターネットでは、sakura.ioのコントロールパネルを始めとした、様々な管理画面が存在しています。状況に合わせた最適解を選んでいるので、様々な設計やフレームワークの利用などが行われています。その中で、最近社内でもよく使われるようになった babel について、掘り下げて調べてみました。

この記事はbabel7を参照しています。記事を書いている時点では、betaなので正式リリースの際には変更される点が有るかもしれません。

目次

  • babelの簡単なおさらい
    • パッケージ構成
    • 処理のフローの理解
  • babelプラグインの作成
    • 簡単なプラグインを作成
    • テストを実行
  • babelプラグインを使ってコード自動生成を行う
    • s2sの紹介

パッケージ構成

Babelは、JavaScriptで書かれたJavaScriptのコンパイラーです。
非常にコード量の多いプロダクトになっており、以下のようにたくさんの小さいパッケージで構成されています。

インタフェース

  • @babel/core: .babelrc 等の設定を読み込み、ソースコードに対してプラグインの適用を順次行う
  • @babel/polyfill: ブラウザなどのランタイムにて、 core-js regenerator による標準ライブラリ等の補完を行う
  • @babel/register: require をフックし、コンパイルされたファイルを返す
  • @babel/cli: コマンドラインからコンパイルするためのコマンド

@babel/plugin-*

  • Transform Plugins
    • コード→コード (AST→AST) の変換を行う
    • ex: @babel/plugin-transform-arrow-functions
  • Syntax Plugins
    • babylon内に実装された、構文解析に使用するプラグインを設定するパッケージ
    • 現時点で構文解析を拡張することは出来ない
    • ex: @babel/plugin-syntax-jsx

※AST: コンパイラが扱う構文木 Abstract Syntax Tree

@babel/preset-*

各種構文をサポートするためのプラグインを一括適用するパッケージです。
1つのパッケージを適用するだけで、複数のパッケージが導入されます。

  • @babel/preset-es2015
    • @babel/plugin-transform-arrow-functions: () => {}function () {}
    • @babel/plugin-transform-spread: [...a, 'foo'];[].concat(a, [ 'foo' ]);
    • etc…
  • @babel/preset-react
    • @babel/plugin-syntax-jsx: <JSX /> のようなコードをパースして、Babelが扱えるようにする
    • @babel/plugin-transform-react-jsx: <span>hoge</span>React.createElement('span', null, 'hoge')
  • @babel/preset-env: ターゲットブラウザ・ランタイムに対応したコードを生成するためのプラグインを自動選択

内部パッケージ

今まで紹介を行ったパッケージの内部で呼ばれるものです。
プラグインを自作する際にも使用します。

  • @babel/babylon
    • 構文解析を行う
    • デフォルトでES2017構文が有効
    • JSX, Flow, Typescriptにも対応
  • @babel/types
    • ASTのジェネレーター・バリデーター
    • プラグインなどでASTを組み立てる時に使う
  • @babel/template
    • ASTのテンプレートを作成可能
    • 文字列で書いたコード内のプレスホルダにASTを差し込める
  • @babel/traverse
    • ASTを辿り、ノードの置換・追加・削除を行う
  • @babel/generator
    • ASTをコードに変換する
  • @babel/code-frame
    • エラーをソースの場所とともに表示する

いままでのあらすじ

ここまでで紹介したパッケージの流れをざっくり図にすると、このようになっています。

ちなみに現時点でまだbetaですが、babel 7は以下のような変更が行われています。

  • Scoped Package化 babel-core@babel/core
  • TypeScriptの標準対応が入る
  • flowで書き直しているっぽい
  • 新しい構文への対応も一緒に追加される予定
    • Optional Chaining: a?.b = 42;
    • BigInt: 50000n + 60n;

Babelプラグインを作る

ここではBabelのTransform Pluginsを作成しようと思います。
処理としては、babylonで解析されたASTを編集し返すみです。
babelプラグインを作るための手順は以下のとおりです。

  1. IN/OUTの形式をコードを定義する
  2. IN/OUTのASTを読む
  3. ASTを読み込んで、OUTの形式のASTを構築するコードを書く

順に追って説明します。

今回紹介する内容は、 https://github.com/kamijin-fanta/babel-example-2017 でコードを公開しています。

IN/OUTの形式をコードを定義する

ここでは、 hoge という識別子を fuga に置き換えるプラグインを作ります。
期待されるコードは以下のとおりです。

IN:

console.log(hoge)

OUT:

console.log(fuga)

ASTを読む

上で定義したASTをそれぞれ読んでみましょう。
ASTを読むには https://astexplorer.net/ が便利です。
ここでは、言語にJavaScriptを選択し、パーサーにbabylon7を選択します。

IN側のコードを入力しました。
console.log(hoge)CallExpression として認識され、呼び出す関数は callee 配列は arguments として格納されています。

callee 側の console.log は、 MemberExpression として定義されます。 object に格納された Identifierproperty に格納された Identifier でコードを表しています。

引数である (hoge)arguments 配列の中に Identifier として定義されています。

プラグインを書く

まず、何もしない最小のプラグインを作ってみます。

$ npm i @babel/core @babel/babel-cli
$ mkdir src
$ touch src/index.js
$ cat '{ "plugins": ["./index.js"] }' > .babelrc

index.jsを編集し、以下の内容にします。

// index.js
module.exports = function({ types: t }) {
  return {
    name: 'babel-example-plugin',
    visitor: {
      Identifier(path) {
        console.log('path: ', path.node)
      }
    }
  };
};

babelを実行し、標準出力に出力される結果を読んでみましょう。

$ node_modules/.bin/babel input.js
path:  Node {
  type: 'Identifier',
  start: 0,
  end: 7,
  loc: ~~~略~~~,
  name: 'console' }
path:  Node {
  type: 'Identifier',
  start: 8,
  end: 11,
  loc: ~~~略~~~,
  name: 'log' }
path:  Node {
  type: 'Identifier',
  start: 12,
  end: 16,
  loc: ~~~略~~~,
  name: 'hoge' }

プラグインで定義している visitor オブジェクトに対して、ノードのTypeに対してのマッチングを書いていきます。再帰的に探索を行い、マッチしたvisitorを呼び出します。Type名のメソッドを定義すると、親ノード・自ノードの情報等が含まれるパスが引数に渡されます。

上の例では、 console log hoge という3つのIdentifierがマッチしました。

次に、ノードの一部を書き換えてみましょう。
path オブジェクトの node プロパティが、現在のASTを表しています。下の例では、Identifierタイプでマッチしたノードのパスがpath引数に渡されます。Identifierは、属性としてnameを持ちます。その属性を書き換えてみましょう。

// index.js
module.exports = function({ types: t }) {
  return {
    name: 'babel-example-plugin',
    visitor: {
      Identifier(path) {
        if (path.node.name === 'hoge') {
          path.node.name = 'fuga';
        }
      }
    }
  };
};

このコードの出力結果はこうなります。

console.log(fuga);

テストを行う

テストは、 jestbabel-plugin-tester を使うのが使いやすそうなので、試してみます。

単純なテスト

先程はCLIから簡単にプラグインを扱うため、 .babelrc に設定しましたが、babel-plugin-testerを使うことで不要になるので、一緒に設定を書き換えます。

$ npm i jset babel-jest babel-plugin-tester @babel/preset-env
// .babelrc
{
  "presets": [
    ["@babel/preset-env", {
      "targets": {
        "node": "6.10"
      }
    }]
  ]
}

次にテストコードを書きます。codeが入力コードで、outputが出力コードです。

// src/index.test.js

import plugin from './index';
import pluginTester from 'babel-plugin-tester';

pluginTester({
  plugin,
  tests: [
    {
      title: 'example',
      code: `console.log(hoge);`,
      output: `console.log(fuga);`,
    },
  ],
});
$ node_modules/.bin/jest

 PASS  src\index.test.js
  babel-example-plugin
    √ example (6ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.603s, estimated 1s
Ran all test suites related to changed files.

スナップショットテスト

jestには、入出力の値の変化をテストするためのスナップショットテストの機能が有ります。 https://facebook.github.io/jest/docs/en/snapshot-testing.html

babel-plugin-testerはjestのスナップショットテストに対応しているため、非常に簡単にこの機能を使うことが出来ます。テストコードを書き換えて試します。 snapshot: true を設定し、outputを指定しないのがポイントです。

// src/index.test.js

import plugin from './index';
import pluginTester from 'babel-plugin-tester';

pluginTester({
  plugin,
  snapshot: true,
  tests: [
    {
      title: 'snapshot test',
      code: `console.log(hoge);`,
    },
  ],
});

テストを動かすと、 __snapshots__/index.test.js.snap ファイルが生成されます。次回このファイルと異なる値が出力された際は、エラーとなります。

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`snapshot test 1`] = `
"
console.log(hoge);

      ↓ ↓ ↓ ↓ ↓ ↓

console.log(fuga);
"
`;

こうして、スナップショットをとっておくことで、プラグインの変更を手軽に検知できるようになります。

s2sの紹介

Babalプラグインを作ることで、Babelの機能などが理解できたかと思います。基本的に行っていることがコード→コードということがよく理解できたかなと思います。そのコードからコードを生成するBabelPluginの性質に着目したのが、s2sです。

https://github.com/akameco/s2s

s2sと適切なBabelPluginを組み合わせると、Redux等で大量に必要となるボイラープレートの削減を行うことができます。

基本的な動作は、このような流れです。エディタでファイルを保存すると、即時にコードが自動生成され、追記されたように見えます。

  1. ファイルが保存されたタイミングでハンドラ(babel-plugin)が起動
  2. babel-pluginはASTを受け取り、編集を行って返す
  3. 結果の文字列でファイルを上書きする

例えば、FlowでのReduxアプリケーションで使用する babel-plugin-s2s-action-types は、以下のような変換を行います。

In:

export type Action = Increment

Out:

// @flow
export const INCREMENT: "app/counter/INCREMENT" = "app/counter/INCREMENT";

export const Actions = {
  INCREMENT
};

export type Increment = {
  type: typeof INCREMENT
};

export type Action = Increment;

タイプ数が半分以下になっている事がお分かりいただけると思います。ただ、内部でやっていることは基本的に、ファイルのwatch・BabelPluginを使用したtransform・ファイルの書き込みの3点です。なので、ユーザが自由に拡張することが出来ます。

私も、babylon7がTypeScriptに対応していたので、上のプラグインのTS版のプラグインを作ってみました。非常に簡単に実装を行うことが出来ました。

https://github.com/kamijin-fanta/babel-plugins/tree/master/packages/babel-plugin-s2s-action-types-ts

まとめ

  • babel怖くない
    • babelのプラグインはASTを理解すると作れる
  • AST怖くない
    • astexplorer等、ASTの理解を助けるツールが存在する

冬休みはカスタムのBabelPluginを作ってみてはいかがでしょうか。

資料

  • https://github.com/thejameskyle/babel-handbook/blob/master/translations/en/plugin-handbook.md
    • もう少し掘り下げてプラグインを作る方法がかかれている
  • https://github.com/kamijin-fanta/babel-example-2017
    • 今回のサンプルプログラムを置いたリポジトリ
Continue Reading
Close Menu