Greengrassなクリリンが太陽拳するまで - koudenpaのブログ では AWS IoT GreengrassのGPIOコネクター を使った。AWSリソースの管理にはAWS CDKをC#で使ってみた。
この記事にはCDKでのGreengrass管理をしてみた際の備忘を書く。そんなに気合いは入っていないので箇条書きと乱文だ!
C#でCDKを使ってみた触感はClassmethodさんの記事(WindowsでAWS CDK(C#)の開発環境を整えてみた | Developers.IO)の末尾に書かれている感想と大体同じで、C#慣れしているなら普通に使えるけれど大体の場合でTypeScriptの方が良さそうだ、といった感じだった。
CDKとGreengrass
大前提として、2020初春現在ではこの組み合わせは遊びでしか使えない。
頑張れば商売にも使えるといった要素は一切ない。
aws-greengrass module · AWS CDK
This is a developer preview (public beta) module. Releases might lack important features and might have future breaking changes.
そもそもパブリックベータであるということもあるのだが、
AWS::Greengrass::CoreDefinitionVersion.Cores
The Greengrass core in this version. Currently, the Cores property for a core definition version can contain only one core.
無数のモノをCoreとして管理したいだろうところで1つだけしかCoreを定義できないようではどうにもならない。
CDKでGreengrass備忘
InitialVersion は指定しないで、別途個別のバージョンのリソースを定義しておくのが良さそう
- GreengrassはGroupという単位で関連リソースを管理する。また、それらのリソースは不変で、リソースに変更を行う場合は別のバージョンのリソースを作成する形になる。これは、リソースをAWSの外(Coreデバイス)にデプロイする都合であると思う。何時、どのバージョンを、どのCoreデバイスにデプロイしたのか? を管理する必要があるからだ。
- GroupのInitialVersionを指定していると、InitialVersionの内容を変更はすなわちGroupの変更、つまり置換となってGroupが破棄されてしまう。Group毎置換しない場面ではInitialVersionは指定しない方が取り回しが良さそうだ。
- 逆に遊びとして常に破棄でいいならInitialVersionだけ使う割り切りも良さそう。
CDKのデプロイ前にGreengrassのデプロイをリセットする
- リソースの変更はできないので、あるリソースの内容を変えると置換になる。この際、それらのリソースがCoreにデプロイされていると破棄できない。
- デプロイをリセットしておくことで破棄できるようにある(はず)。
- 自分は AWS CLI でやったり、AWSコンソールでやったりしていた。
- デプロイしていると破棄できない(これはぐグループ自体の破棄に関するメッセージだけれど)。
MackerelAlertLampGroup This group is currently deployed, so it cannot be deleted. (Se null; Status Code: 400; Error Code: InvalidDeleteGroupRequest; Request ID: null; Gre Trace ID: Root=xxx) (Service: null; Status Code: 40r Code: InvalidDeleteGroupRequest; Request ID: null)
Lambda関数の内容を変更したら関連するリソースのバージョンも変えるようにする
所感
この1年は完全にWebアプリケーションの世界で生きていたのでIoT関連の要素技術にはまったっく触れていなかったのだけれど、久しぶりに触れてなかなか楽しかった。
Raspberry Pi(Zero)をGreengrassのCoreとして構成さえしておけば、インターネット上からLambda関数をデプロイできるのはなかなか良い体験だった。
コネクタ(今回は特定のMQTTトピックにメッセージを送るとGPIOの状態を変更したりしてくれるコネクタを使った)もなかなか良い感じで、コネクタさえ構成しておけば様々なリソースと平易に接続できそう。典型的には「様々なソース -> 加工用のLamda関数 -> コネクタ」のような形になるのだろう。
CDK(CFn)に関しては宣言型である都合上相性はよくなさそうで、現状でGreengrassを使い込むのであれば、それを内包するアプリケーションでAPIを使って関連リソースを管理するのが無難なところであるように感じた。
試行コード抜粋
リポジトリ公開には若干センシティブな内容が含まれているので、Greengrassの管理を試行していた部分のみ参考として貼っておく。
using Amazon.CDK; using Amazon.CDK.AWS.Events; using Amazon.CDK.AWS.Events.Targets; using Amazon.CDK.AWS.Greengrass; using Amazon.CDK.AWS.IoT; using Amazon.CDK.AWS.Lambda; using System.Collections.Generic; using System.Linq; namespace MackerelAlertToAwsIot { public class MackerelAlertLampProps : StackProps { public IEventBus AlertBus { get; set; } public string GroupRoleArn { get; set; } public string[] ThingCerts { get; set; } } // Ref: https://dev.classmethod.jp/cloud/aws/aws-cdk-greengrass-rasberrypi/ public class MackerelAlertLampStack : Stack { internal MackerelAlertLampStack(Construct scope, string id, MackerelAlertLampProps props) : base(scope, id, props) { // クラウドで動かすLambda関数 // アラートの通知ハンドラ var cloudReceiveAlertFunction = new Function(this, "CloudReceiveAlert", new FunctionProps() { Runtime = Runtime.PYTHON_3_7, Code = Code.FromAsset("handlers/cloud"), Handler = "ReceiveAlert.handler", }); // Coreで動かすLamda関数 // アラートの通知ハンドラ var deviceReceiveAlert = new Function(this, "DeviceReceiveAlert", new FunctionProps() { Runtime = Runtime.PYTHON_3_7, Code = Code.FromAsset("handlers/device"), Handler = "ReceiveAlert.handler", }); var deviceReceiveAlertVersion = deviceReceiveAlert.AddVersion("v1"); var deviceReceiveAlertAlias = new Alias(this, "DeviceReceiveAlertAlias", new AliasProps() { AliasName = "v" + deviceReceiveAlertVersion.Version, Version = deviceReceiveAlertVersion, }); // テスト用のGPIO操作関数 var toggleGpio = new Function(this, "DeviceToggleGpio", new FunctionProps() { Runtime = Runtime.PYTHON_3_7, Code = Code.FromAsset("handlers/device"), Handler = "ToggleGpio.handler", }); var toggleGpioVersion = toggleGpio.AddVersion("v1"); var toggleGpioAlias = new Alias(this, "DeviceToggleGpioAlias", new AliasProps() { AliasName = "v" + toggleGpioVersion.Version, Version = toggleGpioVersion, }); var thingPolicy = new CfnPolicy(this, "MackerelAlertLampThingPoilcy", new CfnPolicyProps() { PolicyName = "MackerelAlertLampThingPoilcy", PolicyDocument = new Dictionary<string, object> { ["Version"] = "2012-10-17", ["Statement"] = new object[] { new Dictionary<string, object> { ["Effect"] = "Allow", ["Action"] = new string[] { "iot:*", "greengrass:*", }, ["Resource"] = new string[] { "*" }, } } } }); // IDなどに使うために証明書のARNを加工しておく。 var certs = props.ThingCerts .Select(x => new { Arn = x, Hash = Utils.ToHash(x), }).ToList(); // 証明書へのポリシーアタッチ var certAttaches = certs.Select(x => { var attach = new CfnPolicyPrincipalAttachment(this, "MackerelAlertLampCertAttach-" + x.Hash, new CfnPolicyPrincipalAttachmentProps() { PolicyName = thingPolicy.PolicyName, Principal = x.Arn, }); attach.AddDependsOn(thingPolicy); return attach; }).ToList(); // Greengrass Coreとするモノを作って証明書をアタッチ var things = certs.Select(x => new { CertArn = x.Arn, Thing = new CfnThing(this, "MackerelAlertLampThing-" + x.Hash, new CfnThingProps() { ThingName = "MackerelAlertLamp-" + x.Hash, }) }).ToList(); var thingAttaches = things.Select(x => { var attach = new CfnThingPrincipalAttachment(this, x.Thing.ThingName + "Attach", new CfnThingPrincipalAttachmentProps() { ThingName = x.Thing.ThingName, Principal = x.CertArn, }); attach.AddDependsOn(x.Thing); return attach; }).ToList(); // モノをCoreデバイスにする var ggCore = new CfnCoreDefinition(this, "MackerelAlertLampCore", new CfnCoreDefinitionProps() { Name = "MackerelAlertLampCore", InitialVersion = new CfnCoreDefinition.CoreDefinitionVersionProperty() { Cores = things.Select(x => new CfnCoreDefinition.CoreProperty() { Id = x.Thing.ThingName, CertificateArn = x.CertArn, // XXX ARN参照できないの? //ThingArn = x.Thing.GetAtt("Arn").Reference.ToString(), //ThingArn = x.Thing.GetAtt("resource.arn").Reference.ToString(), ThingArn = $"arn:aws:iot:{this.Region}:{this.Account}:thing/{x.Thing.ThingName}", }).ToArray(), } }); things.ForEach(x => { ggCore.AddDependsOn(x.Thing); }); // Coreで使うリソース // Raspberry Pi のGPIO操作用リソース var gpioRw = new CfnResourceDefinition.ResourceInstanceProperty() { Id = "gpio-rw", Name = "RaspberryPiGpioRw", ResourceDataContainer = new CfnResourceDefinition.ResourceDataContainerProperty() { LocalDeviceResourceData = new CfnResourceDefinition.LocalDeviceResourceDataProperty() { SourcePath = "/dev/gpiomem", GroupOwnerSetting = new CfnResourceDefinition.GroupOwnerSettingProperty() { AutoAddGroupOwner = true, }, } }, }; var ggResource = new CfnResourceDefinition(this, "MackerelAlertLampResource", new CfnResourceDefinitionProps() { Name = "MackerelAlertLampResource", InitialVersion = new CfnResourceDefinition.ResourceDefinitionVersionProperty() { Resources = new CfnResourceDefinition.ResourceInstanceProperty[] { gpioRw, } }, }); // Coreへ配信する関数 var ggFunction = new CfnFunctionDefinition(this, "MackerelAlertLampFunction", new CfnFunctionDefinitionProps() { Name = "MackerelAlertLampFunction", InitialVersion = new CfnFunctionDefinition.FunctionDefinitionVersionProperty() { Functions = new CfnFunctionDefinition.FunctionProperty[] { new CfnFunctionDefinition.FunctionProperty(){ Id = deviceReceiveAlert.FunctionName + "-" + deviceReceiveAlertAlias.AliasName, FunctionArn = deviceReceiveAlertAlias.FunctionArn, FunctionConfiguration = new CfnFunctionDefinition.FunctionConfigurationProperty() { // MemorySize と Timeout は必須である様子 MemorySize = 65535, Timeout = 10, // 秒 }, }, new CfnFunctionDefinition.FunctionProperty(){ Id = toggleGpio.FunctionName + "-" + toggleGpioAlias.AliasName, FunctionArn = toggleGpioAlias.FunctionArn, FunctionConfiguration = new CfnFunctionDefinition.FunctionConfigurationProperty() { // MemorySize と Timeout は必須である様子 MemorySize = 65535, Timeout = 10, // 秒 }, }, }, }, }); // Coreで使うコネクタ // GPIOコネクタ、GPIOはこちらのコネクタ経由で操作することにする // https://docs.aws.amazon.com/ja_jp/greengrass/latest/developerguide/raspberrypi-gpio-connector.html var gpioConnector = new CfnConnectorDefinition.ConnectorProperty() { Id = "gpio-connector", ConnectorArn = $"arn:aws:greengrass:{this.Region}::/connectors/RaspberryPiGPIO/versions/1", Parameters = new Dictionary<string, object>() { ["GpioMem-ResourceId"] = gpioRw.Id, // 入力はさしあたってなし //["InputGpios"] = "5,6U,7D", //["InputPollPeriod"] = 50, // 10, 9, 11番は配置が連続しているのでとりあえずそれを使う ["OutputGpios"] = "9L,10L,11L", } }; var ggConnector = new CfnConnectorDefinition(this, "MackerelAlertLampConnector", new CfnConnectorDefinitionProps() { Name = "MackerelAlertLampConnector", InitialVersion = new CfnConnectorDefinition.ConnectorDefinitionVersionProperty() { Connectors = new CfnConnectorDefinition.ConnectorProperty[]{ gpioConnector, }, } }); // Coreで使うサブスクリプション // XXX うっそだろ? ってほどに量があるけれどマジか? var ggSubscription = new CfnSubscriptionDefinition(this, "MackerelAlertLampSubscription", new CfnSubscriptionDefinitionProps() { Name = "MackerelAlertLampSubscription", }); var ggSubscriptions = new CfnSubscriptionDefinitionVersion.SubscriptionProperty[] { new CfnSubscriptionDefinitionVersion.SubscriptionProperty() { Id = "gpio-error", Source = gpioConnector.ConnectorArn, Target = "cloud", Subject ="gpio/+/error", }, // XXX Currently, when you create a subscription that uses the Raspberry Pi GPIO connector, you must specify a value for at least one of the + wildcards in the topic. new CfnSubscriptionDefinitionVersion.SubscriptionProperty() { Id = "gpio-read-9", Source = "cloud", Target = gpioConnector.ConnectorArn, Subject ="gpio/+/9/read", }, new CfnSubscriptionDefinitionVersion.SubscriptionProperty() { Id = "gpio-write-9", Source = "cloud", Target = gpioConnector.ConnectorArn, Subject ="gpio/+/9/write", }, new CfnSubscriptionDefinitionVersion.SubscriptionProperty() { Id = "gpio-state-9", Source = gpioConnector.ConnectorArn, Target = "cloud", Subject ="gpio/+/9/state", }, new CfnSubscriptionDefinitionVersion.SubscriptionProperty() { Id = "gpio-read-10", Source = "cloud", Target = gpioConnector.ConnectorArn, Subject ="gpio/+/10/read", }, new CfnSubscriptionDefinitionVersion.SubscriptionProperty() { Id = "gpio-write-10", Source = "cloud", Target = gpioConnector.ConnectorArn, Subject ="gpio/+/10/write", }, new CfnSubscriptionDefinitionVersion.SubscriptionProperty() { Id = "gpio-state-10", Source = gpioConnector.ConnectorArn, Target = "cloud", Subject ="gpio/+/10/state", }, new CfnSubscriptionDefinitionVersion.SubscriptionProperty() { Id = "gpio-read-11", Source = "cloud", Target = gpioConnector.ConnectorArn, Subject ="gpio/+/11/read", }, new CfnSubscriptionDefinitionVersion.SubscriptionProperty() { Id = "gpio-write-11", Source = "cloud", Target = gpioConnector.ConnectorArn, Subject ="gpio/+/11/write", }, new CfnSubscriptionDefinitionVersion.SubscriptionProperty() { Id = "gpio-state-11", Source = gpioConnector.ConnectorArn, Target = "cloud", Subject ="gpio/+/11/state", }, // new CfnSubscriptionDefinitionVersion.SubscriptionProperty() { Id = "gpio-test-write", Source = toggleGpioAlias.FunctionArn, Target = gpioConnector.ConnectorArn, Subject ="gpio/+/11/write", }, new CfnSubscriptionDefinitionVersion.SubscriptionProperty() { Id = "gpio-test", Source = "cloud", Target = toggleGpioAlias.FunctionArn, Subject ="gpio/test", }, }; var ggLatestSubscription = new CfnSubscriptionDefinitionVersion(this, "MackerelAlertLampSubscriptionVersion-" + Utils.ToHash(string.Join("-", ggSubscriptions.Select(x => x.Id))), new CfnSubscriptionDefinitionVersionProps() { SubscriptionDefinitionId = ggSubscription.AttrId, Subscriptions = ggSubscriptions, }); // Greengras Groupを作る var ggGroup = new CfnGroup(this, "MackerelAlertLampGroup", new CfnGroupProps() { Name = "MackerelAlertLamp", RoleArn = props.GroupRoleArn, }); var ggVersionHash = Utils.ToHash(string.Join("-", ggCore.AttrLatestVersionArn, ggFunction.AttrLatestVersionArn, ggResource.AttrLatestVersionArn, ggConnector.AttrLatestVersionArn, ggSubscription.AttrLatestVersionArn)); var ggLatestVersion = new CfnGroupVersion(this, "MackerelAlertLampGroupVersion-" + ggVersionHash, new CfnGroupVersionProps() { GroupId = ggGroup.AttrId, CoreDefinitionVersionArn = ggCore.AttrLatestVersionArn, FunctionDefinitionVersionArn = ggFunction.AttrLatestVersionArn, ResourceDefinitionVersionArn = ggResource.AttrLatestVersionArn, ConnectorDefinitionVersionArn = ggConnector.AttrLatestVersionArn, SubscriptionDefinitionVersionArn = ggSubscription.AttrLatestVersionArn, }); ggLatestVersion.AddDependsOn(ggGroup); ggLatestVersion.AddDependsOn(ggCore); ggLatestVersion.AddDependsOn(ggResource); ggLatestVersion.AddDependsOn(ggFunction); ggLatestVersion.AddDependsOn(ggConnector); ggLatestVersion.AddDependsOn(ggSubscription); // TODO わかったら書く var mackerelAlertRule = new Rule(this, "mackerel-alert-rule", new RuleProps() { EventBus = props.AlertBus, EventPattern = new EventPattern() { Source = new string[]{ "aws.partner/mackerel.io", }, }, Targets = new IRuleTarget[] { new LambdaFunction(cloudReceiveAlertFunction), new LambdaFunction(deviceReceiveAlert), }, }); } } }