terraformとクラウドの状態の差分を定期的に監視する仕組みを作った話

はじめに

イタンジSREチームの田渕です!

イタンジではクラウド管理のIaCツールとしてterraformを使っています。
terraformとクラウドの状態の差分を定期的に監視する仕組みを作ったところ、うまくワークし、かなり運用負荷が下がったため、皆様にも紹介します!

導入の背景

terraformを理想的に運用できていれば、常にtfstateの状態とクラウドの状態が一致しているはずですが、現実はいくつかの理由で差分ができてしまうことがあります。

代表的なものは以下の3つでしょうか。

  • applyする際に不適切な書き方をした場合に、applyは想定通りに成功するがplanをした際に差分が出てしまう。
  • awsやterraform provider等の仕様変更により、今までは差分がなかった部分で差分が出てしまう。
  • 稀に何らかの事情で手作業を行ってしまい、terraformへの反映も漏れてしまい差分が出てしまう。

確実に差分が出ないようにすることは難しいです。
しかし、差分が残ってしまうと、以下のような弊害があると考えられます。

  • すでに差分が出てしまっているtfファイルを変更した際に、想定していない部分の差分に気づかず、誤った変更を加えてしまう。
    • インシデントが発生するか、防げたとしてもオートメーション恐怖症の原因になり得る。
  • 差分が出てしまっているtfファイルを参考に構成を作り、クラウドの構成に差ができてしまう。
    • 構成ドリフトの発生に繋がる。
  • 差分に気づいた場合でも、いつまでは差分がなかったかを確かめる手段がないため、原因特定に時間がかかってしまう。

イタンジSREチームでもこのような悩みを抱えていました。
そこで、terraformとクラウドの状態の差分を定期的に監視する仕組みを作ってみました。

構成

terraform planを-detailed-exitcodeのオプション付きで実行し、差分があった場合にはSlackに通知するRubyスクリプトを作成しました。
そのスクリプトを、CircleCIのワークフローの定期実行という機能を使い定期的に実行しました。

CircleCIを使った理由は、そもそもterraformのCI/CD環境をCircleCI上で作っており、実装の都合が良かっただけなので、定期実行できるサーバであればどんなものでも問題ありません。

実際のスクリプト

実際に動いているものとは少し違いますが、重要な部分だけ取り出します。

# frozen_string_literal: true

require 'open3'
require 'shellwords'
require 'uri'
require 'net/http'
require 'json'


def check(target)
  command = "cd #{Shellwords.escape(target)} && terraform init && " \
            'terraform plan --detailed-exitcode'
  Shell.execute(command)
rescue StandardError
  SlackClient.post("#{target}に差分があるよ!!!!!!!!")
end

class Shell
  def self.execute(command)
    puts command
    o, e, s = Open3.capture3(command)
    raise e if s.exitstatus.positive?

    puts o, e
  end
end

class SlackClient
  def self.post(message)
    uri = URI.parse(ENV['SLACK_WEBHOOK_URL'])
    req = Net::HTTP::Post.new(uri)
    req['Content-Type'] = 'application/json'
    req.body = { text: message }.to_json
    Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
      http.request(req)
    end
  end
end

all_tf_directories = Dir.glob('**/*.tf').map { |f| File.dirname(f) }.uniq

all_tf_directories.each { |target| check(target) }

やっていることは簡単で、tfファイルが存在する全てのディレクトリで、terraform plan --detailed-exitcodeを実行し、exit codeが1以上だったらslackに通知しています。

slackの通知はディレクトリのみを通知しています。

f:id:ktabuchi:20200831225637p:plain
実際に監視が通知されたときのログ

CircleCIの設定ファイル

こちらも実際に動いているものとは少し違いますが、重要な部分だけ取り出します。

jobs:
  terraform_check:
    steps:
      - checkout
      - setup_env
      - install_terraform
      - run:
          name: terraform check
          command: ruby lib/terraform_check.rb

workflows:
  version: 2
  weekly:
    triggers:
      - schedule:
          cron: "0 22 * * 0"
          filters:
            branches:
              only:
                - master
    jobs:
      - terraform_check:
          context: AWS

先ほど作成したスクリプトを、定期的に実行しているだけです。

slackに通知が来た場合は、気づいた人が差分が出ないように修正しています。
週次で実行しており、最大でも数件程度しか差分が出てこないので、修正も数分から数十分程度で終わります。
差分があった際も、1週間前までは差分がなかったことが保証されるので、原因特定が非常にスムーズです。

おわりに

terraformは便利ですが、どうしても差分が出てしまうとお悩みの方は多いのではないでしょうか? それほど実装は大変ではなく、効果はかなり大きいと思いますので、是非試して見てください!

今後も、SRE業務中にいい感じのハックができたら、皆さまに紹介していきたいと思います。