amazon-ssm-agent のハードウェアフィンガープリントの仕組み
この記事は Calendar for Akerun | Advent Calendar 2021 - Qiita の 22日目の記事です。
どうもおなじみ daikw - Qiita です。
Amazon Systems Manager の動作を調べているとき、「ハードウェアフィンガープリント」という言葉で厨二病を悪化させてしまいました。
つい時間をかけて調べてしまったため、記事にまとめて懺悔します。
契機
弊チームでは、raspiマシンの複製や、バックアップからの復元をすることがよくあります。
Hybrid Activation 済のマシンの復元をしようとしている最中に、ふと 「Systems Manager は混乱しないのかな ...?」と疑問に思ったことがきっかけで調べ始めました。
調査
概ね 抽象 -> 具体 の流れで書いてあります。あたかもこの通り綺麗に調べたように見えますが、実際にはあっちに行ったりこっちに行ったりと迷いながらやっています。探索的な調査は割と好きな daikw です。
ドキュメント
まずは最も抽象的なドキュメントから当たって行きましょう。テクニカルリファレンスにそれっぽい記述がありました!
SSM Agent technical reference - AWS Systems Manager#fingerprint-validation より、
When running on-premises servers, edge devices, and virtual machines (VMs) in a hybrid environment, SSM Agent gathers a number of system attributes (referred to as the hardware hash) and uses these attributes to compute a fingerprint. The fingerprint is an opaque string that the agent passes to certain Systems Manager APIs. This unique fingerprint associates the caller with a particular on-premises managed node. The agent stores the fingerprint and hardware hash on the local disk in a location called the Vault.
If the current machine attributes don't match the stored hardware hash, SSM Agent computes a new fingerprint, ~. This causes
RequestManagedInstanceRoleToken
to fail, and the agent won't be able to obtain a role token to connect to the Systems Manager service. This failure is by design and is used as a verification step to prevent multiple on-premises managed nodes from communicating with the Systems Manager service as the same managed node.
内容をざっくりまとめると、
hardware hash
なるものを使い、サーバ固有のfingerprint
が計算されるhardware hash
とfingerprint
はVault
に保管されるfingerprint
がVault
の値と一致していれば、RequestManagedInstanceRoleToken
なるリクエストを成功として捌いてくれる
ということがわかります。ここでさらに次のような疑問が湧きます。
hardware hash
とfingerprint
の実体は何か?Vault
とはどういうものか?- 構成変更によって
fingerprint
はどの程度変わるのか?(similarityThreshold
はどうチューンされるのか)
フィンガープリント
フィンガープリント(fingerprint)とは - IT用語辞典 e-Words より、
フィンガープリントとは、指紋(を採る)、拇印という意味の英単語で、IT分野では人物や端末などの識別や同定、真正性の確認に用いられる短いデータ列などを指すことが多い。
です。
「マシンのフィンガープリントをする」というのは、「マシン同士の識別・同定が可能な情報を採取する」という意味になります。
静的解析
実装が公開されていたので、疑問の回答を見つけることができるかもしれません。 GitHub - aws/amazon-ssm-agent を見ると Go で開発されていました。じっくり読んでいきましょう。
まず、 vendor
と extra
は vendoring されたソースコードに見えるため、除外しながら調べます。ソースコードは主に agent
/ common
/ core
の三つのディレクトリに収められているようです。
雑に fingerprint
でgrepすると、 InstanceFingerprint
という関数が目につきました。
これは内部関数 generateFingerprint
にキャッシュとリソースロックの機能をつけるラッパー関数で、他モジュールに対し公開されています。
func InstanceFingerprint(log log.T) (string, error) { if isLoaded() { return fingerprint, nil } lock.Lock() defer lock.Unlock() var err error fingerprint, err = generateFingerprint(log) if err != nil { return "", err } loaded = true return fingerprint, nil }
また、同じファイルを眺めると、先頭に核心となる構造体 hwInfo
があるのが目につきます。この構造体の全てのフィールドが説明できるようになれば、当初の疑問の答えになりそうです。
type hwInfo struct { Fingerprint string `json:"fingerprint"` HardwareHash map[string]string `json:"hardwareHash"` SimilarityThreshold int `json:"similarityThreshold"` }
さて、 generateFingerprint
を詳しく読む前に、試しに参照元の一覧を追ってみましょう。
┬─[daiki~@photosyth~:~/g/g/a/amazon-ssm-agent]─[18:41:09]─[G:mainline=] ╰─>$ rg -g !extra -g !vendor InstanceFingerprint agent/managedInstances/registration/instance_info.go 80: return fingerprint.InstanceFingerprint(log) agent/managedInstances/fingerprint/fingerprint_test.go 33:func ExampleInstanceFingerprint() { 58: val, _ := InstanceFingerprint() agent/managedInstances/fingerprint/fingerprint.go 44: vaultKey = "InstanceFingerprint" 54:func InstanceFingerprint(log log.T) (string, error) { 178: _ = log.Warnf("Could not read InstanceFingerprint file: %v", err)
これと同様にコードジャンプやgrepで頑張って追っていくと、 amazon-ssm-agent
の main
まで辿っていけます。
-> `fingerprint.InstanceFingerprint` (`agent/managedInstances/fingerprint/fingerprint.go`) -> `onpremRegistation#Fingerprint` (`agent/managedInstances/registration/instance_info.go`) -> `onpremCredentialsProvider#Retrieve` (`agent/managedInstances/registration/instance_info.go`) *ここでやや複雑なことをしている -> `credentialsRefresher#retrieveCredsWithRetry` (`core/app/credentialrefresher/credentialrefresher.go`) -> `credentialsRefresher#credentialRefresherRoutine` (`core/app/credentialrefresher/credentialrefresher.go`) -> `credentialsRefresher#Start` (`core/app/credentialrefresher/credentialrefresher.go`) -> `SSMCoreAgent#Start` (`core/app/agent.go`) -> `start` (`core/agent.go`) -> `run` (`core/agent.go`) -> `main` (`core/agent_unix.go`)
agent/managedInstances/fingerprint/fingerprint.go
に戻りましょう。generateFingerprint
の骨子を抜き出して抜粋すると以下のようになります。
func generateFingerprint(log log.T) (string, error) { var hardwareHash map[string]string var savedHwInfo hwInfo var err error var hwHashErr error for attempt := 1; attempt <= 3; attempt++ { hardwareHash, hwHashErr = currentHwHash() savedHwInfo, err = fetch(log) <omit> } <omit> uuid.SwitchFormat(uuid.CleanHyphen) new_fingerprint := "" if !hasFingerprint(savedHwInfo) { new_fingerprint = uuid.NewV4().String() } else if !isSimilarHardwareHash(log, savedHwInfo.HardwareHash, hardwareHash, savedHwInfo.SimilarityThreshold) { new_fingerprint = uuid.NewV4().String() } else { return savedHwInfo.Fingerprint, nil } updatedHwInfo := hwInfo{ Fingerprint: new_fingerprint, HardwareHash: hardwareHash, SimilarityThreshold: savedHwInfo.SimilarityThreshold, } // save content in vault if err = save(updatedHwInfo); err != nil { log.Errorf("Error while saving fingerprint data from vault: %s", err) } return new_fingerprint, err }
ここで fingerprint
の正体がわかりました。ただの uuid
です。
hardwareHash
と fingerprint
は生成タイミングが同じであること以外に共有する情報はありません。
確かに考えてみると、fingerprint
を再計算して、一致するかを調べる必要は特になく(hardwareHash
の比較で十分である)、これらの生成タイミングが同じであるのを保証できれば良いはずです。わかりやすい。
さらに掘っていきます。 hwInfo
のうち、HardwareHash
が何者なのかわかっていません。
agent/managedInstances/fingerprint/hardwareInfo_unix.go
に実装があります。
const ( systemDMachineIDPath = "/etc/machine-id" upstartMachineIDPath = "/var/lib/dbus/machine-id" dmidecodeCommand = "/usr/sbin/dmidecode" hardwareID = "machine-id" ) var currentHwHash = func() (map[string]string, error) { hardwareHash := make(map[string]string) hardwareHash[hardwareID], _ = machineID() hardwareHash["processor-hash"], _ = processorInfoHash() hardwareHash["memory-hash"], _ = memoryInfoHash() hardwareHash["bios-hash"], _ = biosInfoHash() hardwareHash["system-hash"], _ = systemInfoHash() hardwareHash["hostname-info"], _ = hostnameInfo() hardwareHash[ipAddressID], _ = primaryIpInfo() hardwareHash["macaddr-info"], _ = macAddrInfo() hardwareHash["disk-info"], _ = diskInfoHash() return hardwareHash, nil } func processorInfoHash() (value string, err error) { value, _, err = commandOutputHash(dmidecodeCommand, "-t", "processor") return } <omit>
func commandOutputHash(command string, params ...string) (encodedValue string, value string, err error) { var contentBytes []byte if contentBytes, err = exec.Command(command, params...).Output(); err == nil { value = string(contentBytes) // without encoding sum := md5.Sum(contentBytes) encodedValue = base64.StdEncoding.EncodeToString(sum[:]) } return }
実装から、以下のようなことがわかります。
- 特定のコマンドを実行したり、特定のファイルの値を持ってきた結果の
md5.Sum
がhardware hash
の値となる currentHwHash
のhash
は、 ハッシュマップ とも チェックサム とも取れる/usr/sbin/dmidecode
を主に使っていて、/etc/machine-id
といったファイルも使う(ちなみにhardwareinfo_windows.go
の場合はwmic.exe
)
dmidecode
dmidecode
は SMBIOS - Wikipedia を取得するための *nix 系OSに提供されるユーティリティです。
適当な EC2 サーバで叩くとこんな感じの出力がとれます。
[ec2-user@ip-172-31-47-131 ~]$ sudo dmidecode -t dmidecode: option requires an argument -- 't' Type number or keyword expected Valid type keywords are: bios system baseboard chassis processor memory cache connector slot [ec2-user@ip-172-31-47-131 ~]$ sudo dmidecode -t processor # dmidecode 3.2 Getting SMBIOS data from sysfs. SMBIOS 2.7 present. Handle 0x0401, DMI type 4, 35 bytes Processor Information Socket Designation: CPU 1 Type: Central Processor Family: Other Manufacturer: Intel ID: F2 06 03 00 FF FB 8B 17 Version: Not Specified Voltage: Unknown External Clock: Unknown Max Speed: 2394 MHz Current Speed: 2394 MHz Status: Populated, Enabled Upgrade: Other L1 Cache Handle: Not Provided L2 Cache Handle: Not Provided L3 Cache Handle: Not Provided Serial Number: Not Specified Asset Tag: Not Specified Part Number: Not Specified
試しに raspi で叩いてみたら、うまく動作しません。
dmidecode - Raspberry Pi Forums に、 It is not available on non-x86 architectures.
との記述がありました。
pi@raspberrypi:~ $ sudo dmidecode -t processor # dmidecode 3.3 # No SMBIOS nor DMI entry point found, sorry.
似たような情報を得るには、/proc
や /sys
の情報をうまく利用する必要があるようです *1。
pi@raspberrypi:~ $ cat /proc/cpuinfo | grep Hardware -A10 Hardware : BCM2835 Revision : a020d3 Serial : 000000009cc7694f Model : Raspberry Pi 3 Model B Plus Rev 1.3
複製されたraspiマシン同士の区別
複製されたraspiが区別されることは 以前の調査 でわかっています。
従って、 dmidecode
が使えないとなると、raspiの区別をしている部分は別にあるようです。agent/managedInstances/fingerprint/hardwareInfo_unix.go
をよく読むと、
func diskInfoHash() (value string, err error) { value, _, err = commandOutputHash("ls", "-l", "/dev/disk/by-uuid") return }
がありました。これだ!
pi@raspberrypi:~ $ ls -l /dev/disk/by-uuid total 0 lrwxrwxrwx 1 root root 15 Dec 20 01:17 568caafd-bab1-46cb-921b-cd257b61f505 -> ../../mmcblk0p2 lrwxrwxrwx 1 root root 15 Dec 20 01:17 C839-E506 -> ../../mmcblk0p1 <omit> pi@raspberrypi-2:~ $ ls -l /dev/disk/by-uuid total 0 lrwxrwxrwx 1 root root 15 Dec 8 06:17 7616-4FD8 -> ../../mmcblk0p1 lrwxrwxrwx 1 root root 15 Dec 8 06:17 87b585d1-84c3-486a-8f3d-77cf16f84f30 -> ../../mmcblk0p2
複製を作る場合、ほぼ確実にディスクを変更することになるので、この出力が変わって区別されるのだと考えられます。
Vault
vaultの意味・使い方・読み方 | Weblio英和辞書 より、
アーチ形天井、アーチ形天井のようなおおい、(食料品・酒類などの)地下貯蔵室、(地下)金庫室、(銀行などの)貴重品保管室、(教会・墓所の)地下納骨所
金庫室という意味があるので、暗号化や鍵のかかったイメージを持ってソースコードを読んでみましょう。agent/managedInstances/vault/fsvault/fsvault.go
に実装があります。
func Store(key string, data []byte) (err error) { lock.Lock() defer lock.Unlock() if err = ensureInitialized(); err != nil { return } p := filepath.Join(storeFolderPath, key) if err = fs.HardenedWriteFile(p, []byte(data)); err != nil { return fmt.Errorf("Failed to write data file for %s. %v\n", key, err) } manifest[key] = p if err = saveManifest(); err != nil { delete(manifest, key) return fmt.Errorf("Failed to save manifest when storing %s. %v\n", key, err) } return }
fs.HardenedWriteFile
がそれらしい名前ですね。さらに追ってみましょう。 agent/fileutil/harden.go
を見ると
const ( RWPermission = 0600 ) // HardenedWriteFile calls ioutil.WriteFile and guarantees a hardened permission // control. If the file already exists, it hardens the permissions before // writing data to it. func HardenedWriteFile(filename string, data []byte) (err error) { if _, err = os.Stat(filename); err != nil { if os.IsNotExist(err) { f, err := os.Create(filename) if err != nil { return fmt.Errorf("Failed to create the file, %s", err) } defer f.Close() } else { return } } if err = Harden(filename); err != nil { return } if err = ioutil.WriteFile(filename, data, RWPermission); err != nil { return } return }
これで harden
の意味がわかりました。ファイルパーミッションを適切に設定すること(ここでは 600
)ですね。
saveManifest
の方も追ってみましたが、 json.Marshal
でシリアライズしているだけでした。
つまり Vault
は、暗号化なしの、パーミッション 600
が維持されるただのファイルである、ということがわかりました。
Similarity
最後に、 isSimilarHardwareHash
を、ログやコメントを一部抜粋して引用します。
func isSimilarHardwareHash(log log.T, savedHwHash map[string]string, currentHwHash map[string]string, threshold int) bool { var totalCount, successCount int isSimilar := true // similarity check is disabled when threshold is set to -1 if threshold == -1 { return true } // check input if len(savedHwHash) == 0 || len(currentHwHash) == 0 { return false } // check whether hardwareId (uuid/machineid) has changed // this usually happens during provisioning if currentHwHash[hardwareID] != savedHwHash[hardwareID] { isSimilar = false } else { // check whether ipaddress is the same - if the machine key and the IP address have not changed, it's the same instance. if currentHwHash[ipAddressID] == savedHwHash[ipAddressID] { } else { // identify number of successful matches for key, currValue := range currentHwHash { if prevValue, ok := savedHwHash[key]; ok && currValue == prevValue { successCount++ } } // check if the changed match exceeds the minimum match percent totalCount = len(currentHwHash) if float32(successCount)/float32(totalCount)*100 < float32(threshold) { isSimilar = false } } } return isSimilar }
ロジックはドキュメントそのままでしたが、以下の点が新たにわかりました。
threshold
は -1 ~ 100 の 整数であることhash
を取得する項目の総数に占める、一致したhash
の数の割合を一致率 (similarity
) としていること
この similarityThreshold
を設定できる機能は、ソースを読まないと使うの難しそうですね。ただ、使うことは滅多になさそうです。
まとめ
冒頭の疑問に回答する形でまとめます。
hardware hash
とfingerprint
の実体は、/usr/sbin/dmidecode
等の出力のチェックサムである。Vault
は暗号化されていない、パーミッション 600 のファイルである。- 構成変更による
similarity
の差の出方は、アーキテクチャやOSによってかなり異なる。similarityThreshold
のチューンは、結構使い込んでみないと難しいと予想される。
nmap
*2 や hping
*3で使われるような、TCP の実装差を利用した特殊な fingerprinting はなくて、割と単純な仕組みになっていることがわかりました。
あとがき
windows と linux のビルドスイッチが見つけられなかったです。悲しい。Golang力が足りませんでした。
参考リンク
株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co
Akerun Proの購入はこちらから akerun.com
iPhone X RasPiで外出先で使える高機能なモバイルルーターを自作する
この記事は Akerun Advent Calendar 2021 - Qiita の20日目の記事です。
ハードウェアグループの石井 @ishturk です
弊社フォトシンスが提供しているAkerun入退室管理システムは
- オフィス
- プライベートサロン
- シェアスペース
- 無人のフィットネスジム
- etc...
と、いろいろなところで使われています。安定したサービスを提供するべく開発部・カスタマーサクセス部で協力しています。
顧客から問い合わせがあった場合、設置先に出向いて調査することもしばしば発生します。
そんなときに使える便利ネットワークデバイスがあったらいいのにな ((=^♀^=))
こんなこといいな
( ³з³):PCをインターネットに繋げられて、設置している弊社機器に同一ネットワークになるといいな
( ³з³):あ、ネットワークはWi-Fiと有線どっちも
( ³з³):会社の携帯がiPhoneだからiPhoneのテザリングがいい
( ³з³):電源とれないこともあるので電池じゃないと
( ³з³):客先に行くのはエンジニアじゃないこともあるから、スクリプト仕込めるといい
レシピ
- ラズパイ(最近全然買えないので、眠っているストックを。。。) Raspberry Pi 3 Model B+ | Raspberry Pi 3 B+ | RS Components
- SDカード(適当に)
- モバイルバッテリー(10000mAh程度あれば3〜5時間くらいつかえます) cheero Power Plus 3 13400mAh | 《安全安心》cheero(チーロ)モバイルバッテリー
- 電源付きUSBケーブル(RasPi電源のおすすめ。今回はケーブル部分のみ使います) Raspberry Pi用電源セット(5V 3.0A)-Pi3フル負荷検証済
- ランプ・スイッチ(RasPiのシャットダウンに使います) ainex アイネックス KM-01 [実験用スイッチ・LEDセット] https://www.yodobashi.com/product/100000001000605327/
ラズパイケース(お好みのものを)
iPhone・Lightningケーブル(手持ちを)
できたらいいな
RasPiのiPhoneテザリング
Wi-FiはPCからの接続に使用したいので、USBテザリングを選択します。 Apple公式には iTunes が必要と書いています。が、ありがたいことにLinux向けにパッケージ開発されているので、それを使用します。
sudo apt-get install ipheth-utils libimobiledevice-utils ifuse usbmuxd sudo mkdir /media/iphone sudo ifuse /media/iphone
何度か抜き挿ししているとiPhone側に 「信頼する」のポップアップが出てくるので選択
eth1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 172.20.10.3 netmask 255.255.255.240 broadcast 172.20.10.15 inet6 fe80::b3a4:f864:f44a:cbe prefixlen 64 scopeid 0x20<link>
eth1 として認識されました!
WiFi設定・ルーティング設定
hostapd と dnsmasq をインストール
sudo apt install hostapd dnsmasq
ポートフォワーディング、IPマスカレードなどを設定 このあたりは情報充実しているのでそのあたりを参照ください 有線とWiFiをどちらも使うためにハマりました。その記事はまたどこかで、、、、
また、ルーターとして機能させてるのであれば OpenWrtという選択も良いと思います。RaspberrPi向けにもイメージが配布されており、GUIで設定するだけでネットワークルーターを立ち上げることができます。
[OpenWrt Wiki] Welcome to the OpenWrt Project
今回は、サポート作業用のスクリプトを実行したいという事情があったため、より汎用的なRaspberryPiOSを採用しました。
シャットダウン機能
RaspberryPiは物理スイッチがついておらず、通電で起動しますが、シャットダウンする機能はハードウェア提供されていません。
電源ぶちぎりでもよいのですが、ここはちゃんと作りましょう。
今回はスイッチとパイロットランプで実現します。LEDは5Vそのままではとても眩しかったのでPWMで輝度を落としてます。 スイッチは内部プルアップでアクティブロー(スイッチ押下で0)にしてます。
#!/usr/bin/env python3 import RPi.GPIO as GPIO import time import os from multiprocessing import Process, Condition gpio_led = 18 gpio_sw = 24 GPIO.setmode(GPIO.BCM) # LEDピンを出力に設定 GPIO.setup(gpio_led, GPIO.OUT) GPIO.setup(gpio_sw, GPIO.IN, pull_up_down=GPIO.PUD_UP) c = Condition() def led(): pin = GPIO.PWM(gpio_led, 2000) pin.start(0) pin.ChangeDutyCycle(5) c.acquire() c.wait() c.release() def sw(): while True: sw = GPIO.input(gpio_sw) if sw == 0 : c.acquire() c.notify() c.release() break time.sleep(1) if __name__ == '__main__': p1 = Process(target=led) p2 = Process(target=sw) p1.start() p2.start() p1.join() p2.join() GPIO.cleanup(gpio_led) GPIO.cleanup(gpio_sw) os.system( "sudo shutdown -h now" ) time.sleep(1000)
完成
LEDとスイッチがぷらんぷらんしているので、このままではまだ持っていくのは難しいですね。 社内に3Dプリンタ(form3)が2台あるので、いい感じにケースを自作しても良さそうです。
株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co
Akerun Proの購入はこちらから akerun.com
やっすいデバッガでnRF52のデバッグしてみる (Jeff Probe編)
この記事は Akerun Advent Calendar 2021 - Qiita の18日目の記事です。
こんにちは。 いとう です。ファームウェアやってます。
普段フォトシンスのお仕事では Seggerの J-Link を使っているのですが、個人の趣味プロジェクトで使用しようとすると少々お高いのでFlirc社のBlackMagic ProbeクローンのJeff ProbeをAmazonで¥1800だったの妻に内緒でブラックフライデーセールのどさくさに紛れて購入してみました。
https://flirc.tv/more/flirc-jeff-probe-bmp-jtag-black-magic-probe
https://www.amazon.co.jp/dp/B07ZK4LFTQ
可愛い小箱で届きます。内容物は Jeff Probe本体と お馴染みのSWDデバッグ用の10ピンリボンケーブル、UART接続に使えるプローブです。USB mini-B ケーブルは付属しません。
ものすごくコンパクトです。基板剥き出しなので取扱注意。
セットアップ
ハードウェア
今回は Nordic semiconductor のnRF52832搭載の nRF52 dkボードを使用しました。
書き込むファームウェア
とりあえずのテストなので Zephyr のサンプルに添付されている Lチカ "blinky" を使ってみました。Zephyr環境で dkボード用にビルドします
west build -b nrf52dk_nrf52832 zephyr/samples/basic/blinky
デバッガインストール
デバッガにはgdbを使用していますが arm用をARM社が用意してくれていますのでこちらをダウンロード。ホストはmacOSを使用しました。
接続
おもむろにJeff ProbeをmacにUSBケーブルで接続してみましょう。うまく接続できていると /dev/
にttyデバイスとしてマウントされます。
デバイス名はシリアル番号によって変わるようで私の環境では下記のようなデバイスとして見えていました。 同時にusbmodemが二つありますが数値が若い方を指定するとデバッガとして使用できています。
$ ls /dev/ | grep tty.usbmodem tty.usbmodem34C775A71 tty.usbmodem34C775A73
接続確認
デバッガを立ち上げて
arm-none-eabi-gdb
繋がっているか確認してみましょう。先程のttyデバイスを指定します
(gdb) target extended-remote /dev/tty.usbmodem34C775A71
下記のコマンドでJeff Probe経由で電源供給ができます。Lチカへのファームウェア書き換えだけであればdkボードに別途電源供給不要でした。
(gdb) monitor tpwr enable
接続してます。
(gdb) monitor swdp_scan Target voltage: 3.2V Available Targets: No. Att Driver 1 ARM Cortex-M (gdb) attach 1 Attaching to Remote target warning: No executable has been specified and target does not support determining executable automatically. Try using the "file" command. 0x0000176a in ?? ()
J-Linkでは書き換えるROMがイレースされてなければ自動的にイレースしてくれていたんですがgdbはそこまではしてくれないようで毎回 monitor erase_mass
してからリセットしないと次のファームウェアは書き込めませんでした。
(gdb) monitor erase_mass erase.. (gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: warning: No executable has been specified and target does not support determining executable automatically. Try using the "file" command. Program received signal SIGSEGV, Segmentation fault. 0x00000000 in ?? ()
loadコマンドでファイルを読み込んでみましょう。
(gdb) load build/blinky/zephyr/zephyr.hex Loading section .sec1, size 0x43c8 lma 0x0 Start address 0x1704, load size 17352 Transfer rate: 19 KB/sec, 964 bytes/write. (gdb) run The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: warning: No executable has been specified and target does not support determining executable automatically. Try using the "file" command.
無事にLチカできています。
あとは普通に使えると思うのですが慣れていないとcliは辛いので今回はvscode上からデバッガとして使えるかを確認してみます。
vscodeでのデバッグ
Cortex-Debug
こちらのcoretex-debugを使用するとvscodeをからJ-Linkや他のデバッガを用いてIDEのようにデバッグができます。
https://marketplace.visualstudio.com/items?itemName=marus25.cortex-debug
Cortex-Debug 設定
Jeff ProbeはBlackMagic Probeから派生している製品ですのでservertype
にbmp
を使用するとCoretex-Debugから使用できます。あとは毎回erase_mass
を実行するようにoverrideLaunchCommands
を使用してデバッガ起動時の処理をオーバーライドしています。
{ "version": "0.2.0", "configurations": [ { "name": "JeffProbeDebug", "cwd": "${workspaceRoot}", "executable": "build/button/zephyr/zephyr.elf", "request": "launch", "type": "cortex-debug", "servertype": "bmp", "device": "nrf52", "BMPGDBSerialPort": "/dev/tty.usbmodem34C775A71", "targetId": 1, "powerOverBMP": "disable", "showDevDebugOutput": true, "interface": "swd", "armToolchainPath": "/usr/local/bin/", "breakAfterReset": false, "overrideLaunchCommands": [ "monitor erase_mass", "SoftwareReset", "load", "SoftwareReset" ], "postLaunchCommands": [ "set substitute-path /workspaces/ak /Users/yuito/repo/ak" ] } ] }
postLaunchCommands
はビルドをDockerコンテナの中で実行しているのでパスを差し替えるために行っています。
なぜかファームウェアをダウンロードするたびにProbeの抜き差しをしないといけませんでしたが、これでステップ実行とかブレイクポイントとかも動く立派なデバッグ環境が整えられました。
まとめ
他にも中華系偽J-Link、CMSIS-DAPなどありますがお値段と気軽にAmazonで買えるのでJeffProbe、個人のプロジェクトにはいいのではないでしょうか?
(番外)Jeff Probeのアップデート
手元に届いた時にはv1.6.2だったのですがgithub上で最新バージョンのビルド済みファームウェアバイナリが配布されていたのでアップデートしました。
https://github.com/flirc/blackmagic/releases/tag/v1.6.3
brew install dfu-util
dfu-util --device 1d50:6017 -s 0x00002000:leave -D ./blackmagic.bin
GDB経由でバージョンを確認してみます。
(gdb) monitor version Black Magic Probe (Firmware v1.6.3) (Hardware Version 0) Copyright (C) 2015 Black Sphere Technologies Ltd. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
:+1:
株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co
Akerun Proの購入はこちらから akerun.com
AWS Systems Managerを使ってraspiマシンを運用する時の注意点
この記事は Calendar for Akerun | Advent Calendar 2021 - Qiita の 19日目の記事です。
今年の初めに AWS Solution Architect Associate
を取得した daikw - Qiita です 🎉
今日は AWS Systems Manager
(SSM
) に関する話をします。
はじめに
SSM で解決したいこと
クラウド側で全てprovisionしてくれる仮想マシンとは別に、オンプレマシン も 扱いたい、というのは、実は結構よくある要求ではないでしょうか。
弊社のFW開発チームも、raspiをあちこちで使っていて、正直管理が辛いところがあります。
クラウド・オンプレどちらもできるだけ同じように扱うことができると、管理コストが減ってとてもハッピーになれそうです。
ところで、 AWS Systems Manager(運用時の洞察を改善し、実行)| AWS によれば、
AWS Systems Manager は、ハイブリッドクラウド環境のための安全なエンドツーエンドの管理ソリューションです。
とあります。もしや、これを使ったらハッピーになれるのでは???
raspi x SSM
クラスメソッドさんの記事で、 raspi x SSM カテゴリのものがかなり豊富にありました。
- Amazon EC2 Systems ManagerがRaspbian OSに対応したのでRaspberry Piにインストールしてみた | DevelopersIO
- AWS Systems ManagerをRaspberry Piで使用してみた | DevelopersIO
- [AWS Systems Manager] アクティベーションの終わったRaspberryPiのイメージをコピーして、複数起動した時の挙動を確認してみました | DevelopersIO
- 【小ネタ】1台のマネージドインスタンスを踏み台にして、多数のRasPiに選択メニューからsshするシェルを作ってみました | DevelopersIO
- 【小ネタ】 AWS Systems Manager のマネージドインスタンス(RaspberryPi)を踏み台にして、Windows10にリモートデスクトップで接続してみました。 | DevelopersIO
- 【小ネタ】 AWS Systems Managerに登録した多数のRaspberryPiに識別しやすいように名前をつける | DevelopersIO
本記事では、これらを参考に運用を試みた際に感じた、導入にあたって注意すべき点をまとめておきます。 また、記事の後半に用語リストをまとめてあります。参考までに。
運用して気がついたこと
セッションログは、セッションの開始方法によっては残せない
AWS Console の Session Manager を経由した SSHセッションログを、S3 や Cloudwatch logs に流すことができます。
AWS Systems Manager Session Managerのシェル操作をログ出力する | DevelopersIO
しかし、 aws ssm start-session
コマンドによるログインセッションのログは同じログストリームに流すことができません。
不便といえば不便ですが、当たり前といえば当たり前(接続元と接続先で鍵交換するので、session-manager は復号できない、タブン)で、ややもどかしくもあります。
Quick Setup を組み合わせる場合、セッションログを取得するために小細工が必要
Quick Setup は基本 EC2 インスタンスに対して行うものですが、ターゲットに Managed Instance を選択することもできます。ハイブリッド環境もまとめて扱えるのが Systems Manager のイイトコロなので、まとめて選択してしまうこともあるでしょう。
デフォルトでは、Hybrid Activationの際に AmazonEC2RunCommandRoleForManagedInstances
が Managed Instance にアタッチされます。これは Identity Provider が ssm
になっています。
Quick Setupの後に、それらしい Role (AmazonSSMRoleForInstancesQuickSetup
)が作成されるのですが、これは Managed Instance に自動でアタッチされません。さらにこちらの Identity Provider は ec2
になっています。
以下のポリシーを含む ssm-managed-instance
role を作成して、
CloudWatchAgentServerPolicy
AmazonSSMManagedInstanceCore
AmazonSSMPatchAssociation
ssm-managed-instance
を Fleet Manager (AWS Systems Manager - Managed Instances) から 対象の Managed Instance にアタッチし、さらにこの際、RoleのIdentity Provider を ssm
にすることで、cloudwatch logs に ssm session のログが流せるようになります。
この辺りは IAM Role, Policy の構成をよく調べないとうまく使うことができなそうです。来れ、aws猛者たちよ。
ssm-user の権限が広い
Session Manager を利用する場合、特に aws-console 上からのログインを許可する場合は、デフォルトで ssm-user
が利用されます。
この ssm-user
は sudoers
なので、Session Manager を下手に許可してしまうと 最小権限の原則 - Wikipedia に簡単に反します。
「Session Manager を利用するのは、特権管理者である」と想定されているのかと思います。
チームで Session Manager を共有して使う場合は、コンプラという言葉が飛び交う現代IT産業界の皆様が口から泡を吹いて倒れることのないように、 ssm-user
を sudoers から除去しておくのが無難です。
ついでに pi ユーザの nopasswd
も除去しておきましょう。
sudo su cd /etc/sudoers.d echo "#User rules for ssm-user" > ssm-agent-users rm 010_pi-nopasswd
参考:
- [小ネタ]新機能Session Managerで使うssm-userの権限が気になった話 | DevelopersIO
- ステップ 7: (オプション) ssm-user アカウントの管理アクセス許可を有効または無効にする - AWS Systems Manager
インスタンスのクローンを作るときは、再度アクティベーションする
Raspberry Pi で、既にあるマシンと同等のマシンを作りたい(複製する)場合は、 MicroSDカードからイメージを dd
コマンド等で吸い出し、別のカードに書き込んで用意することがあります。
アクティベート済みのマシンを複製する場合、 ssm-agent
によるハードウェアフィンガープリント に引っかかるため、そのままでは使えないであろうことが予想できました。
自分でも挙動を調べたんですが、クラスメソッドさんの記事が僕よりよくまとまっていました。
[AWS Systems Manager] アクティベーションの終わったRaspberryPiのイメージをコピーして、複数起動した時の挙動を確認してみました | DevelopersIO より引用すると、
アクティベーションが済んだイメージを複製する場合、 - 1台だけ起動する場合、どちらでも接続可能 - 複数台起動した場合、後から起動したものが接続される - 複数台起動した場合、リブートすると、どちらに接続されるかは不定 - 複数台起動した場合、セッションマネージャから再アクティベートすると別のインスタンスとして管理される
session-manager を利用できるIAM Roleの設定
セッション開始には、"ssm:StartSession"
アクションの許可が必要になります。
例えば、 Session Manager の追加サンプル IAM ポリシー - AWS Systems Manager から抜粋すると、
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ssm:StartSession" ], "Resource": [ "arn:aws:ec2:us-east-2:123456789012:instance/i-1234567890EXAMPLE", "arn:aws:ec2:us-east-2:123456789012:instance/i-abcdefghijEXAMPLE", "arn:aws:ec2:us-east-2:123456789012:instance/i-0e9d8c7b6aEXAMPLE" ] }, { "Effect": "Allow", "Action": [ "ssm:TerminateSession", "ssm:ResumeSession" ], "Resource": [ "arn:aws:ssm:*:*:session/${aws:username}-*" ] } ] }
.ssh/config を適切に設定する
いろいろと試した結果、以下2点わかっています。
AWS-StartSSHSession
document は、ProxyCommand
を使わないと動作しないaws ssm
コマンドを利用する場合、2段以上のProxyCommand
チェーンは効かない
1 は、ターミナルにコマンド直打ちする時以外は特に困らないですが、 Protocol mismatch
となります。
┬─[daiki~@photosyth~:~]─[15:23:10] ╰─>$ aws ssm start-session --target mi-xxxxxxxxx --document-name AWS-StartSSHSession --parameters 'portNumber=22222' Starting session with SessionId: daiki.watanabe@photosynth.co.jp-0b95bd5ed82475bae SSH-2.0-OpenSSH_7.9p1 Raspbian-10+deb10u2+rpt1 ls Protocol mismatch. Exiting session with sessionId: daiki.watanabe@photosynth.co.jp-0b95bd5ed82475bae.
2 は少しだけ困っていて、以下のような記法でのログインに失敗します。
# SSH over Session Manager host i-* mi-* ProxyCommand sh -c "aws ssm start-session --profile akerun --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'" Host target User pi ProxyCommand ssh mi-xxxxxxxxx -l pi -p 22222
┬─[daiki~@photosyth~:~]─[10:52:12] ╰─>$ ssh target Pseudo-terminal will not be allocated because stdin is not a terminal. daikiwaranabe@mi-01630ac873d5fe084: Permission denied (publickey). kex_exchange_identification: Connection closed by remote host
config の設定をもっと頑張ればいけるかもしれません。ホストが増えないうちは特に困らないので、緩めのマサカリが来ることを期待しつつ。
その他参考記事
- AWS Systems Manager セッションマネージャーでSSH・SCPできるようになりました | DevelopersIO
- AWS System Managerセッションマネージャーでポートフォワードする | DevelopersIO
- send-ssh-public-key と ssm start-session の合わせ技 | 1Q77
- SSM Session Manager 経由での SSH | 1Q77
用語リスト
SSM固有の用語が多く、概念になれるのに少し時間がかかるので、先んじてまとめておきます。
Hybrid Activation
用意したマシンを Systems Manager に登録する作業のことを、ハイブリッドアクティベーションと呼びます。
ステップ 4: ハイブリッド環境のマネージドインスタンスのアクティベーションを作成する - AWS Systems Manager より、
ハイブリッド環境でサーバーと仮想マシン (VM) をマネージドインスタンスとして設定するには、マネージドインスタンスのアクティベーションを作成する必要があります。アクティベーションが完了するとすぐに、アクティベーションコードとアクティベーション ID が送信されます。ハイブリッド環境でサーバーと VM に AWS Systems Manager SSM Agent をインストールするときに、このコードと ID の組み合わせを指定します。このコードと ID は、マネージドインスタンスから Systems Manager サービスへの安全なアクセスを提供します。
つまり、以下を実行するだけ。
sudo service amazon-ssm-agent stop sudo amazon-ssm-agent -register -code $activation_code -id $activation_id -region $region sudo service amazon-ssm-agent start
Managed Instance
Systems Manager で管理下にある、自分で用意したマシンのことを言います。オンプレミスサーバであることもあるし、何か別の形態でプロビジョンされたサーバであることもあり得ます。
SSM Agent
エージェント(agent)とは - IT用語辞典 e-Words によれば、
エージェントとは、代理人、代理店、仲介人、取次業者、などの意味を持つ英単語。ITの分野では、利用者や他のシステムの代理として働いたり、複数の要素の間で仲介役として機能するソフトウェアやシステムなどを指すことが多い。
SSM Agent は ManagedInstance に常駐し、Systems Manager にインスタンスの状態を渡したり、Systems Managerからのリクエストを処理する機能を持ちます。 SSM Agent の使用 - AWS Systems Manager によれば、
エージェントは、 AWS クラウド で Systems Manager サービスからのリクエストを処理し、リクエストに指定されたとおりに実行します。SSM Agent は、Amazon Message Delivery Service (サービスプレフィックス: ec2messages) を使用して、Systems Manager サービスにステータスと実行情報を返します。
Session Manager
Systems Manager
の一つの機能で、対象ホストとのSSHログインセッションを 直接 貼ることができるようになります。
AWS Systems Manager Session Manager - AWS Systems Manager より *1、
Session Manager はフルマネージド型 AWS Systems Manager 機能で、インタラクティブなワンクリックブラウザベースのシェルや AWS Command Line Interface (AWS CLI)を介して Amazon Elastic Compute Cloud (Amazon EC2) インスタンス、オンプレミスインスタンス、および仮想マシン (VM) を管理できます。Session Manager を使用すると、インバウンドポートを開いたり、踏み台ホストを維持したり、SSH キーを管理したりすることなく、監査可能なインスタンスを安全に管理できます。
株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co
Akerun Proの購入はこちらから akerun.com
パフォーマンスチューニング: 手を動かす前に考える
この記事は Akerun Advent Calendar 2021 - Qiita の16日目の記事です。
こんにちは。@ps-tsh です。API Server などバックエンドシステムの開発を担当しています。
フォトシンスも創業から8年目に突入し、サービスを安定稼働させるためにパフォーマンスチューニングを行う機会も増えてきました。開発当時はしっかりテストしてリリースしたプロダクトであっても、運用が長期化すると「ファーストビューは速いが、ページングするとだんだん遅くなっていく」「1分で終了していたバッチ処理に15分かかるようになってきた」といった課題が発生します。特に、ビジネス面がうまくいくほどユーザ数や履歴データの蓄積ペースも上がるので、システムにかかる負荷は急激に増大します。
そんなわけで今回は、パフォーマンスチューニングの話、とくに「コードを直す前」の話をしようと思います。
取り組む前に
パフォーマンスチューニングに取り組んだエンジニアに話を聞いてみると、よく「N+1が発生していたので解消しました」「インデックスを貼って検索を速くしました」といった回答が返ってきます。決して間違いではないのですが、ときどき「ちょっと手段の実践に傾倒しているな」と思うことがあります。手段の実践が先行すると、原因がわからない問題を調べるときにも「N+1がないか確認します」「インデックスが貼られているか確認します」のように、手段を軸とした調査になってしまい、当てが外れた時に打ち手がなくなってしまいます。つまり、手持ちの道具が限られていることで、対応できる課題の幅も狭まってしまうという罠に陥るのです。パフォーマンス問題を解決する方法は、DBクエリのチューニングに限られません。
チューニングで行き詰まったら、まずは一歩引いて問題を俯瞰してみましょう。いちど席を立って深呼吸し、コーヒーを淹れて飲んだりするのも良いかもしれません。
性能目標を確認する
チューニングに取り組むにあたり、最初にやることは「性能目標を確認する」です。これ、意外とみんなやっていないんですよね。ひとくちに「遅い」「重い」というのは簡単ですが、よく話を聞いてみたら「処理データ件数を考えると妥当な水準」「決して早くはないが、特段の実害もない」というケースも結構あります。あるいは、もともと性能目標が定められていないケースもあると思います。そんな場合でも「どれくらいになればひとまずOKといえるのか」を明確にしましょう。現在地と目的地があって初めて、取れる手段を適切に選べるのです。なお、性能目標はレスポンスタイムやスループットだけではありません。たとえば「表面上は性能に問題がなくても、裏ではCPU利用率が100%に張り付いてしまうことがあり、常時50%以下に押さえたい」といった状況の解消が目標になることもあります。
最初に目標を確認するのには、もう一つ理由があります。「チューニングは楽しい」のです。具体的に困っている人がいる状況で、解決すれば感謝される。技術的スキルを発揮する場としては申し分ありませんし、エンジニアとして最善の解決策を追求したいというマインドも理解できます。しかし、チューニング作業には時間的な制約を伴うトラブルシュートという側面もあります。
常時稼働のSaaSにおいては1週間かけて100%の解決策を提供するより、まず2時間で70%を救えるアプローチがあるならそちらを選ぶ状況もあります(この場合、最終的には暫定対応と恒久対応の二段構えで100%にもっていきます)。チューニングの実施においては技術的な正解ばかりを優先するのではなく、現実的な解決策としての有効性が求められます。「いま最優先で求められているのは何か」の視点をもち、本来の目標にフォーカスしましょう。
測定する
次にやることは「問題の事象を測定する、再現性を確認する」です。実際に起こっていない問題を解決することはできないからです。測定の過程では実測値と一緒に、問題となっている事象の発生条件も確認します。「特定のデータ入力で起こる」「特定の時間帯に起こる」「処理データ量が一定を超えると起こる」など、ここで得られる情報の質が高ければ、問題に対してよりよい解決方法を選択することができます。
ひと昔前のエンタープライズシステムであれば、ユーザに影響が出ないよう休日や深夜に検証する」という方法がとられることもありましたが、インターネットに公開された現在の SaaS においては24時間365日の連続稼働が前提です。本番システムにデバッグ用のコードを仕込んで経過を見る、試行錯誤をするといった時間的な余裕がないこともあります。パフォーマンス問題が起きる前に、普段から運用するシステムの各種指標を収集できる体制を構築しておくことが大事です。
こういった道具立てを自分たちで全て用意するのは大変なので、外部の APM(Application Performance Management: アプリケーションパフォーマンス管理)サービスを使うのもひとつの選択肢です。
ボトルネックを特定する
パフォーマンス低下に影響を与えている要因のうち、最も問題視される箇所のことをボトルネックといいます。測定が終わったら、パフォーマンスを低下させている要因を列挙し、性能にもっとも大きな影響を与えているのが何なのかを明らかにします。「APIサーバのレスポンスが遅い」という問題ひとつとっても、パフォーマンス劣化の原因にはデータベースの検索クエリが遅い、外部API呼び出しのレスポンス待ちが長い、排他制御のロック解放待ちが長い…と、いろいろあるわけです。冒頭にも説明した通り、パフォーマンスチューニングは時間との戦いなので、複数の要因がある場合は最も大きな影響があるところから解決に着手していきます。パフォーマンス低下要因としてよく挙げられるN+1やフルテーブルスキャンが実際に起こっていたとしても、処理プロセス全体に占める割合が少ないのであればいったん後回しにする、という判断もありえます。
対応策を選ぶ
ボトルネックを特定したら、負荷の種類に合わせた対策を検討します。システムの負荷は CPU の計算速度に依存するもの(CPU bound) と、入出力に依存するもの(I/O bound) の2種類に大別されます。I/O は狭義にはディスクの読み書きを指しますが、広義には通信速度(ネットワークのI/O)なども含まれます。
CPU bound の負荷が問題になっている場合は「計算量を減らす」または「計算資源を追加する」アプローチで対策します。具体的には「効率の良いアルゴリズムを採用して計算量を減らす」「サーバ増設でCPUリソースを追加する」といった方法が取られます。
I/O bound の負荷が問題になっている場合は「入出力の総量を減らす」アプローチで対策します。こちらも「キャッシュを活用し、APIやクエリの呼び出し回数を減らす」「APIやクエリで出力するデータ量(HTTP(S)のレスポンスボディ、クエリの結果セット)を減らす」「インデックスを追加して検索のために走査するデータ量を減らす」「逐次処理の書き込みを一括登録に変更する」など、さまざまな方法があります。
終わりに
対応策が決まったら具体的な実装に落とし込み、効果を検証し、テストしてリリースという流れになります。このあたりも掘り下げると色々なネタがあるのですが、今回は「手を動かす前に考えてほしいこと」という形でいくつかのポイントを紹介してみました。手を動かすこれらを確認しておくことで対応の幅も広がりますし、完了時の報告もスムーズに行うことができます。
株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co
Akerun Proの購入はこちらから akerun.com
便利な解錠方法-まばたき解錠
この記事は Akerun Advent Calendar 2021 - Qiita の15日目の記事です。
WebエンジニアのBunです。主にiOSアプリの開発を担当しています。
日々いろんな解錠方法について考えています。特にiOSアプリで使えるいろんな便利な機能を使った解錠方法とその実装手順をまとめます。 今回はiOS(iPhone / Apple Watch) + JINS MEMEでの解錠について書きます。
JINS MEME
JINS MEMEはスマホ、スマートウォッチとつながるウェアラブルメガネです。つい最近最新版がリリースされました。
JINS MEMEは鼻当て部分のCOREに2つのセンサーが搭載されたウェアラブルデバイスです。COREに内蔵されたBluetoothでスマホアプリと連携。センサーが取得したさまざまな情報をスマホで解析し、あなたのカラダとココロの状態を計測します。
6軸モーションセンサー、眼電位センサーから体と目の動きのデータが取れるので、姿勢チェック、集中度計測などヘルスケアに関連する色々面白いことができそうです。
ちなみに、以前初代JINS MEMEのハッカソンに参加し、JINS MEMEと連携したアプリ(the zen)を作ったことがあります。
JINS MEMEで解錠
JINS MEMEと連携したアプリを使えば、Akerunの近くてまばたきするだけで解錠できます。
弊社の「タッチレスエントリー・ソリューション」と組み合わせると、完全タッチレスでより快適な解錠を実現できます。
JINS MEMEとの連携
JINS MEMEから「20Hzデータ」、「15秒間隔データ」、「60秒間隔データ」を取得できますが、 リアルタイムでAkerunを制御するには、JINS MEMEからBluetooth経由で20Hzデータを取得する必要があります。
※https://jins-meme.github.io/sdkdoc2/
JINS MEME SDK
SDK共通事項 - JINS MEME DEVELOPER DOCUMENTS
JINS MEMEを購入する前にちゃんと調べていなかったので、購入後SDKが使えないことを知って少しがっかりしましたが、お問い合わせしたところ、期間限定でご提供いただきました。有り難く使わせていただきます。
非公開情報なので、JINS MEME SDKの詳細については触れません。JINS MEME Platformに公開されている範囲内の内容についてご説明します。
JINS MEME SDK(framework)導入
SDKは一般的なframeworkとして提供されるので、Targetの「Frameworks, Libraries, and Embedded Content」にframeworkを追加すれば使えます。
Akerun施錠解錠実装
施錠解錠API
今回はAkerunアプリで使われているBluetooth専用APIを使っていますが、Akerun公開APIからも施錠解錠が可能です。 具体的な実装は割愛します。弊社のAkerun Developersと下記の関連記事をご参照ください。
施錠解錠トリガー
下記通りシンプルです。
JINS MEMEから取得したモーションセンサーデータ(頭の動き)と眼電位センサーデータ(まばたき)から施錠解錠コマンドを判定し、Akerunにコマンドを送信するだけです。
施錠解錠判定
以下の二つの方法で試してみました。
頭の動き(傾き)とまばたき回数の組み合わせ
まばたき回数と間隔
頭の動き(傾き)とまばたき回数の組み合わせ
単純にまばたきだけだと、意図しない施錠解錠処理が走る可能性が高いので、頭の動き(傾き)も判定するようにしました。
※https://jins-meme.github.io/sdkdoc2/basics/definition.html
- 頭の動き
普段の動きから見ると、前後の傾きとか左右回転は多少不自然な動きになるので、左右傾きのrollのデータを使います。
右方向への傾きを解除とし、左方向への傾きを施錠とします。それぞれrollの範囲は「-180-0」と「0-180」になります。
閾値ですが、小さすぎると、誤動作が発生やすいし、大きい過ぎると、不自然な動きになります。何より首が痛い。。。
色々試した結果20前後が割と良さそうなので、今回は20を取ることにしました。
if data.roll > 20 { // 右傾き:解錠判定 } else if data.roll < -20 { // 左傾き:施錠判定 } else { // クリア }
- まばたきと回数
まばたき速度(blinkSpeed)とまばたき強度(blinkStrength)両方を見てまばたきの判定をします。
こちらについても色々試した結果、まばたき速度は60以上、まばたき強度は30以上にすれば問題なさそうです。
まばたき回数ですが、1回だと誤動作が発生しやすいので、2回まで見ることにしました。
ただ、普段も頭を傾いてまばだきする時もよくあるので、意図しない施錠解錠を減らすために、さらにタイマーを掛けて、「2s以内にまばたき2回」の条件をつけました。
最初は1s以内にしてみましたが、JINS MEMEかSDKの制限(?)で1s以内に複数回まばたきしても検出できないようなので、2sにしています。
多分、下記だと思うので、来年改善を期待します。
まばたきの検出が遅い→2022年に対応ファームウェアを公開する方向で検討中
まばたきの検出は前世代では0.3s、現世代では0.5sのディレイがあります。これは「ライフログで一番精度が上がる」ように誤判定の原因になりそうな前後のシグナル推移のウィンドウを確保しているのが理由です。こちらは2022年以降に対応ファームウェアを公開する方向で検討しております。
※https://jins-meme.github.io/sdkdoc2/basics/motion-tracking-howto.html
解錠判定処理を下記になります(一部抜粋)。
if data.roll < -20 { if self.unlockBlinkCount < 2 { if data.blinkSpeed > 60 || data.blinkStrength > 30 { self.unlockBlinkCount += 1 if self.unlockBlinkCount == 1 { self.startSignalTimer() } else if self.unlockBlinkCount == 2 { self.stopSignalTimer() return .unlock } } } } private func startSignalTimer() { self.signalTimer = Timer.scheduledTimer( timeInterval: self.signalTimeout, target: self, selector: #selector(self.notifySignalTimeout(_:)), userInfo: nil, repeats: false ) } @objc func notifySignalTimeout(_ tm: Timer) { self.unlockBlinkCount = 0 }
まばたき回数と間隔
詳細は割愛しますが、解錠だけの場合まばたき回数と間隔を調整すれば、頭の動きを見なくても特に問題なく解錠ができます。
例えば、1s前後の間隔でまばたき3回で解錠します。
この方法だと、JINS MEMEのジャイロセンサーを使わないので、バッテリー持ちが良くなります。また、首の負担が大分減ると思います。これ大事です。
Akerunはautolock機能があるので、解錠さえできれば十分だと思います。
今後の改善
今回はシンプルにまばたき回数しか使っていないですが、さらにまばたきの詳細パターン(回数、間隔、速度、強度)をチューニング、学習すれば、精度を上げられそうです。
まばたきは「静止指標」で、頭の動きも含めて歩いている状態だと少しノイズが入るので、精度が低くなります。noiseStatusを使って補正ロジックを入れるなど工夫が必要そうです。
まばたきの検出が遅い問題について、次バージョンファームウェアを待つしかないですが、検出が早くなると、施錠解錠精度がもっと上がるように色々工夫ができそうです。
視線の動きも入れることで誤動作などを減らせそうですが、うまく検出できない時もあるので、今後ファームウェア改善されたら試してみます。
まとめ
非公開のため詳細は割愛しましたが、JINS MEME SDKはシンプルで使いやすかったです。サンプルコードもあったので、すぐアプリに組み込むことができました。
スマホの場合、画面をみないと処理が始まったのか、検出と判定にエラーが出たのか分からないので、Apple Watchのバイブレーションを使えば、分かりやすくなると思います。次回試してみたいと思います。
日常で使えるウェアラブルデバイスとしてJINS MEMEはとても素晴らしい製品なので、今後も色々連携してみたいと思います。SDKが一般公開されれば、さらに広がりそうですね。
株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co
Akerun Proの購入はこちらから akerun.com
施解錠遅延の限度見本
この記事は Calendar for Akerun | Advent Calendar 2021 - Qiita の 12日目の記事です。
最近は越境活動が板についてきた daikw - Qiita です。
部署や役職を跨いで共通の認識を作ることで、プロジェクトが円滑に回る、というのはあるあるです。 今日は、この共通認識を作るために造ったものを、一つ取り上げます。
結論
3行でまとめると、
- 「包括的なドキュメントよりも動くソフトウェアを」の実践は、越境的活動に良い効果があった。
- 生産現場では、「動くソフトウェア」に近い概念の「限度見本」を使っている。
- ngrok は良い、vercel も良い。
限度見本とは
僕はソフトウェア技術者でして、なるべくテストコードを書きながら開発しています。 いちいち手動でテストするのは面倒だけど、作ったものが意図通りに動くことは確かねばならないわけでして、 つまり ソフトウェアの品質 を保つために、テストコードを書いて自動で走らせます。
一方で、製造した 製品の品質 を保つための活動として、検査治具を設けたり、外観検査をしたりします。 外観検査ではヒトの五感を使うため、感覚によるバラツキが発生しないようなわかりやすい判定基準を設けるのが望ましく、基準となるサンプルを用意して比較対象とします。
この基準となるサンプルを、「限度見本」と一般的に呼んでいるようです。 いくつかweb上のコンテンツから引用すると、
外観検査を始める前に | 外観検査の基本 | 外観検査.com | キーエンス によれば、
目視による外観検査は、人間に依存する割合が高く、ヒューマンエラーや検査員によるばらつきが発生しやすい工程です。それらを防ぐためには、良品と不良品の境界線を明確に決めておくことが大切です。 仕様書・検査基準書のような文字と写真ではなく、合否の判定基準を目で見てわかるようにする「限度見本」
限度見本の意味と定義とは|限度見本を英語でいうと によれば、
ある製品を品質上合格とするか、不合格とするかの限度を示した製品見本 boundary sampleやlimit sampleもしくはcriteria sampleといった表現がよく使われます。
ソフトウェア限度見本
つまり限度見本は、「検査員」と「設計者・技術者」との間のコミュニケーションツールとして機能しています。 しかも文字ベースの情報伝達と比べて、ディティールを書き下す・読む必要がなく、とても低コストです。
ここで アジャイルソフトウェア開発宣言 より、「包括的なドキュメントよりも動くソフトウェアを」を引用しましょう。
御託を並べるより、動くものをスパッと見せてやれば、コミュニケーション齟齬は一発で無くなったりするものです。
よってここでは、「限度見本」の定義を少し緩和して、「ソフトウェア限度見本」を
「品質を敢えて落とした製品・サービスであって、品質基準の検討のための見本となるもの」
と定義し、それを満たすようなものを作ってみました。
スコープ・要件
ここでは試みとして、弊社スマートロックサービスの「施解錠速度」の品質基準検討に使えるようなものを目指しましょう。
要件をざっくり洗い出し、実装中に目標を見失わないようにします。
- Akerunに遠隔でコマンドを送信する
- コマンドの種類
- 施錠
- 解錠
- 状態取得
- コマンドの性質
- コマンドの実行完了を検知できる
- コマンドの予定の実行時間を変更できる
- コマンドの実際の実行時間を測定できる
- コマンドの種類
実装
なんやかんやあってできました! 施解錠遅延によって、どの程度違和感が発生するのか、それを体感してもらうのが目的です。
右上の delay input でどのくらいの遅延が発生したか、を模擬します。
設計ざっくり
技術的に新しいところは一切なく、Web開発で使いそうな基本的な技術だけで作っています。 Akerun Remote 内部に Akerunの管理コンソール画面 Akerun Connect の機能縮小版を作ったようなものです。
- 通信の流れは、ブラウザ -http-> Remote -ble-> Akerun
- Remote 実機内部にAPIサーバ / フロントエンドサーバ を立てて、ブラウザからの解錠指示をAPIサーバで受け取り、Akerunへのコマンド送信を直接行う
- ブラウザで遅延時間の調整ができる
- ngrok を活用して、ブラウザへの公開を簡単にする
- ついでに雑にレスポンシブにして、適当なサイズのスマホと、PCブラウザどちらからも操作可
作り方
追加で作るものは、ざっくり2つだけでした。
- APIサーバ
- フロントエンドサーバ
あとは、APIサーバからAkerunのコマンド送信を直接行うツールが必要です。今回はCLIツールを事前にご用意しました。3分クッキングでは定番です。
APIサーバ
こちらは複雑さをCLIに押し込んだので、とても簡単な構成です。
┬─[daiki~@photosyth~:~/g/s/w/r/api]─[15:40:51]─[G:main=] ╰─>$ tree -I '__pycache__|venv' . ├── README.md ├── app.py ├── appstart.sh ├── docs │ └── package.json └── requirements.txt 1 directory, 5 files
app.py
はごく単純で
import os import json import subprocess as sp from flask import abort from flask import Flask from flask import jsonify from flask_cors import CORS app = Flask(__name__) CORS(app) akerun_id = os.environ.get("AKERUN_ID") cwd = os.environ.get("CLI_DIR") # remote_cli's bleMode proxys def bleMode_call(cmd): if akerun_id: command = ["node", "cli", "ble", cmd, "-a", akerun_id] else: command = ["node", "cli", "ble", cmd] res = sp.run(command, cwd=cwd, stdout=sp.PIPE, stderr=sp.STDOUT) print(res.stdout) return json.loads(res.stdout.decode('utf-8')) @app.route('/ble/toggle', methods=['POST']) def bleMode_toggle(): res = bleMode_call('toggle') if res["level"] == "error": return abort(500, res) else: return jsonify(res) @app.route('/ble/infopro', methods=['POST']) def bleMode_infopro(): res = bleMode_call('infopro') if res["level"] == "error": return abort(500, res) else: return jsonify(res)
フロントエンドからのリクエストを、Akerun本体にプロキシするような作りになっています。簡単ですね。
フロントエンドサーバ
- 僕が謎にセットアップに慣れている
nextjs
を使い、 - さらに僕が謎になんとなく好きな material-ui を使って
作りました。単一ページで、少しコンポーネントに分けていますが、ごく単純な設計です。
┬─[daiki~@photosyth~:~/g/s/w/j/t/remote-instructor]─[15:11:25]─[G:master=] ╰─>$ tree -I node_modules . ├── README.md ├── jest.config.js ├── next-env.d.ts ├── next.config.js ├── package.json ├── public │ └── favicon.ico ├── src │ ├── components │ │ ├── large_text_field.tsx │ │ ├── popup.tsx │ │ ├── progress_bar.tsx │ │ ├── pseudo_delay_input.tsx │ │ ├── record_table.tsx │ │ └── timer.tsx │ ├── layouts │ │ └── main.tsx │ ├── modules │ │ ├── logger.ts │ │ └── utils.ts │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ └── index.tsx │ └── styles │ └── theme.ts ├── tsconfig.json └── yarn.lock
package.json
には特に特徴はなく、
{ "name": "remote-instructor", "version": "0.1.0", "private": true, "scripts": { "dev": "DEBUG=remote-instructor-* next dev", "debug": "NODE_OPTIONS='--inspect' next dev", "test": "jest", "build": "next build", "start": "DEBUG=remote-instructor-audit-* next start -p 80" }, "dependencies": { "@material-ui/core": "^4.11.4", "@material-ui/icons": "^4.11.2", "@mui/x-data-grid": "^4.0.1", "debug": "^4.3.1", "next": "10.0.8", "react": "17.0.1", "react-dom": "17.0.1" }, "devDependencies": { "@types/debug": "^4.1.5", "@types/jest": "^26.0.23", "@types/node": "^14.14.37", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.3", "dotenv": "^8.2.0", "jest": "^26.6.3", "ts-jest": "^26.5.5", "typescript": "^4.2.3" } }
nextjs
に依存しているため、pages/index.tsx
にほとんど全て詰まっています(省略するところがほとんどなかった ... )。
// Framework import React, { useState, useRef } from "react" // UI import { makeStyles } from "@material-ui/styles" import Grid from "@material-ui/core/Grid" import CircularProgress from "@material-ui/core/CircularProgress" import Icon from "@material-ui/core/Icon" import IconButton from "@material-ui/core/IconButton" import { sleep } from "@/modules/utils" import { Timer, timeMeasureAround, getTime } from "@/components/timer" import { PseudoDelayInput } from "@/components/pseudo_delay_input" import { RecordTable, pushToTable } from "@/components/record_table" const useStyles = makeStyles({ ... }) const LockState = { Unknown: 0, // default Indeterminate: 1, Open: 2, Close: 3, } as const type LockState = typeof LockState[keyof typeof LockState] const LockStateKeys = { 0: "unknown", 1: "indeterminate", 2: "open", 3: "close", } export default function Home(props: any) { const classes = useStyles(props) const timer = useRef<any>() // reference to the Timer Element const table = useRef<any>() // reference to the RecordTable Element const [pseudoDelay, setPseudoDelay] = useState<number>(0) // [sec]. input by the user const [lockState, setLockStateRaw] = useState<LockState>(LockState.Unknown) const lockStateTimeLimit = useRef(null) const setLockState = (input: LockState) => { setLockStateRaw(input) clearTimeout(lockStateTimeLimit.current) if (input === LockState.Open || input === LockState.Close) { lockStateTimeLimit.current = setTimeout(() => { setLockState(LockState.Unknown) // clear lockState after 30 sec }, 30 * 1000) } } const processing = useRef(false) const iconClickAround = (func: () => Promise<LockState>) => { return async () => { if (processing.current) return processing.current = true setLockState(LockState.Indeterminate) const nextState = await timeMeasureAround(async () => { await sleep(pseudoDelay * 1000) // wait for `pseudoDelay`, to simulate the Akerun Remote delay time return await func() }, timer)() processing.current = false setLockState(nextState as LockState) } } const onReplayIconClick = iconClickAround(async () => { const url = `/ble/infopro` console.log(url) return await fetch(url, { method: "POST" }) .then((resp) => { if (!resp.ok) { console.error(resp) pushToTable({ state: "unknown", cmd: "infopro", time: getTime(timer) }, table) throw new Error("server error") } return resp.json() }) .then((body) => { const lock_state = body["message"]["lock_state"]["data"][0] const nextState = { "0": LockState.Open, "1": LockState.Close, }[lock_state] pushToTable({ state: LockStateKeys[nextState], cmd: "infopro", time: getTime(timer) }, table) return nextState }) .catch((err) => { console.error(err) return LockState.Unknown }) }) const onLockIconClick = iconClickAround(async () => { const url = `/ble/toggle` console.log(url) return await fetch(url, { method: "POST" }) .then((resp) => { if (!resp.ok) { console.error(resp) pushToTable({ state: "unknown", cmd: "infopro", time: getTime(timer) }, table) throw new Error("server error") } pushToTable({ state: "open", cmd: "toggle", time: getTime(timer) }, table) return LockState.Open }) .catch((err) => { console.error(err) return LockState.Unknown }) }) const onUnlockIconClick = iconClickAround(async () => { const url = `/ble/toggle` console.log(url) return await fetch(url, { method: "POST" }) .then((resp) => { if (!resp.ok) { console.error(resp) pushToTable({ state: "unknown", cmd: "infopro", time: getTime(timer) }, table) throw new Error("server error") } pushToTable({ state: "close", cmd: "toggle", time: getTime(timer) }, table) return LockState.Close }) .catch((err) => { console.error(err) return LockState.Unknown }) }) return ( <Grid container direction="row"> <Grid container direction="row" className={classes.indicatorContainer}> <Timer ref={timer} /> <PseudoDelayInput value={pseudoDelay} handleChange={(ev: React.ChangeEvent<HTMLInputElement>) => { setPseudoDelay(Number(ev.target.value)) }} /> </Grid> <Grid container direction="column" className={classes.iconContainer}> {(() => { switch (lockState) { case LockState.Open: return ( <IconButton onClick={onUnlockIconClick}> <Icon className={classes.bigIcon}>lock_open</Icon> </IconButton> ) case LockState.Close: return ( <IconButton onClick={onLockIconClick}> <Icon className={classes.bigIcon}>lock</Icon> </IconButton> ) case LockState.Indeterminate: return <CircularProgress /> case LockState.Unknown: default: return ( <IconButton onClick={onReplayIconClick}> <Icon className={classes.bigIcon}>replay</Icon> </IconButton> ) } })()} </Grid> <Grid container direction="column" className={classes.tableContainer}> <RecordTable ref={table} /> </Grid> </Grid> ) }
少々複雑ですが、保持する状態は少なく、フロントエンド特有のものです。仕方ない。 ただ一応、 react-hook を使って多少イイカンジに書いています。素敵!!
ngrok
ngrok は、ローカルPC(この場合、Akerun Remote内部)で動作させているTCPアプリケーションを簡単に外部公開できるサービスです。
プログラマ3大美徳 を黙らせてくれる大変便利なツール。
- ただのデモアプリだろうと、関係者はまとめて触るのが当然(傲慢)
- 本番相当のホスト環境を用意するなんて面倒なことをするはずが(怠惰)
- そもそもそんなことに時間をかけるのはいやだ(短気)
このデモを作った際に個人的に課金しました。
え? vercel を使えばいいじゃないかって?ソウデスネ .....
CORS対策
さて、実はこのままでは動かないです…
nextjs と別の APIサーバ を共有するのに、クロスオリジン制約を突破する必要があります。
nextjs の rewrite
を使うか、nextjs API Routeで一旦受け取ってプロキシすると良いです。そのうち追記するかも。。
限度見本が与えた影響
目の前に動くものがあることで、障害発生時の振る舞いを(開発・サポート・営業の誰もが)想像しやすくなり、コミュニケーションが簡単になりました。 おかげで、部署・関係者間の齟齬なく、やるべきことだけに取り組めるようになりました。
副次的な効果として、アドベントカレンダーのネタになったことに感謝しつつ、今日は終わり。
参考リンク
- アジャイルソフトウェア開発宣言
- ngrokが便利すぎる - Qiita
- Next.jsを使うべき5つの理由 + 実装Tips - Qiita
- フック早わかり – React
- Welcome to Flask — Flask Documentation (2.0.x)
- About this documentation | Node.js v17.2.0 Documentation
株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co
Akerun Proの購入はこちらから akerun.com