公式チュートリアルで Kubenetes 入門(前編)

 今まではなんとなくの知識だけで、自分で触ってみたことはなかったので、今更ですが公式チュートリアルで Kubernetes に入門してみました。

Tutorials - Kubernetes

 チュートリアルの中にも色々あるのですが、今回は一番基本的な Kubernetes Basics をやってみます。

kubernetes.io

 チュートリアルはブラウザ上で仮想ターミナルからコマンドを実行できる Katacoda の環境の上で提供されています。

www.katacoda.com

 また、 Katacoda の上で Minikube という簡単にローカルに Kubernetes 環境を構築するツールが使われています。

github.com

 Kubernetes Basics は6つの Modules に分かれているので、一つずつ進めてみます。分量が多くなってしまったので、今回は Module 1 から 3 までを進めます。

Kubernetes Clusters(Module 1)

 まずは Kubernetes Clusters についてです。 Kubernetes は可用性の高いクラスタ環境を管理し、アプリケーションコンテナの管理を自動化します。

 クラスタの中には Master と Node が存在し、 Master がクラスタを管理します。 Node は実際にサービスを提供するVMもしくは物理コンピュータになります。

 また、 Node が Master と連携するために Kubelet というエージェントが存在します。

 さらに Node はコンテナのオペレーションをハンドリングするために、 Docker または rkt を使用します。

 Kubenetes Cluster ではトラフィックを処理するためには最低3つの Node を保持する必要があります。

 では実際にチュートリアルを進めてみます。チュートリアルは Katacoda 環境でコマンドを実行しながら進めることができます。

f:id:akanuma-hiroaki:20181229171152p:plain

 まずは Minikube のバージョンを確認します。

$ minikube version
minikube version: v0.28.2

 そして Minikube を実行します。

$ minikube start
Starting local Kubernetes v1.10.0 cluster...
Starting VM...
Getting VM IP address...
Moving files into cluster...
Setting up certs...
Connecting to cluster...
Setting up kubeconfig...
Starting cluster components...
Kubectl is now configured to use the cluster.
Loading cached images from config file.

 Minikube によって Kubernetes 環境が構築されました。

 Kubernetes の操作には CLI の kubectl を使用します。まずはバージョンを確認してみます。

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"11", GitVersion:"v1.11.0", GitCommit:"91e7b4fd31fcd3d5f436da26c980becec37ceefe", GitTreeState:"clean", BuildDate:"2018-06-27T20:17:28Z", GoVersion:"go1.10.2", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"10", GitVersion:"v1.10.0", GitCommit:"fc32d2f3698e36b93322a3465f63a14e9f0eaead", GitTreeState:"clean", BuildDate:"2018-04-10T12:46:31Z", GoVersion:"go1.9.4", Compiler:"gc", Platform:"linux/amd64"}

 Client Version は kubectl のバージョンで、 ServerVersion は Master にインストールされている Kubernetes のバージョンになります。

 次にクラスタの情報を確認してみます。

$ kubectl cluster-info
Kubernetes master is running at https://172.17.0.48:8443

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

 チュートリアル中は CLI のみで進めますが、表示された URL にアクセスすると、クラスタの情報が確認できるダッシュボードを表示することができます。

 続いて Node を確認してみます。

$ kubectl get nodes
NAME       STATUS    ROLES     AGE       VERSION
minikube   Ready     <none>    7m        v1.10.0

 現在は Node は一つだけで、 STATUS が Ready になっていて、アプリケーションのデプロイが可能な状態になっています。

Kubernetes Deployments(Module 2)

 Kubernetes Cluster を稼働させていれば、その上にコンテナ化されたアプリケーションをデプロイすることができます。そのためには Kubernetes Deployment の設定を作成します。

 もし Node がダウンしたり削除されたりすると、 Deployment コントローラはそれをリプレイスします。これによってメンテナンス時や障害時の自律回復機構を提供します。

 Deployment の管理は kubectl で行うことができます。 Deployment を作成するときは、アプリケーションのコンテナイメージと、稼働させるレプリカの数を指定します。このチュートリアルでは Docker コンテナにパッケージされた Node.js アプリケーションが使われます。

 チュートリアルを開始すると下記のように自動的にシェルが実行され、クラスタが起動します。

$ sleep 1; launch.sh
Starting Kubernetes...
Kubernetes Started

 Node を確認すると下記のように一つだけ稼働していることがわかります。

$ kubectl get nodes
NAME       STATUS    ROLES     AGE       VERSION
minikube   Ready     <none>    2m        v1.10.0

 新しい Deployment を作成するには、 kubectl run コマンドを使用します。実行時には Deployment名とコンテナイメージのロケーションを指定する必要があります。また、特定のポートでアプリを実行するためには --port オプションを使用します。

$ kubectl run kubernetes-bootcamp --image=gcr.io/google-samples/kubernetes-bootcamp:v1 --port=8080
deployment.apps/kubernetes-bootcamp created

 Deployment を確認してみます。

$ kubectl get deployments
NAME                  DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
kubernetes-bootcamp   1         1         1            1           30m

 Deployment が一つあり、一つのインスタンスで稼働していることがわかります。このインスタンスは Node の Docker コンテナ上で稼働しています。

 Kubernetes 内で稼働している Pod はプライベートで独立したネットワーク内で稼働しています。デフォルトでは同じクラスタ内からのみアクセス可能です。 kubectl 使用時には API エンドポイントを通して連携が行われています。また、 kubectl コマンドでクラスタ内のプライベートネットワーク内でのアクセスを転送するプロキシを作成することができます。作業用のターミナルとは別ターミナルで下記のコマンドを実行します。

$ kubectl proxy
Starting to serve on 127.0.0.1:8001

 これでオンラインターミナルが稼働しているホストと Kubernetes Cluster 間でのコネクションが作成されました。プロキシによって API にターミナルからダイレクトにアクセスできるようになっています。

$ curl http://localhost:8001/version
{
  "major": "1",
  "minor": "10",
  "gitVersion": "v1.10.0",
  "gitCommit": "fc32d2f3698e36b93322a3465f63a14e9f0eaead",
  "gitTreeState": "clean",
  "buildDate": "2018-04-10T12:46:31Z",
  "goVersion": "go1.9.4",
  "compiler": "gc",
  "platform": "linux/amd64"
}

 API サーバは自動的に Pod名をベースにしたエンドポイントを作成します。プロキシを通してそのエンドポイントにもアクセスすることができます。そのためには Pod名が必要なので、 Pod名を取得して環境変数に保持します。

$ export POD_NAME=$(kubectl get pods -o go-template --template '{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}')
$ echo Name of the Pod: $POD_NAME
Name of the Pod: kubernetes-bootcamp-5c69669756-288s7

 この Pod名を使ってエンドポイントにアクセスしてみます。 Pod の API の URL は下記のようになります。

$ curl http://localhost:8001/api/v1/namespaces/default/pods/$POD_NAME/proxy/
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-5c69669756-288s7 | v=1

Kubernetes Pods And Nodes(Module 3)

 Pod は一つまたはそれ以上のアプリケーションコンテナ(Docker または rkt)を表していて、いくつかのリソースをコンテナ間で共有します。リソースには下記のようなものが含まれます。

  • 共有ストレージ

  • ネットワーク(ユニークなクラスタIP)

  • コンテナイメージのバージョンや使用するポートなどの情報

 Pod 内のコンテナは IP アドレスとポートの空間を共有します。

 Pod は Kubernetes プラットフォームでの最小ユニットになります。

 また、 Pod は常に Node 上で稼働します。

 全ての Node では少なくとも Kubelet(Pod とコンテナを管理し、Master と Node の連携を行う)と、コンテナのランタイム(Docker や rkt など)が稼働します。

 現在の Pod を確認するには kubectl の get コマンドを使用します。

$ kubectl get pods
NAME                                   READY     STATUS    RESTARTS   AGE
kubernetes-bootcamp-5c69669756-sv4wc   1/1       Running   0          1m

 Pod 内のコンテナや、そのコンテナで使われているイメージを確認するには、 kubectl describe pods コマンドを使用します。このコマンドによって Pod の IP アドレスやポート、ライフサイクルイベントなども確認することができます。

$ kubectl describe pods
Name:           kubernetes-bootcamp-5c69669756-sv4wc
Namespace:      default
Node:           minikube/172.17.0.4
Start Time:     Sat, 29 Dec 2018 10:40:18 +0000
Labels:         pod-template-hash=1725225312
                run=kubernetes-bootcamp
Annotations:    <none>
Status:         Running
IP:             172.18.0.2
Controlled By:  ReplicaSet/kubernetes-bootcamp-5c69669756
Containers:
  kubernetes-bootcamp:
    Container ID:   docker://051074e6dc2c7b6fb75c92ace46fad77838d4392b7270a7b2fa9dad372a9086d
    Image:          gcr.io/google-samples/kubernetes-bootcamp:v1
    Image ID:       docker-pullable://gcr.io/google-samples/kubernetes-bootcamp@sha256:0d6b8ee63bb57c5f5b6156f446b3bc3b3c143d233037f3a2f00e279c8fcc64af
    Port:           8080/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Sat, 29 Dec 2018 10:40:18 +0000
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-l6t4t (ro)
Conditions:
  Type           Status
  Initialized    True
  Ready          True
  PodScheduled   True
Volumes:
  default-token-l6t4t:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-l6t4t
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type     Reason                 Age              From               Message
  ----     ------                 ----             ----               -------
  Warning  FailedScheduling       4m (x4 over 4m)  default-scheduler  0/1 nodes are available: 1 node(s) were not ready.
  Normal   Scheduled              4m               default-scheduler  Successfully assigned kubernetes-bootcamp-5c69669756-sv4wc to minikube
  Normal   SuccessfulMountVolume  4m               kubelet, minikube  MountVolume.SetUpsucceeded for volume "default-token-l6t4t"
  Normal   Pulled                 4m               kubelet, minikube  Container image "gcr.io/google-samples/kubernetes-bootcamp:v1" already present on machine
  Normal   Created                4m               kubelet, minikube  Created container
  Normal   Started                4m               kubelet, minikube  Started container

 プロキシ経由で Pod 関連の操作を行うために再度別ターミナルでプロキシを実行します。

$ kubectl proxy
Starting to serve on 127.0.0.1:8001

 Pod 名を環境変数に格納します。

$ export POD_NAME=$(kubectl get pods -o go-template --template '{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}')
$ echo Name of the Pod: $POD_NAME
Name of the Pod: kubernetes-bootcamp-5c69669756-sv4wc

 curl コマンドでアプリケーションのアウトプットを確認します。

$ curl http://localhost:8001/api/v1/namespaces/default/pods/$POD_NAME/proxy/
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-5c69669756-sv4wc | v=1

 アプリケーションが標準出力に送ったものは Pod 内のコンテナのログになります。そのログは kubectl logs コマンドで確認することができます。 Pod 内にコンテナが一つの場合はコンテナ名を指定する必要はありません。

$ kubectl logs $POD_NAME
Kubernetes Bootcamp App Started At: 2018-12-29T10:40:18.825Z | Running On:  kubernetes-bootcamp-5c69669756-sv4wc

Running On: kubernetes-bootcamp-5c69669756-sv4wc | Total Requests: 1 | App Uptime: 725.334 seconds | Log Time: 2018-12-29T10:52:24.159Z

 Pod が稼働していれば、コンテナ上で直接コマンドを実行することができます。そのためには exec コマンドと Pod 名を使用します。環境変数のリストを表示する例は下記の通りです。

$ kubectl exec $POD_NAME env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=kubernetes-bootcamp-5c69669756-sv4wc
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT=tcp://10.96.0.1:443
NPM_CONFIG_LOGLEVEL=info
NODE_VERSION=6.3.1
HOME=/root

 bash のセッションを開始することもできます。

$ kubectl exec -ti $POD_NAME bash
root@kubernetes-bootcamp-5c69669756-sv4wc:/#

 セッション開始後はそのままコマンドが実行できます。

root@kubernetes-bootcamp-5c69669756-sv4wc:/# ls
bin   core  etc   lib    media  opt   root  sbin       srv  tmp  var
boot  dev   home  lib64  mnt    proc  run   server.js  sys  usr

 NodeJS コンテナ内からの実行になるので、 curl も localhost 指定で実行可能です。

root@kubernetes-bootcamp-5c69669756-sv4wc:/# curl localhost:8080
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-5c69669756-sv4wc | v=1

まとめ

 仮想環境が提供されているので、すぐにチュートリアルが始められるのはハードルが低くて良いですね。今回は前半まででしたが、クラスタ環境の構成まではイメージすることができました。後編ではスケーリングやローリングアップデートも扱われているので、サクッとやってみたいと思います。

SORACOM Krypton で AWS IoT にデバイスを登録する

 SORACOM Technology Camp 2018 で関連するセッションに参加してから気になっていたものの触れていなかった SORACOM Krypton を触ってみました。公式のチュートリアルを参考に、 AWS IoT へのデバイス登録を試してみます。

dev.soracom.io

SORACOM Krypton とは

 詳細な説明は公式ページ(SORACOM Krypton とは)をご覧いただければと思いますが、すごく簡単に言うと、 IoT デバイスのクラウドサービスへの接続設定をセキュアに行うためのサービスです。

 IoT デバイスをクラウドサービスへ接続するには認証情報が必要になりますが、デバイス上に認証情報を配置するには出荷時に組み込んでおくか、あとで個別にデバイス上に配置していく必要がありますが、いずれもかなり手間がかかりますし、配置する認証情報とデバイスの組み合わせを間違う可能性もあります。SORACOM Krypto を使用すると、 SORACOM Air のセルラー回線の情報を使った認証か、SIM の情報を使って SORACOM Endose での認証を行い、認証情報を自動的に発行してデバイスを登録することができますので、初期設定の手間をかなり減らすことができます。

AWS IoT ポリシー設定

 まずは登録するデバイスに付与する権限を設定するため、 AWS IoT のポリシーを作成します。 AWS IoT Core の 安全性 -> ポリシー の画面から 作成 をクリックします。

f:id:akanuma-hiroaki:20181222201556p:plain

 ポリシーの作成フォームが表示されますので、任意の名前を設定します。アクションとしては、今回はとりあえず AWS IoT のフル権限を与えておくために、 iot:* を設定します。リソース ARN も任意に設定し、効果は 許可 を選択して 作成 をクリックすると AWS IoT ポリシーが作成されます。

f:id:akanuma-hiroaki:20181222201638p:plain

AWS ユーザ設定

 次にデバイス登録時に証明書を発行する権限を持つ IAM ユーザを作成します。IAM のユーザーメニューで ユーザーを追加 をクリックします。

f:id:akanuma-hiroaki:20181222202207p:plain

 ユーザー名は任意に設定し、アクセスの種類は プログラムによるアクセス を選択して 次のステップ: アクセス権限 をクリックします。

f:id:akanuma-hiroaki:20181222202247p:plain

 アクセス許可の設定として、 既存のポリシーを直接アタッチ を選択し、「CreateKeysAndCertificate」ポリシーを付与します。

f:id:akanuma-hiroaki:20181222202322p:plain

 ポリシーを設定したら ユーザーの作成 をクリックしてユーザーを作成します。

f:id:akanuma-hiroaki:20181222202444p:plain

 ユーザーが作成されたら、あとで SORACOM 側の設定時に必要になるので、 csv ファイルで認証情報をダウンロードしておくか、アクセスキー ID とシークレットアクセスキーをメモしておきます。

f:id:akanuma-hiroaki:20181222213222p:plain

 ただ実際にこの権限だけで試してみると、あとで登録を実行しようとすると、下記のように権限が足りないと言うことでエラーになります。

$ curl -X POST -H 'content-type: application/json' https://krypton.soracom.io:8036/v1/provisioning/aws/iot/bootstrap
{"code":"DIC0001","message":"Error occurred: User: arn:aws:iam::365361468908:user/krypton-aws-iot-provisioner is not authorized to perform: iot:CreateKeysAndCertificate on resource: * (Service: AWSIot; Status Code: 403; Error Code: Access
DeniedException; Request ID: e5908284-05a0-11e9-a0d6-5f7d4bdeb37f)"}

 なので今回はとりあえずインラインポリシーで「iot:CreateKeysAndCertificate」を追加しました。

f:id:akanuma-hiroaki:20181222205329p:plain

 最終的に下記二つのポリシーを設定しています。

f:id:akanuma-hiroaki:20181222205458p:plain

SORACOM Krypton セットアップ

 ここまでで AWS 側の設定は完了なので、続いて SORACOM User Console での設定を行います。

 ユーザーアカウントのメニューから、 セキュリティ をクリックします。

f:id:akanuma-hiroaki:20181222203011p:plain

 認証情報ストアメニューから 認証情報を登録 をクリックします。

f:id:akanuma-hiroaki:20181222203402p:plain

 登録フォームが表示されたら、認証情報 ID と概要に任意の内容を設定します。種別は AWS 認証情報を選択して、先ほど作成した IAM ユーザのアクセスキー ID とシークレットアクセスキーを設定して 登録 をクリックします。

f:id:akanuma-hiroaki:20181222203524p:plain

 次に Krypton を使用するための SIM のグループを作成します。 SIM グループの画面から 追加 をクリックします。

f:id:akanuma-hiroaki:20181222204030p:plain

 登録フォームが表示されたら任意のグループ名を設定して グループ作成 をクリックします。

f:id:akanuma-hiroaki:20181222204230p:plain

 グループが作成されたら、グループ設定の「SORACOM Krypton 設定」の項目の設定を ON にして、 + をクリックして AWS IoT を選択します。

f:id:akanuma-hiroaki:20181222204525p:plain

 設定フォームが表示されますので、 AWS リージョンには AWS IoT の設定を行ったリージョンを設定し、認証情報は先ほど SORACOM User Console から登録した認証情報を選択します。 Policy name は AWS IoT で登録したポリシー名を設定します。 Thing name pattern には Krypton 経由で AWS IoT のデバイスを登録するときのネーミングのパターンを指定します。 $imsi は実際の IMSI に置き換えられます。ホスト名はアカウント固有の AWS IoT エンドポイントです。ルート認証局証明書はブランクで問題ありません。以上の設定ができたら OK をクリックします。

f:id:akanuma-hiroaki:20181222204701p:plain

 上記の設定内容については公式の下記ページの一番下にも説明があります。

dev.soracom.io

 グループの設定はここまでで完了なので、このグループに SIM を紐付けます。 SIM の管理画面で対象の SIM のチェックボックスにチェックを入れて、 操作 メニューから 所属グループ変更 を選択します。

f:id:akanuma-hiroaki:20181222204944p:plain

 変更フォームが表示されたら、先ほど作成したグループを選択して グループ変更 をクリックします。

f:id:akanuma-hiroaki:20181222205214p:plain

Raspberry Pi から登録確認

 ここまでで全ての設定は完了なので、実際の登録を行ってみたいと思います。今回は Rapberry Pi 3 Model B を USB ドングル + SORACOM Air でセルラー回線に接続し、 curl で Krypton の API にアクセスします。すると認証情報が作成され、下記のように JSON 形式でレスポンスを受け取ることができます。

$ curl -X POST -H 'content-type: application/json' https://krypton.soracom.io:8036/v1/provisioning/aws/iot/bootstrap | jq                                                                                                    
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  4945  100  4945    0     0    658      0  0:00:07  0:00:07 --:--:--  1216
{
  "region": "ap-northeast-1",
  "certificate": "-----BEGIN CERTIFICATE-----\nMIIDWTCCAkGgAwIBAgIUYrFCx+WJLTtlZNHykz2jt2B2y8swDQYJKoZIhvcNAQEL\nBQAwTTFLMEkGA1UECwxCQW1hem9uIFdlYiBTZXJ2aWNlcyBPPUFtYXpvbi5jb20g\nSW5jLiBMPVNlYXR0bGUgU1Q9V2FzaGluZ3RvbiBDPVVTMB4XDTE4MTIyMjA0Mzg0\nNFoXDTQ5MTIzMTIzNTk1OVowHjEcMBoGA1UEAwwTQVdTIElvVCBDZXJ0aWZpY2F0\nZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMiKSq8YxvnVHyTVRApZ\nundMbttTH9iP87NiwbKKQWPwFyRQGUIhaLxamIcjRV9FBwdj5F4eTBtGdKjSrjbG\nS0DlLoyL4rkNKRoVYpn8Rnr2Akt9LUmzf8ckQMZEhn04U5Ac8Q8YlkPvY+r4Lfb1\n〜〜〜中略〜〜〜-----END CERTIFICATE-----\n",
  "privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAyIpKrxjG+dUfJNVEClm6d0xu21Mf2I/zs2LBsopBY/AXJFAZ\nQiFovFqYhyNFX0UHB2PkXh5MG0Z0qNKuNsZLQOUujIviuQ0pGhVimfxGevYCS30t\nSbN/xyRAxkSGfThTkBzxDxiWQ+9j6vgt9vXgQJfvwalMHBdb99NEtOiLuWVTl8jC\nDR/yKcIMe4DSIO18UZS1P4KI48WtwTMQN1DiZ1CYEHTEME+AWSggrl6edAox4wBD\nmcGFCOYF/sLZq/c4c/EKo8qW/G7NVZyFUDNbgAHe3X+eASR0Zk8uLUjU2LLSK6a8\ncQNTcVrmOYbN87buqxUxG750HGI+UEq80kdF4QIDAQABAoIBAQCKt4ZDqjrw0+gw\nHUlsc3bVUMyZ79zme2TSoCxmKNZkn84SM0nVgbCmIXDCgbZZ2TCsfv+XzxNMBqdp\n〜〜〜中略〜〜〜-----END RSA PRIVATE KEY-----\n",
  "rootCaCertificate": "-----BEGIN CERTIFICATE-----\r\nMIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB\r\nyjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL\r\nExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp\r\nU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW\r\nZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0\r\n〜〜〜中略〜〜〜-----END CERTIFICATE-----",
  "host": "XXXXXXXXXXXXXX-ats.iot.ap-northeast-1.amazonaws.com",
  "clientId": "KryptonDevice_XXXXXXXXXXXXXXX"
}

 AWS IoT のコンソールから確認すると、下記のようにデバイスが追加されていることがわかります。

f:id:akanuma-hiroaki:20181222205640p:plain

 何度 API にアクセスしても、同じ SIM からであればデバイス情報は重複して作成されることはありませんが、証明書はアクセスする度に追加で作成されてしまいますのでご注意ください。

f:id:akanuma-hiroaki:20181222205852p:plain

まとめ

 今回は Krypton の API を呼んで認証情報を登録するところまででしたが、実際はレスポンスとして受け取った認証情報をファイルに保存して、それ以降のアクセス時に使用する形になるかと思います。最初の設定さえ済ませてしまえば、それ以降は新しいデバイスを追加する際には API を呼ぶだけでクラウドサービスへの登録ができてしまうので、とても便利ですね。ただラズパイのようにLinuxベースでリソースが豊富なデバイスと違い、 M5Stack などリソースが限られているマイコンでは X.509 認証などが使えないケースもあるので、その際はまた違った対応が必要かと思います。

mruby 2.0.0 を Mac で動かしてみる

 以前少し触ったことはあったのですが、まともに触ったことがなかったのと、先日まつもとゆきひろさんとお話しさせていただいたときに mruby の話題もあったので、改めて mruby に触ってみました。とりあえず Mac 上で動かすところまでやってみたのでまとめてみます。

mruby とは

 mruby とは組み込み等でハードウェアリソースが限られる環境でも動かせるように、メモリ消費量を小さくした Ruby 実装です。また、様々なアプリケーションにも組み込みやすくなっています。メモリ消費量を抑えるために実装されている機能は絞られていますので、普通の Ruby 実装では使えても mruby では使えないものも多くなっています。

 mruby の開発は GitHub の下記リポジトリで行われています。

github.com

 また、公式サイトも公開されています。

http://mruby.org/

mruby 2.0.0 リリース

 この記事を書いている数日前の 2018/12/11 に mruby 2.0.0 がリリースされていました。その前が 1.4.1 ですので、メジャーバージョンアップになります。

mruby 2.0.0 released

とりあえず動かしてみる

 mruby でのコードの実行方法については下記サイトでも紹介されていまして、今回参考にさせていただきました。

Executing Ruby code with mruby

 とりあえずデフォルトの構成で触ってみるということであれば、 mruby は rbenv でもインストールが可能です。下記のように rbenv のインストール可能なバージョンのリストに mruby 2.0.0 も既に含まれています。

$ rbenv install --list | grep mruby
  mruby-dev
  mruby-1.0.0
  mruby-1.1.0
  mruby-1.2.0
  mruby-1.3.0
  mruby-1.4.0
  mruby-1.4.1
  mruby-2.0.0

 これをインストールします。

$ rbenv install mruby-2.0.0
Downloading 2.0.0.tar.gz...
-> https://dqw8nmjcqpjn7.cloudfront.net/fa495898d51130c69480a13e90df5dc18cb1a9d9a31836268a895989d902048f
Installing mruby-2.0.0...
Installed mruby-2.0.0 to /Users/akanuma/.rbenv/versions/mruby-2.0.0

 インストールされたら使用するバージョンを mruby 2.0.0 に設定します。

$ rbenv versions
* system (set by /Users/akanuma/.rbenv/version)
  2.0.0-dev
  2.3.1
  2.5.3
  mruby-2.0.0
$ rbenv local mruby-2.0.0
$ rbenv version
mruby-2.0.0 (set by /Users/akanuma/workspace/mruby_test/.ruby-version)
$ ruby -v
mruby 2.0.0 (2018-12-11) 

 mruby での REPL は mirb で、 irb を実行すると mirb が起動します。

$ irb
mirb - Embeddable Interactive Ruby Shell

> 

 irb と同じようにインタラクティブに mruby のコードを実行することができます。

> puts 'Hello World!'
Hello World!
 => nil

 ちなみに mirb での入力内容はコードが正しいかチェックされた後に、バイトコードにコンパイルされてから実行されているようで、二回のパースが必要ということのようです。

 REPL 以外にも、もちろんコードを書いたファイルを実行することもできます。下記は hello.rb というファイル内のコードを実行する例です。

$ cat hello.rb 
puts 'Hello World!'
$ ruby hello.rb 
Hello World!

 この方法も、実行時に rb ファイルの内容がバイトコードにコンパイルされます。実行時のコンパイルを避けるため、あらかじめコンパイルしておくことも可能です。 mrbc というコンパイラを使ってコンパイルします。生成されたバイナリファイルを実行する時には、それがバイナリファイルであると示すために -b オプションをつけて実行します。

$ mrbc hello.rb 
$ ls -l
total 16
-rw-r--r--  1 akanuma  staff  98 Dec 15 12:21 hello.mrb
-rw-r--r--  1 akanuma  staff  20 Dec 15 12:09 hello.rb
$ ruby -b hello.mrb 
Hello World!

mrbgems

 Ruby ではパッケージ管理システムとして RubyGems を使いますが、 mruby では gem コマンドはなく、 RubyGems の代わりに mrbgems を使用します。

$ gem
rbenv: gem: command not found

 mrbgems では RubyGems のように gem コマンドでインストールしたり require で読み込むのではなく、ビルド時の設定ファイルに使用するライブラリを設定して一緒にコンパイルし、起動時に全てロードします。そのためにはソースコードからビルドする必要がありますので、 GitHub からソースコードを clone してきます。

$ git clone https://github.com/mruby/mruby.git
$ cd mruby

 まずはそのままデフォルトの構成でビルドしてみます。ディレクトリの root で minirake を実行します。

$ ./minirake

 ビルドが成功すると、設定ファイルの内容に従って各環境用にビルドされたもののサマリが下記のように表示されます。 Included Gems の項目が、実際にコンパイルされた mrbgems の gem です。 mruby では環境に応じて必要なもののみを使えるように、 Ruby ではコア機能として組み込まれていたものも、 mruby では mrbgems として切り出して、必要に応じて使用するかどうかを切り替えられるようになっています。

Build summary:

================================================
      Config Name: host
 Output Directory: build/host
         Binaries: mrbc
    Included Gems:
             mruby-metaprog - Meta-programming features for mruby
             mruby-io - IO and File class
             mruby-pack - Array#pack and String#unpack method
             mruby-sprintf - standard Kernel#sprintf method
             mruby-print - standard print/puts/p
             mruby-math - standard Math module
             mruby-time - standard Time class
             mruby-struct - standard Struct class
             mruby-compar-ext - Enumerable module extension
             mruby-enum-ext - Enumerable module extension
             mruby-string-ext - String class extension
             mruby-numeric-ext - Numeric class extension
             mruby-array-ext - Array class extension
             mruby-hash-ext - Hash class extension
             mruby-range-ext - Range class extension
             mruby-proc-ext - Proc class extension
             mruby-symbol-ext - Symbol class extension
             mruby-random - Random class
             mruby-object-ext - Object class extension
             mruby-objectspace - ObjectSpace class
             mruby-fiber - Fiber class
             mruby-enumerator - Enumerator class
             mruby-enum-lazy - Enumerator::Lazy class
             mruby-toplevel-ext - toplevel object (main) methods extension
             mruby-compiler - mruby compiler library
             mruby-bin-mirb - mirb command
               - Binaries: mirb
             mruby-error - extensional error handling
             mruby-bin-mruby - mruby command
               - Binaries: mruby
             mruby-bin-strip - irep dump debug section remover command
               - Binaries: mruby-strip
             mruby-kernel-ext - Kernel module extension
             mruby-class-ext - class/module extension
             mruby-bin-mrbc - mruby compiler executable
================================================

 bin ディレクトリに mruby や mirb 等の実行ファイルが生成されますので、これを使ってみます。

$ bin/mirb
mirb - Embeddable Interactive Ruby Shell

> Time.now
 => Sat Dec 15 17:12:35 2018

 Time クラスもコア機能としては含まれていませんが、 mruby-time gem が含まれているので、 Time クラスを使用することができています。試しに ENV オブジェクトを使ってみると、 mruby では ENV はコアには含まれていないのでエラーになります。

> ENV
(mirb):2: uninitialized constant ENV (NameError)

 ENV は mrbgems の mruby-env を組み込むことで使用できるようになりますので、 build_config.rb に下記のように設定を追加します。

conf.gem :mgem => 'mruby-env'

 config.gem での指定は、 gem のタイプによって少々異なります。今回は mgem-list に含まれているので、上記のように gem 名のみで使用可能です。

 ちなみに複数の gem をひとまとめにしたものを GemBox と言い、デフォルトでは build_config.rb 内で default という GemBox が指定されていて、デフォルトで使用する gem が指定されています。

conf.gembox 'default'

 では再度 minirake でビルドします。

$ ./minirake

 Build Summary をみると mruby-env が含まれているのがわかります。

Build summary:

================================================
      Config Name: host
 Output Directory: build/host
         Binaries: mrbc
    Included Gems:
             mruby-env
〜〜〜以下略〜〜〜

 下記のように実行してみると、 ENV モジュールが使用できるようになっています。

$ export DEBUG="TRUE"
$ bin/mirb
mirb - Embeddable Interactive Ruby Shell

> ENV['DEBUG']
 => "TRUE"

 ちなみに mruby 2.0.0 での変更点の一つとして、メタプログラミング関連の機能は組込みシステム開発ではあまり使われないという前提のもと、 mruby-metaprog という gem に切り出されました。

1.4.1 と 2.0.0 の違い

 リリースノートの内容を参考に、 1.4.1 と 2.0.2 の違いを確認してみます。

 2.0.0 では基本的な言語の機能として、キーワード引数に対応しています。サンプルとして下記のようなコードを書いて hello.rb として保存します。

def hello(name: 'World')
  puts "Hello #{name}!"
end

hello(name: 'Hiro')

 これを mruby 2.0.0 で実行すると下記のように正しく実行されます。

mruby_test  $ ruby hello.rb
Hello Hiro!

 mruby 1.4.1 で試してみると下記のように Syntax Error になります。

$ ruby hello.rb 
hello.rb:1:15: syntax error, unexpected tLABEL_TAG, expecting ')'
SyntaxError: syntax error

 また、詳細は割愛しますが、下記の Core Libraries にもメソッドの追加等の変更が入っています。

  • mruby-kernel-ext

  • mruby-array-ext

  • mruby-string-ext

  • mruby-pack

  • mruby-sleep

 mrbgems のところでも少し触れましたが、メタプログラミング関連の機能はコア機能から切り出されて、 mruby-metaprog という gem が追加されています。

 その他メモリ使用量削減の対策が行われていたり、インタプリタにデバッグモード実行用のオプションや、ライブラリ読み込み用のオプションが追加されたりしています。詳細はリリースノートをご覧いただければと思います。

http://mruby.org/releases/2018/12/11/mruby-2.0.0-released.html

まとめ

 組込み開発についてはまだまだ素人なのですが、Ruby が組込みでも使えるようになってくるとハードルが低くなってきますね。 mruby 2.0.0 になって Ruby にさらに近づいてきましたし、より開発しやすくなってそうです。実際に組込み開発で使うには、これをマイコン上で動かせるようにビルドしていったり、深く知っていく必要があるかと思いますが、少しずつでも掘り下げていければと思っています。

LTE-M Button で M5Stack Avatar とメールに「今から帰るよ」通知を送る

 この記事は「SORACOM LTE-M Button powered by AWS Advent Calendar 2018」の 12月4日(火)の記事になります。

 LTE-M Button で何を作ろうかと考えたのですが、今年から娘が中学生になり、徒歩30分ぐらいかかる学校に通い始めたので、ボタンを押したら今から帰るよメールが送られるようにしたら便利かなと考えました。ですがそれだけだと他にも多くの方がすでにやられているので、今回はさらに M5Stack と連携して Avatar にも通知が表示されるようにしてみました。

f:id:akanuma-hiroaki:20181204210313p:plain

動作の様子

 今回はボタンの3種類の操作に合わせて下記のようにメールとアバターで通知します。日本語表示は面倒だったので今回は英語表示のみです。

  • シングル: I've Arrived At The Venue.(目的地に着いたよ)

  • ダブル:I'm Going Home.(今から帰るよ)

  • ロング:I Left My Key At Home.(家に鍵忘れた)

 上記の内容がメールでも送られつつ、アバターが喋ってる風にスクロール表示します。アバターの動作の様子は下記動画をご覧ください。動画だと吹き出し部分が白いだけのように見えてしまっていますが、実際は上記のテキストがスクロール表示されています。

 メールは下記のような感じで送られてきます。一応子どもの名前はマスクしてあります。

f:id:akanuma-hiroaki:20181204125253p:plain:w400

f:id:akanuma-hiroaki:20181204125312p:plain:w400

f:id:akanuma-hiroaki:20181204125329p:plain:w400

システム構成

 かなり適当な図ですが、今回関連するコンポーネントの構成は下記のような形になります。

 LTE-M Button をクリックすると AWS IoT 1-Click を経由して Lambda 関数にリクエストが渡されます。 Lambda 関数では AWS IoT の Device Shadow をアップデートします。それによって AWS IoT Rule が発火して Amazon SNS にメール送信リクエストが投げられ、登録してあるアドレスにメールが送信されます。

 M5Stack は SORACOM Air を挿した SIM ルータに Wi-Fi 接続しておき、 SORACOM Beam 経由で AWS IoT の delta トピックに MQTT で Subscribe しておきます。 LTE-M Button 側の処理で Device Shadow が更新されると delta トピックにメッセージが Publish されるので、それに応じて M5Stack 側でアバターの表示の変更等を行うという構成です。

 本当は M5Stack を直接 MQTTS で AWS IoT に Subscribe したかったのですが、現在の MicroPython の実装だと X.509 の証明書を使用した TSL 認証に対応していないようで、色々試したのですが接続できませんでした。AWS 署名バージョン 4 での REST API も試したのですが、成功ステータス(200)は返って来るものの、本文を取得しようとするとうまくいかないという感じで、最終的には SORACOM Beam 経由でなんとか接続したという形です。

f:id:akanuma-hiroaki:20181204204404p:plain

Lambda 実装

 LTE-M Button の AWS IoT への登録は今回は割愛します。以前記事にも書いてますのでよろしければご参照ください。

blog.akanumahiroaki.com

 今回 Lambda 関数は Python3.7 ランタイムで下記のように作成しています。

f:id:akanuma-hiroaki:20181204124126p:plain

 LTE-M Button が押された時に送られてくるリクエストは下記のような json になります。下記はシングルクリックの例で、クリックの種類によって deviceEvent.buttonClicked.clickType の値が SINGLE DOUBLE LONG と変化します。

{
    "deviceInfo": {
        "deviceId": "7MF6JKC6XXXXXXXX",
        "type": "button",
        "remainingLife": 94.19284,
        "attributes": {
            "projectRegion": "ap-northeast-1",
            "projectName": "sendGoHomeNotice",
            "placementName": "XXXXXX",
            "deviceTemplateName": "SendGoHomeNotice"
        }
    },
    "deviceEvent": {
        "buttonClicked": {
            "clickType": "SINGLE",
            "reportedTime": "2018-11-25T13:15:55.696Z"
        }
    },
    "placementInfo": {
        "projectName": "sendGoHomeNotice",
        "placementName": "XXXXXX",
        "attributes": {
            "name": "XXXXXX"
        },
        "devices": {
            "SendGoHomeNotice": "7MF6JKC6XXXXXXXX"
        }
    }
}

 今回 Lambda 関数でやることは、クリック種別に応じて AWS IoT の Device Shadow のステータスを変更することです。 Lambda から Device Shadow を変更するには IoTDataPlane API の update_thing_shadow() メソッドを使用します。ドキュメントは下記リンク先にあります。

IoTDataPlane — Boto 3 Docs 1.9.57 documentation

 Lambda の実装の全体は下記のようになります。 LTE-M Button はレスポンスは受け取らないので適当な文字列を返しています。また、 Device Shadow のステータスには desired と reported がありますが、今回は desired を変更しています。

import json
import logging

import boto3

logger = logging.getLogger()
logger.setLevel(logging.INFO)

iot_client = boto3.client('iot-data')

def lambda_handler(event, context):
    logger.info('Received event: {}'.format(json.dumps(event)))
    name = event['placementInfo']['attributes']['name']
    click_type = event['deviceEvent']['buttonClicked']['clickType']
    
    if click_type == 'SINGLE':
        status = "I've Arrived At The Venue."
    elif click_type == 'DOUBLE':
        status = "I'm Going Home."
    elif click_type == 'LONG':
        status = 'I Left My Key At Home.'
    else:
        status = 'Unknown'
    
    shadow = { 'state': { 'desired': { 'status': status }}}
    
    response = iot_client.update_thing_shadow(
        thingName = 'GoHomeNoticeButton{}'.format(name),
        payload = json.dumps(shadow)
    )
    
    return {
        'statusCode': 200,
        'body': "{}: {}".format(name, click_type)
    }

 Device Shadow の初期状態は今回は手を抜いてコンソールから下記のように設定しておきます。

f:id:akanuma-hiroaki:20181204152859p:plain

AWS IoT ルール

 AWS IoT では Device Shadow のステータスが変更された時に Amazon SNS からメールが送信されるようにルールを設定します。 Device Shadow のステータスの desired と reported に差分が発生すると、 MQTT の $aws/things/<THING_NAME>/shadow/update/delta トピックに差分が publish されるので、そこからステータス情報を抜き出してメール送信用文言を生成します。

f:id:akanuma-hiroaki:20181204123912p:plain

 アクションの内容は下記のようにしています。詳細説明は割愛しますが Amazon SNS で作成したトピックには受信したいメールアドレスを Subscribe してアドレスの確認まで済ませておきます。

f:id:akanuma-hiroaki:20181204124035p:plain

SORACOM Beam 設定

 今回は M5Stack と AWS IoT の接続は SORACOM Beam を介して行いますので、 SORACOM User Console から該当の SIM グループで SORACOM Beam の設定を行っておきます。詳細は下記サイトで紹介されていますのでご参照ください。

dev.soracom.io

 設定が終わると下記のように MQTT 接続のエンドポイントが作成されます。

f:id:akanuma-hiroaki:20181204172744p:plain

M5Stack ファームウェア実装

 ここまででボタンを押したらメールが送信されるところまではできたので、最後に M5Stack 側の実装を行います。 M5Stack でのアバター表示には、私が以前作成した MicroPython 版の M5StackAvatar を使用します。使い方等は下記記事で紹介していますのでよろしければご参照ください。

blog.akanumahiroaki.com

 実装としては下記のようになります。やっていることとしては、 MQTT のクライアントインスタンス生成時に MQTT サーバへの接続時、切断時、データ取得時のコールバックメソッドを指定して delta トピックに Subscribe し、データ取得時のコールバックメソッドの中で Device Shadow のステータスの内容によってアバターの表示を切り替えているというものになります。 M5Stack のボタンがどれか押されたら、 Device Shadow のステータスとアバターの表示を元に戻しています。 SORACOM Beam を介しているので MQTT 接続は TSL 版ではなく通常の MQTT 接続になっています。ちなみに私のケースでは MQTT クライアントのインスタンス生成時のオプションで cleansession = True を指定しないと接続時にエラーになってしまいました。

from m5stack import buttonA, buttonB, buttonC
from m5stack_avatar import M5StackAvatar

import machine
import network
import time
import ujson

MQTT_SERVER = 'beam.soracom.io'
THING_NAME  = 'GoHomeNoticeButtonXXXXXX'
UPDATE_TOPIC = '$aws/things/{}/shadow/update'.format(THING_NAME)
DELTA_TOPIC = '$aws/things/{}/shadow/update/delta'.format(THING_NAME)
CLIENT_NAME = 'GoHomeAvatar'

avatar = M5StackAvatar()
avatar.start()

def connect_callback(task):
    print('MQTT Server [{}] Connected'.format(task))

def disconnect_callback(task):
    print('MQTT Server [{}] Disconnected'.format(task))

def data_callback(data):
    status = ujson.loads(data[2])['state']['status']
    print('Delta Status: {}'.format(status))

    if status == 'I Left My Key At Home.':
        avatar.pale_on()
    else:
        avatar.exclamation_on()
        
    avatar.speak(status)

def reset_avatar():
    shadow = { 'state': { 'desired': { 'status': "I'm Staying." }}}
    mqtt.publish(UPDATE_TOPIC, ujson.dumps(shadow))
    avatar.exclamation_off()
    avatar.pale_off()

mqtt = network.mqtt(
    CLIENT_NAME,
    MQTT_SERVER,
    autoreconnect   = True,
    cleansession    = True,
    connected_cb    = connect_callback,
    disconnected_cb = disconnect_callback,
    data_cb         = data_callback
)

mqtt.start()
tmo = 0
while mqtt.status()[0] != 2:
    time.sleep_ms(100)
    tmo += 1
    if tmo > 80:
        print("Not connected")
        break
    
print("Subscribing to the delta topic.")
mqtt.subscribe(DELTA_TOPIC)

while True:
    if buttonA.isPressed() or buttonB.isPressed() or buttonC.isPressed():
        reset_avatar()
    time.sleep_ms(10)

 これを M5Stack にフラッシュすれば一通りの実装は終了なので、 LTE-M Button をクリックすると冒頭の動画のように、 M5Stack のアバターとメールで通知が行われます。

まとめ

 今回 M5Stack を AWS IoT に接続するまでにかなり苦戦しましたが、 LTE-M Button から Lambda 関数をコールして Device Shadow を更新するまではあまり苦労せずにいけてしまいました。 Wi-Fi 接続と違って外でもネットワークの心配をする必要なく使えるというのもやはりとても良いですね。 SORACOM Beam のおかげで M5Stack を AWS IoT に接続することもできましたし、 ソラコムさんのサービスはうまく活用していきたいですね。

 かなり説明を省略してしまってるところも多いので、もし気になる点等ありましたらコメント欄などでご質問ください。

 ちなみに LTE-M Button 特化でないソラコムさんのアドベントカレンダーはこちらです。

SORACOM Advent Calendar 2018

AWS Lambda の Ruby ランタイムを試す

 AWS re:Invent 2018 の Keynote の中で AWS Lambda や Serverless 関連のアップデートが色々と発表されましたが、その中に AWS Lambda で Ruby がサポートされたという発表がありました。

aws.amazon.com

 下記のように AWS のブログでチュートリアルも公開されているので、今回はとりあえず Ruby で Lambda を動かしてみます。

aws.amazon.com

 現在サポートされている Ruby のバージョンは 2.5 で、 AWS SDK for Ruby はデフォルトで使えるようになっているようです。

まずは Hello World

 とりあえずは Lambda Function を作成して実行してみます。関数の作成画面では下記画像のようにランタイムとして Ruby2.5 が選択できるようになっています。

f:id:akanuma-hiroaki:20181201133930p:plain

 関数名やロール名は任意に決めて下記のような内容で作成します。

f:id:akanuma-hiroaki:20181201134519p:plain

 デフォルトで生成されるコードは下記のような内容です。

require 'json'

def lambda_handler(event:, context:)
    # TODO implement
    { statusCode: 200, body: JSON.generate('Hello from Lambda!') }
end

 テストイベントを下記のように空のリクエストで設定します。

f:id:akanuma-hiroaki:20181201135128p:plain

 正しく実行できていれば下記のように成功レスポンスが表示されます。 

f:id:akanuma-hiroaki:20181201135426p:plain

 ログは下記のような出力になります。

Response:
{
  "statusCode": 200,
  "body": "\"Hello from Lambda!\""
}

Request ID:
"04397307-f507-11e8-abba-97121f20de35"

Function Logs:
START RequestId: 04397307-f507-11e8-abba-97121f20de35 Version: $LATEST
END RequestId: 04397307-f507-11e8-abba-97121f20de35
REPORT RequestId: 04397307-f507-11e8-abba-97121f20de35  Duration: 10.04 ms  Billed Duration: 100 ms     Memory Size: 128 MB Max Memory Used: 31 MB  

RubyGems を使う(Zipアップロード)

 Ruby を使うからにはやっぱり gem が使いたくなります。AWS ブログのチュートリアルでは SAM でアップロードする方法が紹介されていますが、先にシンプルに zip に固めたものをアップロードする方法を試してみます。

 まずは作業用ディレクトリを作ります。

$ mkdir lambda_gem_sample
$ cd lambda_gem_sample/

 gem は bundler を使って管理するので、下記の内容で Gemfile を作成します。今回はとりあえず gem が使えることが確認できれば良いので、 uuid という gem を使ってみます。

source 'https://rubygems.org'
gem 'uuid'

 bundler のインストール等については説明は割愛しますが、 インストールされているものとして下記コマンドを実行します。ローカルで使うだけであれば bundle install だけで使えますが、 zip に固めてアップロードするには gem のファイルもローカルにダウンロードしておく必要があるので、 bundle install --deployment も実行します。

$ bundle install
$ bundle install --deployment

 実際に実行する Ruby スクリプトは下記のような内容で lambda_function.rb というファイル名で作成します。生成した UUID をレスポンスの文字列に含めているだけのものになります。

require 'json'
require 'uuid'

def lambda_handler(event:, context:)
    uuid = UUID.new
    { statusCode: 200, body: JSON.generate("Generated UUID: #{uuid.generate}") }
end

 ここまでで必要なものは用意できたので、作業ディレクトリのファイルを zip に固めます。

$ zip -r lambda_gem_sample.zip ./*

 作成した zip ファイルを Lambda のコンソールからアップロードします。正しくアップロードされていれば下記のようにアップロードしたファイルがコンソールに表示されます。

f:id:akanuma-hiroaki:20181201141646p:plain

 空のテストイベントを作成してテストを実行すると、下記のように UUID の gem を使用したコードが実行され、生成された UUID が含まれた文字列がレスポンスとして返ってきます。

f:id:akanuma-hiroaki:20181201141943p:plain

RubyGems を使う(SAM)

 チュートリアルで紹介されている SAM を使う方法も試してみます。まずは作業ディレクトリを作成します。

$ mkdir hello_lambda_ruby
$ cd hello_lambda_ruby/

 Gemfile は下記のような内容で作成します。 aws-record は DynamoDB を操作するための gem です。

source 'https://rubygems.org'
gem 'aws-record', '~> 2'

 Gemfile を作成したら bundle install を実行します。

$ /Users/akanuma/.rbenv/shims/bundle install
$ /Users/akanuma/.rbenv/shims/bundle install --deployment

 Ruby のスクリプトは下記のような内容で hello_lambda_ruby_record.rb というファイル名で作成します。ちなみに AWS ブログに掲載されているコードをそのままコピペすると、 ENV[‘DDB_TABLE’] のシングルクォートが正しくない(アポストロフィーになってる?)ので実行時にエラーになります。

require 'aws-record'

class DemoTable
  include Aws::Record
  set_table_name ENV['DDB_TABLE']
  string_attr :id, hash_key: true
  string_attr :body
end

def put_item(event:,context:)
  body = event["body"]
  item = DemoTable.new(id: SecureRandom.uuid, body: body)
  item.save! # raise an exception if save fails
  item.to_h
end 

 AWS SAM はサーバレスアプリケーションの構成を管理するためのツールで、 Lambda アプリケーションの構造やセキュリティーポリシーの定義、AWSリソースの作成や管理を行うことができます。今回は DynamoDB を使用するのでそのための設定も含んでいます。設定ファイルは YAML で下記のような内容を template.yaml として作成します。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: 'sample ruby application'

Resources:
  HelloLambdaRubyRecordFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: hello_lambda_ruby_record.put_item
      Runtime: ruby2.5
      Policies:
      - DynamoDBCrudPolicy:
          TableName: !Ref RubyExampleDDBTable 
      Environment:
        Variables:
          DDB_TABLE: !Ref RubyExampleDDBTable

  RubyExampleDDBTable:
    Type: AWS::Serverless::SimpleTable
    Properties:
      PrimaryKey:
        Name: id
        Type: String

Outputs:
  HelloLambdaRubyRecordFunction:
    Description: Hello Lambda Ruby Record Lambda Function ARN
    Value:
      Fn::GetAtt:
      - HelloLambdaRubyRecordFunction
      - Arn

 AWS SAM のテンプレートは CloudFormation のコンソールか、 AWS CLI、 AWS SAM CLI のいずれかを使ってデプロイすることができます。このチュートリアルでは AWS SAM CLI でデプロイしていますので、まだインストールしていない場合はインストールしておきます。また、 s3 のバケット作成に AWS CLI も使用していますので、こちらもまだであればインストールしておきます。

 CLI の準備ができたら、アプリケーションのコードをホストするための s3 のバケットを作成します。

$ aws s3 mb s3://hello-lambda-ruby
make_bucket: hello-lambda-ruby

 AWS SAM CLI を使用して、アプリケーションをパッケージングします。これによって packaged-template.yaml というファイル名のテンプレートファイルが作成されます。

$ sam package --template-file template.yaml \
> --output-template-file packaged-template.yaml \
> --s3-bucket hello-lambda-ruby                                                                                                                                                                                                               
Uploading to 02055cc5fec807d137f97cd532f60cd5  993290 / 993290.0  (100.00%)
Successfully packaged artifacts and wrote output template to file packaged-template.yaml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /Users/akanuma/workspace/hello_lambda_ruby/packaged-template.yaml --stack-name <YOUR STACK NAME>

 続いて AWS SAM CLI を使ってアプリケーションをデプロイします。この時にAWSユーザに CloudFormation の権限が足りていないと下記のようにエラーになります。

$ sam deploy --template-file packaged-template.yaml \
> --stack-name helloLambdaRubyRecord \
> --capabilities CAPABILITY_IAM

An error occurred (AccessDenied) when calling the CreateChangeSet operation: User: arn:aws:iam::365361468908:user/hiroaki.akanuma is not authorized to perform: cloudformation:CreateChangeSet on resource: arn:aws:cloudformation:ap-northeast-1:365361468908:stack/helloLambdaRubyRecord/*

 とりあえず CloudFormation のフルアクセス権限を追加したかったのですが、ポリシーを検索してもそれに当たるものが見つからなかったので、インラインポリシーで直接フルアクセス権限を追加してみました。

f:id:akanuma-hiroaki:20181201151456p:plain

 追加後に実行すると下記のようにデプロイが成功しました。

$ sam deploy --template-file packaged-template.yaml --stack-name HelloLambdaRubyRecord --capabilities CAPABILITY_IAM

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - HelloLambdaRubyRecord

 デプロイが終わると Lambda のアプリケーションコンソールに下記のようにアプリケーションが表示されます。

f:id:akanuma-hiroaki:20181201151636p:plain

 アプリケーションのリソースには Lambda Function と DynamoDB のテーブルが含まれています。

f:id:akanuma-hiroaki:20181201151828p:plain

 Lambda Function の方を選択して Lambda のコンソールを開き、下記のような内容のテストイベントでテストを実行します。

f:id:akanuma-hiroaki:20181201152026p:plain

 正しくデプロイされていてコードの内容に間違いがなければ、下記のように成功レスポンスが返ってきます。

f:id:akanuma-hiroaki:20181201152126p:plain

 DynamoDB のコンソールからテーブルの中身を見てみると、テストイベントで送信したリクエストの内容が保存されています。

f:id:akanuma-hiroaki:20181201152216p:plain

まとめ

 Lambda で Ruby が使えるようになったことで、 Ruby エンジニアにとってはかなりハードルが下がりましたね。bundler 管理で gem を使うこともできますので、色々と便利に使えそうです。ローカルでの開発と比べると、開発途中での gem の追加の容易さや、修正から実行確認の手返しの良さは劣るところになりそうですが、サーバレス環境での開発方法も最適な方法を探していきたいと思います。

MicroPython版の M5Stack Avatar を作ってみた

 以前の記事で MicroPython のスレッドを使ってみたときに、簡単にアバター表示をしてみました。

blog.akanumahiroaki.com

Arduino 版のアバターは @meganetaaan さんが公開されていて、こちらを参考にさせていただいています。

github.com

 前回はとりあえず自分のところで表示させるために実装してましたが、その時のアバターをもう少し使いやすくして、とは言えだいぶ雑ではありますが GitHub に載せてみました。

github.com

機能

 今のところはまだ大した機能はなく、下記のみとなっています。

  • アバターの顔表示&瞬き

  • アバターが話している風にテキストをスクロール表示する

  • 何か気づいた風にエクスクラメーションマークを表示する

  • 青ざめた感じの表情にする

 アバター表示と瞬きはそれぞれスレッドを使用して実行しています。それ以外は単純にLCDに描画しているだけです。

使い方

 使い方は単純で、 m5stack_avatar.py をダウンロードし、 main.py 等から import します。今のところは特に依存している外部ライブラリはないので、 M5Stack での開発ができる環境ができていればその他には必要ありません。

 初期化とアバターの表示までは下記のようにします。

from m5stack_avatar import M5StackAvatar

avatar = M5StackAvatar()
avatar.start()

 喋っている風にテキストをスクロール表示するには speak() メソッドを使用します。

avatar.speak('Hello from M5StackAvatarPython!!')

f:id:akanuma-hiroaki:20181124215207j:plain:w500

 エクスクラメーションを ON/OFF するには exclamation_on()/exclamation_off() メソッドを使用します。

avatar.exclamation_on()
avatar.exclamation_off()

f:id:akanuma-hiroaki:20181124215233j:plain:w500

 青ざめた感じで顔に縦線表示するのは pale_on()/pale_off() メソッドを使います。

avatar.pale_on()
avatar.pale_off()

f:id:akanuma-hiroaki:20181124215303j:plain:w500

使用例

簡単な使用例として、上記機能をループで順番に繰り返す処理は下記のようになります。

from m5stack_avatar import M5StackAvatar

import time

avatar = M5StackAvatar()
avatar.start()

while True:
    avatar.speak('Hello from M5StackAvatarPython!!')
    time.sleep(10)
    avatar.exclamation_on()
    time.sleep(5)
    avatar.exclamation_off()
    time.sleep(5)
    avatar.pale_on()
    time.sleep(5)
    avatar.pale_off()
    time.sleep(5)

 これを実行した様子は下記のような感じになります。

まとめ

ある程度汎用的に使えるようにしようと思うとなかなか難しいところも多いですね。あと太めの斜線を描画しようと思うと簡単にはいかなかったりと色々とありますが、まずは不十分でも公開してみようということで、今後機能追加や改善していけると良いなと思っています。改善点のご指摘等ありましたらぜひいただければと思います。

M5Stack で Google Calendar のスケジュールを表示する(MicroPython)

 M5Stack でスケジュール管理に役立つ機能が実装できないかなと思い、 Google Calendar に登録しているスケジュールを表示させてみました。

Google Calendar API の利用設定

 まずは Google Calendar API を利用できるように設定する必要があります。GCP のコンソールから API とサービスを追加 をクリックします。

f:id:akanuma-hiroaki:20181117143616p:plain

 API のリストの中から Google Calendar API をクリックします。

f:id:akanuma-hiroaki:20181117143733p:plain

 API の詳細ページで 有効にする をクリックして API を使える状態にします。

f:id:akanuma-hiroaki:20181117143844p:plain

 次に API の認証情報を作成します。左メニューから 認証情報 をクリックします。

f:id:akanuma-hiroaki:20181117143949p:plain

 認証情報の種別を選択するプルダウンで サービスアカウントキー を選択します。

f:id:akanuma-hiroaki:20181117144053p:plain

 サービスアカウント名には任意の名前を設定します。役割については今回は参照だけできれば良いので、 閲覧者 を設定しました。サービスアカウント ID はサービスアカウント名から自動的に設定されます。キーのタイプはデフォルトが JSON になっているのでそのままにしておきます。最後に 作成 をすると認証情報が作成され、ダウンロードできるようになりますので、ローカルに取得しておきます。

f:id:akanuma-hiroaki:20181117161048p:plain

Google Calendar の共有設定

 次に先ほど作成したサービスアカウントからカレンダーを参照できるように、共有設定を行います。共有するカレンダーの共有設定画面から ユーザーの追加 をクリックします。

f:id:akanuma-hiroaki:20181117144504p:plain

 作成したサービスアカウントのサービスアカウント ID を設定します。権限は閲覧権限のみにしておきます。設定したら 送信 をクリックします。

f:id:akanuma-hiroaki:20181117144710p:plain

AWS Lambda の関数作成

 Google Calendar 側の設定はここまでで完了なので、次に AWS Lambda の関数を作成します。関数の作成画面で任意の関数名を指定します。ランタイムは今回は Python 3.6 を使用しています。

f:id:akanuma-hiroaki:20181117144835p:plain

 認証情報はコードの中に極力書きたくないので、 Lambda の実装画面で環境変数にサービスアカウントのキー ID を設定しておきます。

f:id:akanuma-hiroaki:20181117144930p:plain

 Google Calendar API を使うために Google が提供しているクライアントモジュールを使用します。 Lambda で外部モジュールを使用するにはローカルで zip に固めたものをアップロードする形になります。まずはローカルのプロジェクト用ディレクトリで pip を使用して google-api-python-client と oauth2client をインストールします。

$ pip3 install --upgrade google-api-python-client oauth2client -t ./

 下記のようなファイルがインストールされます。 Google Calendar API の認証情報作成時にダウンロードした認証情報ファイルも google_key.json として同じディレクトリに置いておきます。

$ ls -l
total 112
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 __pycache__
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 apiclient
drwxr-xr-x  12 akanuma  staff    384 Nov 17 12:39 cachetools
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 cachetools-3.0.0.dist-info
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 google
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 google_api_python_client-1.7.4.dist-info
-rw-r--r--   1 akanuma  staff    539 Nov 17 12:39 google_auth-1.6.1-py3.7-nspkg.pth
drwxr-xr-x   8 akanuma  staff    256 Nov 17 12:39 google_auth-1.6.1.dist-info
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 google_auth_httplib2-0.0.3.dist-info
-rw-r--r--   1 akanuma  staff   8434 Nov 17 12:39 google_auth_httplib2.py
-rw-r--r--@  1 akanuma  staff   2345 Nov 17 11:24 google_key.json
drwxr-xr-x  15 akanuma  staff    480 Nov 17 12:39 googleapiclient
drwxr-xr-x   8 akanuma  staff    256 Nov 17 12:39 httplib2
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 httplib2-0.12.0-py3.6.egg-info
-rw-r--r--   1 akanuma  staff    625 Nov 17 12:37 lambda_function.py
drwxr-xr-x  17 akanuma  staff    544 Nov 17 12:39 oauth2client
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 oauth2client-4.1.3.dist-info
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 pyasn1
drwxr-xr-x  11 akanuma  staff    352 Nov 17 12:39 pyasn1-0.4.4.dist-info
drwxr-xr-x  31 akanuma  staff    992 Nov 17 12:39 pyasn1_modules
drwxr-xr-x  11 akanuma  staff    352 Nov 17 12:39 pyasn1_modules-0.2.2.dist-info
drwxr-xr-x  19 akanuma  staff    608 Nov 17 12:39 rsa
drwxr-xr-x  11 akanuma  staff    352 Nov 17 12:39 rsa-4.0.dist-info
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 six-1.11.0.dist-info
-rw-r--r--   1 akanuma  staff  30888 Nov 17 12:39 six.py
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 uritemplate
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 uritemplate-3.0.0.dist-info

 この中で *.dist-info は不要なので削除しておきます。

$ rm -rf *.dist-info 

 削除後のリストは下記のようになります。

$ ls -l
total 112
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 __pycache__
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 apiclient
drwxr-xr-x  12 akanuma  staff    384 Nov 17 12:39 cachetools
drwxr-xr-x   4 akanuma  staff    128 Nov 17 12:39 google
-rw-r--r--   1 akanuma  staff    539 Nov 17 12:39 google_auth-1.6.1-py3.7-nspkg.pth
-rw-r--r--   1 akanuma  staff   8434 Nov 17 12:39 google_auth_httplib2.py
-rw-r--r--@  1 akanuma  staff   2345 Nov 17 11:24 google_key.json
drwxr-xr-x  15 akanuma  staff    480 Nov 17 12:39 googleapiclient
drwxr-xr-x   8 akanuma  staff    256 Nov 17 12:39 httplib2
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 httplib2-0.12.0-py3.6.egg-info
-rw-r--r--   1 akanuma  staff    625 Nov 17 12:37 lambda_function.py
drwxr-xr-x  17 akanuma  staff    544 Nov 17 12:39 oauth2client
drwxr-xr-x   9 akanuma  staff    288 Nov 17 12:39 pyasn1
drwxr-xr-x  31 akanuma  staff    992 Nov 17 12:39 pyasn1_modules
drwxr-xr-x  19 akanuma  staff    608 Nov 17 12:39 rsa
-rw-r--r--   1 akanuma  staff  30888 Nov 17 12:39 six.py
drwxr-xr-x   7 akanuma  staff    224 Nov 17 12:39 uritemplate

 これを zip に圧縮しておきます。

$ zip -r google_calendar_m5stack.zip ./*

 圧縮した zip ファイルを Lambda のコンソールからアップロードします。

 メインの関数(lambda_function.py)の内容は下記のようにしました。クラスの初期化時に認証情報を取得し、 get_schedules() メソッドで Google Calendar API をコールしてスケジュールの情報を取得しています。今回はとりあえず直近5件のスケジュールの開始日時、終了日時とサマリだけ使用しています。

from dateutil.parser import parse
from apiclient import discovery
from oauth2client.service_account import ServiceAccountCredentials
import datetime
import httplib2
import json
import logging
import os

logger = logging.getLogger()
logger.setLevel(logging.INFO)

class GoogleCalendar:
    def __init__(self):
        self.service_account_id = os.environ['GOOGLE_SERVICE_ACCOUNT_ID']
        scopes = 'https://www.googleapis.com/auth/calendar.readonly'
        
        self.credentials = ServiceAccountCredentials.from_json_keyfile_name(
            'google_key.json',
            scopes = scopes
        )
        
        self.calendar_id = 'XXXXXXXXXXXXXXX@gmail.com'
        self.max_results = 5

    def get_schedules(self):
        http = self.credentials.authorize(httplib2.Http())
        service = discovery.build('calendar', 'v3', http = http)
        
        now = datetime.datetime.utcnow().isoformat() + 'Z'
        
        events_result = service.events().list(
            calendarId   = self.calendar_id,
            timeMin      = now,
            maxResults   = self.max_results,
            singleEvents = True,
            orderBy      = 'startTime'
        ).execute()
        
        events = events_result.get('items', [])
        
        if not events:
            logger.info('No upcoming events found.')
        
        schedules = []
        for event in events:
            start   = event['start'].get('dateTime', event['start'].get('date'))
            end     = event['end'].get('dateTime', event['end'].get('date'))
            summary = event['summary']
            schedules.append({
                'start':   parse(start).strftime('%Y/%m/%d %H:%M:%S'),
                'end':     parse(end).strftime('%Y/%m/%d %H:%M:%S'),
                'summary': summary
            })

        return schedules

def lambda_handler(event, context):
    calendar = GoogleCalendar()
    schedules = calendar.get_schedules()
    
    return {
        'statusCode': 200,
        'body': json.dumps({'schedules': schedules})
    }

API Gateway の設定

 Lambda の関数が作成できたので、次に関数を API として実行できるように、 API Gateway の設定を行います。 AWS Lambda の実装画面でトリガーの追加メニューから API Gateway を選択します。

f:id:akanuma-hiroaki:20181117145413p:plain

 トリガーの設定フォームで API Gateway の設定が行えます。新規の API を作成するか既存の API から選択するかを選べますので、今回は 新規 API の作成 を選択します。セキュリティでは API キー使用でのオープン を選択して、 API キーで認証するようにしておきます。

f:id:akanuma-hiroaki:20181117145549p:plain

 下記のように未保存の状態で API が作成されますので、 Lambda コンソールの 保存 ボタンをクリックして設定を保存します。

f:id:akanuma-hiroaki:20181117145636p:plain

 すると実際に API が作成され、下記のように API の情報が表示されます。下記画像は色々とマスクしてあります。

f:id:akanuma-hiroaki:20181117145914p:plain

 試しに API キーなしで API にアクセスしてみると、 Forbidden となりアクセスが拒否されます。

$ curl https://XXXXXXXXXX.XXXXXXXXXXX.ap-northeast-1.amazonaws.com/default/googleCalendarM5Stack
{"message":"Forbidden"}

 API キーを Header に設定してリクエストを投げると下記のように情報を取得することができます。

$ curl https://XXXXXXXXXX.XXXXXXXXXXX.ap-northeast-1.amazonaws.com/default/googleCalendarM5Stack --header 'x-api-key:EnXb6oRB957iVzKXXXXXXXXXXXXXXXXXXXXXXXXX'
{"schedules": [{"start": "2018-11-18", "end": "2018-11-19", "summary": "\u6771\u4eac\u30aa\u30d5\u30a3\u30b9\u79fb\u8ee2"}, {"start": "2018-11-18", "end": "2018-11-19", "summary": "\u30bf\u30c3\u30d7\u516c\u6f14\u30ea\u30cf"}, {"start": "2018-11-22", "end": "2018-11-23", "summary": "SORACOM Technology Camp 2018"}, {"start": "2018-11-22T13:30:00+09:00", "end": "2018-11-22T19:30:00+09:00", "summary": "SORACOM Technology Camp 2018"}, {"start": "2018-11-23T12:00:00+09:00", "end": "2018-11-23T22:00:00+09:00", "summary": "TAP\u516c\u6f14\u30ea\u30cf"}]}

M5Stack のファームウェア実装(MicroPython)

 では最後に M5Stack 側の実装です。コードの全体は下記の通りです。主な処理は Lambda 側でやっているので、 M5Stack 側では単純に API を呼んで結果をループで表示しているだけのものになります。

from m5stack import lcd

import time
import ujson
import urequests

class GoogleCalendar:
    def __init__(self):
        self.base_url = 'https://XXXXXXXXXX.XXXXXXXXXXX.ap-northeast-1.amazonaws.com/default/googleCalendarM5Stack'
        self.api_key = 'EnXb6oRB957iVzKXXXXXXXXXXXXXXXXXXXXXXXXX'

        lcd.setCursor(0, 0)
        lcd.setColor(lcd.WHITE)
        lcd.font(lcd.FONT_DejaVu18)
        self.fw, self.fh = lcd.fontSize()

    def get_schedules(self):
        headers = {'x-api-key': self.api_key}
        response = urequests.get(self.base_url, headers = headers)
        json = response.json()
        return json['schedules']

    def display(self, schedules):
        lcd.clear()
        lcd.setCursor(0, 0)
        for schedule in schedules:
            print(schedule)
            lcd.println("{}".format(schedule['start']))
            lcd.println(" {}".format(schedule['summary']))

calendar = GoogleCalendar()
while True:
    schedules = calendar.get_schedules()
    calendar.display(schedules)
    time.sleep(60)

動作確認

 実行した結果は下記のようになります。表示は適当ですが、とりあえず Google Calendar の情報を M5Stack に表示することができました。ただ、日本語はそのままでは表示できませんので、テスト用のスケジュールを英語で登録して表示してみました。

f:id:akanuma-hiroaki:20181117155257j:plain

まとめ

 今回ひとまず Google Calendar から情報が取得できるようになりました。実際に使用するには表示を見やすく工夫したり、エラーハンドリングなどももっとちゃんと作りこむ必要がありますが、スケジュールの情報が使えると色々やれそうな気がします。また、今回 Lambda を経由して Google Calendar にアクセスすることで、 Google Calendar の認証情報は Lambda 側に保持し、デバイス上には持たせない構成になっているのはセキュリティ面では良い点かと思います。

 今回 Google Calendar API の使い方については下記チュートリアルを参考にしました。

Python Quickstart  |  Calendar API  |  Google Developers

 また、 Lambda で外部モジュールを使う方法については下記サイトを参考にさせていただきました。

qiita.com

 Lambda から Google Calendar API を利用する方法については下記サイトを参考にさせていただいています。

www.yamamanx.com