最近の砂場活動その16: 機密データをパラメータストアに保存して、AWS Batch & ECS(ecspresso)上のアプリから参照する

アプリケーションのコード内に機密情報(例: DBのユーザー名やパスワード)を直接ハードコードしないのは当然として、環境変数経由で渡すことは多いと思います。CloudFormationのようなInfrastructure as Code(IaC)をしている場合、環境毎の設定をyamlなどで書きますが、「SlackのIncoming WebHookならまあいいか...」といった具合にIaCの設定内にハードコードされてしまうこともまあなくはないでしょう(一度もねえよという人だけ石を投げてください...)。

自分の趣味サービスもその辺、いい加減になっている箇所がありました。春の安全週間ということで、機密データをパラメータストアに保存するようにして、AWS BatchとECS(deployの設定にecspressoを利用)から参照するようにしたので、メモ書きしておきます。

機密データをパラメータストアに入れる

特に迷うところはないかと思う。パラメータのキー名を適当に書いてしまうと、あとで「これは何のやつだ...」となってしまうので、以下のように命名規則をある程度付けておくと安心。

  • /sentry.io/organizations/MY_ORG/projects/MY_PROJECT/keys/dns

一手間増えるけど、SecureStringで暗号化して保存している。

パラメータストアから機密データを取得するためのRoleを作る

ECS(AWS Batchも内部でECSが動くので、ECS用のものだけを準備しておけばよい)で動かす際に、パラメータストアから機密データを取得するための権限が必要になるので、CloudFormationで対応するRoleを事前に作っておく。元々ECSを動かすのに必要なポリシーのアタッチとパラメータストア関係のものを追加して定義する。Resourceに関しては適当に絞ってもよい。

Resources:
  BatchExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "ecs-tasks.amazonaws.com"
            Action: "sts:AssumeRole"
      Policies:
        - PolicyName: "ExecuteEcsTask"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "ecr:GetAuthorizationToken"
                  - "ecr:BatchCheckLayerAvailability"
                  - "ecr:GetDownloadUrlForLayer"
                  - "ecr:BatchGetImage"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                Resource: "*"
        - PolicyName: "GetSSMParameters"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "ssm:GetParameters"
                  - "secretsmanager:GetSecretValue"
                  - "kms:Decrypt"
                Resource: "*"
Outputs:
  BatchExecutionRole:
    Value: !GetAtt BatchExecutionRole.Arn
    Export:
      Name: !Sub "${AWS::StackName}:BatchExecutionRole"

AWS Batch内でパラメータストアの機密データにアクセスする

簡単。Environmentではなく、Secretsで指定してやる。パラメータストアにアクセスしたり、暗号化されたものをデコードするために、先程作ったロールをExecutionRoleArnに指定してあげればOK。

Parameters:
  IAMStackName:
    Type: String
Resources:
  MyJobDefinition:
    Type: "AWS::Batch::JobDefinition"
    Properties:
      Type: container
      ContainerProperties:
        Command:
          - "/bin/sh"
          - "/app/execute_my_job"
        Memory: 2500
        Privileged: false
        Environment:
          - Name: "AWS_REGION"
            Value: !Sub "${AWS::Region}"
          - Name: "MACKEREL_SERVICE_NAME"
            Value: "my-app-staging"
        Secrets:
          - Name: DB_USER
            ValueFrom: "/example.com/rds/user_name"
          - Name: DB_PASSWORD
            ValueFrom: "/example.com/rds/password"
          - Name: MACKEREL_APIKEY
            ValueFrom: "/mackerel.io/orgs/syou6162/api-key/aws-batch"
        ReadonlyRootFilesystem: false
        Vcpus: 1
        Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/my-image:latest"
        ExecutionRoleArn: 
          Fn::ImportValue: !Sub "${IAMStackName}:BatchExecutionRole"
      JobDefinitionName: my_job_definition
      RetryStrategy:
        Attempts: 1
      Timeout:
        AttemptDurationSeconds: 1800

AWS ECS(ecspresso)内でパラメータストアの機密データにアクセスする

ECSも昔はCloudFormationで管理していたが、deployがしやすいようにkayac/ecspressoでdeployするように最近変更した。

ecspressoもパラメータストアからSecretsを設定するのに対応している。こちらもAWS Batchの時と同じく、secretsexecutionRoleArnを指定してあげればよい。

{
  "containerDefinitions": [
    {
      "command": [],
      "cpu": 0,
      "dnsSearchDomains": [],
      "dnsServers": [],
      "dockerLabels": {},
      "dockerSecurityOptions": [],
      "entryPoint": [
        "/app/my_app.bin",
        "serve"
      ],
      "environment": [
        {
          "name": "POSTGRES_HOST",
          "value": "abc123.def456.us-east-1.rds.amazonaws.com"
        }
      ],
      "environmentFiles": [],
      "essential": true,
      "extraHosts": [],
      "image": "1234567890.dkr.ecr.us-east-1.amazonaws.com/my-app-api:latest",
      "links": [],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/logs/my-app-groups",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "ecs"
        },
        "secretOptions": []
      },
      "mountPoints": [],
      "name": "my-app",
      "portMappings": [
        {
          "containerPort": 7778,
          "hostPort": 7778,
          "protocol": "tcp"
        }
      ],
      "secrets": [
        {
          "name": "DB_USER",
          "valueFrom": "arn:aws:ssm:us-east-1:1234567890:parameter/www.example.com/rds/user_name"
        },
        {
          "name": "DB_PASSWORD",
          "valueFrom": "arn:aws:ssm:us-east-1:1234567890:parameter/www.example.com/rds/password"
        }
      ],
      "systemControls": [],
      "ulimits": [
        {
          "hardLimit": 65536,
          "name": "nofile",
          "softLimit": 65536
        }
      ],
      "volumesFrom": []
    },
    {
      "command": [],
      "cpu": 0,
      "dnsSearchDomains": [],
      "dnsServers": [],
      "dockerLabels": {},
      "dockerSecurityOptions": [],
      "entryPoint": [],
      "environment": [
        {
          "name": "MACKEREL_ROLES",
          "value": "my-app-staging:backend"
        },
        {
          "name": "MACKEREL_CONTAINER_PLATFORM",
          "value": "ecs"
        }
      ],
      "secrets": [
        {
          "name": "MACKEREL_APIKEY",
          "valueFrom": "arn:aws:ssm:us-east-1:1234567890:parameter/mackerel.io/orgs/syou6162/api-key/mackerel-container-agent"
        }
      ],
      "environmentFiles": [],
      "essential": false,
      "extraHosts": [],
      "image": "mackerel/mackerel-container-agent:latest",
      "links": [],
      "mountPoints": [],
      "name": "mackerel-container-agent",
      "portMappings": [],
      "secrets": [],
      "systemControls": [],
      "ulimits": [],
      "volumesFrom": []
    }
  ],
  "cpu": "1024",
  "executionRoleArn": "arn:aws:iam::1234567890:role/my-iam-BatchExecutionRole-ABCDEFGHIJK",
  "family": "my-app-family",
  "memory": "2048",
  "networkMode": "awsvpc",
  "placementConstraints": [],
  "requiresCompatibilities": [
    "FARGATE"
  ],
  "volumes": []
}