AWS Cloud9 で Rails 開発環境をつくる

 Cloud9 が AWS に買収され、AWS Cloud9 としてリリースされてからもう結構経ってしまいましたが、 Lambda 等のサーバレスなサービスを使う機会が増えてきたり、コンテナ等を活用するようになってきたので、クラウドの IDE が便利に活用できるようなら使ってみたいということで、今更ではありますが Cloud9 で Rails の開発環境を作ってみました。

環境の作成

 まずは AWS のマネジメントコンソールから Cloud9 を検索して Cloud9 のコンソールにアクセスします。

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

 リリースからそれなりに経っているものの、まだ東京リージョンでは Cloud9 は提供されていないので、東京リージョンを選択していると下記のようにリージョンの選択画面が表示されます。今回はシンガポールリージョンを使用します。

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

 Cloud9 のコンソールが表示されたら Create environment をクリックします。

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

 環境の作成フォームで環境の名前を設定します。今回は rails-dev としました。説明はオプションなので設定しなくても問題ありません。入力したら Next step をクリックします。

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

 次に環境の設定を行います。今回は全てデフォルト値をそのまま使います。

 Cloud9 では IDE の接続先を EC2 かその他のリモート環境に ssh で接続するかを選択できます。今回は EC2 を使用するので、デフォルトの Create a new instance for environment [EC2] を選択します。

 EC2 への接続を選択した場合は EC2 インスタンスが新たに作成されますが、そのインスタンスタイプも選択できます。デフォルトの t2.micro を使用します。

 また、 Cloud9 では使用していない時間が続くと EC2 が停止しますが、停止するまでの時間も選択できます。これも今回はデフォルトのまま、 After 30 minutes を使用します。

 Next step をクリックして次へ進みます。

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

 設定内容の確認画面が表示されますので、内容に問題がなければ Create environment をクリックします。

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

 環境が作成されるまでには少し時間がかかります。作成されるまでの間は下記のような画面が表示されます。

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

 環境が作成されると下記のように IDE の画面が表示されます。作成されたインスタンスのホームディレクトリには environment というディレクトリが作成され、デフォルトの構成では、左ペインに environment ディレクトリ配下のファイルがツリー表示されます。右側の上部ペインにはエディタ、下部ペインにはターミナルが表示されます。

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

 ファイルツリーからファイルをダブルクリックすると右側のエディタに表示されます。

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

 右上の設定マーク(歯車マーク)等から設定画面を開くことができ、エディタ等の設定を変更することができますので、とりあえずいくつか設定してみます。

 まず Code Editor の設定で Soft Tabs をデフォルトの 4 から 2 に変更します。

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

 私は普段エディタには Vim を使ってるので、 Keyboard Mode を Vim に変更します。

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

 テーマもターミナルっぽい方が好きなので、 Cloud9 Night に変更してみます。

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

 ターミナルのスタイルも変更できますので、背景色を黒、文字色を白に変更します。また、スクロールバックのバッファもデフォルトの 1,000 だと少ないので、 10,000 に変更しておきます。

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

 設定変更後の画面は下記のような感じになります。

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

 ターミナルで作業するときにはターミナルを全画面表示することもできます。

rvm のアンインストール

 ひとまず Cloud9 の環境としては作成されたので、 Rails の環境を作っていきます。まずは Ruby からですが、デフォルトでも Ruby は使えるようになっています。

$ ruby -v
ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-linux]

 デフォルトでは Ruby の実行環境の管理には rvm が使われています。

$ which ruby
/usr/local/rvm/rubies/ruby-2.4.1/bin/ruby

 Rails も入っています。

$ which rails
/usr/local/rvm/rubies/ruby-2.4.1/bin/rails
$ rails -v
Rails 5.2.2

 これは好みですが、私は rvm より rbenv の方が好きなので、 rvm をアンインストールして、 rbenv をインストールしたいと思います。 rvm のアンインストールは rvm implode でできますが、 root 権限が必要なので、 sudo でフルパス指定で実行します。

$ sudo /usr/local/rvm/bin/rvm implode
Are you SURE you wish for rvm to implode?
This will recursively remove /usr/local/rvm and other rvm traces?
(anything other than 'yes' will cancel) > yes
Removing rvm-shipped binaries (rvm-prompt, rvm, rvm-sudo rvm-shell and rvm-auto-ruby)
Removing rvm wrappers in /usr/local/rvm/bin
Hai! Removing /usr/local/rvm
/usr/local/rvm has been removed.

Note you may need to manually remove /etc/rvmrc and ~/.rvmrc if they exist still.
Please check all .bashrc .bash_profile .profile and .zshrc for RVM source lines and delete or comment out if this was a Per-User installation.
Also make sure to remove `rvm` group if this was a system installation.
Finally it might help to relogin / restart if you want to have fresh environment (like for installing RVM again).

 これで rvm がアンインストールされ、 Ruby も使えなくなりました。

$ which ruby
/usr/bin/which: no ruby in (/home/ec2-user/.nvm/versions/node/v6.15.1/bin:/usr/local/bin:/bin:/usr/bin:/home/ec2-user/.local/bin:/home/ec2-user/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/aws/bin:/home/ec2-user/.local/bin:/home/ec2-user/bin:/home/ec2-user/.local/bin:/home/ec2-user/bin)

rbenv のインストール

 では rbenv をインストールします。と言っても特に Cloud9 固有なことはありません。 git はデフォルトで入っているので、 rbenv を git clone します。

$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
Cloning into '/home/ec2-user/.rbenv'...
remote: Enumerating objects: 2744, done.
remote: Total 2744 (delta 0), reused 0 (delta 0), pack-reused 2744
Receiving objects: 100% (2744/2744), 523.32 KiB | 532.00 KiB/s, done.
Resolving deltas: 100% (1720/1720), done.

 そしてコンパイルします。

$ cd ~/.rbenv && src/configure && make -C src
make: Entering directory `/home/ec2-user/.rbenv/src'
gcc -fPIC     -c -o realpath.o realpath.c
gcc -shared -Wl,-soname,../libexec/rbenv-realpath.dylib  -o ../libexec/rbenv-realpath.dylib realpath.o 
make: Leaving directory `/home/ec2-user/.rbenv/src'

 起動時にパスが通るように .bash_profile に設定を追加します。

$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile

 また、起動時に実行されるように設定を .bash_profile に追加します。

$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile

 別ターミナルを起動して rbenv コマンドが使えるようになっていることを確認します。

$ rbenv -v
rbenv 1.1.1-39-g59785f6

 続けて ruby-build を git clone します。

$ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
Cloning into '/home/ec2-user/.rbenv/plugins/ruby-build'...
remote: Enumerating objects: 29, done.
remote: Counting objects: 100% (29/29), done.
remote: Compressing objects: 100% (18/18), done.
remote: Total 9620 (delta 9), reused 22 (delta 7), pack-reused 9591
Receiving objects: 100% (9620/9620), 2.03 MiB | 1.40 MiB/s, done.
Resolving deltas: 100% (6275/6275), done.

 Ruby 2.6 は12月にリリースされたばかりで、まだ Rails のバージョンとの相性等は確認してませんが、今回はとりあえず細かいことは考えずに最新の 2.6 をインストールしてみます。

$ rbenv install 2.6.0
Downloading ruby-2.6.0.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.0.tar.bz2
Installing ruby-2.6.0...
Installed ruby-2.6.0 to /home/ec2-user/.rbenv/versions/2.6.0

 インストールしただけだと下記のようにエラーになってしまってまだ使えません。

$ ruby -v
rbenv: ruby: command not found

The `ruby' command exists in these Ruby versions:
  2.6.0

 なのでデフォルトのバージョンを 2.6 に設定します。

$ rbenv global 2.6.0
$ ruby -v
ruby 2.6.0p0 (2018-12-25 revision 66547) [x86_64-linux]

 これでひとまず Ruby が使える環境ができました。ついでに gem インストール時にドキュメントがインストールされないように .gemrc に設定を追加しておきます。

$ printf "install: --no-rdoc --no-ri\nupdate:  --no-rdoc --no-ri\n" >> ~/.gemrc

Rails インストール

 では最後に Rails をインストールします。

$ gem install rails
Fetching rack-test-1.1.0.gem
Fetching activesupport-5.2.2.gem
Fetching i18n-1.5.1.gem
Fetching tzinfo-1.2.5.gem
Fetching thread_safe-0.3.6.gem
Fetching rack-2.0.6.gem
Fetching concurrent-ruby-1.1.4.gem
Fetching loofah-2.2.3.gem
Fetching rails-html-sanitizer-1.0.4.gem
〜〜〜以下略〜〜〜

 これで最新の Rails が使えるようになりました。ちなみに gem も rails も rbenv でインストールされた現在のバージョンの Ruby 環境に紐づいているので、 rbenv で Ruby のバージョンを切り替えたときには改めて rails 等もインストールする必要があります。

$ rails -v
Rails 5.2.2
$ which rails
~/.rbenv/shims/rails

 試しに Rails アプリを作成してみます。

$ rails new hello_cloud9
      create  
      create  README.md
      create  Rakefile
      create  .ruby-version
      create  config.ru
      create  .gitignore
      create  Gemfile
         run  git init from "."
Initialized empty Git repository in /home/ec2-user/environment/hello_cloud9/.git/
      create  package.json
      create  app
      create  app/assets/config/manifest.js
      create  app/assets/javascripts/application.js
      create  app/assets/javascripts/cable.js
〜〜〜以下略〜〜〜

 問題なく作成されたらアプリを起動してみます。

$ cd hello_cloud9/
$ rails s
=> Booting Puma
=> Rails 5.2.2 application starting in development 
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.0 (ruby 2.6.0-p0), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:8080
Use Ctrl-C to stop

 ローカルで開発しているときはブラウザを開いてアクセス確認をしますが、 Cloud9 の場合はコンソールからプレビュー用のメニューを選択します。

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

 コンソール内にプレビュー用のペインが表示されますが、その中では表示されないので(原因はまだ調べてませんが)、ブラウザの別タブとして開くためのボタンをクリックします。

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

 ブラウザの新しいタブで下記のように Rails のデフォルト画面が表示されれば成功です。

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

まとめ

 今更ながらに Cloud9 で環境を作ってみましたが、思ったよりもかなり使いやすそうでした。エディタも用意されていますが、普段 Vim を使っているので、 Vim の環境を整えてターミナルの方を全画面表示してしまえば、ローカルで使っているのに近い感じで使えそうです。

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

 クラウドの IDE はローカルの環境に依存せずに使えるのも良いですし、Lambda 等の AWS リソースとの連携もやりやすくなっているようなのでその辺りも今後試してみようと思います。

 利用料は Cloud9 自体は無料ですが EC2 の利用料金はかかるので、実際にどれぐらいになるかも確認してみたいと思います。

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

 前回の記事で Kubernetes の公式チュートリアル Kubernetes Basics の前半3 Modulesを進めてみました。

blog.akanumahiroaki.com

 今回は後半の 4 〜 6 Modules を進めてみます。

Services and Labels(Module 4)

 Service は Pod とそれらにアクセスするためのポリシーの論理的なセットです。 Service が対象とする Pod は LabelSelector によって定義されます。

 Pod はユニークな IP アドレスを保持していますが、 Service なしではその IP アドレスはクラスタの外へ公開されることはありません。

 Service は ServiceSpec で指定した type によって異なる方法で公開されます。

  • ClusterIP(デフォルト):クラスタ内の internal IP 上に Service を公開します。クラスタ内からのみ到達可能です。

  • NodePort:NAT を使用して Node 上のポートに Service を公開します。外部からは Node の IP とポートを指定することでアクセス可能です。 ClusterIP の Superset です。

  • LoadBalancer:使用しているクラウド環境でサポートされていれば、外部ロードバランサーを作成し、 Service に外部 IP アドレスをアサインします。 NodePort の Superset です。

  • ExternalName:任意の名称を使用して、 CNAME レコードでその名称を返すことで Service を公開します。プロキシは使用されません。このタイプを使用するには v1.7 以上の kube-dns が必要です。

 Selector なしで作成された Service はエンドポイントも作成しません。これによってマニュアルで Service を特定のエンドポイントにマップすることも可能になります。

 Service は Pod のセットを横断してトラフィックをルーティングします。

 Label はオブジェクトにアタッチされたキーバリューペアで、様々なケースで使用可能です。

 まず Pod の状態を確認してみます。

$ kubectl get pods
NAME                                   READY     STATUS    RESTARTS   AGE
kubernetes-bootcamp-5c69669756-dg96d   1/1       Running   0          31s

 Service の一覧を確認してみます。

$ kubectl get services
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   59s

 kubernetes という名前の Service が Minikube でのクラスタ作成時にデフォルトで作成されています。新しい Service を作成して公開するには、 kubectl の expose コマンドを使用します。 type には NodePort を指定します。(Minikube ではまだ LoadBalancer オプションがサポートされていません)

$ kubectl expose deployment/kubernetes-bootcamp --type="NodePort" --port 8080
service/kubernetes-bootcamp exposed

 作成された Service を確認してみます。

$ kubectl get services
NAME                  TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
kubernetes            ClusterIP   10.96.0.1      <none>        443/TCP          3m
kubernetes-bootcamp   NodePort    10.108.91.62   <none>        8080:31092/TCP   31s

 kubernetes-bootcamp という名前の Service が作成されていて、 CLUSTER-IP や EXTERNAL-IP、 PORT が確認できます。

 どのポートが外部に公開されているか確認するには、 describe service コマンドを使用します。

$ kubectl describe services/kubernetes-bootcamp
Name:                     kubernetes-bootcamp
Namespace:                default
Labels:                   run=kubernetes-bootcamp
Annotations:              <none>
Selector:                 run=kubernetes-bootcamp
Type:                     NodePort
IP:                       10.108.91.62
Port:                     <unset>  8080/TCP
TargetPort:               8080/TCP
NodePort:                 <unset>  31092/TCP
Endpoints:                172.18.0.2:8080
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

 API エンドポイントにアクセスして動作を確認するためにポート番号を取得して環境変数に格納します。

$ export NODE_PORT=$(kubectl get services/kubernetes-bootcamp -o go-template='{{(index .spec.ports 0).nodePort}}')
$ echo NODE_PORT=$NODE_PORT
NODE_PORT=31092

 curl で外部にアプリケーションが公開されているかを確認してみます。

$ curl $(minikube ip):$NODE_PORT
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-5c69669756-dg96d | v=1

 Deployment 作成時には Pod に対して自動的に Label が作成されます。 describe deployment コマンドで Label を確認することができます。

$ kubectl describe deployment
Name:                   kubernetes-bootcamp
Namespace:              default
CreationTimestamp:      Sat, 29 Dec 2018 11:21:24 +0000
Labels:                 run=kubernetes-bootcamp
Annotations:            deployment.kubernetes.io/revision=1
Selector:               run=kubernetes-bootcamp
Replicas:               1 desired | 1 updated | 1 total | 1 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:  run=kubernetes-bootcamp
  Containers:
   kubernetes-bootcamp:
    Image:        gcr.io/google-samples/kubernetes-bootcamp:v1
    Port:         8080/TCP
    Host Port:    0/TCP
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Available      True    MinimumReplicasAvailable
  Progressing    True    NewReplicaSetAvailable
OldReplicaSets:  <none>
NewReplicaSet:   kubernetes-bootcamp-5c69669756 (1/1 replicas created)
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  9m    deployment-controller  Scaled up replica set kubernetes-bootcamp-5c69669756 to 1

 Pod のリストを取得するコマンドに Label を使用してみます。 kubectl get pods コマンドに -l オプションで Label を指定します。

$ kubectl get pods -l run=kubernetes-bootcamp
NAME                                   READY     STATUS    RESTARTS   AGE
kubernetes-bootcamp-5c69669756-dg96d   1/1       Running   0          11m

 Service についても同様に実行可能です。

$ kubectl get services -l run=kubernetes-bootcamp
NAME                  TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
kubernetes-bootcamp   NodePort   10.108.91.62   <none>        8080:31092/TCP   9m

 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-dg96d

 新しい Label を適用するには label コマンドをオブジェクトタイプ、オブジェクト名、新しい Label と共に使用します。

$ kubectl label pod $POD_NAME app=v1
pod/kubernetes-bootcamp-5c69669756-dg96d labeled

 適用された Label を確認してみます。

$ kubectl describe pods $POD_NAME
Name:           kubernetes-bootcamp-5c69669756-dg96d
Namespace:      default
Node:           minikube/172.17.0.54
Start Time:     Sat, 29 Dec 2018 11:21:31 +0000
Labels:         app=v1
                pod-template-hash=1725225312
                run=kubernetes-bootcamp
〜〜〜以下略〜〜〜

 新しい Label で Pod のリストを取得することも可能です。

$ kubectl get pods -l app=v1
NAME                                   READY     STATUS    RESTARTS   AGE
kubernetes-bootcamp-5c69669756-dg96d   1/1       Running   0          16m

 Service を削除するには delete service コマンドを使用します。ここでも Label を使用することができます。

$ kubectl delete service -l run=kubernetes-bootcamp
service "kubernetes-bootcamp" deleted

 Service が削除されたことを確認します。

$ kubectl get services
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   18m

 外部へ公開されなくなったことを確認します。

$ curl $(minikube ip):$NODE_PORT
curl: (7) Failed to connect to 172.17.0.54 port 31092: Connection refused

 Pod の中ではアプリケーションが稼働していることを確認します。

$ kubectl exec -ti $POD_NAME curl localhost:8080
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-5c69669756-dg96d | v=1

Scaling an application(Module 5)

 Scaling は Deployment 内のレプリカ数を変更することで達成されます。

 Service は統合されたロードバランサーを持っていて、ネットワークトラフィックを全ての Pod に配分します。また、Service はエンドポイントを使用して継続的に監視を続け、使用可能な Pod にのみトラフィックが送られるようにします。

 複数のインスタンスでアプリケーションを稼働させていれば、ダウンタイムなしでのローリングアップデートが可能です。

 まずは現在の Deployment の状態を確認してみます。

$ kubectl get deployment
NAME                  DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
kubernetes-bootcamp   1         1         1            1           12s

 各 state の内容は下記の通りです。

  • DESIRED: 設定したレプリカ数

  • CURRENT:現在稼働しているレプリカ数

  • UP-TO-DATE:設定にマッチするようにアップデートされたレプリカ数

  • AVAILABLE:ユーザーに対して有効なレプリカ数

 Deployment のレプリカ数を 4 に設定してみます。 kubectl scale コマンドで Deployment Type、Deployment Name、レプリカ数を指定します。

$ kubectl scale deployments/kubernetes-bootcamp --replicas=4
deployment.extensions/kubernetes-bootcamp scaled

 Deployment の状態を確認します。

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

 変更が適用されて、4インスタンスが有効になっていることがわかります。 Pod も確認してみます。

$ kubectl get pods -o wide
NAME                                   READY     STATUS    RESTARTS   AGE       IP     NODE
kubernetes-bootcamp-5c69669756-nkddq   1/1       Running   0          3m        172.18.0.2   minikube
kubernetes-bootcamp-5c69669756-sddhp   1/1       Running   0          1m        172.18.0.7   minikube
kubernetes-bootcamp-5c69669756-xvv29   1/1       Running   0          1m        172.18.0.6   minikube
kubernetes-bootcamp-5c69669756-zsvnd   1/1       Running   0          1m        172.18.0.5   minikube

 4つの Pod が異なる IP アドレスで稼働していることがわかります。この変更は Deployment のイベントログに記録されます。 describe コマンドで確認してみます。

$ kubectl describe deployments/kubernetes-bootcamp
〜〜〜中略〜〜〜
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  5m    deployment-controller  Scaled up replica set kubernetes-bootcamp-5c69669756 to 1
  Normal  ScalingReplicaSet  2m    deployment-controller  Scaled up replica set kubernetes-bootcamp-5c69669756 to 4

 Service がトラフィックを分散させていることを確認します。公開されている IP と Port を確認するには describe service コマンドを使用します。

$ kubectl describe services/kubernetes-bootcamp
Name:                     kubernetes-bootcamp
Namespace:                default
Labels:                   run=kubernetes-bootcamp
Annotations:              <none>
Selector:                 run=kubernetes-bootcamp
Type:                     NodePort
IP:                       10.101.88.200
Port:                     <unset>  8080/TCP
TargetPort:               8080/TCP
NodePort:                 <unset>  30677/TCP
Endpoints:                172.18.0.2:8080,172.18.0.5:8080,172.18.0.6:8080 + 1 more...
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

 ポート番号を環境変数に格納します。

$ export NODE_PORT=$(kubectl get services/kubernetes-bootcamp -o go-template='{{(index .spec.ports 0).nodePort}}')
$ echo NODE_PORT=$NODE_PORT
NODE_PORT=30677

 curl で複数回エンドポイントにアクセスしてみます。

$ curl $(minikube ip):$NODE_PORT
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-5c69669756-xvv29 | v=1
$ curl $(minikube ip):$NODE_PORT
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-5c69669756-nkddq | v=1
$ curl $(minikube ip):$NODE_PORT
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-5c69669756-sddhp | v=1
$ curl $(minikube ip):$NODE_PORT
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-5c69669756-xvv29 | v=1

 アクセスする度に違う Pod にアクセスしているので、ロードバランシングが機能している事がわかります。

 レプリカ数を 2 にダウンするには再度 scale コマンドを使用します。

$ kubectl scale deployments/kubernetes-bootcamp --replicas=2
deployment.extensions/kubernetes-bootcamp scaled

 Deployment の設定が変更されたことを確認します。

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

 Pod の状態も確認しておきます。

$ kubectl get pods -o wide
NAME                                   READY     STATUS    RESTARTS   AGE       IP     NODE
kubernetes-bootcamp-5c69669756-nkddq   1/1       Running   0          11m       172.18.0.2   minikube
kubernetes-bootcamp-5c69669756-xvv29   1/1       Running   0          8m        172.18.0.6   minikube

Updating an application(Module 6)

 ローリングアップデートではインクリメンタルに Pod のインスタンスを新しいものにアップデートしていくことでダウンタイムゼロでのデプロイを可能にします。

 Kubernetes ではアップデートはバージョン管理され、後で以前のバージョンに切り戻す事が可能です。

 アップデート中は Service によって有効な Pod にのみロードバランスされます。

 まずは Deployment と Pod の状態を確認しておきます。

$ kubectl get deployments
NAME                  DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
kubernetes-bootcamp   4         4         4            0           8s
$
$ kubectl get pods
NAME                                   READY     STATUS    RESTARTS   AGE
kubernetes-bootcamp-5c69669756-2tl2v   1/1       Running   0          17s
kubernetes-bootcamp-5c69669756-4khpv   1/1       Running   0          17s
kubernetes-bootcamp-5c69669756-d5t5l   1/1       Running   0          17s
kubernetes-bootcamp-5c69669756-vmm2h   1/1       Running   0          17s

 現在のアプリケーションのバージョンを確認するには describe pods コマンドを使用し、 Image フィールドを確認します。

$ kubectl describe pods
Name:           kubernetes-bootcamp-5c69669756-2tl2v
Namespace:      default
Node:           minikube/172.17.0.27
Start Time:     Sat, 29 Dec 2018 12:10:42 +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://2c2beb5e1f0cc006400c48838e0cb42d41d0a8b86c69e0688de54ac5e6589f2d
    Image:          gcr.io/google-samples/kubernetes-bootcamp:v1
    Image ID:       docker-pullable://gcr.io/google-samples/kubernetes-bootcamp@sha256:0d6b8ee63bb57c5f5b6156f446b3bc3b3c143d233037f3a2f00e279c8fcc64af
〜〜〜中略〜〜〜
Name:           kubernetes-bootcamp-5c69669756-4khpv
Namespace:      default
Node:           minikube/172.17.0.27
Start Time:     Sat, 29 Dec 2018 12:10:42 +0000
Labels:         pod-template-hash=1725225312
                run=kubernetes-bootcamp
Annotations:    <none>
Status:         Running
IP:             172.18.0.5
Controlled By:  ReplicaSet/kubernetes-bootcamp-5c69669756
Containers:
  kubernetes-bootcamp:
    Container ID:   docker://5bee3f08bfda17166d4692cf58ab626c8e1f9260fa28ab09537427615a84d053
    Image:          gcr.io/google-samples/kubernetes-bootcamp:v1
    Image ID:       docker-pullable://gcr.io/google-samples/kubernetes-bootcamp@sha256:0d6b8ee63bb57c5f5b6156f446b3bc3b3c143d233037f3a2f00e279c8fcc64af
〜〜〜中略〜〜〜
Name:           kubernetes-bootcamp-5c69669756-d5t5l
Namespace:      default
Node:           minikube/172.17.0.27
Start Time:     Sat, 29 Dec 2018 12:10:42 +0000
Labels:         pod-template-hash=1725225312
                run=kubernetes-bootcamp
Annotations:    <none>
Status:         Running
IP:             172.18.0.3
Controlled By:  ReplicaSet/kubernetes-bootcamp-5c69669756
Containers:
  kubernetes-bootcamp:
    Container ID:   docker://3251469b06d51131b426bfb5a50fd67b0e2c66b6d0a4dc54a56aa6f5e307dc63
    Image:          gcr.io/google-samples/kubernetes-bootcamp:v1
    Image ID:       docker-pullable://gcr.io/google-samples/kubernetes-bootcamp@sha256:0d6b8ee63bb57c5f5b6156f446b3bc3b3c143d233037f3a2f00e279c8fcc64af
〜〜〜中略〜〜〜
Name:           kubernetes-bootcamp-5c69669756-vmm2h
Namespace:      default
Node:           minikube/172.17.0.27
Start Time:     Sat, 29 Dec 2018 12:10:42 +0000
Labels:         pod-template-hash=1725225312
                run=kubernetes-bootcamp
Annotations:    <none>
Status:         Running
IP:             172.18.0.4
Controlled By:  ReplicaSet/kubernetes-bootcamp-5c69669756
Containers:
  kubernetes-bootcamp:
    Container ID:   docker://5b05a470deddcd0b461430ad441aaa3bcb9e6ffe58062365451fc9404568e24a
    Image:          gcr.io/google-samples/kubernetes-bootcamp:v1
    Image ID:       docker-pullable://gcr.io/google-samples/kubernetes-bootcamp@sha256:0d6b8ee63bb57c5f5b6156f446b3bc3b3c143d233037f3a2f00e279c8fcc64af
〜〜〜以下略〜〜〜

 現在 v1 になっているアプリケーションのバージョンを v2 にアップデートするには、 set image コマンドを使用して、 Deployment Name と新しいイメージのバージョンを指定します。

$ kubectl set image deployments/kubernetes-bootcamp kubernetes-bootcamp=jocatalin/kubernetes-bootcamp:v2
deployment.extensions/kubernetes-bootcamp image updated

 このコマンドによって Deployment に新しいイメージを使うことを通知し、ローリングアップデートを開始します。新しい Pod のステータスや古い Pod の停止を確認するには get pods コマンドを使用します。

$ kubectl get pods
NAME                                   READY     STATUS    RESTARTS   AGE
kubernetes-bootcamp-7799cbcb86-4b57t   1/1       Running   0          46s
kubernetes-bootcamp-7799cbcb86-9dp7j   1/1       Running   0          47s
kubernetes-bootcamp-7799cbcb86-9jvwg   1/1       Running   0          46s
kubernetes-bootcamp-7799cbcb86-t9qxj   1/1       Running   0          47s

 アプリケーションの稼働を確認してみます。公開されている IP と Port を確認するには describe service コマンドを使用します。

$ kubectl describe services/kubernetes-bootcamp
Name:                     kubernetes-bootcamp
Namespace:                default
Labels:                   run=kubernetes-bootcamp
Annotations:              <none>
Selector:                 run=kubernetes-bootcamp
Type:                     NodePort
IP:                       10.103.83.106
Port:                     <unset>  8080/TCP
TargetPort:               8080/TCP
NodePort:                 <unset>  30046/TCP
Endpoints:                172.18.0.10:8080,172.18.0.11:8080,172.18.0.8:8080 + 1 more...
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

 ポート番号を環境変数に格納しておきます。

$ export NODE_PORT=$(kubectl get services/kubernetes-bootcamp -o go-template='{{(index .spec.ports 0).nodePort}}')
$ echo NODE_PORT=$NODE_PORT
NODE_PORT=30046

 curl でエンドポイントにアクセスしてみます。

$ curl $(minikube ip):$NODE_PORT
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-7799cbcb86-9dp7j | v=2
$ curl $(minikube ip):$NODE_PORT
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-7799cbcb86-4b57t | v=2
$ curl $(minikube ip):$NODE_PORT
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-7799cbcb86-4b57t | v=2
$ curl $(minikube ip):$NODE_PORT
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-7799cbcb86-9jvwg | v=2

 アクセスごとに違う Pod にアクセスしていて、バージョンも v2 になっている事がわかります。

 アップデートのステータスは rollout status コマンドでも確認できます。

$ kubectl rollout status deployments/kubernetes-bootcamp
deployment "kubernetes-bootcamp" successfully rolled out

 describe pods でイメージのバージョンも確認してみます。

$ kubectl describe pods | grep Image
    Image:          jocatalin/kubernetes-bootcamp:v2
    Image ID:       docker-pullable://jocatalin/kubernetes-bootcamp@sha256:fb1a3ced00cecfc1f83f18ab5cd14199e30adc1b49aa4244f5d65ad3f5feb2a5
    Image:          jocatalin/kubernetes-bootcamp:v2
    Image ID:       docker-pullable://jocatalin/kubernetes-bootcamp@sha256:fb1a3ced00cecfc1f83f18ab5cd14199e30adc1b49aa4244f5d65ad3f5feb2a5
    Image:          jocatalin/kubernetes-bootcamp:v2
    Image ID:       docker-pullable://jocatalin/kubernetes-bootcamp@sha256:fb1a3ced00cecfc1f83f18ab5cd14199e30adc1b49aa4244f5d65ad3f5feb2a5
    Image:          jocatalin/kubernetes-bootcamp:v2
    Image ID:       docker-pullable://jocatalin/kubernetes-bootcamp@sha256:fb1a3ced00cecfc1f83f18ab5cd14199e30adc1b49aa4244f5d65ad3f5feb2a5

 それでは次に別のアップデートとして、 v10 というイメージをデプロイしてみます。

$ kubectl set image deployments/kubernetes-bootcamp kubernetes-bootcamp=gcr.io/google-samples/kubernetes-bootcamp:v10
deployment.extensions/kubernetes-bootcamp image updated

 Deployment の状態を確認してみます。

$ kubectl get deployments
NAME                  DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
kubernetes-bootcamp   4         5         2            3           13m

 今度は AVAILABLE が期待した通りになっていません。 Pod のリストを確認してみます。

$ kubectl get pods
NAME                                   READY     STATUS             RESTARTS   AGE
kubernetes-bootcamp-5f76cd7b94-tgshj   0/1       ImagePullBackOff   0          1m
kubernetes-bootcamp-5f76cd7b94-zvkd6   0/1       ImagePullBackOff   0          1m
kubernetes-bootcamp-7799cbcb86-4b57t   1/1       Running            0          8m
kubernetes-bootcamp-7799cbcb86-9dp7j   1/1       Running            0          8m
kubernetes-bootcamp-7799cbcb86-t9qxj   1/1       Running            0          8m

 上の二つの Pod の状態が ImagePullBackOff になっていて、 READY が 0/1 になっています。

 さらに調べるために describe コマンドを使用します。

$ kubectl describe pods
Name:           kubernetes-bootcamp-5f76cd7b94-tgshj
Namespace:      default
Node:           minikube/172.17.0.27
Start Time:     Sat, 29 Dec 2018 12:23:17 +0000
Labels:         pod-template-hash=1932783650
                run=kubernetes-bootcamp
Annotations:    <none>
Status:         Pending
IP:             172.18.0.3
Controlled By:  ReplicaSet/kubernetes-bootcamp-5f76cd7b94
Containers:
  kubernetes-bootcamp:
    Container ID:
    Image:          gcr.io/google-samples/kubernetes-bootcamp:v10
    Image ID:
    Port:           8080/TCP
    Host Port:      0/TCP
    State:          Waiting
      Reason:       ImagePullBackOff
    Ready:          False
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-qcw86 (ro)
Conditions:
  Type           Status
  Initialized    True
  Ready          False
  PodScheduled   True
Volumes:
  default-token-qcw86:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-qcw86
    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
  ----     ------                 ----              ----               -------
  Normal   Scheduled              2m                default-scheduler  Successfully assigned kubernetes-bootcamp-5f76cd7b94-tgshj to minikube
  Normal   SuccessfulMountVolume  2m                kubelet, minikube  MountVolume.SetUp succeeded for volume "default-token-qcw86"
  Normal   Pulling                49s (x4 over 2m)  kubelet, minikube  pulling image "gcr.io/google-samples/kubernetes-bootcamp:v10"
  Warning  Failed                 48s (x4 over 2m)  kubelet, minikube  Failed to pull image "gcr.io/google-samples/kubernetes-bootcamp:v10": rpc error: code = Unknown desc = unauthorized: authentication required
  Warning  Failed                 48s (x4 over 2m)  kubelet, minikube  Error: ErrImagePull
  Normal   BackOff                23s (x6 over 2m)  kubelet, minikube  Back-off pullingimage "gcr.io/google-samples/kubernetes-bootcamp:v10"
  Warning  Failed                 23s (x6 over 2m)  kubelet, minikube  Error: ImagePullBackOff
〜〜〜以下略〜〜〜

 イベントログを見ると、 v10 というイメージがなくてエラーになっているという事がわかります。以前のバージョンにロールバックしてみたいと思います。ロールバックするには rollout undo コマンドを使用します。

$ kubectl rollout undo deployments/kubernetes-bootcamp
deployment.extensions/kubernetes-bootcamp

 Pod の状態を確認してみます。

$ kubectl get pods
NAME                                   READY     STATUS    RESTARTS   AGE
kubernetes-bootcamp-7799cbcb86-4b57t   1/1       Running   0          12m
kubernetes-bootcamp-7799cbcb86-9dp7j   1/1       Running   0          13m
kubernetes-bootcamp-7799cbcb86-jv7k7   1/1       Running   0          48s
kubernetes-bootcamp-7799cbcb86-t9qxj   1/1       Running   0          13m

 4つの Pod が全て Running になっている事が確認できました。 Image も確認してみます。

$ kubectl describe pods | grep Image
    Image:          jocatalin/kubernetes-bootcamp:v2
    Image ID:       docker-pullable://jocatalin/kubernetes-bootcamp@sha256:fb1a3ced00cecfc1f83f18ab5cd14199e30adc1b49aa4244f5d65ad3f5feb2a5
    Image:          jocatalin/kubernetes-bootcamp:v2
    Image ID:       docker-pullable://jocatalin/kubernetes-bootcamp@sha256:fb1a3ced00cecfc1f83f18ab5cd14199e30adc1b49aa4244f5d65ad3f5feb2a5
    Image:          jocatalin/kubernetes-bootcamp:v2
    Image ID:       docker-pullable://jocatalin/kubernetes-bootcamp@sha256:fb1a3ced00cecfc1f83f18ab5cd14199e30adc1b49aa4244f5d65ad3f5feb2a5
    Image:          jocatalin/kubernetes-bootcamp:v2
    Image ID:       docker-pullable://jocatalin/kubernetes-bootcamp@sha256:fb1a3ced00cecfc1f83f18ab5cd14199e30adc1b49aa4244f5d65ad3f5feb2a5

 全て v2 のイメージが使われています。

まとめ

 後編ではスケーリングやローリングアップデートといった内容も試す事ができ、 Kubernetes を使った運用のイメージをつかむ事ができました。まだ用意された仮想環境で想定された手順を踏んでいっただけなので、今後は自分のローカル環境やクラウド環境で動かせるように挑戦してみたいと思います。チュートリアルも Kubernetes Basics 以外にも色々あるので、試してみたいと思います。

公式チュートリアルで 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 の追加の容易さや、修正から実行確認の手返しの良さは劣るところになりそうですが、サーバレス環境での開発方法も最適な方法を探していきたいと思います。