はじめに — Terraformドリフトはなぜ怖いのか
Terraformでインフラをコード管理していても、誰かがAWSコンソールから直接変更してしまうことは日常的に起こります。これがいわゆる「ドリフト(drift)」です。
たとえば、障害対応中にセキュリティグループのインバウンドルールを手動で開放した。あるいは、検証のためにEC2のインスタンスタイプをコンソールから変更した。こうした変更はTerraformのstateには反映されず、コードと実態が乖離したまま放置されます。
従来、ドリフト検知の定番は terraform plan の定期実行です。しかし、このアプローチには明確な限界があります。
- 検知の遅延: cronで回すにしても数時間〜1日単位のバッチ処理。変更の瞬間から検知までにタイムラグが生まれる
-
コストの問題: 大規模環境では
plan1回に数分〜数十分かかる。実行頻度を上げれば API Rate Limit にも抵触しうる -
コンテキストの欠落:
planは「何が変わったか」はわかるが、「誰が・いつ・なぜ変えたか」は教えてくれない
本記事では、Falcoのプラグインシステムを活用してTerraformドリフトをリアルタイムに検知するOSSツール「TFDrift-Falco」のアーキテクチャと実装を解説します。
Falcoとは — ランタイムセキュリティからインフラ監視へ
FalcoはCNCF Graduatedプロジェクトのランタイムセキュリティツールです。もともとはeBPFを使ってLinuxカーネルレベルのシステムコールを監視し、コンテナの不審な挙動(予期しないシェル起動、機密ファイルへのアクセスなど)をリアルタイムに検知するためのツールとして広く使われてきました。
Falcoの強力なポイントはプラグインシステムにあります。v0.35以降、Falcoはカーネルイベントだけでなく、プラグイン経由でさまざまなイベントソースを取り込めるようになりました。特に重要なのが以下のプラグインです。
- aws_cloudtrail: AWS CloudTrailのイベントをリアルタイムに取得
- gcpaudit: GCP Cloud Audit Logsを取得
- azure_activity (カスタム): Azure Activity Logsを取得
つまり、「カーネルイベント監視」から「クラウドイベント監視」への拡張がFalcoの設計思想の中で自然に実現されています。TFDrift-Falcoはこの仕組みに乗る形で、クラウドの変更イベントをリアルタイムに受け取り、Terraformのstateと比較するというアプローチを取ります。
TFDrift-Falcoのアーキテクチャ全体像
TFDrift-Falcoのデータフローを図にすると以下のようになります。
各クラウドプロバイダのイベントログ(CloudTrail / Audit Logs / Activity Logs)がFalcoのプラグインを通じてgRPCストリームとして流れ込み、TFDrift-Falcoがリアルタイムに処理するという構成です。
対応するイベント数は以下のとおりです。
| プロバイダ | イベントソース | イベント数 | サービス数 |
|---|---|---|---|
| AWS | CloudTrail | 500+ | 40+ |
| GCP | Cloud Audit Logs | 170+ | 27+ |
| Azure | Activity Logs | 119 | 20+ |
実装の核心: クラウドイベントをドリフトアラートに変換する
1. Falco Subscriber — gRPCストリームの購読
TFDrift-Falcoはまず、FalcoのgRPC APIに接続してイベントストリームを購読します。pkg/falco/subscriber.goがその実装です。
// Subscriber subscribes to Falco outputs via gRPC
type Subscriber struct {
cfg config.FalcoConfig
client *client.Client
grpcConn *grpc.ClientConn
gcpParser *gcp.AuditParser // GCP Audit Log parser
azureParser *azure.ActivityParser // Azure Activity Log parser
}
func (s *Subscriber) Start(ctx context.Context, eventCh chan<- types.Event) error {
// Falco gRPC APIに接続し、イベントストリームを購読
// TLS / Insecure の両方に対応
// ...
}
Falcoから受信した各イベントは parseFalcoOutput メソッドでソース別に振り分けられます。
func (s *Subscriber) parseFalcoOutput(res *outputs.Response) *types.Event {
switch res.Source {
case "aws_cloudtrail":
return s.parseAWSEvent(res)
case "gcpaudit":
return s.gcpParser.Parse(res)
case "azure_activity":
return s.azureParser.Parse(res)
default:
return nil
}
}
2. Event Parser — CloudTrailイベントの正規化
AWSのCloudTrailイベントを例にとると、pkg/falco/event_parser.goでFalcoの出力フィールドから構造化イベントへの変換が行われます。
func (s *Subscriber) parseAWSEvent(res *outputs.Response) *types.Event {
fields := res.OutputFields
eventName := getStringField(fields, "ct.name") // 例: "AuthorizeSecurityGroupIngress"
eventSource := getStringField(fields, "ct.src") // 例: "ec2.amazonaws.com"
resourceType := s.mapEventToResourceType(eventName, eventSource)
userIdentity := types.UserIdentity{
Type: getStringField(fields, "ct.user.type"),
ARN: getStringField(fields, "ct.user.arn"),
UserName: getStringField(fields, "ct.user"),
}
changes := s.extractChanges(eventName, fields)
return &types.Event{
Provider: "aws",
EventName: eventName,
ResourceType: resourceType, // "aws_security_group"
ResourceID: resourceID,
UserIdentity: userIdentity, // 誰が変更したか
Changes: changes, // 何が変わったか
}
}
ここで重要なのは、CloudTrailのイベント名(AuthorizeSecurityGroupIngress)からTerraformのリソースタイプ(aws_security_group)へのマッピングです。
3. イベント→リソースタイプのマッピング
pkg/falco/mappings/配下に、サービスカテゴリごとのマッピング定義があります。
// pkg/falco/mappings/networking.go
var NetworkingMappings = map[string]string{
"AuthorizeSecurityGroupIngress": "aws_security_group",
"AuthorizeSecurityGroupEgress": "aws_security_group",
"RevokeSecurityGroupIngress": "aws_security_group",
"CreateVpc": "aws_vpc",
"DeleteVpc": "aws_vpc",
"CreateSubnet": "aws_subnet",
"CreateNatGateway": "aws_nat_gateway",
// ... 他多数
}
// pkg/falco/mappings/security.go
var SecurityMappings = map[string]string{
"PutRolePolicy": "aws_iam_role_policy",
"UpdateAssumeRolePolicy": "aws_iam_role",
"AttachRolePolicy": "aws_iam_role_policy_attachment",
"CreatePolicy": "aws_iam_policy",
"CreateKey": "aws_kms_key",
// ... 他多数
}
マッピングファイルは compute.go、networking.go、security.go、storage_and_database.go、other_services.go の5カテゴリに分かれており、合計500以上のCloudTrailイベントをカバーしています。
4. 競合解決 — 同名イベントの曖昧さを解消する
AWSでは異なるサービスが同じイベント名を使うケースがあります。たとえば CreateCluster はEKS、ECS、Redshift、ElastiCacheのいずれでも使われます。pkg/falco/mappings/conflicts.goがこの曖昧さを eventSource フィールドで解決します。
func ResolveEventSourceConflict(eventName string, eventSource string) string {
switch eventName {
case "CreateCluster", "DeleteCluster", "ModifyCluster":
if eventSource == "eks.amazonaws.com" {
return "aws_eks_cluster"
}
if eventSource == "ecs.amazonaws.com" {
return "aws_ecs_cluster"
}
if eventSource == "redshift.amazonaws.com" {
return "aws_redshift_cluster"
}
return "aws_eks_cluster" // デフォルト
case "CreateAlias", "DeleteAlias":
if eventSource == "lambda.amazonaws.com" {
return "aws_lambda_alias"
}
return "aws_kms_alias" // デフォルト
// ... 15以上の競合パターンに対応
}
}
5. Change Extractor — 変更属性の抽出
pkg/falco/change_extractor.goは、イベントの種類に応じて「何が変わったか」をTerraformの属性名レベルで抽出します。
func (s *Subscriber) extractChanges(eventName string, fields map[string]string) map[string]interface{} {
changes := make(map[string]interface{})
switch eventName {
case "ModifyInstanceAttribute":
if val, ok := fields["ct.request.instancetype"]; ok && val != "" {
changes["instance_type"] = val
}
case "PutBucketEncryption":
if config, ok := fields["ct.request.serversideencryptionconfiguration"]; ok {
changes["server_side_encryption_configuration"] = config
}
case "AuthorizeSecurityGroupIngress":
if val, ok := fields["ct.request.ippermissions"]; ok {
changes["ingress"] = val
}
// ... イベントごとの抽出ロジック
}
return changes
}
6. Drift Detector — Terraform Stateとの比較
最終段階はpkg/detector/event_handler.goです。抽出されたイベントをTerraformのstateと突き合わせ、ドリフトを判定します。
func (d *Detector) handleEvent(event types.Event) {
// 1. Terraform stateでリソースを検索
resource, exists := d.stateManager.GetResource(event.ResourceID)
if !exists {
// state に存在しない → 非管理リソース (unmanaged)
d.sendUnmanagedResourceAlert(&event)
return
}
// 2. 属性の差分を検出
drifts := d.detectDrifts(resource, event.Changes)
if len(drifts) == 0 {
return // 差分なし
}
// 3. ルール評価 → 重大度決定 → アラート送信
for _, drift := range drifts {
matchedRules := d.evaluateRules(resource.Type, drift.Attribute)
severity := d.getSeverity(matchedRules)
d.sendDriftAlert(event, drift, severity)
}
}
この検知はThree-way分析と呼ばれるアプローチを取っています。
- Unmanaged: クラウドに存在するがTerraform stateにない(手動作成)
- Missing: Terraform stateにあるがクラウドから削除された
- Modified: 両方に存在するが属性値が異なる(コンソールからの変更)
デモモードで試す
TFDrift-Falcoは --demo フラグでクラウドの認証情報なしに動作を体験できます。以下はデモモードで表示されるダッシュボードUIの画面です。AWS/GCPのドリフトイベントがリアルタイムに一覧表示され、重大度・プロバイダ・リソースタイプ・変更者・リージョンが一目で把握できます。

TFDrift-Falcoのイベント監視画面。AWS/GCPのドリフトをリアルタイムに検知・表示している
# リポジトリをクローン
git clone https://github.com/higakikeita/tfdrift-falco.git
cd tfdrift-falco
# デモモードで起動(クラウド認証不要)
go run ./cmd/tfdrift --demo
Docker Composeを使ったフルスタック環境の構築も簡単です。
# 設定ファイルを用意
cp config.yaml.example config.yaml
# 全サービスを起動(Falco + Backend + React UI)
docker compose up -d
# 各サービスへのアクセス
# Frontend: http://localhost:3000
# Backend: http://localhost:8080/api/v1
# WebSocket: ws://localhost:8080/ws
docker-compose.yml では、Falco(gRPCサーバ)、TFDriftバックエンド(APIサーバ)、React フロントエンドの3つのサービスが定義されています。
アラート出力のJSON例を示します。
{
"event_type": "terraform_drift_detected",
"provider": "aws",
"resource_type": "aws_security_group",
"resource_id": "sg-0123456789abcdef0",
"change_type": "modified",
"detected_at": "2026-03-29T10:15:30Z",
"severity": "high",
"region": "us-east-1",
"user": "john.doe",
"source": "tfdrift-falco",
"expected": { "ingress": [{"from_port": 443, "to_port": 443}] },
"actual": { "ingress": [{"from_port": 443, "to_port": 443}, {"from_port": 22, "to_port": 22}] },
"cloudtrail_event": "AuthorizeSecurityGroupIngress",
"falco_rule": "AWS Security Group Modification",
"version": "1.0.0"
}
Falcoルールの設定例
TFDrift-Falcoが使うFalcoルールの定義は rules/terraform_drift.yaml に格納されています。ルールの記述は宣言的で、どのCloudTrailイベントを監視するかを条件式で定義します。
- rule: AWS Security Group Modification
desc: Detect security group rule changes
condition: >
ct.name in ("AuthorizeSecurityGroupIngress", "RevokeSecurityGroupIngress",
"AuthorizeSecurityGroupEgress", "RevokeSecurityGroupEgress",
"CreateSecurityGroup", "DeleteSecurityGroup")
output: >
AWS Security Group modified (event=%ct.name user=%ct.user region=%ct.region)
priority: WARNING
source: aws_cloudtrail
tags: [terraform, drift, security, network]
- rule: AWS IAM Modification
desc: Detect IAM policy and role changes
condition: >
ct.name in ("PutRolePolicy", "DeleteRolePolicy", "UpdateAssumeRolePolicy",
"PutUserPolicy", "DeleteUserPolicy", "CreateRole", "DeleteRole")
output: >
AWS IAM modification detected (event=%ct.name user=%ct.user)
priority: CRITICAL
source: aws_cloudtrail
tags: [terraform, drift, iam, security]
まとめ・今後の展望
TFDrift-Falcoは、Falcoのプラグインシステムを活用することで terraform plan の定期実行では得られないリアルタイム性とコンテキスト(誰が・いつ・何を変更したか)を実現しています。
現在のv0.9.0では3大クラウド(AWS / GCP / Azure)に対応し、合計789以上のイベントを検知可能です。今後の開発ロードマップとしては以下を予定しています。
- OPA/Rego統合(Policy-as-Code): ドリフトの重大度や対応方針をポリシーとしてコード化(次回記事で詳しく解説予定)
- Grafanaダッシュボード連携: Loki + Promtailによるログ集約とアラート可視化
- v1.0.0 GA: 10,000+ノード対応のスケーラビリティ検証
リポジトリは以下からアクセスできます。スターやIssueでのフィードバックをお待ちしています。
👉 https://github.com/higakikeita/tfdrift-falco
この記事はTFDrift-Falcoシリーズの第1回です。第2回「OPA/Regoでドリフトポリシーをコード化する」、第3回「AWS/GCP/Azure 3クラウドのドリフト相関分析ダッシュボードを作った話」も予定しています。