LoginSignup
187
163

More than 3 years have passed since last update.

packer と terraform と ansible でインフラを作る

Last updated at Posted at 2015-08-07

packer と terraform と ansible でインフラを作る

概要

長くなるので概要とつまづいた点に絞ってます。
packer, terraform, ansibleの使い方がわかっていている前提です。

インフラはAWSを使います。

  1. packer(+ansible)でアプリケーションが乗ったAMIを作る
  2. terraformでインスタンスを動かす
  3. ansibleで環境を整える
  4. codedeploy でデプロイ

packer, terraform, ansibleの実行にはお互いを必須としないようにしています。そのため入れ替えることは可能なはずです。
terraformはcloudformation、ansibleはchefに変えられるかと思います。

  1. packer
    AMIをタグ付きで登録する

  2. terraform
    data "aws_ami"としてAMIを受け取る
    インスタンス起動時にAWSタグを登録する。

  3. ansible
    AWSタグを元に構成する。

目標

  1. AWSのから割り当てられる動的なアドレスを参照しないで済ます
  2. コードに乗らないインフラの設定をなくす

ディレクトリの構成は下記のようにしています。

web_build.json
web_build.sh
batch_build.json
batch_build.sh
dev.json
production.json
scripts/web.sh
scripts/batch.sh
scripts/delete_old_ami.sh
terraform/
    dev/
        ...
    production/
        ...
ansible/
    ...

必要なもの

  1. packer
  2. terraform
  3. ansible
  4. awscliでaws configure
  5. export AWS_DEFAULT_REGION=region名

packer

イメージを作る

buildersにはamazon-ebs or amazon-instanceで、provisionerでansibleを行うscriptを走らせます。

ロールごとのビルド用のpacker jsonと環境ごとの必要なパラメータを持ったjsonを用意しておくのがいいでしょう。

例えばdev環境のwebロールのビルドはこんな感じで済ませれるようにします。

packer build -var-file dev.json build_web.json

ansible-localでは後のansibleで紹介するAWSタグを変数として取ってくる機能が扱いづらいので、タグ相当のパラメータをここで渡してしまいます。

また、ビルド時にはやって欲しくないtaskにwhen: not buildを設定しておいて、build=falseを全体のデフォルトにし、ビルド時のみbuild=trueを渡します。具体的にはビルド時にはログを送りたくないのでfluentdは起動しないなどです。

ansibleがローカルモードで動かされるのでhosts127.0.0.1とされてます。
そのため、inventory_fileを使って下記のようなファイルを作っていきます。

build_inventory/dev-web
[dev_web]
127.0.0.1
[web:children]
dev_web
[dev:children]
web

よって、ansibleで設定するinventoryによるホストのグループ作成には一定のルールで統一が必要です。
ここでは環境名,役割名,環境名_役割名というグループでまとめることにしています。devが環境名、webが役割名です。
playbookは役割名ごとに作成しているので、playbook_fileの指定は役割名です。

ここでAMIのタグを打っておくことで、後ほどterraformでAMIを取得できます。

また、ansible-vaultを使っているので一時配置して削除しています。これはpacker実行する前に環境変数に ANSIBLE_VAULT として入れておきます。
それとansibleを入れるのにgcc必要になってしまったので"Development Tools"追加してます。

build_web.json
{
  "variables": {
    "aws_profile": "{{env `AWS_PROFILE`}}",
    "role": "web",
    "environment": "dev",
    "region": "{{env `AWS_DEFAULT_REGION`}}",
    "ansible_vault": "{{env `ANSIBLE_VAULT`}}",
    "source_ami": ""
  },
  "builders": [{
      "tags": {
      "environment": "{{user `environment`}}",
      "role": "{{user `role`}}",
      "version": "{{timestamp}}",
      "buildYear": "{{isotime \"06\"}}",
      "buildMonth": "{{isotime \"01\"}}",
      "buildDay": "{{isotime \"02\"}}"
    },
    "ami_name": "{{user `environment`}}-{{user `role`}} {{timestamp}}"
  }],
  "provisioners": [
    {
      "type": "shell",
      "inline": [
        "echo {{user `ansible_vault`}} > ~/.ansible-vault",
        "sudo yum groupinstall -y \"Development Tools\"",
        "sudo yum install -y libffi-devel openssl-devel",
        "sudo pip install --upgrade setuptools",
        "sudo pip install cffi",
        "sudo pip install ansible"
      ]
    },
    {
      "type": "ansible-local",
      "extra_arguments": [
        "--extra-vars",
        "'{\"tags\": {\"role\":\"{{user `role`}}\", \"environment\":\"{{user `environment`}}\"}, \"build\":\"true\"}'",
        "-D",
        "-v",
        "--vault-password-file",
        "~/.ansible-vault"
      ],
      "playbook_file": "./ansible/{{user `role`}}.yml",
      "playbook_dir": "./ansible/",
      "inventory_file": "./ansible/build_inventory/{{user `environment`}}-{{user `role`}}"
    },
    {
      "type": "shell",
      "inline": [
        "rm -f ~/.ansible-vault"
      ]
    }
  ]
}

環境設定のjsonで environmentやらsource_amiなどを埋めるようにしたものを dev.json として用意した上で

packer build -var-file dev.json build_web.json

でビルドができるようになります。

その他スクリプト

古くなったAMIを削除するといった素敵機能はないため、AMI作成時にversionのタグ付けておいてタイムスタンプを値に入れておき、タグversionの値から${REMOVE_TIMESTAMP}より低い対象をのAMIを一覧することで削除しています。

aws ec2 describe-images --owner self --filters 'Name=tag-key,Values=version' --query "Images[?Tags[?Key==\`version\`].Value|[0]<=\`\"${REMOVE_TIMESTAMP}\"\`][].ImageId" --output text

デプロイされてるAMIを無視するなどの機能を付与してスクリプトを配置してビルド時についでに実行させます。

他に、S3に上げる古いバージョンのデータ削除するとかpackerで定期的に実行する必要があるものがあればscripts/に追加しています。

terraform

AMIは下記のように取得できます。

data "aws_ami" "web" {
  owners = ["self"]

  filter {
    name   = "state"
    values = ["available"]
  }

  filter {
    name   = "tag:role"
    values = ["web"]
  }

  most_recent = true
}

data.aws_amiは下記のように参照できます。
インスタンスの設定にはansibleのために必ずサーバの環境名と役割名を記述したAWSのタグを付与するようにタグを付与してます。

  image_id              = data.aws_ami.web.id
  tag {
    key                 = "Name"
    value               = "web-${data.aws_ami.web.tags["version"]}"
    propagate_at_launch = true
  }

  tag {
    key                 = "role"
    value               = "web"
    propagate_at_launch = true
  }

  tag {
    key                 = "environment"
    value               = "dev"
    propagate_at_launch = true
  }

  tag {
    key                 = "version"
    value               = data.aws_ami.web.tags["version"]
    propagate_at_launch = true
  }

ビルドしたインスタンス

ビルドしたAMIを参照するのには上記の通りdata.aws_amiを使っています。これによって、新しくビルドが行われバージョンが変わった際にterraform applyでインスタンスを切り替えることができます。

ただし、削除してから起動するのがデフォルトの動作で、新しいインスタンスを作ってから削除する場合にはlifecycle { create_before_destroy = true }
が必須です。

そのため、フロントのアプリケーションサーバのような一時的にでも消えてもらっては困るサーバはaws_autoscaling_group, aws_launch_configurationを作ります。

または、route53で指定しているサーバでも、下記手順でサーバの移動が可能です。複数台同時に起動してもいい前提です。

  1. terraformのtfstateから該当サーバの情報を削除
  2. terraformを-target付きでinstanceのみ適用し、新しいインスタンスを起動する
  3. インスタンスが起動し終わったら、再びterraformを適用してroute53も更新する
  4. 古いインスタンスを手動で削除する

ローリングアップデート

aws_autoscaling_groupinstance_refresh が追加されているので、自動でやりたければそちらを設定しましょう。
手動でやりたければautoscaling画面からも実行できます。

その他インスタンス

データベースサーバのようなものはAMIを作ってもデータの入っているEBSがなければ意味がないので、ansibleのみで環境設定しています。

入れ替えは自動で起こらないので初回起動時にansibleを実行してセットアップするという方針にしています。

route53

terraformではroute53は絶対に活用すべきです。すべてのELB及びインスタンスなどにDNSを設定しておけば、環境を削除して作り直しても同じ名前が設定不要で使えます。

ansible

dynamic inventory

ansibleのdynamic inventory機能を使ったec2から情報を取ってくるinventory pluginを使うことで指定した範囲で起動しているインスタンスがタグ付きで管理できます。よってterraformで一律設定したAWSのタグが生きてきます。

環境ごとのinventoryのディレクトリ内において、さらにタグごとにホストをまとめるようなファイルを配置しておくと扱いやすいです。
また、親子関係を作っておくことで、変数の上書きが使えるようになるので環境ごとの違いを設定しやすくなります。

タグはtags.tagkey の形式で受け取れます。この辺はansible-inventory -i 環境名 --graph --varを直接実行すると確認できます。

まとめかたはpackerで利用している都合上統一する必要があるので環境名,役割名,環境名_役割名で作ります。

dev/aws_ec2.yml
plugin: aws_ec2
regions:
  - us-east-1
strict: False
hostnames:
  - private-ip-address
# tag_role_`roleValue` と tag_Name_`NameValue` という名前でグルーピングする
keyed_groups:
  - prefix: tag_role
    key: tags.role
  - prefix: tag_Name
    key: tags.Name
# VPCとVPNして利用しているので、ansibleのhostはprivate_ip_addressを使ってます
compose:
  ansible_host: private_ip_address
dev/groups.yml
dev:
  children:
    web:
      children:
        dev_web:
          children:
            tag_role_web
    batch:
      children:
        dev_batch:
          children:
            tag_role_batch
    db:
      children:
        dev_db:
          children:
            tag_role_db:
            db_master:
              children:
                tag_Name_dev_db01
              vars:
                db_master: true

これでanasible playbookのhosts指定やgroup_varsの指定としてwebやbatchが使えます。
ec2のインスタンスのアドレスやインスタンス数がどう変わろうがansible側の設定は変更不要ということです。
ただ、前述のpackerのビルドのinventoryファイルがあるため完全に一意の場所で管理できていない残念なところです。

そうなってしまう理由としてpackerでは

  1. inventory_groupsはchildrenが使えない
  2. inventory_groupsinventory_filesは両立しない

ことが挙げられます。よってplaybookファイルを分けるかinventory_filesをビルドように作るしかありません。他に何か方法があればいいのですが・・・。

ビルドについて

デプロイに必要なデータ一式を適当なディレクトリに配置するビルドスクリプトを用意しておいてCodeDeployにpushしs3にアップロードします。

codedeployにpushする方法はCIツールによるので省略します。jenkinsのプラグインもありますし、codeship, werckerなどにもexampleが存在しています。

デプロイしてその結果が成功かどうかまで面倒を見てくれるのでawscliのコマンドからスクリプトを作るよりいいでしょう。
codedeployの設定はterraformで記述できるようになっているので、ありがたく利用させてもらうことにしましょう。

codedeployでデプロイ対象としてautoscalingを指定している場合は新しく起動するインスタンスに最新の成功したビルドが起動時にデプロイされますが、autoscalingではないインスタンスはやってくれないので、下記のようなスクリプトをuserdataに指定して最新をデプロイさせるようにしました。

#!/bin/bash -ex
APPLICATION=dev-batch
REGION=us-east-1

exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1
aws s3 cp s3://aws-codedeploy-us-east-1/latest/install . --region ${REGION}
chmod +x ./install
./install auto

DEPLOY_GROUP=$(aws --region ${REGION} deploy get-deployment-group --application-name ${APPLICATION} --deployment-group-name ${APPLICATION}|jq '.deploymentGroupInfo.targetRevision.s3Location')

if [ -z "${DEPLOY_GROUP}" ]
then
echo "no deployment"
exit 0
fi

ETAG=$(echo "${DEPLOY_GROUP}" | jq .eTag)
S3_BUCKET=$(echo "${DEPLOY_GROUP}" | jq .bucket)
S3_KEY=$(echo "${DEPLOY_GROUP}" | jq .key)
BUNDLE_TYPE=$(echo "${DEPLOY_GROUP}" | jq .bundleType)

aws --region "${REGION}" deploy create-deployment --application-name "${APPLICATION}" --deployment-group-name "${APPLICATION}" --s3-location bucket="${S3_BUCKET}",key="${S3_KEY}",bundleType="${BUNDLE_TYPE}",eTag="${ETAG}"

ビルド時に必要な変数はpackerと同じく環境用のjsonから取ってくれば無駄がありません。jqあたりで簡単に値を取得できるので、packerと同じ変数を渡す開発用のビルドスクリプトはそう手間がかからずに出来上がります。

CIでの実行

環境変数に下記を設定してビルドスクリプトを実行します。

AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_DEFAULT_REGION

ansible

ansibleで行うような変更は自動化しないほうが扱いやすかったので、変更時に自分で実行するようにしています。

terraform

ansibleと同様に変更時に実行するようにしています。

terraform cloudがHost単位ではなくユーザ単位の料金プランになってくれたので乗り換え検討してもよさそうです。

アプリケーションの配置

Codedeploy, ECSなどを利用します。
長くなるので別記事にしてます。
codedeployならば、デプロイの度にAMIを作り直さなくても、最新のデプロイデータを配置することが簡単に実現できます。

ECSだとアプリケーションはdockerイメージがあればいいのでDocker使うならこのほうが楽です。

最後に

全インフラ構成がgitに乗っかるので

  1. コードレビューできる
  2. すべてコードに乗るので作り直しが簡単。不要な時間インスタンスを落とせて、好きな時に立ち上げることもできる。

サーバのローカルに、codedeployに乗らないようなファイルや、保存されるファイルがないようになっていれば、AMIからすぐインスタンスの追加ができます。

一応、保存されるファイルがある場合にも、terraformでebsを別記述としてdestroyする際にインスタンスのみを削除することebsは消さずに次回インスタンス作成時に同じebsをattachさせるようなことはtfファイルを書き換えずに実現は可能です。

187
163
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
187
163