QuipperにおけるTerraformの運用

f:id:quipper-ja:20161024115945p:plain

こんにちは、Engineeringチームの石村(kamatama41)です。Engineeringチームの主な役割はインフラ構築や監視、パフォーマンス改善などのいわゆるDevOpsやSREと言われる領域になります。 Quipperでは現在グローバル向けであるQuipper (School, Video)と日本向けのスタディサプリという二つのプロダクトを運用しており、それらのプロダクトはAWS上に構築され、Terraformを使って構成管理をしています。そこで、私達がTerraformを運用するために工夫している点を紹介したいと思います。

Terraformとは?

Vagrantなどを提供するHashiCorp社製のインフラ構成管理ツールで、AWSGCPなどが提供している各種クラウドサービスをリソースという単位で構築、コード化出来るツールになります。

(AWSのEC2インスタンスを5台立ち上げるための設定例)

resource "aws_instance" "app" {
    count = 5

    ami = "ami-408c7f28"
    instance_type = "t1.micro"
}

なぜTerraformを使うのか?

コード化することでGitHubによるコードレビュー、テスト、デプロイというCIのサイクルを適用できるようにする、いわゆるInfrastructure as Codeを実践するために使っています。

ディレクトリ構成

Terraformのファイル群を管理するGitHubリポジトリは、2016年10月現在以下のような構成を取っています。

├── environments
│   ├── common.tfvars
│   ├── management
│   ├── production
│   └── staging
│       ├── main.tf
│       ├── output.tf
│       └── terraform.tfvars
├── modules
│   ├── base_network
│   │   ├── vpc.tf
│   │   ├── subnet.tf
│   │   ├── variables.tf
│   │   ├── ...
│   ├── reverse_proxy
│   │   ├── ec2.tf
│   │   ├── security_group.tf
│   │   ├── variables.tf
│   │   ├── ...
│   ├── mongodb
│   │   ├── ...
│   ├── ...

Quipperでは本番環境、ステージング環境、社内システム用環境といった環境ごとにVPCを立てていますが、他環境への影響を最小限にするためVPCごとに独立したstateファイルを用意しています。 environments/(production|staging|management)の各ディレクトリが、それぞれの環境におけるTerraformコマンドの実行ディレクトリになります。main.tfには後述のmodulesを呼んだりその環境固有のリソースを定義します。 terraform.tfvarsはその環境のみで使う変数を記述し、一階層上のcommon.tfvarsは全環境で共通して使う変数を記述しTerraformの-var-fileオプションを使って読み込こむようにしています。 また、VPC IDなどの環境間で共有したい値はoutput.tfに記述してterraform_remote_stateを使って参照しています。

modulesは共通で使うmodule群を管理するためのディレクトリです。各moduleはreverse_proxyといったロール単位で作り、その中はec2.tfといったAWSのリソース単位でファイルを作っています。variables.tfでmoduleに渡されるべき変数を管理しています。

ラッパースクリプト

plan.sh, apply.sh, refresh.sh, import.shといったTerraformコマンドのラッパースクリプトを用意して、開発者やCircleCIはこのスクリプト経由でTerraformコマンドを実行するようにしています。スクリプト内では以下のような環境の切り替えや、安全性を高めるための処理を行います。

  • 環境に対応したstateファイルをリモート(S3 backend)から取得する
    • 取得後、一度リモートとの関連付けを無効にする
      • planやrefreshなどを実行したときに、S3上のstateファイルが即座に更新されないようにするため
      • 更新する場合は、実行前後のdiffとyes/noプロンプトを出したうえで、再度関連付けて、remote pushする
  • terraform getでmoduleを読み込む
  • applyコマンドをCircleCI以外ではできないようにバリデートする
  • 実行前*1にローカルの.terraformディレクトリを消す
    • 違う環境のstateファイルでrefreshなんかをやってしまった日には...((((;゚Д゚))))

例1: plan.sh

実際のものとはちょっと違いますが概ねこんな感じです。

#!/bin/bash -xe

ENV=$1
ENV_PATH=./environments/${ENV}
S3_BUCKET=quipper-terraform-state-files

# 前処理
rm -rf .terraform

# remote stateを取得、リモートとの関連付けを無効にする
terraform remote config \
  -backend=S3 -backend-config="bucket=$S3_BUCKET" -backend-config="key=${ENV}.tfstate"
terraform remote config -disable -state=./.terraform/${ENV}.tfstate

# module読み込み
terraform get ${ENV_PATH}

# 別環境のremote state更新
terraform refresh -state=./.terraform/${ENV}.tfstate \
  --target=data.terraform_remote_state.production \
  --target=data.terraform_remote_state.staging \
  --target=data.terraform_remote_state.management \
  -var-file=./environments/common.tfvars \
  -var-file=./environments/${ENV}/terraform.tfvars \
  ${ENV_PATH}

# plan実行
terraform plan \
  -state=./.terraform/${ENV}.tfstate \
  -refresh=false \
  -var-file=./environments/common.tfvars \
  -var-file=./environments/${ENV}/terraform.tfvars \
  ${ENV_PATH}

# 後処理
rm -rf .terraform

(使用例)

$ ./scripts/plan.sh staging

例2: refresh.sh

実際のものとはちょっと違いますが(ry

#!/bin/bash -xe

ENV=$1
RESOURCE=$2
ENV_PATH=./environments/${ENV}
S3_BUCKET=quipper-terraform-state-files

# 前処理
rm -rf .terraform

# remote stateを取得、リモートとの関連付けを無効にする
terraform remote config \
  -backend=S3 -backend-config="bucket=$S3_BUCKET" -backend-config="key=${ENV}.tfstate"
terraform remote config -disable -state=./.terraform/${ENV}.tfstate

# module読み込み
terraform get ${ENV_PATH}

# refresh実行
terraform refresh \
  -state=./.terraform/${ENV}.tfstate \
  -backup=./.terraform/${ENV}.tfstate.backup \
  --target=${RESOURCE} \
  -var-file=./environments/common.tfvars \
  -var-file=./environments/${ENV}/terraform.tfvars \
  ${ENV_PATH}

# refresh前後のdiffを出す, OKだったらstateファイルをremoteにpushする
diff -u ./.terraform/${ENV}.tfstate.backup ./.terraform/${ENV}.tfstate || true
read -p 'Are you sure? [Y/n]' ANSWER
case $ANSWER in
  '' | [Yy]* )
    terraform remote config \
      -state=./.terraform/${ENV}.tfstate -pull=false \
      -backend=S3 -backend-config="bucket=$S3_BUCKET" -backend-config="key=${ENV}.tfstate"
    terraform remote push
    ;;
  * )
    echo "Importing was aborted."
esac

# 後処理
rm -rf .terraform

(使用例)

$ ./scripts/refresh.sh staging module.reverse_proxy

CircleCI

実際に各環境へのapplyを行ったり、GitHubへのpushごとにplanを実行したりと大活躍です。apply方法は環境ごとにrelease/stagingといったリリースブランチを用意しており、masterからそれらのブランチへのPull Requestを作りマージするという運用にしています。またplanやapplyの結果はそれぞれのPRへ都度POSTされる仕組みになっており、結果の確認も簡単に出来るようになっています。

f:id:quipper-ja:20161009043510p:plain

tfenv

開発者、CircleCIの間で利用するTerraformのバージョンを合わせると言うのも結構重要なポイントです。異なったバージョンでstateファイルを更新してしまうと互換性がなかったりする場合があるためです。それを実現するためにtfenvというTerraformのバージョンマネジメントツールを作りました。このツールの.terrafom-versionという機能*2を使うと、リポジトリ内でTerraformコマンドを使う時のバージョンを揃えることができます。Quipper School/Videoとスタディサプリは、プロダクト間でバージョン0.6と0.7が混在していた時期があったので非常に役立ちました。

まとめ

いろいろ紆余曲折や失敗(主にtfstateまわり)がありましたが、現在は結構安心して運用できる仕組みになったと思います。何かの参考になれば幸いです。


★Quipper日本オフィスでは仲間を募集しています。是非お気軽にご応募ください。★

*1:と、念のため実行後

*2:rbenvの.ruby-versionと同じような機能