ビルドが成功したら緑に光るやつ(物理)を作った

# はじめに

現代的なソフトウェア開発においてビルドやデプロイの仕組みは欠かせないものですが、手動でやるととても大変です。
そのあたりを色々とサポートしてくれるツールやサービスは様々あり、今回はAWSのCodeシリーズ(CodeCommit、CodeBuild、CodeDeploy、CodePipeline)を使ってみました。
こんな感じでCI/CDパイプラインが割と簡単に作れます。いい感じですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/5828dfcb-aec7-0666-d64a-35da1735c8e5.png)

とりあえずgitリポジトリにソースコードをPushしたら、.NET Coreのプロジェクトが自動的にビルドされて、EC2にデプロイされる、というのが作れました。

こういうのができると、CI/CDがうまくいってるかどうかを可視化したくなります。
もちろんメールやSlackで通知するのもいいのですが、一目でパッとわかるようにしたい。
以前作ったマイコンの遠隔操作プログラム[easi](https://github.com/1stship/easi)がありますので、これでうまくいってたら緑、失敗してたら赤く光るようにすれば、一目でわかっていいのではないかと思ったので作りました。制作時間30分くらいです。

![success.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/46b3477c-71a8-3179-7861-3ab87c183298.jpeg)

# 必要なもの

パイプラインはすでにできているものとします。(そこまで書いてるとすごく長くなるので)

- 遠隔操作プログラムが書き込まれたWio LTE(通信はSORACOM Air)
- デバイス操作権限を持つSAMユーザー
- SNSから通知を受けてSORACOM APIにコマンドを送るLambda
- CodePipelineからの通知を受け取りLambdaを起動するSNSトピック

![pipeline.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/1ea29ee9-6492-baf5-ae78-66c836cdfeae.png)

それぞれ準備していきましょう。

# Wio LTE

[easiのWioLTE用コード](https://github.com/1stship/easi/releases/download/V1.1.0/easi_wiolte_V1.1.0.zip)をダウンロードして展開し、easi.inoを以下のように修正します。

```c++
#include "easi.h"

#define EASI_WIO_LTE
#define EASI_VERSION "V1.1.0"

Lwm2m lwm2m;
#include <WioLTEforArduino.h>
WioLTE wio;
static bool greenOn = false;
static uint32 greenStartTime = 0;

void setup() {
  // 初期化
  delay(200);
  wio.Init();
  wio.PowerSupplyLTE(true);
  logText("POWER ON LTE");
  delay(500);
  if (!wio.TurnOnOrReset()) {
    logText("Turn on error");
    return;
  }

  // 接続
  if (!wio.Activate("soracom.io", "sora", "sora")) {
    logText("Connect error");
    return;
  }
  delay(1000);

  // LWM2Mエンドポイントとブートストラップサーバの設定
  lwm2mInit(&lwm2m, "wiolte");
  udpInit(&lwm2m.bootstrapUdp, "bootstrap.soracom.io", 5683);

  // ブートストラップをせず払い出したデバイスIDとキーを使用する場合はこちら
  // char identity[] = "d-01234567890123456789";
  // uint8 psk[16] =  { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff };
  // lwm2mSetSecurityParam(&lwm2m, identity, &psk[0]);

  // オブジェクトの設定のサンプル
  // 以下のオブジェクトリストのうちID:0 〜 ID:9、ID:3200 〜 3203、ID:3300 〜 3350を登録済み
  // http://www.openmobilealliance.org/wp/omna/lwm2m/lwm2mregistry.html
  
  // デバイスオブジェクトをインスタンス0番として登録
  addInstance(3, 0); 

  // Light Controlオブジェクトをインスタンス0番として登録
  addInstance(3311, 0);

  // オペレーションとメソッドを対応づけのサンプル
  setReadResourceOperation   (   3, 0,    2, &getSerial);      // READ    /3/0/2       でgetSerialが呼ばれるよう設定
  setWriteResourceOperation  (3311, 0, 5850, &turnOnOffLight); // WRITE   /3311/0/5850 でturnOnOffLightが呼ばれるよう設定
  setWriteResourceOperation  (3311, 0, 5706, &setLEDColor);    // WRITE   /3311/0/5706 でsetLEDColorが呼ばれるよう設定
  setExecuteResourceOperation(   3, 0,    4, &reboot);         // EXECUTE /3/0/3       でrebootが呼ばれるよう設定

  // ブートストラップ(接続情報取得)実行
  // 成功するまで繰り返す
  while (!lwm2mBootstrap(&lwm2m)){ }
}

void loop() {
  // LWm2mのイベントが無いかチェックし、イベントを処理したらtrue、イベントが無ければfalseを返す
  if (!lwm2mCheckEvent(&lwm2m)){
    if (greenOn && (getNowMilliseconds() - greenStartTime) > 60000){
      greenOn = false;
      wio.LedSetRGB(0, 0, 0);
    }
    delay(100);
  }
}

// READの場合は値をtlvの各要素に代入する
// Integer / Boolean / Timeはtlv->intValue
// Floatはtlv->floatValue
// Stringはtlv->bytesValue
// Opaqueはバイナリをtlv->bytesValue、長さをtlv->bytesLen
// ObjlnkはオブジェクトIDをtlv->ObjectLinkValue、インスタンスIDをtlv->InstanceLinkValue
// にそれぞれ代入する
void getSerial(Lwm2mTLV *tlv){
  strcpy((char *)&tlv->bytesValue[0], "123456789");
};

// WRITEの場合は値がtlvの各要素から渡される
// 対応する要素はREADと同じ
void turnOnOffLight(Lwm2mTLV *tlv){
  if (tlv->intValue){
    wio.LedSetRGB(255, 0, 0);
  } else {
    wio.LedSetRGB(0, 0, 0);
  }
};

void setLEDColor(Lwm2mTLV *tlv){
  if (strncmp((const char*)tlv->bytesValue, "red", 3) == 0){
    wio.LedSetRGB(255, 0, 0);
  } else if (strncmp((const char*)tlv->bytesValue, "green", 5) == 0){
    greenOn = true;
    greenStartTime = getNowMilliseconds();
    wio.LedSetRGB(0, 255, 0);
  } else if (strncmp((const char*)tlv->bytesValue, "orange", 6) == 0){
    wio.LedSetRGB(255, 165, 0);
  } else {
    wio.LedSetRGB(0, 0, 0);
  }
};

// EXECUTEの場合、
// パラメータはOpaqueと同じ形式で渡す(使わなくてもよい)
void reboot(Lwm2mTLV *tlv){
  NVIC_SystemReset();
};
```

LEDの色を指定するsetLEDColorメソッドを追加しています。
これをWio LTEに書き込んで動作させるとSORACOM Inventoryに接続されます。

SORACOMコンソールの[SORACOM Inventory] > [デバイス管理]の画面にて、EndpointがwiolteとなっているデバイスのIDを確認しておきます。あとでLambdaの環境変数に使用します。

![スクリーンショット 2020-11-29 21.12.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/6f28c111-77b4-69ca-b398-2c870c3f7970.png)

# SAMユーザー

以下の記事を参考にSAMユーザー(SORACOMを利用するためのユーザー)を作成し、認証キーを作成します。
https://dev.soracom.io/jp/start/sam/

権限はできるだけ狭くした方がよいでしょう。
今回はDevice:writeDeviceResourceだけで良いので、以下の権限とします。

```json
{
  "statements": [
    {
      "api": [
        "Device:writeDeviceResource"
      ],
      "effect": "allow"
    }
  ]
}
```

作成した認証キーはLambdaの環境変数に使用します。

# Lambda

Ruby2.7にてLambda関数を作成します。
以下の３つの環境変数を設定します。

|キー|値|
|:--|:--|
|SORACOM_AUTH_KEY_ID|SAMユーザーの認証キーID(keyId-で始まる)|
|SORACOM_AUTH_KEY_SECRET|SAMユーザーの認証キーシークレット(secret-で始まる)|
|SORACOM_DEVICE_ID|SORACOM InventoryのデバイスID(d-で始まる)|

コードは以下のようになります。
eventにはSNSトピックからのイベントが入り、そのMessageの中にPipelineからの情報が入っています。
その中のstatusにビルドやデプロイなどが成功したか、失敗したかが入っています。
このコードでは開始時にオレンジ、成功したら緑、失敗したら赤を指定するようにしています。

```ruby
require 'json'
require 'net/http'
require 'uri'

def lambda_handler(event:, context:)
  puts JSON.generate(event)
  token = get_soracom_token
  event['Records'].each do |record|
    message = JSON.parse(record['Sns']['Message'])
    color =
      case message['detail']['state']
      when 'SUCCEEDED' then 'green'
      when 'FAILED' then 'red'
      when 'STARTED' then 'orange'
      else 'off'
      end
    send_set_color_command(token, color)
  end
end

def get_soracom_token
  uri = URI.parse("https://api.soracom.io/v1/auth")
  request = Net::HTTP::Post.new(uri)
  request.content_type = "application/json"
  request["Accept"] = "application/json"
  request.body = JSON.generate({
    "authKeyId" => ENV['SORACOM_AUTH_KEY_ID'],
    "authKey" => ENV['SORACOM_AUTH_KEY_SECRET']
  })
  response = Net::HTTP.start(uri.hostname, uri.port, { use_ssl: true } ) do |http|
    http.request(request)
  end
  raise "fail to get soracom token" if response.code != '200'
  JSON.parse(response.body)
end

def send_set_color_command(token, color)
  uri = URI.parse("https://api.soracom.io/v1/devices/#{ENV['SORACOM_DEVICE_ID']}/3311/0/5706")
  request = Net::HTTP::Put.new(uri)
  request.content_type = "application/json"
  request["Accept"] = "application/json"
  request["X-Soracom-Api-Key"] = token['apiKey']
  request["X-Soracom-Token"] = token['token']
  request.body = JSON.generate({"value" => color})

  response = Net::HTTP.start(uri.hostname, uri.port, { use_ssl: true } ) do |http|
    http.request(request)
  end
  raise "fail to send command #{response.code} #{response.body}" if response.code[0] != '2'
  response.code
end
```

# SNSトピック

SNSトピックはCodePipelineのコンソールから作成するとスムーズです。

パイプラインの画面から[通知] > [通知ルールの作成]をクリックします。
![スクリーンショット 2020-11-29 21.57.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/a165326b-dcea-e39b-c319-a8cd0bc45b71.png)

通知名、詳細タイプ、トリガーするイベント、ターゲットを入力して、Submitします。
詳細タイプは「フル」のままで良いです。
イベントはどのくらい細かく通知するかですが、とりあえずパイプライン全体で成功したか/失敗したかを見たいので、パイプライン関連のイベントだけにします。ビルドとデプロイで表示を変える、などが必要であれば、ステージ関連のイベントも通知させれば良いでしょう。

![スクリーンショット 2020-11-29 21.59.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/e3fb8377-da35-e458-e5db-527d30710915.png)

ターゲットはここで作成できます。種類はSNSトピックとして、トピック名は適当に決めて作成します。
![スクリーンショット 2020-11-29 22.00.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/1de0adb1-9749-f234-bbd7-78a47427a428.png)

作成したら通知ルールのターゲットにSNSが出てきますので、リンクをクリックしてSNSのページに移動します。
![スクリーンショット 2020-11-29 22.05.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/6c38ebc0-4b32-6063-cbf8-827ccb6eb382.png)

SNSとLambdaを関連づけるのはサブスクリプションです。「サブスクリプションの作成」をクリックします。
![スクリーンショット 2020-11-29 22.06.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/eda276ce-01e6-09c6-4064-3a3956515e99.png)

トピックARNに作成したSNSトピック(入力済み)、プロトコルにAWS Lambda、エンドポイントに作成したLambda関数を指定し、「サブスクリプションの作成」をクリックします。
![スクリーンショット 2020-11-29 22.08.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/5e6dbcad-6f6f-701d-0b29-4b1d1c37c45e.png)

これでPipelineのステータスが変わるとSNSに通知され、SNSがそのステータス変化を入力としてLambdaを起動されるようになりました。Lambdaはステータスに応じてWio LTEのLEDの色を変えます。

やってみましょう。

# 動作確認

まず成功する場合です。
ビルドが成功するコミットをプッシュしてしばらくするとオレンジ(というか黄色っぽい)に光ります。この時パイプラインは進行中です。
![success-start.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/ed892a0d-ce6e-2fe8-0f2f-b33ab183fb34.jpeg)

成功で終了すると緑に光ります。(緑は1分経つと消してます。ずっと光らせててもいいのですがかなり眩しいので)
![success.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/83f1a49e-17ba-62b1-6e2f-811cd3d728ce.jpeg)

次にビルドが失敗するコミットをプッシュしてしばらくするとまたオレンジに光り、
![fail-start.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/fe1d3a74-f0f7-5af2-d349-ff6a4437af67.jpeg)

ビルドが失敗すると赤く光ります。(赤は見過ごさないように時間が経っても消えないようにしています)
![failed.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/ac3125b4-5cdd-c0bf-bf35-faa1837174be.jpeg)

パイプラインの状況が一目で分かりますね！

# おわりに

今はCI/CDがこんな簡単にできるようになってるのはいいですね。こういうのも身に付けていかないといけない。
クラウドに情報が集まっていればPC/スマホで確認できますがやや手間がかかりますので、IoTデバイスで物理的にわかりやすく見せられるのはこれはこれでいい感じだと思います。
今回はビルドやテスト、デプロイでしたが、例えば監視エラーが出たら赤く光らせるとか、他の使い方も考えられますね。色々やってみたいです。