AWS CDKにAWS IoT Coreの実装がなかったからコントリビュートしてみた

2021.12.16

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

IoT事業部のやまたつです!

AWS CDKにコントリビュートしてAWS IoT Coreの機能を作ってみたので、それについて綴りたいと思います!
今後、コントリビュート時のtipsなども記事にしていこうと思っています。

始まりと成果物

AWS IoT Coreのtopic ruleを素振りすべく、AWS CDKを使ってみたところ、Cfnプレフィックスがついたクラスしかないことに気がつきました。
このCfnがついたクラスはL1と呼ばれ、CloudFormationのインターフェースから自動生成しただけのクラスです。
これしかない、ということはつまり、自動生成コード以外の実装が存在しないことを意味します。

「じゃあコントリビュートしてみるか」ということでコントリビュートしてみました。(かっこつけて軽い感じで書いてみましたが、かなり一念発起でしたw)

いくつかのPRがマージされた結果として、以下のように記述されていたCfnTopicRuleが、、、

import * as iot from '@aws-cdk/aws-iot';

const firehoseActionRole = new iam.Role(this, 'firehoseActionRole', {
  assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'),
});
stream.grantPutRecords(firehoseActionRole);

const s3ActionRole = new iam.Role(this, 's3ActionRole', {
  assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'),
});
bucket.grantPut(s3ActionRole);

new iot.CfnTopicRule(this, 'MyTopicRule' {
  topicRulePayload: {
    sql: "SELECT * FROM 'things/+/data'",
    actions: [
      {
        lambda: {
          functionArn: func.functionArn,
        },
      },
      {
        firehose: {
          deliveryStreamName: stream.deliveryStreamName,
          roleArn: firehoseActionRole.roleArn,
        },
      },
    ],
    errorAction: {
      s3: {
        bucketName: bucket.bucketName,
        key: '${topic()}/${timestamp()}',
        roleArn: s3ActionRole.roleArn,
      },
    },
  },
});

現在は以下の記述だけで同じ機能を実装できるようになりました!

import * as iot from '@aws-cdk/aws-iot';
import * as actions from '@aws-cdk/aws-iot-actions';

new iot.TopicRule(this, 'MyTopicRule' {
  sql: iot.IotSql.fromStringAsVer20160323("SELECT * FROM 'things/+/data'"),
  actions: [
    new actions.FirehoseStreamAction(stream),
    new actions.LambdaFunctionAction(func),
  ],
  errorAction: new actions.S3PutObjectAction(bucket)
});

Roleまわりを勝手に最小権限で作成してくれるのがCDKの魅力なので、そこが実現できてよかったです。

以降の章では、どのようにしてPRを作成し、それがマージされたかを説明していきます。

コードを書く前にやったこと

CloudFormationテンプレートの形式を把握した

AWS CDKはCloudFormationのテンプレートを吐くだけ(ほんとはもちょっと他のこともできる)の装置なので、コントリビュートするならCloudFormationの形を元手に設計する必要があります。なので、以下のテンプレートをよく読みました。

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_IoT.html

簡単なクラス図を書いてみた

とりあえずそれらを俯瞰的に捉える必要があると思い、以下を書き出してみました。

https://zenn.dev/yamatatsu/articles/2021-09-19-aws-cdk-iot-l2

参考になりそうな実装をCDKの中から探した

AWS IoT Core ruleのActionの形を見て「EventBridgeのTargetっぽいな」と思ったので、その実装を真似て作ることを決めました。
「入力があって、それを一定のルールでファンアウトする装置だからEventBridgeっぽいな」と思っていたというのもあります。

実装してみた

見様見真似で実装していきます。実装自体は参考になる実装がたくさんあるので真似していけば実装できます。そんなことよりも問題は別にあります。

  • ビルドが通らないこと
  • 英語のJSDocとREADMEを書くこと
  • CIが通らないこと

順に説明していきます。

ビルドが通らないこと

とにかくビルドが通らない。驚くほどに。
AWS CDKはjsiiでコンパイルされることもあり、多くの独自ルールがあります。そしてそれらのルールは様々なlint toolとしてCDKのリポジトリ内で実装されています。僕が遭遇したlintエラーを思い出せる限りで以下に記します。

Package that contains any L2s cannot declare a 'cfn-only' maturity

実装が全く無いサービスに最初のクラスを作成したときに、まず遭遇するのがこのエラーです。
package.jsonに記述された「まだCFn互換のクラスしかないよ」というマークを外すと解消します。参考

Missing stability banner for experimental in README.md file

上に書いたcfn-onlyの修正に合わせてREADMEの記載を修正する必要があります。参考

dependency @aws-cdk/core must also appear in peerDependencies

自分でdependenciesに足したpackageはpeerDependenciesにも足さないと怒られてしまいます。

Public API element must have a docstring

JSDocが親切なのはAWS CDKの魅力ですよね。今度は自分がそれを書く番です。サービスの公式docやCloudFormationの説明を参考にして(というかほぼコピペして)、説明を書いていきます。

Optional property must have @default documentation

JSDocが親切なのは(略。「絶対default値書いてくれてるなぁ」と思っていましたが、Lintで守られていたんですね?。基本的にはCloudFormationの記載を参考にします。デフォルト値がないものに関しては@default Noneを記載します。

resources must represent all cloudformation attributes as attribute properties.

CloudFormationにて定義されているReturn valuesをクラスのインスタンス属性として定義しないと怒られます。最初知らんがな?になりました。が、たしかに生えてないとガッカリしてしまいますもんね。

初見ではかなり「???」となったのですが、コードを漁っていくとLintの実装を見つけることができます。Lintもmonorepoで管理されているので読めば何で怒られてるのか分かるのはAWS CDKの良いところだと思います。

他にもいろいろビルドで怒られた気がするのですが、思い出したら追記します。

英語のJSDocとREADMEを書くこと

JSDocを書く話は、前章でもしましたが、今度はREADMEを書いていきます。これは単純にコピペというわけには行かないです。とにかく公式Documentや他のpackageのREADMEを眺めて作文するしかないと思います。大丈夫です。僕らにはDeepLがあります。
加えて、助かることに、このREADMEでは長い英作文よりもサンプルコードのほうが望まれるようです。

CIが通らないこと

ローカルでビルド通ったのにCIで無限に怒られて疲弊しました。。。ちょうどそのころCodeBuildで実行されているCI(一時間くらいかかる)がout of memoryで不安定な挙動してて泣きそうになりながらCIと戦いました。今はわりと安定しているので安心してください。

余談ですが、この1時間かかるCIプロセスは、v1のコードを集めてv2のpackageを生成してテストを通すのに時間がかかってる(200個以上ある全部のpackageのビルド通して、集めて、テスト通す)ので、時間かかるのそうれはそう、という感じではあります。

PRができた。

そして生み出されたのがこのPRです。レビューはAWSの中の人であるアダムさんが担当してくれました。

アダムさんもびっくりしただろうなー。。。突然の51ファイル、4855行修正のモンスターPR。。。
自分でもPRを分ける必要があることは分かっていたのですが、「分けると中途半端な機能単位になってしまう。。中途半端な機能単位ではマージしてもらえないかもしれない。。」「分けたらTopicRuleのメイン機能が使えないぞ?」などの疑問を晴らすことができず、僕は覚悟を決めてモンスターPRをぶつけてみることにしたのでした。。

予想通り、アダムさんにより、「大きすぎてマージできなそうであること」「PRを分けていく方針」についてフィードバックをもらいました。 それからPRを小さくして、再度フィードバックをもらって、直して、フィードバックをもらって、を繰り返し、無事にマージされました。

コメント数83コメント。結局このPR自体で作れるTopicRuleではほぼ何もできないのですが、それでも、土台としてマージすることができ、またその後の方針をレビュアとすり合わせることができたので大きな一歩だったと思います。
(粘り強くレビューしてくれたアダムさんに感謝)

Next

前回の反省をもとに、今度はIssueで設計とロードマップを提出してからPRを作成するようにしてみました。

最初のPRは大して機能を盛り込めない(盛り込むとモンスターPRになってしまう)ので、ロードマップを示して進めていくのが良い気がしています。

おわりに

以上で僕が@aws-cdk/aws-iotを育てた話は終わりです。(ワシが育てた、って言いたかった)

ちなみに、まだまだ100個以上の未実装サービスがあるので、無限にコントリビュートチャンスがあります!
TSが書けてCDKが好きな方がいたら「ワシが育てたpackage」を一つ作ってみるのはいかがでしょうか?

以上、やまたつでした!

参考:未実装のpackageたち

@aws-cdk/alexa-ask
@aws-cdk/aws-accessanalyzer
@aws-cdk/aws-amazonmq
@aws-cdk/aws-appconfig
@aws-cdk/aws-appflow
@aws-cdk/aws-appintegrations
@aws-cdk/aws-applicationinsights
@aws-cdk/aws-appstream
@aws-cdk/aws-aps
@aws-cdk/aws-athena
@aws-cdk/aws-auditmanager
@aws-cdk/aws-autoscalingplans
@aws-cdk/aws-budgets
@aws-cdk/aws-cassandra
@aws-cdk/aws-ce
@aws-cdk/aws-codeartifact
@aws-cdk/aws-codegurureviewer
@aws-cdk/aws-codestarconnections
@aws-cdk/aws-connect
@aws-cdk/aws-cur
@aws-cdk/aws-customerprofiles
@aws-cdk/aws-databrew
@aws-cdk/aws-datapipeline
@aws-cdk/aws-datasync
@aws-cdk/aws-dax
@aws-cdk/aws-detective
@aws-cdk/aws-devopsguru
@aws-cdk/aws-directoryservice
@aws-cdk/aws-dlm
@aws-cdk/aws-dms
@aws-cdk/aws-elasticache
@aws-cdk/aws-elasticbeanstalk
@aws-cdk/aws-emr
@aws-cdk/aws-emrcontainers
@aws-cdk/aws-eventschemas
@aws-cdk/aws-finspace
@aws-cdk/aws-fis
@aws-cdk/aws-fms
@aws-cdk/aws-frauddetector
@aws-cdk/aws-gamelift
@aws-cdk/aws-greengrass
@aws-cdk/aws-greengrassv2
@aws-cdk/aws-groundstation
@aws-cdk/aws-guardduty
@aws-cdk/aws-healthlake
@aws-cdk/aws-imagebuilder
@aws-cdk/aws-inspector
@aws-cdk/aws-iot1click
@aws-cdk/aws-iotanalytics
@aws-cdk/aws-iotcoredeviceadvisor
@aws-cdk/aws-iotevents
@aws-cdk/aws-iotfleethub
@aws-cdk/aws-iotsitewise
@aws-cdk/aws-iotthingsgraph
@aws-cdk/aws-iotwireless
@aws-cdk/aws-kendra
@aws-cdk/aws-kinesisanalytics
@aws-cdk/aws-lakeformation
@aws-cdk/aws-licensemanager
@aws-cdk/aws-lightsail
@aws-cdk/aws-location
@aws-cdk/aws-lookoutequipment
@aws-cdk/aws-lookoutmetrics
@aws-cdk/aws-lookoutvision
@aws-cdk/aws-macie
@aws-cdk/aws-managedblockchain
@aws-cdk/aws-mediaconnect
@aws-cdk/aws-mediaconvert
@aws-cdk/aws-medialive
@aws-cdk/aws-mediapackage
@aws-cdk/aws-mediastore
@aws-cdk/aws-memorydb
@aws-cdk/aws-mwaa
@aws-cdk/aws-networkfirewall
@aws-cdk/aws-networkmanager
@aws-cdk/aws-nimblestudio
@aws-cdk/aws-opsworks
@aws-cdk/aws-opsworkscm
@aws-cdk/aws-panorama
@aws-cdk/aws-pinpoint
@aws-cdk/aws-pinpointemail
@aws-cdk/aws-qldb
@aws-cdk/aws-quicksight
@aws-cdk/aws-ram
@aws-cdk/aws-rekognition
@aws-cdk/aws-resourcegroups
@aws-cdk/aws-robomaker
@aws-cdk/aws-route53recoverycontrol
@aws-cdk/aws-route53recoveryreadiness
@aws-cdk/aws-s3objectlambda
@aws-cdk/aws-s3outposts
@aws-cdk/aws-sagemaker
@aws-cdk/aws-sam
@aws-cdk/aws-sdb
@aws-cdk/aws-securityhub
@aws-cdk/aws-ssmcontacts
@aws-cdk/aws-ssmincidents
@aws-cdk/aws-sso
@aws-cdk/aws-timestream
@aws-cdk/aws-transfer
@aws-cdk/aws-waf
@aws-cdk/aws-wafregional
@aws-cdk/aws-wisdom
@aws-cdk/aws-workspaces
@aws-cdk/aws-xray