概要
どうも。篠田です。
「特定の"インフラ担当"・"開発メンバー"」や「古の記憶」に頼らず、『開発メンバー全員が拡張や移行作業を気軽にできるインフラ』を実現するために、私のチームで採用しているTerraformを使ったAWS環境運用フローをご紹介いたします。
Terraformで移行および運用するフローにしたことで、構成全体に対する変更の柔軟性が高まり、コードがあることで運用および拡張期において設計の変更や手戻りを恐れずに開発を進められるようになりました。
次は概要図です。
背景
先日、door賃貸をオンプレミスからAWSに完全移行しました。移行前はテスト・本番各環境合わせて仮想サーバ50台ほどで構成されていました。移行後はマネージドなサービスを活用するなどして30台ほどになっています。
運用とサービス拡充における人的負荷軽減・アジリティおよびメンテナンスビリティの向上を目的としてのAWS環境への全面移行です。移行に際していわゆるInfrastructure as Codeの考え方を参考に、AWS環境のほぼ全てをコードで記述することにしました。メインのツールとしては、PackerとTerraformを採用しました。ここでは主にTerraformの利用を中心に運用フローを述べていきたいと思います。
複数人数で一つの環境をコードで管理する場合の移行期と運用期の特性
複数人数で一度に一つの環境をコードで管理するには、各開発者の変更を矛盾無く対応させる必要があります。修正の種類によっては開発用・ステージング環境に実際に適用してみて正しいかどうかを確認する必要もあります。
今回の作業では移行期とその後の運用期でそれぞれの特性や違いを事前に想定し、運用フローの確立と移行作業を進めました。
移行期
- 移行作業を3名で実施したのでそれぞれが同時並行で一つの環境を修正するため、環境が次々と変更されていく
- 他人の作業を把握していないと自分の作業がかぶったり、矛盾のある設定を入れ込んでしまう
- レビューをする機会が日に何度も訪れる(後述しますがGitHub上でレビュー)
運用期
- 移行直後はチューニングや不具合の修正や段階以降などで軽微な修正が多い
- 変更が蓄積されてきた場合のトレーサビリティの確保
- いわゆるクラウドネイティブへの段階的な構成の変更
- 新機能追加のためのテスト環境の追加
Terraformの採用理由
TerraformとはHashiCorp社が開発した、クラウドっぽい環境に対するオーケストレーションツールです。AWSの他にAzure等のクラウド、OpenStack、VMwareなど十数種類の環境について統合的に記述できます。
AWSには既にオーケストレーションツールとしてAWSよりCloudFormationが用意されておりますが、下記点を評価してTerraformを採用しました。
- 各リソースのidを動的に解決し、さらに変数名で容易に参照出来るなど表現力の高さ
- Packerなどのプロビジョニングツール連携が標準機能で備わっている
- いわゆるDryRunの機構が優れており、ミスに容易に気づける。多人数で一つのインフラ環境を構築する移行期では特に安心感が高まる。
- シェルスクリプトによる運用フローのカスタマイズが容易
- シェルスクリプトと組み合わせることで、パラメータにより一つのコードで複数の環境を構築できる
実際の運用
ディレクトリ構成
セット名と環境名(staging, production)で切り分けてファイルを展開しています。セットとは本エントリで付けた便宜的な名前ですが、サービスのモジュールを構成するファイルのまとまりくらいにとらえてください。
下に示すディレクトリ構造にはセットとして、下記が見えています。いったん次の4つセットを分けて構築しています。
- main: サービスの全体構成
- managemant: 管理用セグメントの構成
- rds: RDSのみの設定
- route53: Route53のみの設定
手戻りに時間がかかる重篤な問題が発生してしまいがちなRoute53とRDSについては分けています。問題発見の自動的な仕組み化によってひとつにしたいですが、問題発生箇所が多岐にわたるので、今のところは分離状態です。
stateファイルの部分で後述しますが、環境毎の基本的な構成はすべてtfファイルに記載されており、具体的な切り替えはtfvarsの値により変更されます。
treeの結果を編集してはしょっていますが、このような内容です。
. ├── README.md ├── circle.yml ├── main │ ├── environments │ │ ├── production │ │ │ └── tfvars │ │ │ └── variables.tfvars │ │ └── staging │ │ └── tfvars │ │ └── variables.tfvars │ ├── templates │ │ ├── policies │ │ │ └── *.json.tpl │ │ └── user_data │ │ └── *.tpl │ └── tf │ ├── *.tf │ ├── production │ │ └── *.tf │ └── staging │ └── *.tf ├── management │ ├── environments │ │ └── production │ │ └── tfvars │ │ └── variables.tfvars │ └── tf │ └── *.tf ├── opt │ ├── README.md │ ├── bin │ │ ├── apply │ │ ├── plan │ │ └── terraform.exec.sh │ ├── circle.yml │ ├── playbooks │ ├── provision │ ├── rds-maintenance │ ├── rds-renew-dev-db │ └── test ├── rds │ ├── README.md │ ├── environments │ │ ├── production │ │ │ └── tfvars │ │ │ └── variables.tfvars │ │ └── staging │ └── tf │ └── *.tf └── route53 ├── environments │ └── production │ └── tfvars │ └── variables.tfvars └── tf └── *.tf
stateファイルの配置
Terraformは直前の状態をstateファイルに残しています。このstateファイルがあることにより変更対象に対して事前に、 変更・削除して再作成・削除・作成を検証する事が出来ます。
stateファイルは常にひとつという前提でTerraformは実行されますが、環境を変更する開発者が複数人数いる場合は、逐次状態が変化していくために開発者端末上でのstateファイルに矛盾が発生します。
これに対する対処法はいくつかありますが、我々はTerraformの機能にあるS3上に配置するという手段を採用しました。 stateファイルをGitのリモートリポジトリ上で管理するという手段もありますが、逐次環境が変わっていく中で各開発者のローカル上で同期させるのが困難であることと、 特に移行期は試行錯誤が高い頻度で入るので特定のブランチを常に見るという運用が難しく、事前検証の結果などで細かい矛盾を把握した方が運用上早いのも理由にあります。
具体的には次のようにパラメータを与えてシェルスクリプトにより実行しています。
実際には ./opt/bin/plan main staging
のように簡便に実行できるようなスクリプトを実行しています。Qiitaのここの記事が参考になると思います。
# stateファイルの置き先を設定 terraform remote config \ -backend=S3 \ -backend-config="bucket=${S3_BUCKET_NAME}" \ -backend-config="key=${APP_NAME}/${ENV}/terraform.tfstate" \ -backend-config="region=${AWS_REGION}" # リモートの配置先からダウンロード terraform remote pull # 実行 terraform plan \ -var-file=./${APP_NAME}/environments/${ENV}/tfvars/variables.tfvars \ -state=${TFSTATE_FILE} \ -refresh=true \ ${TF_WORK_DIR} # 結果を remote にアップロード terraform remote push # 別の環境を実行した場合に誤って手元のtfsateを参照しないようにする terraform remote config -disable -state=${TFSTATE_FILE}
環境の定義
tfvarsによる切り替え
tfvarsで各環境の具体値を決定しています。シェルスクリプトでtfvarsを切り替えます。
切り替え項目は具体的には次のような項目です。
- VPCのセグメント
- 冗長構成のEC2の数
- EC2のAMI id
- インスタンスタイプ
- セキュリティグループのIP制限
- blue, greenの選択
例えばSubnetなら例として次のような記述になります。
varで始まる${var.hoge}
部分が変数化された部分で、実行時に環境毎に設定が入ります。そのほかは、実行時にAWSから取得される情報で補完されます。
# Subnet # Public resource "aws_subnet" "public" { count = "${var.subnet_length}" # public subnetの数 vpc_id = "${aws_vpc.door_vpc.id}" cidr_block = "${var.vpc_cidr_16}.${count.index + 100}.0/24" map_public_ip_on_launch = true availability_zone = "${element(split(",", var.az_list), count.index)}" # AZ名のリストをリスト名から取得 tags = { Name = "door-public-${count.index + 1}-${var.environment}" # 名前の生成 } }
環境固有のリソース定義
開発中のリソースなどは本番に出て欲しくない場合もあります。固有環境のtfファイルだけ別ディレクトリに配置しています。
しかし、tfファイルの配置箇所は一つのディレクトリしか指定できないため、シェルスクリプトで各環境のtfファイルを一つのテンポラリディレクトリにコピーした上でterraform
コマンドを実行しています。
│ └── tf │ ├── *.tf # 例えばstagingならこのファイルと │ ├── production │ │ └── *.tf │ └── staging │ └── *.tf # このファイルをテンポラリディレクトリにコピーする
GitHubのPRフロー
次にフローの概要図を示します。
詳細には次のようなフローでstagingおよび本番に適用されます。masterブランチが本番環境、stagingブランチがstaging環境に対応しており、masterブランチはstagingブランチからPRを受ける前提です。
- staging ブランチからトピックブランチを生やす。
- 各開発者はトピックブランチで修正を実施し、staging環境に対してplanし、問題が無ければstagingブランチにPRを投げる。その際CircleCIによりplanが実施され、内容がレビュワーに共有される。
- コードレビューで問題が無ければ、トピックブランチ上でapplyする。stagingにマージした後にapplyしないのは、Terraformのvalidationだけでは分からない問題がある場合があるためである。実際にapplyで問題があれば、再度コミットし、レビューを受け問題が無くなるまで繰り返す。
- 実際に問題が無ければstagingブランチで本番に対してplan・applyし、masterにマージする。
applyについては各メンバーは一応チャットにて実施前に宣言します。 CI上やchat opts的な指令でapplyまでを実施したかったですが、Terraformのvalidationは基本的なところまでしか実施できないのと、aws specなどの自動テストも実際の環境が存在しないとできないために、人手が介入しています。
よかったこと・課題
よかったこと
- AWSのリソースがtfファイル上に記述されているため無秩序にならない
- AWSのほぼすべての設定について、履歴とコード管理出来るようになった。
- 各設定の意図をコメントや変数名に表現できる。
- 複数人数で修正してもあまり気兼ねせず、スピーディーに開発できる。
- 移行期にstaging環境で構築検証を進めていたが、本番リリース直前期にオペミスの心配も設定だけで一度に環境を新たに構築できて最高に気持ちよかった。
- 環境について説明するとき、GitHubのリンクを送るだけでいい。
- 新規機能のリソースを本番に展開するのが設定の移動だけで楽になった。
- EC2について、AutoScale前提に、Terraformでの管理はAnsibleによるAMI作成とアドホックに稼働マシンに流す仕組みの両方をつくって、cloud-initによる管理を作ればそう問題にならない。
課題
- stagingブランチにマージすれば完了というようにしたい
- AutoScaleに入れていないEC2の管理が面倒