これは 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の活用が始まったばかりなので、色々いじって知見をためていきたいです。