Bicep で IaC を実現する(2)
前回のブログの続きです。
Bicep を利用する上での勘所を書きましょ。だけど今回の記事は文字だらけですw見栄えは最悪ですが、bicep をガチで触った人にだけは伝わる内容かと思います。
■依存性と更新の問題を回避する
依存性というのは Bicep でいうところの parentプロパティやscope、dependsOn、あと暗黙の依存、なんかがあります。いわゆるリソース同士が親子の関係になっている部分や依存性のあるデプロイ処理を指定することで、その処理が終わった後に実行しなさい、といった順序を意識して実装する部分ですね。
1番簡単な例でいうとHub&Spoke vNETを作成した後、Azure Firewallをデプロイしたいとします。この場合はAzure Firewallをデプロイするためには、仮想ネットワークおよびAzureFirewallSubnetが存在していないといけませんよね。ですので Azure FirewallをデプロイするBicepファイルの処理には dependsOn を付けてHub&Spoke vNETを作成する処理の vnet.bicep ファイルが終わってから実施するように考慮する必要があります。
ところが、ここには後々問題になるケースがあります。Spoke-vNET/SubnetにUDR(Route Table)を付与してAzure Firewall のPrivate IP addressへデフォルトルートを入れたい処理をどうするんだ?という問題が発生します。Azure Firewall 経由でSpoke-vNET/Subnetからの外部アウトバウンド通信は全て一任させたいわけです。このまま進めようとすると、Azure Firewallがデプロイ完了して初めてprivate ip addressが確定するため、その情報を既にデプロイが終わった spoke-vnetのsubnetへ関連付けさせたい、だけど更新ってどうするんだ?existing リソースに対する更新はできないよね?再度spoke-vnet/subnetの作り直し処理を行うのか?それって既存リソースで利用されているTCP通信が断絶されることにならない?ってなっちゃいますよね。Azure Portalから操作すれば特にこういった問題は起きないのですが、Bicep ではできれば冪等性を重視して Bicepファイルで一括で実施できるようにしたいですよね。
ここでの考え方は大きく2つあります。1つは Bicep ファイルを1つにしてしまい、そこでvNET/Subnet、Azure Firewall、Routetableを全て作成し、関連付けから何から全てをそのbicepファイル1つで集約させてしまい、暗黙の依存に委任させる、というやり方です。暗黙の依存、と聞くとちょっと細部がわからず困るケースも出てきますが、VS CodeのBicep拡張機能を入れておけば、黄色波線でdependsOnが必要な箇所なのか不要な箇所なのかをちゃんと教えてくれます。例えば module キーワードで別bicepに特定のパラメータを受け渡したい時に、そのパラメーター変数の値を入れているのはそのbicepファイル内に記載している resourceキーワードの処理で生成される場合、わざわざmodule キーワードの処理の最後にdependsOn を指定しなくてもよい、というのが暗黙の依存です。ただし、この1ファイルでまとめて記載する、というやり方は小規模開発においては問題ありませんが、中・大規模開発となり、一人ではなく複数人で開発を行うチーム開発には向きません。ソースレビューの範囲も広く、メンテナンスがしんどいからですね。
ではどうするかというと、Hub&Spoke vNETを1つの bicep ファイルで作成するのではなく、2つの bicep ファイルでhub-vNETを作成する部分とspoke-vNETを作成させる部分と分けるんです。こうすることで、hub-vnetを作成した後にAzure Firewallをデプロイし、private ip addressをspoke-vnet作成する処理のパラメーターとして渡してあげる、という構図が出来上がります。
気付いたと思いますが、そうなんです、Bicep を扱うためにはシーケンシャルマップを最初に設計することが重要なんです。どのリソースがどのリソースとの連携にどんな要素が必要かを最初に洗い出さないと、上記のような実装を進めていく上でデッドロックすることになります。こういったことがないよう、オフィシャルサイトではBicep ファイルを分ける場合には、なるべく疎結合可能な処理を実装することを心がけましょう、っとあります。つまりリソース単位でなるべく小分けにして疎結合可能な処理のみをbicepファイルに記載する、ということです。そうすれば修正する箇所と依存性を考慮する部分を最小単位で対応することができる、というわけです。
■システム運用時にカスタム更新が発生するものと機密性情報を扱うもの
単に動けばよい、というソフトウェア開発のマインドでいけば、ここはたいして考慮する必要はないのですが、IaC となると話は別です。なぜなら冪等性重視の観点からすれば何度デプロイしても同じ結果になることを重視しているため、システム運用も考慮した実装をしなければ単なる初期導入だけに利用される便利なツール、で終わります。
ではどんなシステム運用を想定した実装をすればよいのか、ここでは1番簡単な例を出すと、NSG(Network Security Group)の運用を出しましょう。Azure の初期構築を実施する時には、大抵のケースでログインポート(RDP/SSH)のみ許可し、それ以外はブロックする、といったホワイトリスト運用で開始されるケースが大半だと思います。ですが、特定のプロジェクトではDNSサーバーを立てるので53ポートを開けてほしい、とか、nginxを入れるから8080ポートを開けてほしい、といった個別依頼の要求がシステム運用時に起きます。その依頼を受けた情報システム部門は、NSGのルール設定を確認しては追加作業を行い、Subnetに関連付けさせている他のNSGの設定なども確認しながら作業を行うでしょう。こういった個別更新が起きるたびに、bicep ファイル本体であるソースを修正する、というのはヒューマンエラーも起きやすく、なるべくソースにハードコートされた修正は避けたいものです。
ではどうするかというと、loadJsonContentといった関数を使い、ソースとは別にjsonファイルから必要なkey:valueを外部ファイルから取得する、といった実装をします。こうすることで単なる定義ファイルの1つとして扱う運用ができます。ソース本体に手を下すことなく定義ファイルのみ更新する、なるべくこういった資材の管理をしたいですよね。
ただし、機密情報を扱うケースは別です。機密情報というのは企業によって定義が変わりますが、例えばVMにログインする際に使うパスワードはどうしましょうか。当然VM作成時にハードコートしてべた書きする、var変数に固定値を入れておく、なんてのは論外です。jsonの外部ファイルに保管する、としても、それが漏洩すれば誰だってログインできちゃうことになりますし、誤ってgithubへpush してしまった日にはもう、、となりますよね。
そこで登場するのがAzure keyvaultですね。keyvaultリソースを作成する際にはどうしてもパスワードそのものを記載してデプロイする必要がありますが、それ以降は全てgetSecret関数とSecureデコレータを使って安全に取り出す実装をしたいものです。それであればHSMの強固に守られたkeyvaultに安全管理を委任させ、ソースコードや変数やパラメーターファイルなどで管理する必要もありません。もしくはkeyvaultだけは別のリソースグループでAzure Portalから作成しておき、必要なシークレットのみワークアラウンド作業として手作業で管理形態を別にしておき、bicep ではkeyvaultにアクセス可能なユーザープリンシパルIDだけ使い、scopeでkeyvaultが配置されているリソースグループを既存参照で指定し、getSecret()とsecureデコレータのみの実装をする、というのもアリでしょう。
という具合に頻繁に更新がされるものは外部定義にする、更新はさほどではないかもしれないが機密性の高い情報を扱う必要がある場合はkeyvaultを利用する、といった実装を最初から心がけておきましょう。(keyvaultのgetSecret関数は、moduleキーワードでパラメーターを渡す時のみ利用できる関数、という仕様制限もあるので、bicep全体の処理順序にも影響するのでここは注意です)
■ scopeで追加する拡張機能は1番最後に実装し、個別管理できる状態にする
これはbicepファイルを複数に疎結合した配置で意識した中・大規模開発におけるお作法に近いですが、scopeで追加する処理の中では特に診断設定やRBAC、ロック、この3つは比較的よく利用される機能になります。便利なことにこれらの拡張機能と呼ばれるものは、リソースがデプロイされた後に追加することができます。つまり構築したいコントロールプレーンのリソースを全て整合性を維持した実装ができれば、1番最後にscope処理でRBACの権限設定や診断設定をすればよい、という発想になります。そして2つ目に記載した頻度の高い更新もRBACでは発生します。scopeによる拡張機能の追加はbicepファイル処理では1番最後の後付け処理として位置付け、RBACの処理のみjsonによる個別外部定義で反映できる状態にしておく、というのが管理方法として適切かと思います。
これをやっておかないとハマるケースとしては、例えば以下のようなケースです。最初にLog Analytics Workspaceを作成し、vnet作成の前処理として依存性を持たせます。なぜならvnetの診断設定を入れる際にLAWへデータを飛ばす設定をvnetの処理に追加したいからですね。だけどこれを先に実装してしまうと、後からAMPLS(Azure Monitor Private Link Scopeを追加し、Private Endpointを作り、Private DNS Zoneを作成する処理を行うと、最後にLAWのPublic Access Data Ingestを拒否させたい処理ができなくなってしまいます。LAWは既に存在するし、Public Access Data Ingestの設定はLAWのproperties処理内で記載する仕様になっているため、どうしたらよいかがわからなくなります。こうならないように、順序としてはvnetを先に作り、AMPLS/PE/DNS Zoneの作成とLAWの作成を行う順序に変更し、最後にvnetの既存リソースを参照して、それをscopeさせて診断設定を入れる、という順序にします。
長くなったのでこのあたりにしますが、他にも配列で実装した場合の参照時の罠などもありますが、1番重要なのはシーケンシャルマッピングを最初の設計時に意識し、どういったbicepファイルの管理が1番自分たちの運用において楽になるか、を意識して取り扱いましょう。