packer と terraform と ansible でインフラを作る
概要
長くなるので概要とつまづいた点に絞ってます。
packer, terraform, ansibleの使い方がわかっていている前提です。
インフラはAWSを使います。
- packer(+ansible)でアプリケーションが乗ったAMIを作る
- terraformでインスタンスを動かす
- ansibleで環境を整える
- codedeploy でデプロイ
packer, terraform, ansibleの実行にはお互いを必須としないようにしています。そのため入れ替えることは可能なはずです。
terraformはcloudformation、ansibleはchefに変えられるかと思います。
-
packer
AMIをタグ付きで登録する -
terraform
data "aws_ami"としてAMIを受け取る
インスタンス起動時にAWSタグを登録する。 -
ansible
AWSタグを元に構成する。
目標
- AWSのから割り当てられる動的なアドレスを参照しないで済ます
- コードに乗らないインフラの設定をなくす
ディレクトリの構成は下記のようにしています。
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/
...
必要なもの
- packer
- terraform
- ansible
- awscliで
aws configure
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がローカルモードで動かされるのでhosts
は127.0.0.1
とされてます。
そのため、inventory_file
を使って下記のようなファイルを作っていきます。
[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"追加してます。
{
"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で指定しているサーバでも、下記手順でサーバの移動が可能です。複数台同時に起動してもいい前提です。
- terraformのtfstateから該当サーバの情報を削除
- terraformを
-target
付きでinstanceのみ適用し、新しいインスタンスを起動する - インスタンスが起動し終わったら、再びterraformを適用してroute53も更新する
- 古いインスタンスを手動で削除する
ローリングアップデート
aws_autoscaling_group
に instance_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で利用している都合上統一する必要があるので環境名
,役割名
,環境名_役割名
で作ります。
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:
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では
-
inventory_groups
はchildrenが使えない -
inventory_groups
とinventory_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に乗っかるので
- コードレビューできる
- すべてコードに乗るので作り直しが簡単。不要な時間インスタンスを落とせて、好きな時に立ち上げることもできる。
サーバのローカルに、codedeployに乗らないようなファイルや、保存されるファイルがないようになっていれば、AMIからすぐインスタンスの追加ができます。
一応、保存されるファイルがある場合にも、terraformでebsを別記述としてdestroyする際にインスタンスのみを削除することebsは消さずに次回インスタンス作成時に同じebsをattachさせるようなことはtfファイルを書き換えずに実現は可能です。