EKSのPod起動数の制限

f:id:cloudfish:20200116173700p:plain
最近、いくつかEKSの環境構築を進めているなかで、EKSのPodの起動数の制限がネットワークとインスタンスタイプに依存することが分かりました。すでに知っている人も多いと思いますが、備忘録として残しておきたいと思います。

Podの起動数

通常、Podの起動を考える時に一番気にするポイントとしてはノードのリソース(CPU、メモリ)ではないでしょうか?
当然ですが、ノードインスタンスのCPUやメモリに余裕がないと新たにPodを起動できませんし高負荷時にスケールアウトもできなくなります。
Manifestにもrequestやlimitなどの設定項目がありますしリソースの確保については気を使われているのではないでしょうか?
EKSでは、ノードのリソース状況に加えてネットワークについても意識する必要があります。

Podのネットワーク制限

通常のKubernetesのPod用のネットワークはノードインスタンスとは別の内部ネットワークとなりあまり気にする必要はありません。
しかし、EKSではAmazon VPC CNI plugin for KubernetesというネイティブなVPCを利用可能とするプラグインを用いることで、VPCネットワーク上と同じIPをPodに割り当てることなります。つまり、PodはVPC上のIPを利用して通信するため、Podの起動数がVPCで利用可能なIP数に依存するということになります。
EKSのネットワークの詳細については以下を参照してください。
docs.aws.amazon.com

一般的にAWSのサブネットについては、24ビット以上で切られることも多いと思います。仮に24ビットでサブネットを作成した場合、AWS予約分を除くと利用可能なIP数は251となります。
AWSで指定可能な最も小さな28ビットで作成した場合は、11IPしか利用することができません。
ノードインスタンスをいくつ起動するかにもよりますが、インスタンス自身にもIPが必要となります。
また、RDSやElasticacheなどその他のリソース用のIPも確保しておく必要があるため、実際にPodで利用可能なIPはもっと少なくなります。
さらに、ノードインスタンスのスケールやPodのスケーリングがどれくらいになるかも想定しておく必要があります。
これらを踏まえてネットワーク設計にあたってはIPに余裕をもったCIDRを検討してください。

インスタンスタイプごとの制限

IP数に余裕があって、インスタンスのリソースにも余裕があった場合、1インスタンスに際限なくPodを起動できるのでしょうか?
AWSでは、インスタンスタイプごとに割り当て可能なIP数が制限されているため、こちらも意識しておく必要があります。具体化にはインスタンスタイプごとにアタッチ可能なネットワークインターフェイス(ENI)の数に制限があり、1つのネットワークインターフェイスあたりの利用可能なIP数に制限があります。

実際にc5.large(or m5.large)で見てみると、制限は以下の通りとなります。

ENIの最大数 3
ENIあたりの IPv4 アドレス 10

この場合、利用可能IPは30となります。
特にt系のmicro、smallなどは場合は、利用可能なIP数も少ないため注意する必要があります。
インスタンスタイプごとの制限は以下のドキュメントを参照してください。
https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/using-eni.htmldocs.aws.amazon.com

スケールした場合のIPの消費

例として以下のVPCのケースで考えてみます。

VPC CIDR 192.168.0.0/16

Publicサブネットを2つPrivateサブネットを2つ作成したとして、EKSのノードグループをPrivateサブネットに配置するものとします。

Subnet CIDR 利用可能IP数
Private A 192.168.10.0/24 251
Private C 192.168.11.0/24 251

構成

インスタンスタイプ c5.large
通常時起動台数 10
ピーク時起動台数 20

上記のケースの場合、ノードグループは2つのPrivateサブネットに配置されているため502のIPが利用可能です。
c5.largeは最大で30ip利用可能ですが、1台あたり20ip程度利用していたと想定します。
通常時では200ip程度消費することとなり300程度の余裕があります。
しかし、ピーク時では倍の400ipとなり残りが100しかなくなります。
そうすると、さらに負荷が増大した場合にスケールアウト可能なインスタンスが5台までとなり、あまり余裕がありません。
さらにローリングアップデートやBlue・Greenデプロイメントを行うのであれば、余分にipが必要となるためそれらの考慮も必要となってきます。
通常のEC2であれば、24ビットのCIDRで十分なことも多かったかと思いますが、EKSではかなりのipが消費されることになるため注意が必要です。

まとめ

このようにEKSでは、EC2レベルではなくPodレベルでIPが必要になり、より多くのVPC上のIPを消費するので上記に注意してネットワーク設計を行ってください。
インスタンスタイプについても、想定しているPodが起動可能となるようにタイプを選択しましょう。小さいインスタンスだと想定しているPodが起動できなくなる場合があります。

EKSにおけるSecurity Group変更の注意点

長くなるので結論を先に書きます。
EKSのノードインスタンスにおいてセキュリティグループを変更(アタッチ or デタッチ)する際は、ノードグループを更新しましょう。要するに手動での変更は止めましょうということです。ちなみにアタッチ済のセキュリティグループのルール変更は問題ありません。

手動で変更したために、疎通に関して問題が起こったので経緯と原因について書きたいと思います。

やりたかったこと

以下のような構成でPodからRDSへ疎通させたかった。

構成

f:id:cloudfish:20191202225547p:plain

対応内容

上記のような構成でノードのセキュリティグループを手動で変更しました。
具体的な設定内容としては、自己参照設定されているセキュリティグループををノードインスタンスとRDSにアタッチしました。
そもそもノードグループはオートスケーリンググループのため、手動変更がよくないことは認識していましたが、開発環境ということや諸々の状況から、一時的な対応として手動で新しいセキュリティグループをアタッチしました。(要するに手抜きでやってしまいました。)

発生事象

上記の対応後に、PodからRDSに接続できたりできなかったりする事象が発生。

原因調査

この時点で以下の確認を行いました。

1.セキュリティグループ

ソース、ポートに設定誤りがないことを確認
全てのインスタンスにセキュリティグループが付与されていることを確認

2.ネットワークACL

何も設定されていないことを確認

3.テスト用Podから接続確認

PodをCreateした直後は高確率でRDSに対して接続可能となるが、Podをkillしてリスタートすると接続できたりできなかったりというかなり微妙な結果となりました。特定のノードで接続できないとか特定のPodに問題があるのかなど確認しましたが特にそういった問題は見受けられませんでした。

4.セキュリティグループの設定変更

RDSのSGにおいて、自己参照ではなくインバウンドのソースにSharedNodeSecurityGroup(eksctlでデフォルトで作成されるノードのSG)を指定するように変更したところ、RDSへ接続が可能となることが判明。

この時点では、明確な原因が分かるところまでは調査できませんでした。
他の環境において全く同様の設定でDB接続に問題は発生していなかったため、自己参照のSGに問題があるとは考えづらかったのですが、状況から見ると何か問題がありそうということで、一旦自己参照における設定はやめることとしました。(結果的に自己参照が問題ではありませんでした。)

余談ですが、この調査の際に以下の方法を利用してホスト側に接続しtcpdumpなどで調査を進めました。
SSH接続しなくてよいのでかなり便利です。
dev.classmethod.jp

EKSのネットワークについて調査

原因がはっきりとしなかったため、後日改めてEKSのネットワークについて調べてみました。
EKSのネットワークについては、Amazon VPC CNI plugin for Kubernetesというプラグインを使用することで、PodについてもVPCネットワーク上のアドレスと同じIPを利用することが可能となっています。
このため、ノードインスタンスではPod数分のIPを確保する必要があるため、ネットワークインターフェースを1ノードに複数アタッチする構成となっています。以下AWSの資料です。
https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/images/networking.png

問題が起こった環境では、インスタンスタイプはt3.mediumを利用していました。このタイプのネットワーク制限は以下となり、最大18個のIPが利用可能となります。
■t3.medium

アタッチ可能なENI数 3
ENIあたりのIPv4 6

ここでENIが複数アタッチされていることに改めて着目しました。
そもそもセキュリティグループはインスタンスに対して割り当てられるものではなく、ENIにセットされるものとなります。通常、あまり複数ENIを利用することも少ないこともあり、EC2のコンソールからはインスタンスにセットされているように見えますが実際にはENIにセットされていることになります。

この時、EC2のコンソールからセキュリティグループを追加した際に全てのENIに反映されていないのではないかと思い至り、検証してみました。
もし上記の通りの動作だとすると、セキュリティグループがアタッチされたENIからのみRDSへ疎通が可能となり、それ以外のENIからは通信できないという動きになると考えられます。

ENIに対しセキュリティグループがどのようにアタッチされるか?

以下のようにセキュリティグループが割り当てられているインスタンスを用意しました。
3つのセキュリティグループが割り当てられています。
f:id:cloudfish:20191203100029p:plain

このインスタンスには以下のように3つのENIがアタッチされており、セキュリティグループについても全て同様となっています。
f:id:cloudfish:20191203100408p:plain
f:id:cloudfish:20191203100430p:plain
f:id:cloudfish:20191203100448p:plain

このインスタンスに対してEC2コンソールからadd_sgというセキュリティグループをアタッチしました。
f:id:cloudfish:20191204103257p:plain

アタッチ完了後にENIを確認したところプライマリENIのみadd_sgが付与されていることが分かりました。
■プライマリENI
f:id:cloudfish:20191203100815p:plain
セカンダリENI
f:id:cloudfish:20191203101018p:plain
f:id:cloudfish:20191203100912p:plain

検証結果から上記の想定通りの動作ということが確認できました。

接続不具合となった原因

改めて状態を図にしてみると以下のようなイメージになります。
EC2コンソールから手動でセキュリティグループを変更するとENI_1にのみadd_sgがアタッチされることとなります。
そのため、ENI_1に紐付くIPが割り当てられたPodからのみRDSへの疎通が可能となり、そうでないENIに紐付くIPが割り当てられたPodからは疎通ができません。ということで、今回発生した事象について原因が判明しました。
f:id:cloudfish:20191204184335p:plain

ちなみに何度か検証した際に気付きましたが、EC2コンソールからのセキュリティグループ変更時には、以下のようにインターフェースIDが表示されていました・・・コンソールはしっかり確認する必要がありますね。
f:id:cloudfish:20191203100654p:plain

全てのENIに手動でセキュリティグループをアタッチすれば問題ないか?

完全に一時しのぎとしてなのですが、仮に全てのENIのセキュリティグループを手動で変更すれば問題ないかという観点でもう少し考えてみました。オートスケーリンググループのテンプレートの問題は一旦おいておきます。
ノードインスタンスは、起動直後にPodの数が少ない場合、ENIは1つしかアタッチされていません。
そこからPodの数が増えて1つのENIで割り当て可能なIP数を超えた場合は、新たにENIがアタッチされることになります。その際、増えたENIはどのようなセキュリティグループがセットされているかを確認してみました。

以下のようなENIが一つのみのインスタンスを用意し、add_sgというセキュリティグループをセットします。
f:id:cloudfish:20191203101518p:plain
インスタンにENIが一つアタッチされている状態です。
f:id:cloudfish:20191203101738p:plain

この状態でPodの数を増やし、各ENIの状態を確認しました。
プライマリENIにはadd_sgが付与されていることが分かります。
f:id:cloudfish:20191203102737p:plain

追加された2つのENIを確認すると、add_sgは付与されていませんでした。
おそらくテンプレートから引っ張ってきてると思うので当然の動作かもしれません。
f:id:cloudfish:20191203102759p:plain
f:id:cloudfish:20191203102815p:plain

ということから、仮に全てのENIのセキュリティグループを手動で変更したとしてもENIが勝手に増える可能性があるため、突然、通信できなくなる問題が起こりそうですね。

まとめ

・ノードインスタンスのセキュリティグループの手動変更はやめましょう。(あまりやらないと思いますが)
・eksctlのドキュメントにもありますが、基本的にノードグループはイミュータブルなものとしてデザインされているので変更する時はノードグループの更新で対応しましょう
・セキュリティグループの変更程度でノードグループの更新はあまりやりたくないので、構築時にしっかりセキュリティグループの設計を行いましょう。

今回は余計なことをして、無駄にハマった感がかなりありますが、改めてEKSについて知ったこともあり非常に勉強になりました。
以上、どなたかの参考になれば幸いです。

Amazon CloudWatch SyntheticsでURL監視を設定

Amazon CloudWatch Syntheticsがプレビューされていましたので早速試してみました。
CloudWatch Syntheticsとは、サービスのエンドポイントやAPIのエンドポイントなどをモニタリングするサービスになります。DataDogでも同様のサービスがありますね。

現時点では、US East (N. Virginia), US East (Ohio), and EU (Ireland)のいずれかのリージョンでし利用できませんので、今回は、バージニア北部を利用しました。

Canaryを作成

以下の画面からCanaryを作成を選択します。
f:id:cloudfish:20191127080636p:plain

デフォルトで以下のように選択されていますのでそのまま次進みます。
f:id:cloudfish:20191127081041p:plain

Canary名と監視対象のURLを入力します。
スクリプトエディタの内容は自動で生成されるます。
f:id:cloudfish:20191127081558p:plain

以降はデフォルトのままとし、Canaryを作成します。
f:id:cloudfish:20191127081926p:plain
f:id:cloudfish:20191127082114p:plain

モニタリングの実行

作成直後はステータスがグレイアウトされていますが、しばらくすると以下のように実行結果が表示されます。
f:id:cloudfish:20191127082405p:plain

詳細を確認するとスクリーンショットやHARファイルなどを見ることができます。
実際の画面を取得してくれるのはすごくいいですね。
f:id:cloudfish:20191127082814p:plain

ログを確認してみると、Lambdaのログに似ていたので、Lambdaのコンソールを確認したところsynthetics用の関数が自動で作成されていました。消したりしないように注意が必要ですね。

f:id:cloudfish:20191127100526p:plain

アラート通知

また、Threshholdsを有効にし、Canaryでしきい値を有効にすると、アラームが作成されますので、これをもとにchatbotやLambdaと連携することでアラート通知が可能になります。

カスタマイズ

スクリプトがカスタマイズできるので、Basic認証やログインしての監視なども可能ですね。
ちなみにログイン情報などの秘匿情報が必要な場合はSecrets Managerの利用が推奨されています。

料金

1canaryあたり月額$0.0012となっています。

まとめ

特にドキュメント読むことなく簡単に設定することができました。また、スクリプトをカスタマイズできるなど拡張性も高いと思いますので、ぜひ使っていきたいですね。

CloudFormationでDataDogのIntegration設定

CloudFormationで以下のアップデートがありサードパーティ製品についてもCoudFormationで設定できるようになりました。
DataDogのIntegration設定などをCloudFormationで設定できるようなので、早速試してみました。
aws.amazon.com

CloudFormationへDatadogリソースの登録

aws cliで以下のコマンドを実行しリソースの登録を行います。
実行前にawscliのアップデートが必要になります。(pip install -U awscli)

aws cloudformation register-type \
    --region ap-northeast-1 \
    --type RESOURCE \
    --type-name "Datadog::Integrations::AWS" \
    --schema-handler-package s3://datadog-cloudformation-resources/datadog-integrations-aws/datadog-integrations-aws-1.0.0.zip

※type-name、schema-handler-packageの設定値についてはこちらgithubを参照してください。

以下の通りRegistrationTokenが表示されれば登録は完了です。

{
    "RegistrationToken": "122c2c39-12c8-4e93-a711-f6eb1234eae0"
}

登録されているかは以下で確認できます。

$aws cloudformation list-types
{
    "TypeSummaries": [
        {
            "Description": "Datadog AWS Integrations",
            "LastUpdated": "2019-11-20T03:13:25.218Z",
            "TypeName": "Datadog::Integrations::AWS",
            "TypeArn": "arn:aws:cloudformation:ap-northeast-1:123412341234:type/resource/Datadog-Integrations-AWS",
            "DefaultVersionId": "00000001",
            "Type": "RESOURCE"
        }
    ]
}


リージョンごとにリソース登録が必要となるようですが、このあたりの設定は今後もっと簡略化されていくと嬉しいですね。

Integration設定

事前に設定するDataDogのAPIキーとApplicationキーを取得しておいてください。
以下のyamlをファイルに保存しCloudFormationから実行し、パラメータにて上記のキーを設定して実行してください。

---
AWSTemplateFormatVersion: '2010-09-09'
Description: 'IAM Role and IAM Policy for Datadog AWS Integration'
Parameters:
  DatadogAPIKey:
    Description: "Datadog's API Key"
    Type: String
  DatadogAPPKey:
    Description: "Datadog's APP Key"
    Type: String
Resources:
  DatadogAWSIntegrationResource:
    Type: 'Datadog::Integrations::AWS'
    Properties:
      AccountID: !Ref AWS::AccountId
      RoleName: DatadogAWSIntegrationRoleTest
      HostTags: ["env:staging"]
      AccountSpecificNamespaceRules: {"ec2": true, "api_gateway": false}
      DatadogCredentials:
        ApiKey: !Ref DatadogAPIKey
        ApplicationKey: !Ref DatadogAPPKey

DataDogの設定画面を確認すると以下の通り登録されていれば完了です。
f:id:cloudfish:20191121105655p:plain

AWS側のRoleも合わせて作成してくれないか期待しましたが、残念ながらそこまではしてくれないようでした。
また、ExternalIDの取得方法が現状ではなさそうなので今後に期待したいと思いますが、機能が拡充されていくと思いますので、まとめてCloudFormationで管理することが可能になりますね。

Kubernetes Cluster on Raspberry Pi zero using Cluster Hat

Once I've read this blog(3日間クッキング【Kubernetes のラズペリーパイ包み “サイバーエージェント風”】), I wanted to deploy k8s cluster on Raspberry Pi.
However, It's too expensive for me in this blog's way. Need three Raspberry Pi 3 Model B and switching hub ... etc.
So I've created k8s cluster Raspberry Pi Zero, but It was very hard for me than I thought.
I'll introduce the creation of k8s cluster on raspberry pi zero in this time.
And this is totally useless because of using the older version, and not for production. In addition unstable behavior and very slowly.
I made only for my pleasure. If you're interested in, make it.

f:id:cloudfish:20190827155059j:plain

Component List

Prepare the following components.
We need at least one Raspberry Pi Zero

  • Cluster Hat v2.3 x 1 (23.33£)
  • Raspberry Pi B+ x 1 ($35)
  • Raspberry Pi Zero x 1-4 (3.88£/per)
  • Micro SD Card x 2-5
  • Power Cable x 1
  • Lan Cable or USB Wifi Adaptor

※You can buy Cluster Hat and Raspberry Pi in Pimoroni(https://shop.pimoroni.com/)

Prerequisite

Component Role

Hardware Role of ClusterHat Role of K8S
Raspberry Pi B+ Controller Control-Plane
Raspberry Pi Zero x (1 - 4) Node Node
ClusterHat - -

Raspberry Pi Zero( or Zero W)
This is my environment(actually, I used 3 raspberry pi zero)
f:id:cloudfish:20190827173701p:plain

1. Create Raspberry Pi Zero Image for ClusterHat and k8s

1-1. Create old raspbian image

Create Raspberry Pi Zero image from 2017-11-29-raspbian-stretch-lite because latest raspbian image can unavailable cpuset.
execute the following command in Controller Node of ClusterHat

# wget http://ftp.jaist.ac.jp/pub/raspberrypi/raspbian_lite/images/raspbian_lite-2017-12-01/2017-11-29-raspbian-stretch-lite.zip
# unzip 2017-11-29-raspbian-stretch-lite.zip
# apt install kpartx
# git clone https://github.com/burtyb/clusterhat-image
# cd clusterhat-image/build
# mkdir {img,mnt,dest,mnt2}
# mv 2017-11-29-raspbian-stretch-lite.img img/
# create.sh 2017-11-29

if it is well, the Raspbian image should be in dest directory.

ClusterCTRL-2017-11-29-lite-1-CNAT.img
ClusterCTRL-2017-11-29-lite-1-p1.img 
ClusterCTRL-2017-11-29-lite-1-p2.img
ClusterCTRL-2017-11-29-lite-1-p3.img
ClusterCTRL-2017-11-29-lite-1-p4.img

1-2. Write the image to micro sd

Download this Raspbian image to local.
Write CNAT.img to Controller Node(Raspberry Pi B+), others to the node(Raspberry Pi Zero).
Create ssh file at boot directory for connecting ssh each node.
If you're on a Mac, execute the following command after writing.

# touch /Volumes/boot/ssh

1-3. Start Controller Node

Confirm connecting to Controller Node
From local machine.

# ssh pi@[controller ip_addr]

From Controller Node

# ssh pi@[172.19.181.1-4]

Password is raspberry

2. Setting Node of ClusterHat

2-1. Install library to Controller Node

Install python-smbus because lacked library for clusterhat command. Execute the following command.

# apt-get install python-smbus


After install, execute clusterhat command.

# clusterhat status

2-2. Setting Nat

For Node connect to the internet, Setting Nat.
Execute the following command

# apt-get -y install iptables-persistent
# iptables -t nat -A POSTROUTING -s 172.19.181.0/24 ! -o brint -j MASQUERADE
# iptables -A FORWARD -i brint ! -o brint -j ACCEPT
# iptables -A FORWARD -o brint -m conntrack --ctstate  RELATED,ESTABLISHED -j ACCEPT
# sh -c "iptables-save > /etc/iptables/rules.v4"

3. Setting All Node of ClusterHat

3-1. Hold kernel version

# apt-mark hold raspberrypi-kernel

3-2. upgrade

# apt-get upgrade -s

Check it has not changed kernel version

# uname -a
Linux p1 4.9.59+ #1047 Sun Oct 29 11:47:10 GMT 2017 armv6l GNU/Linux

3-3. Install Docker

Install with fixed version(18.06.1~ce~3-0~raspbian)

# curl -sSL https://get.docker.com | sh
# usermod -aG docker pi
# apt-get install docker-ce=18.06.1~ce~3-0~raspbian

If docker command not well, reinstall docker.

# apt-get remove docker-ce
# apt-get install docker-ce=18.06.1~ce~3-0~raspbian

3-4. Hold Docker version

# apt-mark hold docker-ce

3-5. Install Kubernetes

Install kubernetes with below version.

# curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - 
# echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" > /etc/apt/sources.list.d/kubernetes.list
# apt-get update
# apt-cache madison kubeadm
# apt-get install kubelet=1.5.6-00 kubeadm=1.5.6-00 kubectl=1.5.6-00 kubernetes-cni=0.5.1-00

4. Setting the Control-Plane Node(Controller Node of ClusterHat)

4-1. Initialize a Kubernetes Control-Plane node

Execute the following command on control-plane

# kubeadm init --pod-network-cidr 10.244.0.0/16
Probably it should stop at the following log (in 5-10minitus)
<master/apiclient> created API client, waiting for the control plane to become ready
Check the logs,you should see the following logs
# journalctl -eu kubelet
"error: failed to run Kubelet: unable to load client CA file /etc/kubernetes/pki/ca.crt: open /etc/kubernetes/pki/ca.crt"

※because creating ca(ca.pem) file name and refference ca(ca.crt) file name are different

Solution of this trouble

Change file name as follows

cd /etc/kubernetes/pki
cp ca.pem ca.crt

After change file name, waiting for a while.
if it well, memo the following such a output.

# kubeadm join --token=18b644.0438585f9e20e864 192.168.0.109

4-2. Install flannel

Create file as follows
kube-flannel.yml

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: flannel
  namespace: kube-system
---
kind: ConfigMap
apiVersion: v1
metadata:
  name: kube-flannel-cfg
  namespace: kube-system
  labels:
    tier: node
    app: flannel
data:
  cni-conf.json: |
    {
      "name": "cbr0",
      "type": "flannel",
      "delegate": {
        "isDefaultGateway": true
      }
    }
  net-conf.json: |
    {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "vxlan"
      }
    }
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  name: kube-flannel-ds
  namespace: kube-system
  labels:
    tier: node
    app: flannel
spec:
  template:
    metadata:
      labels:
        tier: node
        app: flannel
    spec:
      hostNetwork: true
      nodeSelector:
        beta.kubernetes.io/arch: arm
      tolerations:
      - key: node-role.kubernetes.io/master
        operator: Exists
        effect: NoSchedule
      serviceAccountName: flannel
      containers:
      - name: kube-flannel
        image: quay.io/coreos/flannel:v0.7.1-arm
        command: [ "/opt/bin/flanneld", "--ip-masq", "--kube-subnet-mgr" ]
        securityContext:
          privileged: true
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        volumeMounts:
        - name: run
          mountPath: /run
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      - name: install-cni
        image: quay.io/coreos/flannel:v0.7.1-arm
        command: [ "/bin/sh", "-c", "set -e -x; cp -f /etc/kube-flannel/cni-conf.json /etc/cni/net.d/10-flannel.conf; while true; do sleep 3600; done" ]
        volumeMounts:
        - name: cni
          mountPath: /etc/cni/net.d
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      volumes:
        - name: run
          hostPath:
            path: /run
        - name: cni
          hostPath:
            path: /etc/cni/net.d
        - name: flannel-cfg
          configMap:
            name: kube-flannel-cfg

Execute the following command.

#kubectl create --validate=false -f kube-flannel.yml

※ it takes a long time(about 30〜40minutes)

5. Setting the Kubernetes Node(Node of ClusterHat)

5-1. Copy ca.crt from Control-Plane

Download ca.crt from Control-Plane, and Upload the file to node.

scp pi@[control-plane ip_addr]:/etc/kubernetes/pki/ca.crt .
scp pi@[node ip_addr]:ca.crt /etc/kubernetes/pki/ca.crt

because when executed join command, not created ca.crt(probably bug)
if you execute join command without this 5-1, you should face the following trouble.

after executing join command, the following output.

Node joins complete:

but node not joined.

Check the log.

# journalctl -eu kubelet
"error: failed to run Kubelet: unable to load client CA file /etc/kubernetes/pki/ca.crt"

5-2. Join to Control-Plane

Execute the following command(at 4-1's command) each node.

# kubeadm join --token=18b644.0438585f9e20e864 192.168.0.109


6. Check if the installation was successful
If you can see the following output, you got success.

# kubectl get nodes
NAME      STATUS         AGE
cnat      Ready,master   2h
p1        Ready          1m
p2        Ready          9m
p3        Ready          19m

and look this command output

# kubectl get pods --all-namespaces -o wide
NAMESPACE     NAME                              READY     STATUS    RESTARTS   AGE       IP              NODE
kube-system   dummy-2501624643-8jrkj            1/1       Running   0          2h        192.168.0.109   cnat
kube-system   etcd-cnat                         1/1       Running   0          2h        192.168.0.109   cnat
kube-system   kube-apiserver-cnat               1/1       Running   0          2h        192.168.0.109   cnat
kube-system   kube-controller-manager-cnat      1/1       Running   0          2h        192.168.0.109   cnat
kube-system   kube-discovery-2202902116-pwwaf   1/1       Running   0          2h        192.168.0.109   cnat
kube-system   kube-dns-2334855451-aq9pj         3/3       Running   1          2h        10.244.0.2      cnat
kube-system   kube-flannel-ds-5tz4s             2/2       Running   2          22m       172.19.181.3    p3
kube-system   kube-flannel-ds-k5vsc             2/2       Running   0          3m        172.19.181.1    p1
kube-system   kube-flannel-ds-kasde             2/2       Running   0          1h        192.168.0.109   cnat
kube-system   kube-flannel-ds-uumv1             2/2       Running   2          11m       172.19.181.2    p2
kube-system   kube-proxy-2ku07                  1/1       Running   0          2h        192.168.0.109   cnat
kube-system   kube-proxy-d9xxo                  1/1       Running   0          22m       172.19.181.3    p3
kube-system   kube-proxy-dz146                  1/1       Running   0          11m       172.19.181.2    p2
kube-system   kube-proxy-gvcs1                  1/1       Running   0          3m        172.19.181.1    p1
kube-system   kube-scheduler-cnat               1/1       Running   0          2h        192.168.0.109   cnat

What kind of trouble occurred

1. Unavailable cpuset on latest raspbian image (1-1)

created old raspbian image with being able to use cpuset.

2. Can't initialize kubernetes on kubeadm(4-1)

kubeadm created ca(ca.pem) file, also kubeadm reference ca(ca.crt) file, but both file name is not same.
So Renamed the ca.pem file to the ca.crt.

3. Can't deploy the latest flannel(4-2)

The latest flannel can unavailable in this version of kubeadm.
I used an old flannel version.

4. Node can't join to Control-Plane(5-1)

When execute join command, node can't join to Control-Node.
The cause of this problem is the node doesn't have ca.crt. Probably should be created automatically when joining.
This can be solved by copying the ca.crt from Control-Plane to Node

AWS Client VPN設定のハマりどころ

AWS Client VPNが東京リージョンでも使えるようになりましたね。
これでVPNサーバを立てる手間が減らせるので、費用面は考慮しつつうまく活用していきたいですね。
今回はClient VPNを検証した際に設定でわかりにくかった箇所があったので備忘録として残しておきたいと思います。

Client VPN設定方法

設定方法については、AWSのドキュメントやブログで色々出ていますのでそれを参照して設定してください。
クライアント VPN の使用開始 - AWS Client VPN
[AWS]踏み台をワンチャンなくせる!?VPC接続にClient VPNを使ってみよう | DevelopersIO

構成

以下の構成で検証しました
f:id:cloudfish:20190725123039p:plain

ハマったポイント

設定後にClient端末からSSHが通らない

VPNは正常に接続できるものの、SSH接続ができない状態となりました。
ルーティングについてはドキュメント通りに設定しており、NACLについては特に設定していませんでした。
セキュリティグループについては、VPCE、EC2のSGそれぞれで以下のとおりClientのCIDR範囲を許可していました。
■SG_VPCE

Port Source
22 192.168.0.0/16

■SG_EC2

Port Source
22 192.168.0.0/16

設定に問題がないか再確認しつつ、切り分けのためにいったんそれぞれのSGを0.0.0.0でフル解放してSSH接続してみました。
結果は接続が成功し、secureログで接続元のIPを確認すると、172.31.16.xxxとなっておりClientVPNのVPCEndpointのIPであることが分かりました。
Client VPNでの接続については、Client端末の接続がVPCEndpointのIPでNatされるようなので、Client端末のIPをSGで許可しても意味がありませんでした。

上記を踏まえて、以下のようにSGを設定しました。
■SG_VPCE

Port Source
22 192.168.0.0/16

■SG_EC2

Port Source
22 SG_VPCE

Client端末からインターネット接続できない

SSH接続ができない問題を対応中に、Client端末からインターネット接続をしようとしましたがドキュメントどおりの設定ではインターネット接続ができないようになっていました。
ルーティング設定かと考え、0.0.0.0/0を追加しましたが接続できませんでした。
もう少し調べたところ、承認設定に0.0.0.0/0を追加が必要になることがわかったので、設定したところVPN接続しながらインターネット接続ができるようになりました。

まとめ

AWS ClientVPNはサーバをきどうする必要もないため手軽に起動できますが、承認設定のような概念が分かりづらい設定もあり少し引っかかりました。トータルではサーバの面倒を見る必要がないという大きなアドバンテージがあるので積極的に活用していきたいと思います。

CO2濃度計をRaspberry Pi Zeroで作ってみた

車の運転中にCO2濃度が高くなると集中力が落ち眠くなりやすくなるそうです。
そこで、車でCO2濃度が測定できるようにRaspberry Piを使って車載用にCO2濃度計を作ることにしました。
まだやりたいことが全部できたわけではないですが、個人的に使うには十分なところまでできましたので作成方法について紹介したいと思います。
f:id:cloudfish:20190529193915j:plain

機能

最終的に実装した機能は以下になります。

  • 定期的にCO2濃度を測定し小型液晶に表示
  • CO2濃度が閾値を超えていたら警告音を鳴らす
  • webで濃度の推移グラフ(1分ごと、1時間ごと)を可視化する
  • CO2濃度のログをAWSのS3に送信(とりあえず蓄積するだけ)

構成

f:id:cloudfish:20190611155546p:plain

必要部品

部品 数量 用途 金額(目安)
Raspberry Pi Zero W 1   1300円
MH-Z19 1 CO2センサー 3800円
PiOled 1 表示用小型液晶 3800円
圧電スピーカ 1 アラーム用 100円

各部品の接続

接続図

f:id:cloudfish:20190611151425p:plain

上記接続の通り各部品を接続していきます。

CO2センサー(mh-z19)接続

CO2センサーを接続し動作確認を行います。

uartの有効化

/boot/config.txtに以下を追加してraspberry piを再起動する

enable_uart=1
モジュールのインストール
pip install getrpimodel
接続確認

以下のプログラムを適当な名前で保存し実行します。

import sys
import serial
import time
import subprocess
import getrpimodel
import datetime
from time import sleep 
import RPi.GPIO as GPIO

if getrpimodel.model() == "3 Model B":
  serial_dev = '/dev/ttyS0'
  stop_getty = 'sudo systemctl stop serial-getty@ttyS0.service'
  start_getty = 'sudo systemctl start serial-getty@ttyS0.service'
else:
  serial_dev = '/dev/ttyAMA0'
  stop_getty = 'sudo systemctl stop serial-getty@ttyAMA0.service'
  start_getty = 'sudo systemctl start serial-getty@ttyAMA0.service'

def mh_z19():
  ser = serial.Serial(serial_dev,
               baudrate=9600,
               bytesize=serial.EIGHTBITS,
               parity=serial.PARITY_NONE,
               stopbits=serial.STOPBITS_ONE,
               timeout=1.0)

  while 1:
    result=ser.write("\xff\x01\x86\x00\x00\x00\x00\x00\x79")
    s=ser.read(9)
    if len(s)!=0 and  s[0] == "\xff" and s[1] == "\x86":
      return {'co2': ord(s[2])*256 + ord(s[3])}
      break

def main():

   subprocess.call(stop_getty, stdout=subprocess.PIPE, shell=True)
   now = datetime.datetime.now()
   now_ymdhms = "{0:%Y/%m/%d %H:%M:%S}".format(now)

   # Get Data
   value = mh_z19()
   co2 = value["co2"]
   print('CO2:' + co2)

   subprocess.call(start_getty, stdout=subprocess.PIPE, shell=True)

if __name__ == '__main__':
   main()

CO2の値が取得されていれば正しく接続できています。
室内だと通常400〜1000ppm程度かと思いますが、異常な値が取得されていれば以下のサイトを参考に補正してみてください。
qiita.com

液晶(PiOled)接続

PiOledを接続後、以下の設定を行います。
手順はRaspberry Pi 3 Model Bに Adafruit PiOLED を接続 - Qiitaを参考にしました。

カーネルモジュール自動ロード設定

/boot/config.txtに以下を追加してraspberry piを再起動する

dtparam=i2c_arm=on
必要ライブラリのインストール
sudo apt-get install python-dev
sudo apt-get install python-imaging
sudo apt-get install libffi-dev
sudo pip install smbus-cffi
sudo pip install RPi.GPIO 
カーネルモジュールのロード
sudo modprobe i2c-dev
$ dmesg | grep i2c

ロードされていることの確認
cat /proc/devices | grep i2c
 89 i2c

/dev/i2c-1 の作成

sudo mknod /dev/i2c-1 c 89 1
$ ls /dev/i2c-1
/dev/i2c-1
PiOledの接続確認

以下のように出力されていれば正しく接続されています。

sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
サンプルの実行確認
git clone https://github.com/adafruit/Adafruit_Python_SSD1306.git

cd Adafruit_Python_SSD1306
sudo python ./setup.py build
sudo python ./setup.py install
python ./examples/stats.py &

液晶にIPアドレス、CPU、メモリ、ディスクが表示されていればOKです。

圧電スピーカ接続

接続後に以下のプログラムを実行してスピーカーから音が鳴ればOKです。

import RPi.GPIO as GPIO
import time

SOUNDER = 21

GPIO.setmode(GPIO.BCM)
GPIO.setup(SOUNDER, GPIO.OUT, initial = GPIO.LOW)

p = GPIO.PWM(SOUNDER, 6500)
p.start(50)
time.sleep(0.5)
p.stop()
time.sleep(0.5)

p.stop
GPIO.cleanup()

プログラムの配置

CO2濃度取得プログラムの配置

以下の通りプログラムを取得し適当なところに配置します。(ここでは/home/pi/program/配下に配置しました。)

git clone https://github.com/cloudfish7/co2_sensor.git

cronの設定
1分ごとと1時間ごとにデータを取得するように設定します。

*/1 * * * * sudo python /home/pi/program/co2_sensor/mh-z19.py minutes
0 * * * * sudo python /home/pi/program/co2_sensor/mh-z19.py hour

表示用Webプログラムの配置

以下の通りプログラムを取得し適当なところに配置します。(ここでは/home/pi/program/配下に配置しました。)
グラフ表示についてはOpen Source Image Charts Replacement | QuickChartを利用しているためraspberry piについてはインターネット接続されている必要があります。

git clone https://github.com/cloudfish7/co2_web.git

以下コマンドで実行します。(自動起動設定してないです)

python app_co2.py

ブラウザで以下アドレスにアクセスするとweb画面が表示されます。
IPアドレスは液晶にも表示されるようになっています。

http:ip_address:8080

車載にあたっての問題

車に積んだ際に電源が問題になります。
シガーソケットからUSBで電源を取得していた場合、エンジンを切ると電源供給が止まるためRaspberry Piが突然落ちることになります。そうするとSDカードへの書き込み途中の場合、ファイルが壊れてしまい最悪OSが起動しなくなる可能性があります。
組み込み系のデバイスの場合はROM化して書き込みを無くすようなので、Raspberry PiでもROM化できないか調べてみたところoverlayfsという仕組みを使うことでSDカードへの書き込みを制御できることが分かりました。
以下の記事に設定方法が詳しく書かれていましたが、設定後システムが不安定になる場合もあるようなので今回は諦めることにしました。
qiita.com
代わりにモバイルバッテリーを電源に使うことにしました。
少し面倒ですがとりあえずは当面これで使ってみたいと思います。OSのシャットダウンについてはWebの画面にシャットダウンボタンを付けて代用しようかと考えています。

まとめ

CO2濃度計を作ってから自宅、車と両方で使っています。
大気中のCO2濃度は約400ppmくらいなので、換気が行き届いている場合は同様の数値となりますが、窓を締めるとすぐに数値が上がっていきました。
実際に車で4人が乗り、内気循環にしていると30分程度で2000ppmを超える濃度となりました。2500ppmを超えるとパフォーマンスが落ち眠気を誘うとの研究もありますので、運転中は適度に換気したほうがよさそうですね。