予測可能性の3要素

チェシャ猫氏:では、AWSにおいて予測可能性をどう担保するかを、もうちょっと具体的なところに寄せて考えてみましょう。予測可能性と言っていますが、もうちょっと分解して、どういう要素から成り立っているかを少し考えてみたいと思います。

私が立てたテーゼによれば、予測可能性は以下の3要素から作られています。1つ目は再現性、Reproducibility。同じ操作を誰がやっても、いつやっても繰り返せる。repeatableになっているのが1つ。「再現性を担保せよ」みたいによく言われるやつです。

2つ目は純粋性、Purityです。どういう性質かというと、実行前にどんな状態だったかによらず、実行すると同じ結果が導かれるのがPurityです。

3番目はModularityで、要するにモジュールになっていて、再利用可能な部品が作れるようになっていて、組み合わせられるようになっている。そうすると、すでに挙動がわかっているものを組み合わせて、大きいものを作れる。テストのときも、影響範囲が単体に閉じた状態にテストできるので、予測可能性を担保しやすくなる。このあたりは本当にアプリと同じです。よいクラスの分割、よい設計みたいなものです。

この3つが肝であろうというのは、たぶんある程度異論はなく受け止めてもらえるのかなと思います。

ちょっとリマークが必要なのは、2番目のPurityです。インフラのデプロイで、冪等性についてよく言われると思います。なぜここで冪等性という言葉を使っていないかというと、冪等性と純粋性は微妙に違うからです。少なくとも私は求められているのは純粋性であって、冪等性ではないと思っています。

まずデプロイメントが何かというと、例えば与えるテンプレートなどのパラメータxと、例えば今のインフラ状態の事前状態e、両方に影響される関数です。一般には、両方に影響される。

冪等性がどういうのかというと、“2回実行しても1回目と同じ”というものです。要するに、式で書くとf(x.e)というのを、もう1回eのところに入れて、結果が同じになる。fを2回掛けているものと、1回掛けているものとの結果が同じ、というのが冪等です。

純粋性は何かというと、実行前の状態によらず結果が一定なので、e1とe2があったときに、f(x)まで同じであればf(x,e1)とf(x,e2)が同じである、というのがPurityです。これを見ればわかるとおり、条件がPurityのほうが強いです。なぜなら、e2をeにして、e1をf(x,e)にすればいいから。

この2つを分けると、確かにほしいのはPurityだろう、という感じがすると思います。だって、冪等だとそもそも一発目に何が起こるかわからないし。ということで、ここでは冪等性ではなく、純粋性という言い方を使いました。

AWS上のInfrastructure as Codeの4段階

AWSでどうなっているか。これらの要素はどういうふうに満たされるかをちょっと考えてみましょう。AWSの上でInfrastructure as Codeをやろうとすると、いくつかevolution stepsがあって。

だいたい4段階くらいあります。たぶん最も原始的な段階から、最も高度なというとアレですが、最もハイソ(ハイソーシャル)なところまで。だんだんに予測可能性が上がっていく、おおまかなモデルを考えられます。

まず、はじめの最もナイーブな段階は、マネコン(マネジメントコンソール)を手でいじるやつです。ナイーブですが悪くはなくて、直感的だし、人間なら誰でもできる。融通も利きます。

ただ、今回テーマにしたい予測可能性についていえば最悪で、もちろん再現できません。再現しようとするとマネコンのUIはしばしば変わるし、文章で書こうとするとメチャクチャ長くなります。デフォルト値が増えたりすると、手順書を全部書き直しになるわけです。

という意味で再現性は乏しいし、もちろん純粋性もないです。モジュール性もありません。モジュールって何だっていう感じですが(笑)。そのため、予測可能性としては非常に低いです。

次にくる段階は、AWS CLIを使うものです。おそらく、一般的にはこのあたりから自動化している感じになると思います。シェルスクリプトと組み合わせて、AWS CLIコマンドを叩く。

AWS CLIのコマンドは、機能的には最もリッチです。というよりも、例えばマネコンだと押せないような隠れた設定があって、AWS CLIだと使えるのはよくあるパターンです。手でやるよりはスクリプトになっているし、ちょっとはマシになっています。

あまり高くはありませんが、自然言語で書くことに比べると、再現性はあります。スクリプトを実行すればいいので。実行する環境も、例えばコンテナの中で実行するとか、特定の踏み台サーバーで実行するようにしておけば、ある程度は担保できるだろうと。

ただ、純粋ではありません。典型的なのがcreateで、名前が被る。すでにあると、2回目は失敗します。これは典型的な“純粋ではない”ということです。すでにあるかどうかによって、結果が変わってしまっているから。実際にスクリプティングしようとすると、それを回避するために、けっこうアドホックなことを書かなければいけないのは、たぶんみなさん経験があると思います。

シェルマスターみたいな人がいれば別ですが、モジュール性も厳しいです。往々にしてメンテナビリティは低いことになります。

3段階目として出てくるのが、CloudFormationです。みなさん知っているとおり、YAMLによって宣言的な定義が可能になります。重要なのは、ここでYAMLが述べているのは操作ではなく、状態というところです。

そのため、予測可能性としてはAWS CLIみたいなところからだいぶ改善しています。再現性はスクリプトというか、ファイルになっているからある。

純粋性が向上しているのはどこの部分かというと、宣言的に記述するので、例えばType: AWS::EC2::Instanceと書く。なければ作るし、違っていればその設定を置き換える。あるいは一致していればそのままというのが、CloudFormation自体の機能の中にカプセレーションされています。という意味で、前のやつより純粋性は上がっています。

しかもPhysical IDを勝手に生成してくれて、人間がuidをつけることをやらなくてもよくなるので、改善していると言えるでしょう。ただ、モジュール性、あるいはプログラマブル性とはわりと乏しいです。

例えばNested Stackみたいなもの、あるいは外部参照を使ってほかのstackのoutputsを読めますが、最初によく設計しておかないと、あとから変えるときに、例えばデッドロックになるとか、実はほかのリージョンの値が読めないとか、いろいろなところでハマりがちです。

しかも、例えばループなどは書けないので、同じものをたくさん作ろうとすると、AZの数ごと全部一覧を作る必要があったりして、実際やろうとするとわりとつらいです。モジュール性はかなり乏しい。これが3ステップ目でした。

一番上にくるのがCDK(Cloud Development Kit)です。どういうものかというと、プログラムでCloudFormation用のYAMLを生成します。だからプログラムですが、コマンドを実行しているのではなくて、いったんCloudFormationなのが重要なところで。

(スライドの)下にも書きますが、CloudFormationがもっていた純粋性はCloudFormation経由で担保されている。これが中でAWS CLIを呼んでいるとかだと、またif文を書かなければいけなくなるところでした。

TypeScript、Python、Java、.NETと書きましたが、Goも出ましたね。ライブラリがいくつかの言語で書かれています。たぶん、TypeScriptが機能としては一番先に実装されます。

プログラミング言語なのでIDEが使えるし、型があるし、ループも使えるし、さすがにYAMLより書くのは楽だろうとということで、人気が出ているというか、みんな使っている感じです。

予測可能性の話をすると、ファイルなので再現性はいいです。純粋性もさっき言ったとおりです。スクリプト実行しているのではなく、1回CloudFormation、ディクラレイティブ な記述に落とすところがあるので、ここでCloudFormationと同等の性能が確保できる。

最後に言ったのがモジュール性の部分で、プログラム的な構造化ができるのもそうです。あるいは、npmを使って再配布可能なライブラリを作れます。こういうものをCDKの言葉でいうと、“Construct”と言います。

いわゆる、ひとかたまりのリソースのモジュールみたいなものです。再利用可能で、例えばVPCを作ると、VPCの中にマルチAZ分のサブネットを切って、フロントエンドサブネットとプライベートサブネットとパブリックサブネットと切って、ルートテーブルを作って。

あとはNATを作って、インターネットゲートウェイを作り、セキュリティグループを設定して、みたいなことがありますが、そういったものが全部encapsulationされていて、ネットワークで作ると、全部規定で作れるのがConstructです。

サンプルから見る実際の挙動

実際にどうするのかを、サンプルから見てみましょう。例えば、今(スライドのような)Stackを作りたいとします。SQSのQueueとNotificationのTopicがあり、Subscribeしている、というのを作ろうとすると、CDKだとこんな記述になります。

まずQueueをインスタンス化して、SNSのTopicをインスタンス化して、thisのところ。なぜthisかというと、親がStackだからです。木構造になっていて、親が何であるかを下に伝える。そうすると、StackからMyQueueとMyTopicにリンクが貼られて、木構造が作られる。入れ子構造になっています。

cdk synthを打つと、CloudFormationになります。そして、aws cloudformation deployすると、実際にAWSにデプロイされる仕組みになっています。synthはたぶん合成でしょう。

実際打つとどうなるかというと、すごく長いのが左側に出ました。文字の大きさ的に読めないと思いますが、こういうのが出るというサンプルです。なぜメチャクチャ長いかというと、下の3分の2くらいは全部CDK用のメタデータで、実際はそんなに大きくはありません。

実際にどんなものが作られているかというと、囲んだところです。Resourcesのところでxxxに生成されたIDが入って、いわゆるCloudFormationのリソースに対応しています。

さっきQueueとTopicの話はしたと思いますが、Policyは作っていないわけです。Constructのところでencapsulationできると言ったのは、要するにCDKではこれらを作ったらConstructの中に入っていて、一緒にPolicyも作れとモジュールとして定義されているので、人間はその細かいところを考えなくて済むのがCDKの強みです。再利用性の部分です。

実際cdk deployすると、直接デプロイもできると。あとdiffと打つと、今アップロードされている、環境に作られているリソースと、CDKが作るもののdiffを取れます。

先ほどからConstruct、Constructと言っていますが、もちろん自分でも作れます。今出てきたものは公式で提供されているもので、例えば、スライドの図は公式のチュートリアルに出てくる例です。Lambdaを叩いて、APIを叩いたときにヒット数をカウントしたいと。

叩かれた数をカウントしたいので、Dynamoに置くようなLambdaを、間に置きましょうというときに、カウントするDBと、カウントするLambdaを合わせてConstructにしましょうと。いろいろなAPIの前に置くことによって、カウントというアスペクトを切って挿入できる例が出ています。

具体的にどういう書き方をするかというと、まずConstructを作るときはcdk.Constructを継承して、DynamoとLambdaをつけます。

そうすると、先ほどの囲みのところの部分がConstructになっていて、tableNameは環境変数で渡しておけばいいだろうと。Lambdaの典型的な構成ですね。こうすると、先ほど書いた2つが、まとめて1つのConstructにパックされた状態になります。

CDKの純粋性を〇にする方法

というわけで4つ話してきました。どうなっているかというと、マネコンとAWS CLIとCloudFormationとCDK。表を見ると、CDKの純粋性はCloudFormationの純粋性と同じなので、一定程度という言い方をしました。

CloudFormationを使われている方はわかると思いますが、もちろん今の状態によっては、既にあって作れなかったりするじゃないですか。CloudFormation外に原因があったりする。

ではCDKでも純粋性は△なの、そこそこなのはどうすりゃいいんだってなります。できればここを〇にしたい。そのためにCDKにはテストの機能が備わっていて、3種類あります。Snapshotのテスト、Fine-grained Assertion、Validationのテストです。

Snapshot TestはYAMLを生成し直したときに、例えばCDKのバージョンアップによってYAMLの構成が変わったりして、それこそ意図しないような、何が起こるかわからないような状況が起こらないようにする一種の回帰テストを作れます。

2番目のFine-grained Assertionは生成されたYAMLに、例えばこういうセキュリティグループがあるとかないとかという条件が書ける。

最後にValidation Testというのがあります。これは単なるJavaScriptの、TypeScriptのテストですが、実際にモジュールを使うときに、例えばマイナスの引数を与えたらちゃんとエラーが飛ぶみたいなところで、インターフェイスとしてモジュールの境界をしっかり切れる仕組みになっています。

各テストの詳細

ざっと中身を説明しましょう。まずSnapshot Testはどういうものかというと、こんな例を考えます。DeadLetterQueueについて、Stackがあったとき、テストは何を書くかというと、こうです。

先ほどcdk synthするとCloudFormationになるという話をしましたが、テストの中でこれが書けます。toCloudFormation(stack)と書くと、さっきのStackからどういうYAMLが最終的に中で生成されるかがエミュレートできる。

(スライドを見て)実際にはこんな感じです。これはパスになっていますが、Snapshotsというディレクトリができて、スナップショットが保存されていることがわかります。

デフォルト値ではありませんが、仮にできあがるYAMLに影響を与えるような変更が入ったとしましょう。そうすると、すでに保存してあるスナップショットとの差分がテストとして検出されます。

TypeScriptを書かれる方はわかると思いますが、JestのいわゆるSnapshot Test。つまり、画面上でなにか変更がないかを回帰するテストの仕組みと同じものを使っています。先ほど1を入れたから、Periodがもともとは300だったけど、60になっています。

2番目のFine-grained Assertionがどういうものかというと、いわゆるアサーションです。テストケースを考えてみました。DeadLetterQueueはmessage alarmをもっている、というテストケースを書いています。haveResourceすると、YAMLの生成結果に対してこういうリソースがある、というアサーションが書ける。

なぜFine-grainedという言い方をしているかというと、例えば、わざとNamespace: 'AWS / Lambda'と書いたとしましょう。これはSQSなので、実際にはNamespaseはAWS / SQSになります。

なぜFine-grainedかというと、ほかにたくさん属性があったとしてもYAMLのプロパティを辿って、そこに対するアサーションになってくれる。今回はテストケースとしてNamespaceを見たので、そこのみを判定して、いわゆる単なるディープイコールみたいなことより、高度なことができる仕組みになっています。

最後はValidation Testです。ここで書いているのは引数を確認して……これはretentionが多すぎるかな。14を超えていると、Constructの特性として例外送出しようと。不正値であるようなConstructを作ったとしたら、範囲内であれば例外は飛ばないと。

例外が飛んでいないと、範囲内が14なので、toThrowErrorが引っかかります。いわゆる普通のTypeScriptのテストを書けます。

セクション2のまとめ

というわけでセクション2として、AWSの上で予測可能性を担保するための仕組みはどんなものがあるかを俯瞰して見てみたセクションでした。まとめとして、まずデプロイ時の予測可能性を担保するためには、再現性、純粋性、モジュール性という3つの要素が必要だった、というのを今まで見てきました。

AWS上でIaCをやろうとすると進化の段階があって、だんだんと予測可能性が強くなってくる方向に進化していると。1つの現場で、一足飛びにいきなり大上段にバーンとCDKを使うというよりは、今我々がどの段階にいるか、何が足りないかを考えて、階段を登るようにするのがよいでしょう。

それと、CDKには静的テストの機構がそもそも備わっています。プログラミング言語で書くところがあって、最後のValidation Testなどは典型ですが、もともとIaCはアプリの開発のテストの手法、ソースコードの管理手法をインフラに持ってきたものだったので、それを最大限使えるのは、やはりアプリと同じ言葉で書かれているCDKなわけです。

(次回につづく)