Terraformで監視のコード化 ~ 基盤構築からチームへの展開 Part1 ~

はじめに

こんにちは、2019年にSTROBELIGHTSチームからSmart-Cチームに異動しました、安藤です。
普段、バックエンドやフロントエンド周りの開発をしていますが、インフラ周りの改善もたまにやります。

クラウド上の監視設定の再利用性を高めるため、 複数のプロジェクトでTerraformの基盤構築からチームへの展開・運用を行いました。今回はCloudWatchに設定しているEC2の監視設定をTerraformでコード化したので、その際のディレクトリ構成や工夫した点についてお話します。

対象読者は次の条件のうちどれか一つを充たす方を想定しています

  • Terraformを触ったことがある
  • AWS上で監視設定を作ったことがある


概要

対象はRailsを使用している社内プロジェクト、クラウドはAWSを使用しています。
今回EC2のロールごとの監視設定がされているので、それをTerraformで書いていきます。

  • インフラ構成は、よくあるWebサービスのカタチ、今回はEC2に絞っています(表記はロール名) *1

    • Deploy
    • Puma
    • Fluentd
    • Sidekiq
  • 各ロールに対して、それぞれ下記 監視項目をGUIで設定しています。

    • CPU使用率
    • メモリ使用率
    • その他、Diskなど
  • アラートの発火条件

    • ex) CPU使用率 80%を超えた状態が5分間続いた
  • 今回、tfstateの保存先はS3にしました。


ディレクトリ構成

Ansibleみたいにロールを意識した構成を模索した結果、以下の構成にしました。 *2

この構成のメリットとしては、ロール単位での閾値や設定を柔軟にできます。
複数環境への適用は、workspaceを使います。*3

terraform
├── README.md
├── cloudwatch
│   ├── README.md
│   ├── backend.tf
│   ├── deploy.tf
│   ├── fluentd.tf
│   ├── main.tf
│   ├── puma.tf
│   ├── sidekiq.tf
│   ├── variables.tf
│   └── versions.tf
├── docker-compose.yml
├── modules
│   └── cloudwatch
│       ├── main.tf
│       └── variables.tf
└── terraform.tfvars.production

主要なファイルを少し紹介します。
実行ファイルはpuma.tf, fluentd.tf, sidekiq.tf, deploy.tfになり、
cloudwatch/main.tfには、各環境で使う共通のものや外部から変更されたくないものをlocalsとして定義して使っています。

provider "aws" {
  region = var.region
}

locals {
  project = "ProductName"

  roles = { 
    deploy = { 
      name = "Deploy"
    }  
    puma = { 
      name = "Puma"
    }   
    sidekiq = { 
      name = "Sidekiq"
    }   
    fluentd = { 
      name = "Fluentd"
    }   
  }
}

modules/cloudwatch/main.tf
Moduleでリソースのテンプレートを作りました。
アラーム名(alarm_name)説明(description) の形式を統一でき、さらに必要な変数が呼び出し先で設定されていない場合にエラーを出してくれます。

locals {
  alarm_name        = "${var.project}_${var.role}_${var.metric_name}_${var.state}"
  alarm_description = "Alarm if ${var.metric_name} too ${var.state} or metric disappears indicating instance is down"
}

resource "aws_cloudwatch_metric_alarm" "this" {
  count = length(var.instance_ids)

  dimensions = {
    InstanceId = var.instance_ids[count.index]
  }
  // 対象のインスタンス、test、 productionどの環境のアラームかが、通知でひと目で分かる
  alarm_name          = "${local.alarm_name}_${var.private_ips[count.index]}_${terraform.workspace}"
  comparison_operator = var.comparison_operator
  evaluation_periods  = var.evaluation_periods
  metric_name         = var.metric_name
  namespace           = var.namespace
  period              = var.period
  statistic           = var.statistic
  threshold           = var.threshold
  treat_missing_data  = var.treat_missing_data
  alarm_description   = local.alarm_description
  ok_actions          = var.ok_actions
  alarm_actions       = var.alarm_actions
}

puma.tfの一部
主にタグを指定してインスタンスを取得し、moduleを読み込んで、必要な変数に値を設定しています。

data "aws_instances" "role_puma" {

  // 対象とするインスタンスは以下のタグで指定(EC2上のタグに、キー名: Project, Nameを設定しています)
  // Nameはロール名です
  instance_tags = { 
    Project = local.project
    Name    = local.roles.puma.name
  }
}

module "cpu_high_puma" {
  source              = "../modules/cloudwatch"
  project             = local.project
  role                = local.roles.puma.name
  instance_ids        = data.aws_instances.role_puma.ids
  private_ips         = data.aws_instances.role_puma.private_ips
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  state               = "High"
  statistic           = "Average"
  period              = "300"
  threshold           = 80
  ok_actions          = var.ok_actions    // 外部から指定
  alarm_actions       = var.alarm_actions // 外部から指定
}


デプロイ

テスト環境への反映はローカルで手動で行い、本番環境はmasterにマージされた際に、CircleCIからデプロイするようにしています。
workflowは terraforrm init -> plan -> slack通知 -> CircleCI上でplan結果を目視で確認 -> approve -> apply

.circleci/config.ymlの一部

setup_terraform_variables:
  steps:
    - run:
        name: Setup Environment Variables
        # 環境変数の定義 
        command: |
          echo 'export AWS_ACCESS_KEY_ID="${TERRAFORM_AWS_ACCESS_KEY_ID}"' >> $BASH_ENV
          echo 'export AWS_SECRET_ACCESS_KEY="${TERRAFORM_SECRET_ACCESS_KEY}"' >> $BASH_ENV
          echo 'export AWS_DEFAULT_REGION="${TERRAFORM_DEFAULT_REGION}"' >> $BASH_ENV
terraform-setup:
    executor: terraform
    # workspaceの選択, init..etc
terraform-plan:
    executor: terraform
    steps:
      - checkout
      - setup_terraform_variables
      - attach_workspace:
          at: ~/repo
      - run:
          name: plan
          command: |
            source $BASH_ENV
            # exitcode, 0: nochanges, 1: error, 2: changes
            terraform plan -input=false -out=terraform.plan -detailed-exitcode cloudwatch > terraform.plan.result ; echo $? > exitcode
            cat terraform.plan.result
          working_directory: terraform
      - persist_to_workspace:
          root: ~/repo
          paths:
            - terraform
  terraform-apply:
    executor: terraform
    steps:
      - checkout
      - setup_terraform_variables
      - attach_workspace:
          at: ~/repo
      - run:
          name: apply
          command: |
            source $BASH_ENV
            terraform apply -input=false -auto-approve terraform.plan
          working_directory: terraform

工夫した点

planとapplyの結果を担保した点です。
terraform.planというファイルを出力し、Workspaceにおいて、terraform.planファイルを元にapplyすることで計画と実行に差分なく設定を反映することができます。

※exitcodeは別の用途で使うのでここでは無視して下さい

# plan
...
command: |
  terraform plan -input=false -out=terraform.plan -detailed-exitcode cloudwatch > terraform.plan.result ; echo $? > exitcode

...
# apply
command: |
  terraform apply -input=false -auto-approve terraform.plan


チームへの展開

実装してリリースした後、運用や横展開も考えた際に個人での運用ではなく、チームでの運用が望ましいため、モブプロを行いました。

モブプロ

  • 参加者
    • Ansible経験者が何人かいますが、ほとんどTerraformは使ったことないメンバー 2,3人
  • 簡単な説明
    • Terraformとは
    • バケット
    • tfstateについて
    • 実演
  • 実際に本番環境などの監視設定をTerraformで変更してもらう

イメージ f:id:AdwaysEngineerBlog:20200319160456p:plain

思いの外、準備に時間がかかったり、進行が中々思うように行かなかったところもありますが、概ねやるべきポイントは行えました。

本番の追加改修はほんの少しだったので、もう少し応用的なものをやったら良かったんですが、他に監視の改修箇所がほとんどなく、チーム運用を前提としていた場合、中身自体は80%くらいほどのもので、20%の改修箇所をわざと残すとかした方がいいのかなと感じました。

また他のプロジェクトの横展開をする際にサポートとして入ろうと考えていたんですが、
今回EC2ごとの監視でオートスケール対応はやっていないため、対応してから横展開するのが望ましいことだったので、まだ横展開はしていないです。。*4


おわりに

基盤を作る上でディレクトリ構成はとても重要で、一番悩んだ部分でした。
どの粒度で設定を適用させるか、またプロジェクトのインフラ構成や監視設定、workspaceを使うかどうかで大きく変わってきます。調べるといろんな方がベストプラクティスなどを上げていて大変参考になりました。
新しく技術を投入する際はサービスの責任者または、技術的な責任者と予めヒアリングや相談は必要で、色んな人を巻き込んで仕事を進めていくのは結構難しかったりしますが、まずは会議を設定してしまえばあとはどうにでもなります。

参考資料

qiita.com dev.classmethod.jp buildersbox.corp-sansan.com blog.mitsuruog.info note.com


 

次はGCPでの商用サービスを対象とした、監視設定のTerraformのコード化のサポート、監視項目の改善、チームへの展開を書こうと思います。
公開は4月か5月頃の予定です。

*1:PumaはApp/Webサーバー、SidekiqはBatchサーバーです。

*2:ググって、ベストな構成を色々調べた上ところ、ロールごとについては調べても出てこず、近い構成からヒントを得て作りました。ただこれが今回のベストプラクティスかどうかは、あまり自信がありません、ベターだとは思いますが。

*3:testやproduction環境などを容易に切り替えることができます。ただ、プロジェクトによって向き不向きがあると思います。

*4:いつかやる‼️ だと、やらないのでタスク化する