インフラストラクチャルールをコードとして記述すべき理由

あなたのチームには「どのセキュリティグループもSSH(ポート22)をインターネット全体に開放してはならない」というポリシーがあるとします。全員が同意しています。ドキュメントにも記載されています。誰かが印刷して壁に貼ったことさえあります。

ところがある金曜日の午後、開発者が簡単なテストのために新しいセキュリティグループを作成します。彼らは手早くテストしたいだけなので、CIDRブロックを0.0.0.0/0、ポート22に設定します。変更はそのまま通ります。月曜の朝、セキュリティチームが露出したSSHポートに関するアラートを送るまで、誰も気づきません。

このシナリオは、毎週、さまざまなチームで繰り返されます。不注意だからではなく、手動によるポリシー適用には限界があるからです。インフラストラクチャの変更が1日に複数回発生する場合、人間の記憶やドキュメントベースのルールに頼ることは、抜け穴を生むレシピです。

ドキュメントベースのポリシーの問題点

ほとんどのチームは、ドキュメントに保存されたポリシーから始めます。Googleドキュメント、Confluenceページ、共有ドライブにあるPDFなどです。これらのドキュメントには、「すべてのS3バケットは暗号化されなければならない」「承認されたAMIのみ使用可能」「すべてのリソースにコストセンタータグが必要」といった、あるべき姿が記述されています。

問題は、ドキュメントは何も強制しないことです。意図を記述するだけで、実行はしません。誰かがポリシーに違反する変更を行っても、ドキュメントはそれを止めません。ただそこにあり、誰かが確認するのを静かに待っているだけです。

ドキュメントはまた、乖離(ドリフト)します。誰かがポリシーを更新しても、古いバージョンが誰かのブックマークに残ったままになります。あるいはチームが成長し、新しいメンバーがそのドキュメントの存在を知らないこともあります。時間が経つにつれて、ドキュメントに書かれていることと実際にデプロイされているものとの間のギャップは広がります。

ポリシーをコードとして記述するということ

ポリシーアズコード(Policy as Code)とは、それらのルールを機械が読み取り、実行できる形式で記述することを意味します。「SSHを全世界に開放しない」というドキュメントの代わりに、すべてのインフラ変更をその制約に対して自動的にチェックするルールを書きます。

ポリシーは、アプリケーションコードと同様にファイルに格納されます。バージョン管理され、プルリクエストでレビューされます。テスト、更新、ロールバックが可能です。誰かがポリシーに違反する変更を提案すると、リソースが作成される前にパイプラインがそれを検出します。

このドキュメントからコードへの移行は、チームがルールと関わる方法を変えます。ポリシーはエンジニアリングワークフローの一部となり、監査の際に誰かがチェックする後付けのものではなくなります。

Open Policy Agent を用いた具体例

具体的に見ていきましょう。Open Policy Agent(OPA)は、ポリシーアズコードを記述するための人気ツールです。Regoという言語を使用します。以下は、どこからでもSSHアクセスをブロックするシンプルなポリシーです。

deny if {
    input.resource.type == "aws_security_group_rule"
    "0.0.0.0/0" in input.resource.cidr_blocks
    input.resource.port == 22
}

このルールは、誰かがポート22をすべてのIPアドレスに開放するセキュリティグループルールを作成しようとした場合、それを拒否(deny)としてマークすることを意味します。ポリシーファイルはリポジトリに置かれます。セキュリティグループルールを追加するプルリクエストが来ると、CIパイプラインが提案された変更に対してOPAを実行します。ルールが一致するとパイプラインは失敗し、開発者は即座にフィードバックを得られます。

逆に、SSHアクセスを特定のCIDRブロックのみ許可するルールを書くこともできます。

allow if {
    input.resource.type == "aws_security_group_rule"
    input.resource.cidr_blocks[_] != "0.0.0.0/0"
}

正確な構文は使用するツールやポリシー言語に依存しますが、パターンは同じです。ルールはコードであり、コードは自動的に実行されます。

別のアプローチ:TerraformユーザーのためのSentinel

チームがTerraformを広範囲に使用している場合、HashiCorpのSentinelはより緊密な統合を提供します。Sentinelポリシーは、Terraformの実行コンテキストに特化して記述されます。以下は、Sentinelでの同じSSH制限です。

import "tfplan"

allowed_cidrs = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]

main = rule {
    all tfplan.resource_changes as _, change {
        change.type is "aws_security_group_rule" implies
        change.change.after.cidr_blocks all allowed_cidrs
    }
}

このポリシーは、すべてのセキュリティグループルールがプライベートIP範囲のみを使用することをチェックします。誰かがパブリックCIDRブロックを使用しようとすると、ポリシーが変更をブロックします。

OPAとSentinelの違いは、主にエコシステムにあります。OPAは汎用的であり、Terraform以外の多くのツールで動作します。SentinelはHashiCorp製品と深く統合されているため、すでにそのエコシステムにいる場合はセットアップが簡単です。しかし、核となる考え方は同じです。ポリシーはパイプラインで実行されるコードである、ということです。

ポリシーファイルの保存場所

ポリシーファイルを保存する場所には、主に2つの選択肢があります。

インフラストラクチャコードと同じリポジトリ。 これにより、ポリシーはそれらが統制するリソースの近くに置かれます。誰かがインフラを変更するとき、同じプルリクエスト内で関連するポリシーを確認できます。これはチーム固有のポリシーに適しています。

専用のポリシーリポジトリ。 これにより、組織全体のすべてのポリシーを一元管理できます。プラットフォームチームがリポジトリを管理し、複数のインフラリポジトリがそこからポリシーをプルします。これは、チーム間で変わるべきではない組織全体のコンプライアンスルールに適しています。

どちらのアプローチも有効です。ポリシーアズコードが初めてであれば、同じリポジトリのアプローチから始めてください。複数のチームにわたって一貫してポリシーを適用する必要が出てきたら、専用リポジトリに移行します。

始めるための実践的チェックリスト

ポリシーの記述に飛び込む前に、この簡単なチェックリストを実行してください。

  • 最も違反されているポリシー上位3つを特定する。 すべてを一度にコード化しようとしないでください。最も問題やリスクを引き起こしているルールを選びましょう。
  • 1つのツールを選ぶ。 ツール間の柔軟性を求めるならOPAから始めましょう。Terraformに深く投資しているならSentinelから始めましょう。
  • 1つのポリシーを書き、手動でテストする。 既知の違反に対して実行し、問題を正しく検出することを確認します。
  • CIパイプラインにポリシーチェックを追加する。 警告だけでなく、ビルドをブロックするようにします。警告は無視されます。
  • レビューと反復を行う。 1週間後、ポリシーが予期しないものを検出していないか確認します。誤検知(false positive)を調整します。

真の価値はワークフローにある

選択するツールよりも、採用するワークフローの方が重要です。ポリシーがコードになれば、アプリケーションコードと同じ扱いを受けます。レビュー、テスト、バージョン管理、そして時間をかけて改善されます。ポリシーが誤検知を引き起こした場合、誰かがそれを修正するプルリクエストを開きます。新しいコンプライアンス要件が来れば、誰かが新しいルールを書き、同じパイプラインを通じてリリースします。

このワークフローは、ポリシーの意図と実際の適用との間のギャップを排除します。「SSHを全世界に開放しない」というルールは、誰かが確認するのを忘れるかもしれないドキュメントではなくなります。それは、インフラが変更されるたびに毎回実行されるコードの一行です。これが、チームがルールに従っていることを願うことと、実際に従っていることを知ることの違いです。