0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ECS タスクに SSM の秘密情報を渡す — secrets.valueFrom と Terraform data source の注意点

0
Posted at

ECS Fargate のタスクに API キーや DB パスワードを渡すとき、タスク定義の environment に値を直接書くと、その値は ECS タスク定義 に残ります。タスク定義は aws ecs describe-task-definition で参照できます。さらに、Terraform で管理している場合は、Terraform state にも値が残ります。

つまり、「SSM Parameter Store に保存しているから安全」と考えていても、最終的に Terraform やタスク定義へ平文で渡してしまうと、別の場所にも秘密情報が残ります。

ECS には、SSM Parameter Store や Secrets Manager に保存した値を、タスク定義の secrets から参照する仕組みがあります。secretsvalueFrom に SSM パラメーターの ARN を指定すると、タスク定義には ARN だけが入ります。秘密の値そのものはタスク定義に入りません。値はタスク起動時に ECS が取得し、コンテナーの環境変数として注入します。

この記事では、ECS Fargate で SSM Parameter Store の秘密情報を secrets.valueFrom 経由で渡す構成を整理します。あわせて、Terraform の data source を使うべきでない場面、depends_on が必要になる場面、SSM の読み取り権限をどの IAM ロールに付けるべきかを説明します。対象は、Terraform 1.6 以降、AWS Provider 5.x、ECS Fargate、SSM Parameter Store です。

environmentsecrets.valueFrom の違い

ECS タスク定義でコンテナーに値を渡す方法は、大きく environmentsecrets の 2 つです。environment は非秘密の設定値には向いていますが、API キーや DB パスワードを入れると、その値はタスク定義にそのまま残ります。

"environment": [
  { "name": "API_KEY", "value": "sk-xxxxxxxx" }
]

Amazon ECS の secrets では、値そのものではなく、SSM Parameter Store や Secrets Manager の参照先を valueFrom に指定します。

"secrets": [
  {
    "name": "API_KEY",
    "valueFrom": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/siteforce/prod/anthropic/api-key"
  }
]

この構成では、タスク定義に入るのは SSM パラメーターの ARN です。API キーそのものは入りません。タスクが起動するときに、ECS が SSM Parameter Store から値を取得し、コンテナーに API_KEY という環境変数として注入します。

違いを整理すると、次のようになります。

残る場所 environment に直書き secrets.valueFrom
ECS タスク定義 秘密の値が入る ARN だけが入る
Terraform state 秘密の値が入る ARN だけが入る
コンテナー内の環境変数 秘密の値が入る 秘密の値が入る
値の取得タイミング タスク定義に値を登録する時点 タスク起動時
主な用途 非秘密の設定値 API キー、DB パスワード、外部サービスのトークン

重要なのは、secrets.valueFrom は「コンテナー内で値を読めなくする仕組み」ではないという点です。最終的には、コンテナー内の環境変数に秘密の値が入ります。アプリケーションからは通常の環境変数として読めます。secrets.valueFrom が減らすのは、コンテナーに届く前の段階で、タスク定義や Terraform state に秘密情報が残ることです。

SSM パラメーターの値は Terraform に渡さない

valueFrom に指定する SSM パラメーターは、Terraform で管理することもできます。ただし、Terraform が秘密の実値を扱うと、その値は Terraform state に残る可能性があります。そのため、state に秘密情報を残したくない場合は、Terraform では値そのものを扱わない方針にします。

Terraform で扱うのは、SSM パラメーター名、SSM パラメーター ARN、IAM policy、KMS key ARN、ECS タスク定義、ECS タスク実行ロールのような非秘密情報に限定します。API キーそのもの、DB パスワードそのもの、SSM SecureString の復号済みの値、Secrets Manager の secret string は Terraform に渡しません。

実際の値は、Terraform とは別のフローで投入します。たとえば、AWS CLI、専用パイプライン、初期セットアップ手順などです。

aws ssm put-parameter \
  --name "/siteforce/prod/anthropic/api-key" \
  --type SecureString \
  --value "$ANTHROPIC_API_KEY" \
  --overwrite

この分離により、Terraform は API キーそのものを受け取りません。したがって、Terraform state にも秘密の値が入りません。

PLACEHOLDERignore_changes を使う場合の注意点

Terraform で SSM パラメーターの「箱」だけを作るために、value = "PLACEHOLDER"ignore_changes = [value] を使う例があります。

resource "aws_ssm_parameter" "secrets" {
  for_each = toset(var.secret_names)

  name  = "${local.name_prefix}/${each.value}"
  type  = "SecureString"
  value = "PLACEHOLDER"

  lifecycle {
    ignore_changes = [value]
  }
}

この構成の意図は、Terraform で SSM パラメーターを作成し、実際の値はあとから aws ssm put-parameter --overwrite で投入することです。初回 apply で Terraform state に入るのは "PLACEHOLDER" です。実値を Terraform 変数として渡さなければ、初回 apply で API キーそのものが state に入ることは避けられます。

ただし、この構成を「実値が絶対に state に入らない方法」と考えるのは危険です。ignore_changes = [value] は、Terraform の差分判定で value の変更を無視する指定です。Terraform がその値を今後一切読まない、という意味ではありません。また、Terraform で aws_ssm_parameter を管理している以上、そのリソースは Terraform の管理対象です。一方で、実値は別フローで上書きします。つまり、Terraform 管理と別フロー管理が同じ SSM パラメーターに混在します。

この運用を採る場合は、次の点を明確にしておく必要があります。

Terraform が管理するのはパラメーターの名前と型である
実際の値は Terraform では管理しない
再 apply 時に Terraform が値を上書きしないようにする
実値を投入する手順を Terraform とは別に用意する

state に秘密情報を残さないことを強い要件にするなら、より安全なのは、Terraform が値を作らず、読まず、渡さない構成です。この記事では、ECS への受け渡しでは Terraform が値を読まない構成、つまり secrets.valueFrom に ARN だけを渡す構成を中心にします。

ECS タスク定義で valueFrom に SSM の ARN を渡す

ECS タスク定義では、非秘密の設定値は environment、秘密情報は secrets に分けます。Terraform では、container_definitionsjsonencode で組み立てることが多いです。その場合、変数側でも env_varssecrets を分けておくと扱いやすくなります。

resource "aws_ecs_task_definition" "services_api" {
  family                   = "${local.name_prefix}-services-api"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = 512
  memory                   = 1024

  execution_role_arn = aws_iam_role.ecs_task_execution.arn
  task_role_arn      = aws_iam_role.ecs_task_api.arn

  container_definitions = jsonencode([
    {
      name  = "services-api"
      image = "${aws_ecr_repository.services["services-api"].repository_url}:latest"

      environment = [
        for k, v in var.services["services-api"].env_vars : {
          name  = k
          value = v
        }
      ]

      secrets = [
        for k, v in var.services["services-api"].secrets : {
          name      = k
          valueFrom = v
        }
      ]

      logConfiguration = {
        logDriver = "awslogs"
        options = {
          awslogs-group         = aws_cloudwatch_log_group.services_api.name
          awslogs-region        = var.region
          awslogs-stream-prefix = "ecs"
        }
      }
    }
  ])
}

environment に入れるのは、ログレベル、実行環境名、リージョン、機能フラグなどの非秘密値です。secrets に入れるのは、API キー、DB パスワード、外部サービスのトークンなどです。たとえば、変数は次のような形にします。

services = {
  services-api = {
    env_vars = {
      APP_ENV    = "prod"
      LOG_LEVEL  = "info"
      AWS_REGION = "ap-northeast-1"
    }

    secrets = {
      ANTHROPIC_API_KEY = "arn:aws:ssm:ap-northeast-1:123456789012:parameter/siteforce/prod/anthropic/api-key"
    }
  }
}

このとき、Terraform state や ECS タスク定義に残るのは valueFrom の ARN です。秘密の値そのものは Terraform が読みません。タスク起動時に、ECS が SSM Parameter Store から値を取得します。

ECS の valueFrom に Terraform data source は不要

ECS の secrets.valueFrom に渡すのは、SSM パラメーターの ARN です。Terraform が SSM の値を読む必要はありません。したがって、ECS タスクに秘密情報を渡すだけなら、次のような data ブロック は不要です。

data "aws_ssm_parameter" "anthropic" {
  name            = "/siteforce/prod/anthropic/api-key"
  with_decryption = true
}

この data source を使うと、Terraform が SSM の値を読みます。with_decryption = true を指定していれば、Terraform は復号済みの値を扱います。その値は Terraform state に記録されます。

つまり、ECS で秘密情報を state に載せないための要点は、Terraform が値を読まないことです。valueFrom には、data source で読んだ値ではなく、SSM パラメーターの ARN を渡します。

secrets = [
  {
    name      = "ANTHROPIC_API_KEY"
    valueFrom = "arn:aws:ssm:ap-northeast-1:123456789012:parameter/siteforce/prod/anthropic/api-key"
  }
]

もし Terraform が aws_ssm_parameter リソースで SSM パラメーターを作っているなら、その arn 属性を参照できます。

secrets = [
  {
    name      = "ANTHROPIC_API_KEY"
    valueFrom = aws_ssm_parameter.secrets["anthropic/api-key"].arn
  }
]

ここで参照しているのは ARN です。秘密の値ではありません。ここが、data.aws_ssm_parameter.xxx.value を読む構成との大きな違いです。

data source に depends_on が必要になる場面

ECS の secrets.valueFrom だけであれば、data source は不要です。一方で、同じ SSM の値を Terraform 内で別のリソースへ渡したい場合、data source を使うことがあります。たとえば、Lambda の環境変数に SSM の値を注入したい場合です。

data "aws_ssm_parameter" "anthropic" {
  name            = module.secrets.parameter_names["anthropic/api-key"]
  with_decryption = true
}

このとき注意したいのが、data source の評価タイミングです。Terraform の data source は、既存のリソースを読むための仕組みです。対象の SSM パラメーターがまだ存在しない状態で読もうとすると、読み取りに失敗します。たとえば、クリーンな環境で初回 apply を実行し、module.secrets が SSM パラメーターを作る予定でも、data source がそれより先に評価されると失敗します。

このような場合、次のような挙動になりがちです。

初回 apply:
  data source が先に評価される
  SSM パラメーターがまだ存在しない
  読み取りに失敗する

2 回目 apply:
  1 回目の途中で SSM パラメーターが作られていれば
  data source が読めて成功する

同じ Terraform 設定なのに、1 回目は失敗し、2 回目は成功する状態です。CI/CD や環境再作成では扱いにくくなります。

このように、Terraform が依存関係を自動で判断できない場合は、depends_on で順序を明示します。

data "aws_ssm_parameter" "anthropic" {
  name            = module.secrets.parameter_names["anthropic/api-key"]
  with_decryption = true

  depends_on = [module.secrets]
}

depends_on = [module.secrets] を付けることで、Terraform は module.secrets の処理を待ってから data source を読みます。

これにより、クリーンな state からでも、次の順序を作りやすくなります。

SSM パラメーターを作成する
その後で data source が読む

ただし、depends_on が保証するのは Terraform 内の順序だけです。つまり、「SSM パラメーターを作ってから読む」ことは表現できますが、「実値が投入済みである」ことまでは保証しません。たとえば、PLACEHOLDER で SSM パラメーターの箱だけを作り、実値を別フローで投入する運用では、初回 apply で data source が読むのは "PLACEHOLDER" です。

実値を Terraform 内で読む必要があるなら、次のような順序が必要です。

1. terraform apply
   SSM パラメーター定義を作る

2. aws ssm put-parameter --overwrite
   実値を投入する

3. terraform apply
   data source が実値を読む

ただし、ここでも注意が必要です。data source で読んだ値は Terraform state に記録されます。state に載せたくない秘密情報は、Terraform で読まない構成にします。ECS であれば secrets.valueFrom、Lambda であれば実行時取得に寄せます。

SSM 読み取り権限はタスク実行ロールへ

secrets.valueFrom で SSM パラメーターを参照するタスク定義を書いても、権限がなければタスクは起動できません。

ここで混同しやすいのが、ECS の タスク実行ロールタスクロール です。

ロール 使う主体 主な役割
タスク実行ロール ECS エージェント ECR からの image pull、CloudWatch Logs へのログ送信、valueFrom の秘密取得
タスクロール コンテナー内のアプリケーション アプリケーションが AWS API を呼ぶときの権限

secrets.valueFrom の値を取得するのは、コンテナー内のアプリケーションではありません。タスク起動時に ECS が取得します。そのため、SSM Parameter Store の読み取り権限は、タスク実行ロールに付けます。タスクロールにだけ ssm:GetParameter を付けても、secrets.valueFrom の取得には使われません。その場合、タスクは起動時に失敗し、停止理由に ResourceInitializationErrorAccessDeniedException が出ることがあります。

タスク実行ロールへ付けるポリシーは、たとえば次のようになります。

data "aws_caller_identity" "current" {}

resource "aws_iam_policy" "ecs_execution_ssm_read" {
  name = "${var.project}-${var.environment}-ecs-execution-ssm-read"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ssm:GetParameters"
        ]
        Resource = [
          for p in aws_ssm_parameter.secrets : p.arn
        ]
      },
      {
        Effect = "Allow"
        Action = [
          "kms:Decrypt"
        ]
        Resource = var.ssm_parameter_kms_key_arn
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "ecs_execution_ssm_read" {
  role       = aws_iam_role.ecs_task_execution.name
  policy_arn = aws_iam_policy.ecs_execution_ssm_read.arn
}

SSM Parameter Store の SecureString を customer managed KMS key で暗号化している場合は、kms:Decrypt も必要です。

AWS managed key を使う場合、Secrets Manager を使う場合、KMS key policy をどう設計している場合などで、必要な権限は変わります。少なくとも、タスク実行ロールが対象の値を取得し、必要に応じて復号できるようにします。一方で、アプリケーションが実行時に AWS SDK で SSM Parameter Store を読む場合は、権限を付ける先が変わります。その場合、値を取得するのはコンテナー内のアプリケーションです。したがって、タスクロールに ssm:GetParameterkms:Decrypt を付けます。

整理すると、次のとおりです。

取得方式 値を取得する主体 権限を付けるロール
ECS secrets.valueFrom ECS タスク実行ロール
アプリケーションが SDK で実行時取得 コンテナー内のアプリケーション タスクロール

この 2 つを混同しないことが重要です。

Lambda の秘密情報取得との違い

secrets.valueFrom は ECS の仕組みです。Lambda には同じ指定はありません。Lambda で SSM Parameter Store の値を使う場合、state に秘密情報を載せたくないなら、基本は実行時取得に寄せます。

Lambda のアプリケーションが実行時に SSM Parameter Store や Secrets Manager を SDK で読む方法です。この場合、Lambda の環境変数には値そのものではなく、パラメーター名や secret ARN だけを入れます。Terraform は秘密の値を読みません。したがって、Terraform state にも値は入りません。

environment {
  variables = {
    ANTHROPIC_API_KEY_PARAMETER_NAME = "/siteforce/${var.environment}/anthropic/api-key"
  }
}
import os
import boto3

ssm = boto3.client("ssm")


def get_anthropic_api_key() -> str:
    resp = ssm.get_parameter(
        Name=os.environ["ANTHROPIC_API_KEY_PARAMETER_NAME"],
        WithDecryption=True,
    )
    return resp["Parameter"]["Value"]

一方で、デプロイ時に Terraform の data source で値を読み、Lambda の環境変数に注入する方法もあります。これは動きますが、この記事の観点では推奨形ではありません。値が Terraform state に入り、さらに Lambda 環境変数にも秘密情報が入るためです。

この方法は、実行時に SSM Parameter Store へ通信できない、または VPC endpoint / NAT Gateway を置かない判断をした、という制約がある場合の妥協案です。その場合でも、「state と Lambda 設定に秘密情報が残る」ことを受け入れたうえで選びます。state に秘密情報を載せたくないなら、Terraform の data source で値を読まない構成にします。

ECS の valueFrom と Lambda の data source 注入を比較すると、次のようになります。

方式 Terraform state 実行時の取得主体 環境変数に入るもの
ECS secrets.valueFrom ARN のみ ECS 秘密の値
Lambda 実行時取得 パラメーター名 / ARN のみ Lambda アプリケーション パラメーター名 / ARN
Lambda data source 注入 秘密の値が入る Terraform 秘密の値

Lambda data source 注入は、state に秘密情報が残ることを受け入れる場合の妥協案です。state に秘密情報を載せたくない場合は、Terraform で値を読まない構成にします。ECS では secrets.valueFrom、Lambda では実行時取得がそれに当たります。

apply 後の確認

構成ができたら、次の 3 点を確認します。

SSM パラメーターに実値が入っている
ECS タスクが ResourceInitializationError や AccessDeniedException で落ちていない
タスク定義の secrets に秘密の値ではなく ARN が入っている

SSM パラメーターの値は、次のように確認できます。

aws ssm get-parameter \
  --name "/siteforce/prod/anthropic/api-key" \
  --with-decryption \
  --query 'Parameter.Value' \
  --output text

PLACEHOLDER 運用をしている場合、実値を投入していなければ PLACEHOLDER が返ります。実値を投入するには、put-parameter --overwrite を実行します。

aws ssm put-parameter \
  --name "/siteforce/prod/anthropic/api-key" \
  --type SecureString \
  --value "$ANTHROPIC_API_KEY" \
  --overwrite

ECS タスク側は、停止理由を確認します。タスク実行ロールの権限が不足していると、タスクは STOPPED になり、停止理由に SSM や KMS のエラーが出ます。

aws ecs describe-tasks \
  --cluster <cluster> \
  --tasks <task-arn> \
  --query 'tasks[].[lastStatus,stoppedReason]' \
  --output text

ResourceInitializationErrorAccessDeniedException、KMS の復号エラーが出る場合は、まずタスク実行ロールを確認します。secrets.valueFrom の取得に使われるのは、タスクロールではなくタスク実行ロールです。

あわせて、タスク定義に秘密の値が入っていないことを確認します。

aws ecs describe-task-definition \
  --task-definition <task-definition-arn> \
  --query 'taskDefinition.containerDefinitions[].secrets'

ここに出るのは namevalueFrom です。秘密の値ではなく ARN が入っていれば、タスク定義に秘密情報を直接残さない構成になっています。

この構成で防げること

この構成で防げるのは、秘密情報が ECS タスク定義や Terraform state に平文で残ることです。ECS に渡すのは SSM パラメーターの ARN であり、Terraform が値を読みません。data.aws_ssm_parameter.xxx.value を使わない限り、SSM の実値は state に入りません。

また、secrets.valueFrom の取得はタスク実行ロール、アプリケーションによる実行時取得はタスクロールです。この違いを分けておくと、タスク起動時の SSM 取得失敗を調査しやすくなります。

この構成で防げないこと

一方で、この構成はコンテナー内での秘匿を保証しません。secrets.valueFrom は、コンテナーに環境変数として値を注入する仕組みです。コンテナー内のプロセスからは通常の環境変数として読めますし、アプリケーションが誤ってログに出せば CloudWatch Logs にも残ります。

また、SSM Parameter Store 自体は、復号権限があれば読めます。SecureString は KMS で暗号化されますが、ssm:GetParameter / ssm:GetParameters と、必要な kms:Decrypt を持つ主体は値を取得できます。SSM へのアクセス制御は IAM と KMS key policy で別途設計します。

depends_on は実値の投入までは保証しません。Terraform 内で「SSM パラメーター作成後に data source を読む」という順序だけを保証します。PLACEHOLDER 運用で実値投入前に data source を読めば、返る値は PLACEHOLDER です。さらに、data source で読んだ値は Terraform state に載ります。Lambda のデプロイ時注入のように Terraform 内で値を読む構成は制約がある場合の妥協案であり、state に載せたくない秘密情報は Terraform で読まず、ECS なら secrets.valueFrom、Lambda なら実行時取得に寄せます。

まとめ

ECS タスクに秘密情報を渡すなら、environment に値を直接書かず、secrets.valueFrom に SSM パラメーターの ARN を渡します。Terraform の data source で値を読まなければ、ECS タスク定義や Terraform state に残るのは ARN だけです。

SSM の実値を取得するのはタスク起動時の ECS なので、読み取り権限はタスク実行ロールに付けます。アプリケーションが SDK で実行時取得する場合はタスクロールです。data source や Lambda のデプロイ時注入は、値が state に載る構成だと明示して扱います。

secrets.valueFrom が減らすのは、秘密情報がタスク定義や Terraform state に残る経路です。コンテナー内で値が読めること、SSM 自体が権限を持つ主体から読めることは、別の観点として設計します。

参考情報

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?