アプリケーションのコード内に機密情報(例: 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の時と同じく、secrets
とexecutionRoleArn
を指定してあげればよい。
{ "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": [] }