Terraform と CI で実現するインフラのコード化と構築の自動化

Posted on
qiita devops wercker Terraform

この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。

Wantedly Advent Calendar 2015 __18__日目です。


インフラチームインターンの @dtan4 です。

Wantedly では Terraform を用いたインフラのコード化 (Infrastructure as Code) を全面的に取り入れています。インフラリソースの追加や修正は、コードを書くこと・CI 上での自動適用によって行われています。

この記事では、今年5月から半年以上の間 Terraform を運用してきた中での

  • なぜ Terraform でインフラをコード化しようとしたのか
  • どのように Terraform を運用しているのか
  • Terraform 運用にあたって注意すべき点
  • 既存リソースから Terraform コードを生成する Terraforming について

ということを紹介したいと思います。

Terraform とは

Terraform は、Vagrant などで有名な HashiCorp が作っている__コードからインフラリソースを作成する・コードでインフラを管理する__ためのツールです。AWS, GCP, Azure, DigitalOcean といったクラウドプロバイダや DNSimple, Mailgun, Rundeck を含む多くの SaaS に幅広く対応しています。

コードは JSON 互換である HCL (HashiCorp Configuration Language) で記述します。例えば AWS ELB と EC2 インスタンスは

# from https://www.terraform.io/
resource "aws_elb" "frontend" {
    name = "frontend-load-balancer"
    listener {
        instance_port = 8000
        instance_protocol = "http"
        lb_port = 80
        lb_protocol = "http"
    }

    instances = ["${aws_instance.app.*.id}"]
}

resource "aws_instance" "app" {
    count = 5

    ami = "ami-043a5034"
    instance_type = "m1.small"
}

このように書けます。このコードに対して terraform apply コマンドを実行すると、実際の AWS 上に ELB と EC2 インスタンス5個が作られます。内容を変更したいときはコードを書き換えて再度 terraform apply すれば変更が実環境に適用されます。コードを削除して terraform apply すればリソースが削除されます。

ただ Terraform はコードからリソースを作ることはできますが、リソースからコードを生成する機能は今のところ持ちあわせていません(機能要望の Issue は1年以上前からあるのですが)。それを解決するためのツールが、自分 @dtan4 の作った Terraforming なのです。Terraforming については後述します。

なぜインフラをコード化したのか

Terraform 導入前は、以下の様な問題点がありました。

  • 開発チームからインフラチームへの依頼によるスタイル
  • 新しいリソースを作るのに Management Console 上でポチポチ操作してくのが面倒であり、工数もかかる
  • しかもポチポチ作業の履歴を残すのが困難
  • Management Console に行かないとリソースの一覧が把握できない
  • 同じ構成でもリソースの複製が面倒

これらは

  • 開発者が欲しいリソースをコードという形で記述して、インフラチームに Pull Request を送る
  • インフラチームは送られてきたコードを GitHub 上でレビューして、“Merge Pull Request” ボタンを押すだけでリソースが作られる
  • リソースの追加や変更履歴は、git の diff という形で残る
  • リソース複製もコードのコピペで済む

という形で解決しようと考えたのです。

なぜ Terraform にしたのか

Wantedly では多くの AWS サービスを利用していますが、それらをひとつのツールで一元的に管理できるというのが決め手でした。DNS に関しては Route53 ではなく DNSimple を利用しているのですが、それも含めて管理できるというのは大きかったです。

cookpad が作っている codenize.tools も検討しましたが、サービス単位でツールが別れていることと対応サービスが限られていたので採用を見送りました。

GitHub + Terraform + CI (wercker) による Terraform flow

開発者(というかリソースを追加・変更したい人)は Terraform のコードを書いて、Terraform コード専用のプライベートリポジトリに Pull Request を作ります。

image

コードがリポジトリに push されると、wercker 上で自動的に terraform plan が実行され、.tf ファイルの中身が検査されます。

image

Pull Request の内容はインフラチームがレビューして、LGTM :+1: であれば Merge します。master ブランチに Merge されることで wercker 上で terraform apply が実行され、実際の環境に変更が反映されるようになっています。

image

image

リソースがすべて GitHub 上でコードとして見られるようになったので、いろいろと便利にもなりました。

image

wercker.yml

CI 実行内容である wercker.yml は以下のようになっています。

box: wercker-labs/docker
no-response-timeout: 30
build:
  steps:
    - create-file:
        name: Create .env
        filename: $WERCKER_SOURCE_DIR/.env
        hide-from-log: true
        content: |
          AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
          AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY
          AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION
          DNSIMPLE_EMAIL=$DNSIMPLE_EMAIL
          DNSIMPLE_TOKEN=$DNSIMPLE_TOKEN
    - create-file:
        name: Create terraform.tfvars
        filename: $WERCKER_SOURCE_DIR/terraform.tfvars
        hide-from-log: true
        content: |
          "hoge_password"="$HOGE_PASSWORD"
          ...
    - script:
        name: Pull Terraform image
        code: |
          docker pull quay.io/wantedly/terraform:latest
    - script:
        name: terraform remote config
        code: |
          script/ci-terraform remote-config
    - script:
        name: terraform plan
        code: |
          script/ci-terraform plan
  after-steps:
    - wantedly/pretty-slack-notify:
        webhook_url: $SLACK_WEBHOOK_URL
        channel: $SLACK_CHANNEL
deploy:
  steps:
    - script:
        name: Pull Terraform image
        code: |
          docker pull quay.io/wantedly/terraform:latest
    - script:
        name: terraform apply
        code: |
          script/ci-terraform apply
    - script:
        name: terraform remote push
        code: |
          script/ci-terraform remote-push
  after-steps:
    - wantedly/pretty-slack-notify:
        webhook_url: $SLACK_WEBHOOK_URL
        channel: $SLACK_CHANNEL

ここで、script/ci-terraform というのは以下の様なシェルスクリプトになっています。Terraform を Docker contaiener として走らせています。


#!/bin/bash
#
# Usage: ci-terraform apply|plan|remote-config|remote-pull|remote-push
# Description: run Terraform on CI environment
#

if [ $# != 1 ]; then
  exit 1
fi

case $1 in
  "apply" )
    docker run --rm --name terraform --env-file=$WERCKER_SOURCE_DIR/.env -v $PWD:/terraform quay.io/wantedly/terraform:latest terraform apply terraform | script/masking-passwords && exit $PIPESTATUS
    ;;
  "plan" )
    docker run --rm --name terraform --env-file=$WERCKER_SOURCE_DIR/.env -v $PWD:/terraform quay.io/wantedly/terraform:latest terraform plan terraform | script/masking-passwords && exit $PIPESTATUS
    ;;
  "remote-config" )
    docker run --rm --name terraform --env-file=$WERCKER_SOURCE_DIR/.env -v $PWD:/terraform quay.io/wantedly/terraform:latest terraform remote config -backend=S3 -backend-config="bucket=$TFSTATE_BUCKET" -backend-config="key=$TFSTATE_KEY" -backend-config="encrypt=1"
    ;;
  "remote-pull" )
    docker run --rm --name terraform --env-file=$WERCKER_SOURCE_DIR/.env -v $PWD:/terraform quay.io/wantedly/terraform:latest terraform remote pull
    ;;
  "remote-push" )
    docker run --rm --name terraform --env-file=$WERCKER_SOURCE_DIR/.env -v $PWD:/terraform quay.io/wantedly/terraform:latest terraform remote push
    ;;
  * )
    exit 1
    ;;
esac

また、script/masking-passwords は以下の様なシェルスクリプトです。

#!/bin/bash
#
# Usage: terraform plan | script/masking-passwords
# Description: masking passwords from `terraform plan|apply` output
# Example:
#   password:                "before" => "after"
#     is replaced to
#   password:                "********" => "********" [hidden]
#

if [ $(uname) == "Darwin" ]; then
  # BSD sed
  sed -E 's/(password:\s+|auth:\s+)".*?" => ".*?"/\1"********" => "********" [hidden]/g'
else
  # GNU sed
  sed -u -r -e 's/(password:\s+|auth:\s+)".*?" => ".*?"/\1"********" => "********" [hidden]/g'
fi

このシェルスクリプトは、plan, apply の出力に含まれる

#   password:                "before" => "after"

#   password:                "********" => "********" [hidden]

のようにマスキングしてくれます。RDS の .tf には terraform.tfvars 経由でパスワードを含める必要があるのですが、それが実行時の差分に生文で出力されてしまいます。さすがにマズいのでこのスクリプトを噛ませています。

terraform.tfstate の共有

Terraform では、Terraform 自身が管理しているインフラの情報を持つ terraform.tfstate というファイルがあります。このファイルは S3 上に置くことで、ローカルマシンと CI の間で共有できるようにしてあります。詳しくは下の記事を御覧ください。

Amazon S3 で Terraform の状態管理ファイル terraform.tfstate を管理 / 共有する - Qiita

Terraform 実行環境

ローカルマシンで Terraform を実行するときは、CoreOS VM 上で quay.io/wantedly/terraform Docker image を使って Docker container として動かしています。CI 上でも同じイメージを用いています。各開発者間のローカル環境と CI を統一したい、という考えのもとこのような形になりました。

ただ Terraform 自体は単体バイナリなので、特に CI に関しては無理に Docker container にしなくてもいいのかな…とは思ってたりします。

Terraform 運用の実態 @ Wantedly

どれくらいコード化しているのか

数だけで言うと、28種類__のリソースを__390個 (AWS: 222個, DNSimple: 168個) Terraform で管理しています。

管理しているリソースの種類は、

aws_customer_gateway
aws_db_instance
aws_db_parameter_group
aws_db_security_group
aws_db_subnet_group
aws_elasticache_cluster
aws_elasticache_subnet_group
aws_elb
aws_iam_group
aws_iam_group_membership
aws_iam_group_policy
aws_iam_role
aws_iam_role_policy
aws_iam_user
aws_iam_user_policy
aws_instance
aws_internet_gateway
aws_network_acl
aws_route_table
aws_route_table_association
aws_s3_bucket
aws_security_group
aws_subnet
aws_vpc
aws_vpn_connection
aws_vpn_connection_route
aws_vpn_gateway
dnsimple_record

です。

EC2 インスタンスは Terraform で管理していません。ほとんどのインスタンスの増減(= スケーリング)は、自前ツールによって行っているためです。もしコンスタントに立ち続けるインスタンスがあるのであれば、Terraform で管理したほうがよいでしょう。

どれくらいの開発者に使われているのか

wantedly_wantedly-terraform.png

というわけで、__18人__の開発者(インフラチーム3人含む)が Terraform のコードを書いています。新サービス (Sync) チームから S3 bucket や DNSimple DNS record 追加の Pull Request が来ることもあれば、新しく入社したエンジニアから IAM ユーザを追加する Pull Request も来ます。後者は開発環境構築の一部に組み込まれているので、最近入社したエンジニアは皆 Terraform のコードを触っていますね。

Add_IAM_user.png

ところで、インフラチーム以外の Terraform に不慣れな人にとってはこのリポジトリを触るハードルが高くなります。そのハードルを少しでも下げるため、リポジトリに __Markdown 形式 (= GitHub 上で整形プレビューされる) のドキュメント__を用意しています。

README_md.png

Terraform 運用にあたって注意すべきこと :warning:

terraform apply で失敗する

terraform plan はあくまで .tf ファイルのシンタックスチェックを行うだけなので、内容が AWS API 的に不正だったとしても pass します。 例えば aws_elbname に変な文字を含めていたとしても terraform plan は通ります。

これに関しては、ちゃんと AWS のドキュメントを読んで理解した上で書いたりする必要があります…。API の dry-run ができるとベストなのですが、見た感じ EC2 系の API にしか dry-run オプションが用意されてないようです。

また、CI 上で自動 apply する環境を整えている場合はリカバリできるよう、手元でも terraform apply を実行できるような環境を用意しておくべきです。 自分たちの場合は、apply 失敗したら .tf ファイルを修正した新しい Pull Request を作成し、改めて master に merge => deploy するようにしています。

ELB 配下のインスタンスが意図せず置き換わる => lifecycle で解決!?

Wantedly では、ELB 配下のインスタンスのスケーリングや入れ替えを自前ツールで行っています。このため、.tf ファイルにインスタンス ID をハードコードすると予期せず実際の環境と差分が出てしまうようになっていました。

…が :exclamation: 今年10月にリリースされた Terraform v0.6.4 にて ignore_changes というパラメータが新たに導入されました。ignore_changes は、指定した属性の変更を plan, apply 時に無視するようにしてくれます。

例えば aws_elbinstances の変更を無視させたい場合は、

resource "aws_elb" "foo" {
    lifecycle {
        ignore_changes = ["instances"]
    }
}

と書けばよいのです!まさに今回の問題を解決するための機能でした。

IAM ユーザ削除に失敗する

退職などに伴って IAM ユーザを削除するには、そのユーザに紐付いている

  • AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
  • Management Console パスワード
  • IAM Policy
  • IAM Group 所属情報

を前もって削除する必要があります。Management Console から削除する場合は同時に削除されますが、API 経由だと一つ一つ削除していかないといけません。詳しくはこちら:

IAM ユーザーの管理 - AWS Identity and Access Management

ところで、Terraform で IAM ユーザを消す場合はもちろん API 経由のアプローチになるのですが、その場合__紐付いたリソースを無視していきなり IAM ユーザ削除 API を叩く__ので、高確率で terraform apply で以下の様なエラーが出ます。

Error applying plan:

1 error(s) occurred:

* Error deleting IAM User hoge: DeleteConflict: Cannot delete entity, must delete access keys first.

これの対策として、__Management Console 上で IAM ユーザを削除してしまったうえで terraform apply を実行__するようにしています。自動化をすすめる上ではよくないアプローチでと思いますが、現状削除が発生する頻度は少ないのでこうなっています…

Terraforming: 既存のインフラリソースから Terraform のコードを生成する

ここでいきなり拙作の宣伝紹介です。

Terraforming は、既存の AWS リソースから Terraform のコード .tf と状態管理ファイル terraform.tfstate を生成するコマンドラインツールです。Wantedly の Terraform コードの大半は、Terraforming によって生成されたコードです。

Terraforming の詳しい使い方については、Qiita と個人ブログに別記事がありますのでそちらをご参照ください。

企業で Infrastructure as Code を導入するときって、__既存のリソースをコード化したい__という要望が多いんじゃないかと思っています。新規事業を立ち上げるタイミングならともかく…。Wantedly もまさにそうで、最初は対応する .tf 書けばいいのかなと思ったらそれだけじゃ済まないとわかってうわぁああ :scream: となりました。

これは手作業でするには厳しい、でも機能としての要望は多いはず :bulb: そう思って Terraforming を作り始めたのです。

OSS としての Terraforming

おかげさまで国内外問わず色んな方や企業さんに使っていただいているようで、今でも継続的に Pull Request を頂いております。 ぶっちゃけ自分が普段触らないリソースに関してはそれほど追加するモチベーションが沸かないのですが、他の方が使ってみた上でこのリソース欲しい!ということで Pull Request をくださるケースが増えています。本当に感謝です、ありがとうございます :bow:

今後も継続的に開発は続けていきます。とりあえず AutoScaling 対応と、別 gem になってる terraforming-dnsimple をプラグインとして読み込めるようにしたいです。RubyKaigi での古橋さんの発表が参考になりそう。

おわりに

というわけで、Wantedly での Terraform 運用事例の紹介でした。インフラのコード化や、Terraform の導入を考えている方の参考になれば幸いです。 この構築方法はあくまで一例であり、より良いベストプラクティスがあるかもしれません。他の事例も参考にしてよりブラッシュアップしていけたらと思います。

また、今年の8月に開催した HashiCorp Product(Tools) Meetup でも同様の内容を紹介していたのでした。その頃から多少のアップデートもありますが、あわせて御覧ください。

Terraform at Wantedly // Speaker Deck

スクリーンショット 2015-12-18 22.35.14.png