コードによる管理がどうだったら良さそうか

これは CDK Advent Calendarの 6日目のエントリーです。

みなさんこんにちは。大村(@yktko) です。

ときどき Infrastructure as Code (IaC) を頑張った結果辛くなってしまったという話を聞いており、以前 [JAWS DAYS でこんな LT](https://jawsdays2021.jaws-ug.jp/timetable/track-e-1700/) をやらせていただいたことがあります。

* [AWS社員による怒涛のLTチャレンジ! Infrastructure as Codeに疲れたのでなんとかできないかCDKで色々やってみる](https://www.slideshare.net/AmazonWebServicesJapan/awslt-infrastructure-as-codecdk)

このLTでは勢いで話してしまったのですが、このエントリーでコードによる管理がどうだったら良さそうなのか、自分の経験を元に整理してみます。文中に出てくるプレゼン資料はこのLTからとっています。
なお、システムの運用は状況によって適切なやり方が異なります。これが一般に通用する正しい方法だとは思っていませんが、一つの考え方だと思って読んでいただけると幸いです。

ここでお話しするのは次の3つです。

* コードの理解にかかる時間を短くする
* 環境とコードを1:1対応させる
* 特定のツールに固執しない


# 最初に: IaCで本当にやりたかったのは何か

IaCと書いていますが、厳密な定義はここでは置いておきます。広くコードによるシステム構成の管理と考えます。
システム構成の状況をコードで把握でき、その構成と変更作業を（人が読むだけでない）実行可能なコードによって実現するということです。
IaCによって本当にやりたいのはなんでしょうか。
それは構成を管理することそのものではなく、管理対象のシステムによってなされているビジネスを遂行するため。そのシステムが安定的に稼働し、かつユーザーニーズに合わせて迅速に変更を受け入られるということだとここでは考えます。

システム構成の迅速な変更作業を行いたいのに、そうできない理由はどういったものでしょうか。
作業自体に時間がかかる、作業手順の確立に時間がかかる、確立した作業手順の文書化に時間がかかる、作業担当者のアサインに時間がかかる...。
自動化によって作業自体が高速に行われることに加えて、コードやプログラムを使うことで、再現性を持って変更作業ができれば、解決できる部分が多そうです。
ではコードによって自動化すれば、それだけで迅速に変更できるのでしょうか。コードの他に何が必要なのでしょうか。


# コードの理解にかかる時間を短くする

システムの構成を管理するためにはなんらかの記述が必要です。手作業の場合は手順書やパラメータシートであり、コードによる管理の場合はコードやテンプレートになります。これらの記述は増えれば増えるほど、理解しにくくなります。コードであっても文章であっても同じです。理解しにくいものは、作成者以外の人に（あるいは自分自身にも）その設計を伝えるのが難しくなります。人に伝わりにくいということは引き継ぎ作業に時間を要します。また、複数人でコードの意図を共有して管理することが難しくなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221438/119db5ad-5260-83f9-796d-da32cc0d5071.png)

システムに比べてシステムの担当者は永続的に存在するものではありません。転職する場合もありますし、同じ会社にいても転属や昇進などで同じ作業を継続的に続けられるとは限りません。スキルフルなエンジニアがコードによる管理の仕組みを作っても、それを他の人に引き継げないとメンテナンスができません。コードの変更に時間がかかるようになり、結果、迅速な変更ができないシステムになってしまいます。

このように考えると、迅速に変更できる環境を長期にわたって維持するためには、担当者への育成や引き継ぎをできるだけ容易にできる必要があると言えます。そのためには、できるだけ少ないドキュメントやコードで記述できると良さそうです。

その点 CDKではどうでしょうか。特にCFnと比較すると以下のような特徴があり、理解すべきコード量を減らすことに役立ちます。（もちろんCFnにもメリットがありますがここでは割愛します）

* 記述の絶対量が少ないので構成の把握が容易
* 普段使っている言語と同じなのでアプリケーション開発者がインフラを管理をするためにとっつきやすい
* エディタによるサポートが強力なので、リファレンスドキュメントの参照回数と試行錯誤が少なくなる

全リソースにタグづけする作業も、CDKではこのように1行で実現できます。この便利さ、明快さはCDKならではです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221438/fa3eccdc-6984-edb3-92d3-81e935cbe417.png)


コードの理解を促進するという意味では、CDK を使って社内用の共通ライブラリを作り、複雑な仕組みはラッピングして、簡易に使えるようにする、ということも一つのアイディアだと言えます。
ある程度は良いのですが、こちらも行き過ぎると迅速な変更を阻害する要因になってしまうことがあるため、注意が必要です。

ずいぶん昔の話ですが、私が以前の職場で担当したプロジェクトで、「小学生でもわかるように」という考えで設計書と手順書を作ったことがありました。これによって知見のない人が入ってきてもすぐに作業ができるように、という考えでした。しかしその結果は、ドキュメントが膨大になり、そのメンテナンスと維持にハイレベルエンジニアの多大な労力が必要になってしまいました。挙句、誤字脱字が1文字でもあると（これを防ぐのはさらに時間を要します）、担当者の作業がそこで停止してしまう（自分で判断できないので）、という現場が出来上がってしまいました。
この経験から、ある程度は担当者も内容を理解していないといけないと反省しました。手順書では全てのタスクを書くのではなく、作業順のポイントと参照すべきパラメータシートを明示する程度にとどめ、担当者には利用するコンポーネントの基本的な操作方法や設計の概要を理解するよう教育を行いました。これによって結果的に効率の良い作業ができるようになったという経験があります。

CDKのようなコードによる開発も、共通ライブラリを迅速に完全にメンテナンスし続けられる体制があれば良いのですが、そうでなければ、更新できない、内容の複雑な共通ライブラリが、迅速な変更の足枷になってしまいます。実際は、各システムの担当者がライブラリのコードや、それによって作られるシステム構成の詳細を理解して、場合によってはライブラリに手を入れられるようにしておいたほうがよいでしょう。そのためには、コードによる管理を行う各システムの担当者は標準のConstructを使った開発の知識は身に付けておき、共通ライブラリも担当者が自らメンテナンスできる程度に、あまり抽象化、パッケージ化しすぎない構成が良いのではないかと考えています。
標準Constructを使うことは学習上のメリットもあります。社内の共通ライブラリを充実させると、そのライブラリを学習するためのコンテンツなどを自ら拡充する必要があります。標準のConstructを使った開発であれば、Webに多くの情報があり、新たなコンテンツを作らなくても学習が容易にできます。
自社開発のライブラリと標準ライブラリのどちらをどの程度使うか、学習容易性を考慮して検討してみるのも良いのではないでしょうか。

# 環境とコードを1:1対応させる

CDK は汎用的なプログラミング言語で開発できるのがメリットですが、一方で、そのデザイン方法は通常のアプリケーション開発とはやや異なる考え方が必要になると考えています。これは私が元々インフラ系のエンジニアであり、アプリケーションコードを読み書きするのが慣れていないチームで利用する場合の経験から考えたことです。アプリケーション開発を普段からやっているメンバだけで考える場合はまた違った考え方になるかもしれません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221438/7bae48c1-07de-bedb-6a18-24218352fa59.png)

1つ目はConstructの定義とパラメータ値を離しすぎないという考え方です。開発、本番環境で同様の構成を違うパラメータで構築することは多いと思います。その場合、環境名でパラメータ群を分けて同じソースコード（ファイル）を使うことが多いでしょう。  たとえば [BLEA (Baseline Environment on AWS)](https://github.com/aws-samples/baseline-environment-on-aws) でも[そのような構成](https://github.com/aws-samples/baseline-environment-on-aws/blob/main/doc/DeployToControlTower_ja.md#6-1-%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E6%83%85%E5%A0%B1context%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B)をとっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221438/d579156e-78db-3c94-1e07-ea131a704477.png)

しかし、本当に開発環境と本番環境は同じ構成になるでしょうか。
例えば RDS の監視設定をしたとき、本番環境の RDS は止まらないことが前提なので全てのエラーイベントを通知しますが、開発環境の RDS はよく再起動を行うため、起動停止系の通知を送りたくないといった場合があります。この場合例えば EventBridge の RDS のイベント通知設定を本番と開発で変えることになります。
これを実装するには、例えば環境フラグのようなものを使って条件分岐したコードにすることが考えられます。この場合、コードには開発環境と本番環境の両方の設定が書かれており、実環境がどうなっているかは人間がコードを読んで判断する必要があります。少しであれば良いのですが、同じソースコードの中に、条件によって異なるリソースが増えてくると、コードを見ただけでは実際の構成がどうなっているのか判断しづらくなります。
そこで、あまりに差異が増える場合は、開発環境と本番環境のコードを複製して、似た内容の別々のソースコードファイルにしたほうが見通しがよくなります。さらにこの場合、各環境に設定するパラメータをスタックのコードの中に埋め込んでしまえば、対象のスタックの情報を知りたい時に、1つのファイルを読めば済みます。開発環境と本番環境の差異を確認したい場合はそれぞれのソースコードのdiffを取ればよいです。即値をソースコードに埋め込むのはちょっと抵抗があるかもしれませんが、実はこの方が構成を管理するためにはわかりやすいのではないかと思いますが、いかがでしょうか。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221438/158910b6-969d-2896-2d8e-ec8924e93964.png)

2つ目は、本当に同じもの以外には同じソースコード（ファイル）を使わないという考え方です。
アプリケーション開発ではコードのコピペは基本的に好ましくなく、同じ処理は1つのコードで記載することが推奨されます。では、インフラを定義するコードも同じ構成なら1つのスタックとして定義し、1つのファイルに記述するべきでしょうか。

たとえばSaaSを構築して、1アカウントに1テナントが存在するような場合を考えてみましょう。このとき、1つの標準テンプレート（CDKコード)を用意して、それぞれのアカウント（テナント）に環境を展開するような構成が考えられます。標準テンプレートを変更すれば、各テナントに全く同じ設定を一気にデプロイすることができます。
ですが、エンドユーザが違うシステムを運用していると、ユーザごとに異なる要望、要件が出てきて、特別対応をするシーンが出てきます。最初のうちは標準テンプレートに顧客ごとのフラグと条件分岐を追加するような方法で対応していくこともできますが、特別対応が増えていった結果、最終的に多数の条件分岐を含む複雑なコードになってしまうことが考えられます。
本当に全顧客で共通的に利用するものであれば（最初から見通すことは難しいですが）、標準テンプレートで管理しても良いでしょう。ですが、異なるエンドユーザに紐づくなど異なる要件が出てくる可能性がある場合は、結局テナントとそれを管理するコードを1:1で対応させて、テナントごとにあえて別のソースツリーを用意したほうが、要件に柔軟に対応できます。
マルチテナントにおける設計の共通化は一見正しいように見えますが、落とし穴があります。設計変更のためには複数テナントで要件を整合させる必要があり、設計を変更するためにテナント間の調整作業が増え、結果的に迅速な変更が行いにくい硬直的な仕組みを作り出してしまうということです。
設計の標準化や共通化は、全く同じ設定を全テナントに強制しなければいけないものだけにとどめ、あとは個別のテナントごとにコードを用意し、同種のコード間の整合はdiffで確認するといった構成の方が柔軟性を確保できるのではないかと考えています。

条件分岐でわかりにくくなる、といった表現をしてきましたが、具体的にどうなったらわかりにくいか、判断する指標がないか考えてみました。
CDKで書いているコードがわかりやすいかどうかを考える時、私が想定する一つの指標は「CDKコードをみて、[Well-Architectedレビュー](https://aws.amazon.com/jp/architecture/well-architected)ができるか？」というものです。
コードを書いている人以外がCDKコードを見たとき、その構成がイメージしやすいかという観点です。他のレビュー指標があればそれでも良いでしょう。
Well-Architectedの場合は特に[セキュリティ](https://wa.aws.amazon.com/wat.pillar.security.ja.html)、[信頼性](https://wa.aws.amazon.com/wat.pillar.reliability.ja.html)、[オペレーショナルエクセレンス](https://wa.aws.amazon.com/wat.pillar.operationalExcellence.ja.html)の一部は静的構成によるレビューを行います。こういったところで活用してみると良いのではないかと思います。
なお、CDKコードだけではConstructのデフォルト値や、CDKが「よしなに」やってくれる部分は分かりませんので、[先日のブログ](https://qiita.com/yktko/items/7bcc9df940194ee62d8c)に紹介したように、CFnテンプレートによる確認も合わせて行うと良いでしょう。これはCDKと組み合わせるのが良いと思います。CFnテンプレートだけで構成を確認するのはコード量が多くて大変です。

# 特定のツールに固執しないで柔軟に考える

構成管理を自動化するために考えられる手段には、大きく2つのパターンがあります。状態を記述して実行するものと、手順を記述して実行するものです。

* 状態を記述&実行する = CDK / CFn / Terraform ...
* 手順を記述&実行する = CLI / API(SDK) / 手順書 ...

手順は、プロセスや過程にフォーカスしています。システムのコンポーネントは、ある状態から特定の手順を経ると別の状態に移ります。たとえばEC2がない状態から AWS CLIで EC2を2台立ち上げれば、EC2が2台起動している状態になります。これは作業の過程や順序が大事である場合に必要です。たとえば複数サーバへのアプリケーションのBlue/Greenデプロイメントなどです。
また、構成管理では、動いているリソースを止めずに、継続的に変更していく場合も手順による作業が適しています。こちらも「止めない」という作業の過程が重要である場合と考えることができます。

状態は、結果にフォーカスしています。過程はどうあれ、最終的に予定した状態に落ち着いて欲しい場合です。たとえば、CloudFormationでEC2が2台起動している状態、を定義すると、前提の状態や過程がどうであれ、EC2が2台起動している状態に移行します。
こちらは、ある構成情報の静的な断面が、定義している状態と一致していることが優先される場合に適しています。
構成管理する上では現状の把握と計画の検討のため、実環境とは別にドキュメント化された構成情報が必要になりますが、状態を記述するツールはこの用途に適していることがわかります。一方で、過程をどのように変更するかはあまり重視されていません。データベースのような継続性（削除されないこと）が重要なコンポーネントの場合は、注意が必要です。（CloudFormationの場合はリソース変更の時に何を行うかchangesetで明記してくれます）。

CDKは状態を定義するツールです。
これまで便利な側面を紹介してきましたが、全部をCDKにすればよいかというとそうでもありません。状態を定義するものも、手順を定義するものも、他のツールも目的を達成できるなら柔軟に取り入れていくべきです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221438/addab7ff-5033-b5ac-db52-690f299ade70.png)

たとえばCDKで展開した環境に緊急対応が必要な場合や、設定値をマネジメントコンソールで試行錯誤したい場合。
この場合は、変更後に CFn Drift Detection で差分の検出が可能です。そこで得られたパラメータをCDKに反映して、cdk diffで改めて差分内容を確認(Drift Detectionした内容と一致しているはず)、これを再デプロイすればCDKコードと実環境が一致します。

CDKでデプロイする前にマネジメントコンソールで試行錯誤したい場合があります。
この場合は [Former2](https://former2.com/) で実環境からテンプレートを生成し、CfnInclude もしくは CDK コードを書き起こして、CDKからリソースを再作成するのが楽でしょう。すでにあるリソースをCFn Importする方法も考えられますが、CDKで生成したCFnテンプレート必要であったり、CDKの命名規則とリソース名が一致していなかったりなど、なかなか面倒が多いです。

そして、マネジメントコンソールで作ってしまった方が楽な場合もあります。
例えばStepFuntionsのステートマシン定義やCloudWatchのダッシュボード定義などは、GUIでその定義を作成することができ、結果をJSONで出力できます。本来はCDKコードで書いた方が良いのですが、編集は常にマネコンで行い、出力されたJSONをCDKコードにそのまま取り込んで、 再デプロイして辻褄を合わせる、という乱暴な方法もアリ...ではないかと思いますが、どうでしょう。（あまりおすすめはしませんが）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221438/2dda0dd5-12ac-7d5f-2c68-cf7f195d1f2e.png)


最終的にCDKによる状態管理から離れて手順による管理を使うこともアリでしょう。
AWS上の操作は構成を作ることだけでなく、オペレーションもあります。バックアップ取得作業や、デプロイ作業、稼働中のデータベースに対する設定変更などはCLIと実行手順書でやったほうがよい場合もあるでしょう。
構築でも、2-3クリックの簡易な手順であれば、手順書でもよいのではないでしょうか。たとえば SecurityHub や SystemsManager を Organizations 全体に適用する場合は、CDKで書くより[マネジメントコンソールによる操作の方が簡単(SecurityHubの例)](https://docs.aws.amazon.com/ja_jp/securityhub/latest/userguide/designate-orgs-admin-account.html#designate-admin-console)です。
Route53 の HostedZoneの作成のような、世界に1個しかないようなもの（一度作ったら基本的に再作成しないもの)も手作業の方がよいと感じています。これらは CDK で fromXXXX() メソッドを使うことで、CDK内で参照できるようになります。この手法は [BLEA (Baseline Environment on AWS)](https://github.com/aws-samples/baseline-environment-on-aws) でも[使っています](https://github.com/aws-samples/baseline-environment-on-aws/blob/68ec9ffdca7774ee5bff7ff5bc08cc016b0fcf6e/usecases/guest-webapp-sample/lib/blea-frontend-ssl-stack.ts#L33)ので参照してみてください。

```typescript:usecases/guest-webapp-sample/lib/blea-frontend-ssl-stack.ts 
    const appHostedZone = r53.HostedZone.fromHostedZoneAttributes(this, 'appHostedZone', {
      zoneName: props.domainName,
      hostedZoneId: props.hostedZoneId,
    });
```

# おわりに

コードによる管理がどうだったら良さそうか、CDK でなんとかするためにどんなことが考えられるか、私の経験をもとに書いてみました。ご参考になれば幸いです。
最初にも書きましたが、迅速な変更を実現するための手段としてどういう方法が適切かは、ビジネスのステージ、チームやご自身の立場、システム開発フェーズなどによっても変わってきます。
ここで書いたものはあくまで一例です。皆さんの環境に適したコードによる管理方法も、ぜひ教えていただきたいと思います。