これは Datadog Advent Calendar 2019 13日目の記事です。
門脇(@blac_k_ey)です。
PYXISのデータ基盤チームでSREっぽいことを行っている傍ら、インフラチームとして横断的なインフラの管理・運用を行っています。
モチベーション
最近PYXISチームではモニタリングツールとしてDatadogの採用が進んでいます。
PYXISのインフラは主にAWSを利用しており、DatadogのAWS Integrationを導入することで各種サービスのメトリクスを取得することが可能になります。
ここで課題になるのが弊社のAWSのアカウント構成です。
PYXISチームのAWSアカウント管理は以下のような構成になっています。

このように、基幹となるAWSアカウントからAssumeRoleを使って、各プロジェクトで利用しているAWSアカウントにログインするという形を取っています。
Terraformで各プロジェクトアカウントに対してAWS Integrationを導入しようとしたとき、アカウントひとつひとつにTerraform用のユーザを用意することを考えました。
しかし、この場合だと各アカウントにユーザを用意する必要があるためcredentialの管理が大変ですし、stateファイルも各プロジェクトに散らばってしまうので管理が難しくなってしまいます。
なるべく最小の手間でAWS Integrationを導入したいため、これでは骨が折れます。

そこで、今回は以下のような構成を取りました。

基幹アカウントにTerraform用ユーザ、各プロジェクトのアカウントにTerraformを利用できる権限を持ったRoleを用意し、AssumeRoleできるようにします。
これにより、credentialの管理は基幹アカウントのユーザだけになり、stateファイルもいちアカウントにまとめることができました。
必要なものはプロジェクトアカウントのRoleと、基幹→プロジェクトアカウントにAssumeRoleする権限だけになりますが、この部分は弊社の情シスがAWSアカウント作成時に用意してくれるものです。
AWS Integrationの導入のスピードも早まり、Credential管理などの運用の負担もかなり減りました。
以下から、実際のTerraformを見ていきます。
Terraformの準備
表題の通り、Terraformを使ってDatadogのAWS Integrationを設定していきます。
結論から入ってしまいますが、以下は今回の成果物となるTerraformです。
main.tf
terraform {
required_version = "= 0.12.12"
backend "s3" {
bucket = "account-a-terraform"
workspace_key_prefix = "datadog_aws_integration_terraform"
key = "terraform.tfstate"
region = "ap-northeast-1"
role_arn = "arn:aws:iam::111111111111:role/TerraformLoginRole"
}
}
provider "aws" {
version = "~> 2.24.0"
region = local.aws_region
assume_role {
role_arn = var.workspace_iam_role_arn[terraform.workspace]
}
}
provider "datadog" {
version = "~> 2.4.0"
api_key = var.datadog_api_key
app_key = var.datadog_app_key
}
##### AWS #####
data "aws_iam_policy_document" "datadog_integration_iam_policy_document" {
# https://docs.datadoghq.com/integrations/amazon_web_services/?tab=allpermissions#datadog-aws-iam-policy
statement {
sid = "all"
effect = "Allow"
actions = [
"apigateway:GET",
"autoscaling:Describe*",
"budgets:ViewBudget",
"cloudfront:GetDistributionConfig",
"cloudfront:ListDistributions",
"cloudtrail:DescribeTrails",
"cloudtrail:GetTrailStatus",
"cloudwatch:Describe*",
"cloudwatch:Get*",
"cloudwatch:List*",
"codedeploy:List*",
"codedeploy:BatchGet*",
"directconnect:Describe*",
"dynamodb:List*",
"dynamodb:Describe*",
"ec2:Describe*",
"ecs:Describe*",
"ecs:List*",
"elasticache:Describe*",
"elasticache:List*",
"elasticfilesystem:DescribeFileSystems",
"elasticfilesystem:DescribeTags",
"elasticloadbalancing:Describe*",
"elasticmapreduce:List*",
"elasticmapreduce:Describe*",
"es:ListTags",
"es:ListDomainNames",
"es:DescribeElasticsearchDomains",
"health:DescribeEvents",
"health:DescribeEventDetails",
"health:DescribeAffectedEntities",
"kinesis:List*",
"kinesis:Describe*",
"lambda:AddPermission",
"lambda:GetPolicy",
"lambda:List*",
"lambda:RemovePermission",
"logs:Get*",
"logs:Describe*",
"logs:FilterLogEvents",
"logs:TestMetricFilter",
"logs:PutSubscriptionFilter",
"logs:DeleteSubscriptionFilter",
"logs:DescribeSubscriptionFilters",
"rds:Describe*",
"rds:List*",
"redshift:DescribeClusters",
"redshift:DescribeLoggingStatus",
"route53:List*",
"s3:GetBucketLogging",
"s3:GetBucketLocation",
"s3:GetBucketNotification",
"s3:GetBucketTagging",
"s3:ListAllMyBuckets",
"s3:PutBucketNotification",
"ses:Get*",
"sns:List*",
"sns:Publish",
"sqs:ListQueues",
"support:*",
"tag:GetResources",
"tag:GetTagKeys",
"tag:GetTagValues",
"xray:BatchGetTraces",
"xray:GetTraceSummaries",
]
resources = [
"*",
]
}
}
resource "aws_iam_policy" "datadog_integration_iam_policy" {
name_prefix = "DatadogAWSIntegrationPolicy"
policy = data.aws_iam_policy_document.datadog_integration_iam_policy_document.json
}
resource "aws_iam_role_policy_attachment" "datadog_integration_iam_role_policy_attachment" {
policy_arn = aws_iam_policy.datadog_integration_iam_policy.arn
role = aws_iam_role.datadog_integration_iam_role.name
}
data "aws_iam_policy_document" "datadog_integration_assume_role_iam_policy_document" {
statement {
actions = [
"sts:AssumeRole",
]
principals {
identifiers = [
"arn:aws:iam::${local.datadog_account_id}:root",
]
type = "AWS"
}
condition {
test = "StringEquals"
values = [
datadog_integration_aws.aws_integration.external_id,
]
variable = "sts:ExternalId"
}
}
}
resource "aws_iam_role" "datadog_integration_iam_role" {
name = "DatadogAWSIntegrationRole"
assume_role_policy = data.aws_iam_policy_document.datadog_integration_assume_role_iam_policy_document.json
}
##### Datadog #####
resource "datadog_integration_aws" "aws_integration" {
account_id = var.aws_account_id[terraform.workspace]
account_specific_namespace_rules = {}
filter_tags = [
"datadog-enabled:true",
]
host_tags = [
"aws_account_name:${terraform.workspace}",
]
role_name = local.datadog_aws_integration_role_name
}
variables.tf
locals {
aws_region = "ap-northeast-1"
datadog_account_id = "464622532012" # DatadogのアカウントID
}
variable "workspace_iam_role_arn" {
type = map(string)
default = {
account-a = "arn:aws:iam::111111111111:role/TerraformLoginRole"
account-b = "arn:aws:iam::222222222222:role/TerraformLoginRole"
}
}
variable "aws_account_id" {
type = map(string)
default = {
account-a = "111111111111"
account-b = "222222222222"
}
}
variable "datadog_api_key" {
type = string
}
variable "datadog_app_key" {
type = string
}
重要な部分をかいつまんで説明します。
AWS Integrationから参照されるIAMロールを設定
AWS Integrationで各サービスのメトリクスを取得するために必要な権限を追加していきます。
必要な権限はDatadogのドキュメントに記載されており、AWSのサービス追加やDatadog側のIntegrationの追加などで時々変更がありますので、監視したいサービスの権限が含まれているかよく確認しておきましょう。
data "aws_iam_policy_document" "datadog_integration_iam_policy_document" {
# https://docs.datadoghq.com/integrations/amazon_web_services/?tab=allpermissions#datadog-aws-iam-policy
statement {
sid = "all"
effect = "Allow"
actions = [
"apigateway:GET",
"autoscaling:Describe*",
"budgets:ViewBudget",
...
]
resources = ["*"]
}
}
また、Datadogが管理しているAWSのアカウントからAssumeRoleできる権限を追加します。
data "aws_iam_policy_document" "datadog_integration_assume_role_iam_policy_document" {
statement {
actions = ["sts:AssumeRole"]
principals {
identifiers = ["arn:aws:iam::${local.datadog_account_id}:root"]
type = "AWS"
}
condition {
test = "StringEquals"
values = [datadog_integration_aws.aws_integration.external_id]
variable = "sts:ExternalId"
}
}
}
DatadogにAWS Integrationを設定
TerraformのDatadog Providerを使ってAWS Integrationを設定します。
resource "datadog_integration_aws" "aws_integration" {
account_id = var.aws_account_id[terraform.workspace]
account_specific_namespace_rules = {}
filter_tags = [
"datadog-enabled:true",
]
host_tags = [
"aws_account_name:${terraform.workspace}",
]
role_name = local.datadog_aws_integration_role_name
}
filter_tags を使うことで、指定のタグを持ったEC2インスタンスのみを監視下に置くことが可能です。
これにより必要以上にHost単位の課金がかかることを抑えられます。
host_tags はこのAWS Integrationから送信されたメトリクスにタグを振ることができます。
ここではアカウント名を追加することで、どのアカウントから送られてきたメトリクスか分かりやすくしています。
また、ここで重要になるのが role_name をローカル変数から取ってきているという点です。
前項でIAMロールの定義を行っているのでそこからロール名を引っ張りたいところです。
しかし、IAMロールがこの aws_integration の external_id を参照しているため、循環参照となってしまうため無効な定義となってしまいます。
Workspacesでアカウントごとの差分とStateを管理
TerraformにはWorkspacesという機能があります。
これを使うことで、本番・ステージング…といった環境ごとにState管理できるようになります。
今回はAWSアカウント名をworkspace名として管理していきます。
$ terraform workspace new account-a $ terraform workspace new account-b
workspaceごとに設定値を変更したい場合があります。
その場合はvariablesを map とし、workspace名をキーとしてデフォルト値をもたせます。
variable "workspace_iam_role_arn" {
type = map(string)
default = {
account-a = "arn:aws:iam::111111111111:role/TerraformLoginRole"
account-b = "arn:aws:iam::222222222222:role/TerraformLoginRole"
}
}
あとは変数の利用側を var.mapの変数名[terraform.workspace] とすればよいです。
provider "aws" {
version = "~> 2.24.0"
region = local.aws_region
assume_role {
role_arn = var.workspace_iam_role_arn[terraform.workspace]
}
}
上記の設定により、ひとつのIAMユーザから複数のアカウントにAssumeRoleし、Terraformを適用できます。
また、
terraform {
required_version = "= 0.12.12"
backend "s3" {
bucket = "account-a-terraform"
workspace_key_prefix = "datadog_aws_integration_terraform"
key = "terraform.tfstate"
region = "ap-northeast-1"
role_arn = "arn:aws:iam::111111111111:role/TerraformLoginRole"
}
}
GitLab CI で terraform play/apply を自動化する
以上でTerraformの準備は整いました。
GitLab CIを使ってデプロイを自動化していきましょう。
stages:
- plan
- apply
image:
name: hashicorp/terraform:0.12.12
entrypoint: [""]
## 環境変数の設定
.env_account-a: &env_account-a
variables:
AWS_ACCOUNT_NAME: account-a
.env_account-b: &env_account-b
variables:
AWS_ACCOUNT_NAME: account-b
## terraform plan
.plan: &plan
stage: plan
script:
- terraform fmt -check=true -diff=true
- terraform init
- terraform workspace new $AWS_ACCOUNT_NAME || true
- terraform workspace select $AWS_ACCOUNT_NAME
- terraform validate
- terraform plan
artifacts:
paths:
- '.terraform'
plan account-a:
<<: *env_account-a
<<: *plan
plan account-b:
<<: *env_account-b
<<: *plan
## terraform apply
.apply: &apply
stage: apply
script:
- terraform apply -auto-approve
when: manual
only:
- master
apply account-a:
<<: *env_account-a
<<: *apply
dependencies:
- plan account-a
apply account-b:
<<: *env_account-b
<<: *apply
dependencies:
- plan account-b
CI設定のVariablesにTerraform用ユーザのcredentialを追加しておきましょう。
これで、新しいアカウントでAWS Integrationを導入したい場合は、TerraformのvariablesにRoleのARNなどを追加し、GitLab CIのジョブを追加すればOKですね。
デプロイしてみる
あとはGitLab CIのジョブを走らせるだけです!

↓
↓
↓

これで、AWS Integrationを複数のアカウントに展開することができました!
まとめ
- DatadogのAWS IntegrationをTerraformでデプロイするには
- AWSのIAM Roleを設定して、
- Datadog Providerの
aws_integrationを設定する
- 複数のアカウントにAWS Integrationを展開したい場合は
- 各アカウントにAssumeRoleできるユーザを用意するとcredentialとstateファイルの管理が楽になる
このほか、TerraformのDatadog ProviderではMonitorやDashboardの設定も可能なので、
各アカウント共通で使えるアラートや、ダッシュボードなどを用意するのも面白そうだなと思いました。
まだまだDatadogの活用が始まったばかりなので、色々いじって知見をためていきたいです。