/var/www/yatta47.log

/var/www/yatta47.log

やったのログ置場です。スクラップみたいな短編が多いかと。

CrowdStrikeセンサー更新でterraform planに差分が出る原因と対処法

ECSのタスク定義をTerraformで管理していて、何も変更していないのに terraform plan で差分が出てきたんですよね。

Day 1: パッチングツール実行 → terraform apply 成功(センサー v7.10)
Day 2: 何もしていない
Day 3: パッチングツールを再実行 → terraform plan → 差分あり!(センサー v7.11 に上がってた)

最初は「stateがズレた?」「誰か手動で変えた?」って思ったんですけど、どっちでもなかった。

原因

タスク定義パッチング方式の仕組みが原因でした。

パッチングツール実行時の流れ:

  パッチングツール実行
    ↓
  CrowdStrike API から最新センサーイメージを取得
    ↓
  タスク定義JSONに falcon-init コンテナを注入
    ↓
  パッチ済みJSON → Terraform が管理

ポイントは「CrowdStrike APIから最新のセンサーイメージを取得する」ステップです。パッチングツールを実行するたびに、その時点の最新バージョンが入ります。

  • 月曜にパッチすると falcon-sensor:7.10.0
  • 木曜にパッチすると falcon-sensor:7.11.0(CrowdStrike側で更新済み)

同じアプリ、同じTerraformコード、同じ手順でも、パッチ済みJSONの中身が実行日によって変わります。IaCの「同じコードからは同じ結果が出る」という前提が壊れているんですよね。

jq --sort-keys では解決しない

ここでよくある誤解があって、「JSONのキー順序の揺れだろうから jq --sort-keys で正規化すれば消えるはず」って思いがちなんですけど、これは的外れです。

キー順序の問題ではなく、JSONの中身(センサーイメージのURIやバージョン)が実際に変わっているので、正当な差分です。

対処法

選択肢は3つあります。

対処法 効果 トレードオフ
イメージパッチ方式(falconutil)に移行 根本解決 CI/CDパイプラインの変更が必要
ignore_changescontainer_definitions 全体を除外 差分は消える アプリ本体のコンテナ設定変更も検知できなくなる
パッチ済みJSONのバージョンを固定する運用 再現性は確保 センサーの自動更新を捨てることになる

根本解決: イメージパッチ方式への移行

根本的に解決するなら、イメージパッチ方式(falconutil)への移行が筋です。

イメージパッチ方式(falconutil)の流れ:

  CI/CD パイプライン
  1. docker build(通常通り)
  2. falconutil でセンサーを焼き込み
  3. docker push(パッチ済みイメージ)

  タスク定義: SYS_PTRACE を追加するだけ
  (センサーのバージョンはイメージタグで固定)

この方式だと、センサーがアプリイメージに焼き込まれます。タスク定義にはサイドカーもinitコンテナも入らないので、Terraformが管理するタスク定義JSONにセンサーバージョンが露出しません。

センサーバージョンの更新はアプリの再ビルド・再デプロイ時に行われるため、意図しないタイミングで差分が出ることがなくなります。

パッチ方式のまま差分ゼロにするTerraformコード

イメージパッチ方式への移行がすぐにできない場合、タスク定義パッチ方式のままでも差分ゼロを目指せます。パッチャーの出力JSONをTerraformに正しく取り込む方法です。

パッチャー出力JSONをfile()で管理する

パッチャーの出力はフルのタスク定義JSONです。container_definitions に渡すのはその中の containerDefinitions 配列だけなので、jsondecode で抽出します。

locals {
  # パッチャー出力はフルのタスク定義JSON
  # container_definitionsに渡すのはcontainerDefinitions配列だけ
  patched_task_def              = jsondecode(file("${path.module}/patched-task-def.json"))
  patched_container_definitions = jsonencode(local.patched_task_def.containerDefinitions)
}

resource "aws_ecs_task_definition" "app" {
  family                   = "my-app"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "256"
  memory                   = "512"
  execution_role_arn       = aws_iam_role.task_exec.arn
  task_role_arn            = aws_iam_role.task_role.arn

  # パッチ済みコンテナ定義(containerDefinitions配列を抽出済み)を渡す
  container_definitions = local.patched_container_definitions

  # CrowdStrikeが追加するボリュームをTerraformにも定義(必須)
  volume {
    name = "crowdstrike-falcon-volume"
  }
}

ここで注意が必要なのが volume ブロックです。パッチャーはタスク定義JSONに crowdstrike-falcon-volume を追加しますが、Terraform側にも同じ volume を書いておかないと plan で差分が出続けます。

パッチャーを通さずHCLで完全管理する

パッチャーを使わず、CrowdStrikeが追加する要素をすべてHCLで書く方法もあります。センサーイメージのURIとCIDをvariableで管理します。

variable "falcon_sensor_image" {
  description = "ECR上のFalconセンサーイメージURI(タグ含む)"
  type        = string
}

variable "falcon_cid" {
  description = "CrowdStrikeのCustomer ID"
  type        = string
  sensitive   = true
}

resource "aws_ecs_task_definition" "app" {
  family                   = "my-app"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "512"
  memory                   = "1024"
  execution_role_arn       = aws_iam_role.task_exec.arn
  task_role_arn            = aws_iam_role.task_role.arn

  container_definitions = jsonencode([
    {
      name      = "crowdstrike-falcon-init-container"
      image     = var.falcon_sensor_image
      essential = false
      mountPoints = [{
        containerPath = "/tmp/CrowdStrike"
        sourceVolume  = "crowdstrike-falcon-volume"
        readOnly      = false
      }]
    },
    {
      name      = "app"
      image     = "123456789.dkr.ecr.ap-northeast-1.amazonaws.com/my-app:latest"
      essential = true
      portMappings = [{ containerPort = 8080 }]
      entryPoint = [
        "/tmp/CrowdStrike/rootfs/lib64/ld-linux-x86-64.so.2",
        "--library-path", "/tmp/CrowdStrike/rootfs/lib64",
        "/tmp/CrowdStrike/rootfs/bin/bash",
        "/tmp/CrowdStrike/rootfs/entrypoint-ecs.sh"
      ]
      linuxParameters = {
        capabilities = { add = ["SYS_PTRACE"] }
      }
      environment = [
        { name = "FALCONCTL_OPTS", value = "--cid=${var.falcon_cid}" }
      ]
      mountPoints = [{
        containerPath = "/tmp/CrowdStrike"
        sourceVolume  = "crowdstrike-falcon-volume"
        readOnly      = true
      }]
      dependsOn = [{
        containerName = "crowdstrike-falcon-init-container"
        condition     = "SUCCESS"
      }]
    }
  ])

  volume {
    name = "crowdstrike-falcon-volume"
  }
}

この方式のメリットは、パッチャーの出力に依存しないのでTerraformだけで完結すること。デメリットは、CrowdStrikeのパッチャーが追加する要素(entryPoint の書き換え、mountPointsdependsOn 等)を自分で正確に再現する必要があること。パッチャーの仕様変更に追従する責任が自分に移ります。

どちらを選ぶか

  • パッチャーの出力JSONを信頼して取り込むなら file() 方式
  • パッチャーに依存したくない(CI/CDで毎回パッチャーを回すのが面倒)なら HCL完全管理方式
  • どちらも厳しければ次の ignore_changes で応急処置

ignore_changes は応急処置

ignore_changes は差分を消してくれますが、センサーのバージョン変更を検知できなくなります。「何も変えてないのに差分が出る」を消すために「本当に変わったときも検知できない」状態にするのは、問題をすり替えてるだけなんですよね。

応急処置としては使えるけど、長期運用するなら早めにイメージパッチ方式に移行した方がいいです。

これはCrowdStrike固有の問題ではない

同じ構造の問題は、外部APIから最新バージョンを取得してTerraformの管理対象に埋め込むパターン全般で起きます。

共通パターン:
  外部API → 最新バージョン取得 → Terraform管理のリソースに埋め込み
  → 実行日によって結果が変わる = IaCの再現性が壊れる

AMI IDを most_recent = true で取得するケースも同じ構造です。対策の基本は「外部から取得した値をどこで固定するか」を設計に織り込むことです。

まとめ

CrowdStrikeのタスク定義パッチング方式は、パッチング実行時に最新センサーバージョンを取得するため、terraform planの結果が実行日によって変わります。根本解決にはイメージパッチ方式(falconutil)への移行が有効です。

参考