sometimes I laugh

高専卒WEBエンジニアがいろいろ残しておくブログ

PackerとTerraformで始めるミニマムなAWS構成管理

f:id:watass:20151227163838p:plain

前回の記事ではDockerとECSを使ったAWS上でのInfrastructure as codeについて言及しましたが、サーバリソースの構成管理についてはAWSのマネージメントコンソールから手動で行わないといけなかったり、コンテナを用いたアプリケーション構成を強制され、従来の単純なインスタンス構成ができないという問題点がありました。前回の記事はこちら。

後者については、今後コンテナを活用したインフラ構成が普通になっていくことで許容されていくかもしれませんが、普通にインスタンスを立ててインフラを構築している方にとってはInfrastructure as codeをやりたいためにコンテナを前提としたサーバ構成に変更しなくてはいけないなんて、正直気が進まないと思います。

そこで本記事では、今インフラ界隈で非常に強い影響力を持っているHashicorpのプロダクト、PackerとTerraformを使って従来通りのインスタンス構成でInfrastructure as codeとAWSの構成管理を小さく始めてみようと思います。

Packerとは

PackerとはVagrantやSerfといった有名インフラツールを開発しているHashicorpのプロダクトで、マシンイメージをTemplateと呼ばれる設定ファイルから各プラットフォーム向けに生成することに特化したツールです。具体的な例を挙げると、Packerの記法に則った設定ファイルからAWSのAMIを生成したり、Dockerイメージを生成することができます。昨今のクラウドベンダーの多様化、Dockerなどの様々なインフラの選択肢が増えている状況を考えると、各プラットフォームの差異を吸収できる立ち位置で注目されている印象です。

Templateはjsonでシンプルに記述できます。AWSの場合、jsonの設定情報を元に、実際にインスタンスを立ち上げ、指定された操作をインスタンスに行い、最終的なインスタンスに対してAMIを取得してからインスタンスを削除するという流れが自動化できます。つまり、このTemplateをバージョン管理にのせることで、インスタンス1台レベルでInfrastructure as codeを実現できます。

Terraformとは

https://www.terraform.io/

TerraformはPackerと同様、Hashicorpの提供するオーケストレーションツールです。Packerがインタンス一台レベルの内部構造をTemplateで記述するのに対し、TerraformはEC2やRDSなどのリソースをどのように配置するかをConfigurationという設定ファイルに記述することで、構成管理の自動化を図るツールです。同様のツールAWSで言えばCloudFormationが有名ですが、Dry-runの実装や、AWSに限らない他ベンダへの対応などのメリットから広く採用されています。

PackerがAMIを生成し、そのAMIをTerraformで指定してインスタンスを起動する、という流れをそれぞれのTemplateとConfigurationで役割を分離して記述できるので、すっきり管理できます。もちろん、Packerを使わずに生成したAMIをTerraformで使用することもできますし、Packerで生成したAMIをマネージメントコンソールから指定して起動することもできます。この辺りの柔軟さがPackerやTerraformをミニマムなAWS構成管理に採用する上でおすすめできると思います。

versions

いつも同様に操作する環境はMac OS X Yosemiteを使用します。そろそろEl Capitanにあげたいとは思っています。

名前 バージョン
Packer 0.8.6
Terraform 0.6.8

Packerのインストール

公式サイトのDownloadsからバイナリのzip圧縮をダウンロードしてきます。

f:id:watass:20151227171726p:plain

ダウンロードできたら、 /usr/local/ 配下にpackerのインストールディレクトリを作成して、バイナリを展開、PATHを通します。zsh環境の場合、以下のようなコマンドになります。

$ cd /usr/local/
$ mkdir packer
$ cd packer
$ mv ~/Downloads/packer_0.8.6_darwin_amd64.zip .
$ unzip packer_0.8.6_darwin_amd64.zip
$ echo 'export PATH="/usr/local/packer:$PATH"' >>  ~/.zshenv
$ source ~/.zshenv
$ packer -v
0.8.6

問題なくバージョンが表示できればインストール完了です。

Terraformのインストール

こちらも同様に公式サイトのDownloadsからバイナリのzip圧縮をダウンロードしてきます。

f:id:watass:20151227172850p:plain

ダウンロードできたらPacker同様にバイナリを展開して、PATHを通します。

$ cd /usr/local/
$ mkdir terraform
$ cd terraform
$ mv ~/Downloads/terraform_0.6.8_darwin_amd64.zip .
$ unzip terraform_0.6.8_darwin_amd64.zip
$ echo 'export PATH="/usr/local/terraform:$PATH"' >>  ~/.zshenv
$ source ~/.zshenv
$ terraform -v
Terraform v0.6.8

AWS環境変数の設定

ここからTemplateやConfigurationを作成していくのですが、AWSに対して操作をする場合、当然のことながらAWSのクレデンシャルが必要になります。具体的にはアクセスキーとシークレットキーですね。これらを設定ファイルに直接記述するのはバージョン管理にのせる上で非常に問題になりますので、できれば外部で管理したいところです。

PackerやTerraformはその辺り既に考慮されていて、TemplateやConfigurationにAWSのクレデンシャルが記述されていない場合、それぞれ環境変数 AWS_ACCESS_KEY_ID と AWS_SECRET_ACCESS_KEYを参照します。なので、事前にクレデンシャルを環境変数に登録しておくようにしましょう。

$ export AWS_ACCESS_KEY_ID='XXXXXXXXXXXXXXXXXX'
$ export AWS_SECRET_ACCESS_KEY='XXXXXXXXXXXXXXXXXXXXXXXXXXXX'

Templateの作成

さて、以上で準備ができましたので、早速Packerで作成するAMIについて記述したTemplateを作成していきます。Templateはjsonで記述でき、どのプラットフォームに対してイメージをビルドするか、プロビジョニングとしてどんな操作をするかなどが記述できます。今回は前回の記事で作成した環境と同様のものを作成するとして、以下のようなTemplateになります。

example.json

{
  "builders": [{
    "type": "amazon-ebs",
    "region": "ap-northeast-1",
    "source_ami": "ami-383c1956",
    "instance_type": "t2.micro",
    "ssh_username": "ec2-user",
    "ssh_pty": "true",
    "ami_name": "packer-example {{timestamp}}"
  }],
  "provisioners":[{
    "type": "shell",
    "inline": [
      "sudo yum -y update",
      "sudo yum -y install httpd",
      "sudo yum -y install php php-devel",
      "sudo yum -y install git",
      "sudo chkconfig httpd on",
      "cd /var/www/html",
      "sudo git clone https://github.com/wata727/twitterloginsample.git",
      "cd twitterloginsample",
      "sudo php composer.phar self-update",
      "sudo php composer.phar update"
    ]
  }]
}

簡単に解説します。

builders

イメージを生成するプラットフォームに関する設定項目です。今回はAWSなので、どのリージョンにどんな方法でどんな名前のAMIを生成するかを記述しています。本来はこの項目にクレデンシャルも記述しますが、前述の通り、セキュリティの観点から記述しないことで環境変数を参照するようにしています。

他にも設定項目は複数ありますが、今回指定しているものについては以下の通り。

Key 説明
type 作成するインスタンスのタイプ指定です。今回指定している"amazon-ebs"はEBS-Backed AMIであることを指します。"amazon-instance"でInstance store-Backed AMIを指定できますが、AWSも起動の高速さ、ストレージの永続性からEBS-Backed AMIを推奨していますので、特に理由がなければ"amazon-ebs"でよいはずです。
region インスタンスの生成、およびAMIを保存するリージョンの指定です。東京リージョンは"ap-northeast-1"です。
source_ami AMIを生成する上でインスタンスを立ち上げる際に使用する元となるAMIです。今回はAmazon Linux AMI 2015-09-1を使用するので"ami-383c1956"になります。AMIのIDはインスタンス作成時のAMI選択画面で確認できますので、使用したいAMIにあわせて変えてください。
instance_type AMIを生成する上で立ち上げるインスタンスのタイプです。今回は"t2.micro"を指定します。インスタンスタイプは実インスタンス起動時に選択できるので、とりあえず小さなインスタンスでいい気がします。
ssh_username AMIを生成する上で起動したインスタンスにログインする際のユーザ名です。Amazon Linuxでは"ec2-user"ですね。他OSならばそれにあわせたユーザ名を指定してください。
ssh_pty AMIを生成する上で立ち上げたインスタンスSSHで接続する際に、擬似端末を使用するかどうかの設定です。デフォルトが"false"になっていますが、"true"にしないとprovisionersでsudo系のコマンドが実行できませんので、"true"にしておきます。
ami_name 生成されるAMIの名前です。{{timestamp}}でビルド時のタイムスタンプを挿入してくれます。

他項目については公式ドキュメントを確認しましょう。バージョンアップによって推奨される項目が変わることがありますので、一度確認したうえで作成することをおすすめします。

provisioners

イメージを作成する上で行うプロビジョニングの設定項目です。"shell"を使用することでシェルスクリプトを記述するイメージで記述できます。今回は"shell"のみで事足りていますが、他にもビルドを実行するローカル上でシェルスクリプトを実行したり、ファイルのアップロード、Chef cookbookやAnsible playbookの適用などもできるので、様々なプロビジョニングを行うことができます。

また、コマンドはbuildersで指定した"ssh_username"で実行されるので、今回の場合はsudoの記述が必須です。さらに、カレントディレクトリの位置は保存されるので、cdコマンドで移動もちゃんとできます。


この他、post-processorsやpushなどの項目も設定できますが、今回はこの2つで事足りるので以上で設定完了とします。他項目については同様に公式ドキュメントを参照してください。

Templateの検証

それでは作成したTemplateが正常に動作することを検証します。Packerにコマンドが用意されていますので、それを使います。

$ packer validate example.json
Template validated successfully.
$ packer inspect example.json
Variables:

  <No variables>

Builders:

  amazon-ebs

Provisioners:

  shell

Note: If your build names contain user variables or template
functions such as 'timestamp', these are processed at build time,
and therefore only show in their raw form here.

packer validateで構文チェックを、packer inspectでどのようなビルドを行うか確認できます。構文チェックもパスしており、ビルド内容もamazon-ebsに対して、shellでプロビジョニングを行うという感じなので、大丈夫そうです。

ビルドの実行

example.jsonに問題ないことが確認できましたので、早速ビルドを実行します。

$ packer build example.json

実行すると、コマンドライン上に作業ログが表示され、プロビジョニングのログも表示されます。問題なければ以下の様な表示がされてビルドが終了します。

Build 'amazon-ebs' finished.

==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:

ap-northeast-1: ami-XXXXXXX

最後に表示された"ami-XXXXXXX"が生成されたAMIのIDになります。これでTemplateの情報を元に、インスタンスを起動し、AMIを作成し、インスタンスを削除するという一連の作業が自動で完了しました。マネージメントコンソールから確認してみましょう。

f:id:watass:20151227200423p:plain

ちゃんとAMIが生成されているようです。

Configurationの作成

さて、これでAMIが作成されたので、作成されたAMIをAWSマネージメントコンソールから確認して、それを指定してインスタンスを起動しても目的は果たせるのですが、せっかくTerraformをインストールしたので、Configurationを作成してインスタンスの生成も自動化しましょう。今回は既にVPCもサブネットもセキュリティグループも存在する前提で、インスタンスのみをほぼデフォルト設定で起動するような場合のConfigurationを作成してみます。以下のようなConfigurationになります。

example.tf

provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_instance" "server" {
  ami = "ami-XXXXXXX"
  instance_type = "t2.micro"
  key_name = "XXXXX"
  vpc_security_group_ids = [
    "sg-XXXXXXX"
  ]
  subnet_id = "subnet-XXXXXXX"
  associate_public_ip_address = "true"
  instance_initiated_shutdown_behavior = "stop"
  disable_api_termination = "false"
  monitoring = "false"
  ebs_block_device = {
    device_name = "/dev/xvda"
    volume_type = "gp2"
    volume_size = "8"
  }
  tags = {
    Name = "server"
  }
}

output "public ip" {
  value = "${aws_instance.server.public_ip}"
}

こちらも簡単に解説します。

provider

オーケストレーションを実施するプラットフォームの設定です。今回はAWSに対して行うので"aws"を指定します。ここでAWSのクレデンシャルを設定できますが、Packer同様に指定しないことで環境変数を参照させます。環境変数名はPackerと同様なので、特に何もしなくてOKです。今回は東京リージョンのみを指定しています。

resource

Terraformが作成するリソースの設定です。今回はEC2インスタンスを立ち上げるだけなので、resourceに"aws_instance"を指定しています。"server"はTerraformが識別するためのリソースの名前です。他にも、VPCやRDSの生成もできますが、それらについては

resource "aws_db_instance" "rds" {
   # RDS Configuration
}

resource "aws_vpc" "vpc" {
   # VPC Configuration
}

上記のようにリソースとして独立して記述していきます。各リソースごとに複数の設定項目がありますが、今回使用した"aws_instance"の項目については以下の通りです。

項目 詳細
ami 使用するAMIのIDです。Packerのビルド結果で得られたIDを指定しましょう。
instance_type 起動するインスタンスのタイプです。今回は"t2.micro"を使用します。
key_name インスタンスにログインするために使用する鍵の名前です。既に作成してある鍵の名前を使用してください。
vpc_security_group_ids インスタンスに適用するセキュリティグループです。IDで指定します。既にVPCを作成しているならば、VPCに紐づくようにセキュリティグループが存在するはずなので、ここでインスタンスが所属するVPCが指定されます。VPCを作成しておらず、default VPCを使用するならば、"security_groups"に対してセキュリティグループのIDを指定するようにしてください。
subnet_id インスタンスを配置するVPC上のサブネットです。IDで指定します。
associate_public_ip_address インスタンスにパブリックIPを割り当てるかどうかの設定です。
instance_initiated_shutdown_behavior インスタンスのシャットダウン時の振る舞いの設定です。"stop"か"terminate"が選択できます。今回は"stop"にします。
disable_api_termination インスタンスを意図せぬ削除から保護するための設定です。特に重要なインスタンスでもないので"false"にしておきます。
monitoring インスタンスの詳細な監視をするかどうかの設定です。"false"にしておきます。
ebs_block_device.device_name インスタンスにアタッチするEBSのボリューム名です。マネージメントコンソールから設定する際のデフォルト"/dev/xvda"にしておきます。
ebs_block_device.volume_type インスタンスにアタッチするEBSの種類です。"gp2"がEBS汎用SSDで、"io1"がProvisioned IOPS SSDで、"standard"がマグネティックボリュームに該当します。
ebs_block_device.volume_size インスタンスにアタッチするEBSのサイズです。マネージメントコンソールから設定する際のデフォルト値、8GBにしておきます。
tags インスタンスに付与するタグです。"Key"="Value"形式で記述します。今回はTerraformが識別するリソース名とインスタンスの名前を対応させるために、Nameにserverという値を割り当てます。

他の項目については公式ドキュメントを確認してください。

output

Terraformの実行後に表示する値の設定です。設定できて何が嬉しいのかというと、値にはTerraform内で生成されたリソースの情報が使えるので、実際に生成されたリソースのパブリックIPやDNSなどが確認できます。

今回は実行後に"server"のパブリックIPを表示するようにしています。これで、インスタンスの作成後にマネージメントコンソールへアクセスしてパブリックIPを確認しにいかずとも、そのまま確認することができます。

Configurationの検証

では作成したConfigurationが実際にどのような操作を行うのか確認してみましょう。terraform planで実行したカレントディレクトリ配下の.tfファイルを参照し、Dry-runさせることができます。

$ terraform plan
Refreshing Terraform state prior to plan...


The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

+ aws_instance.server
    ami:                                               "" => "ami-XXXXXXX"
    associate_public_ip_address:                       "" => "1"
    availability_zone:                                 "" => "<computed>"
    disable_api_termination:                           "" => "0"
    ebs_block_device.#:                                "" => "1"
    ebs_block_device.3935708772.delete_on_termination: "" => "1"
    ebs_block_device.3935708772.device_name:           "" => "/dev/xvda"
    ebs_block_device.3935708772.encrypted:             "" => "<computed>"
    ebs_block_device.3935708772.iops:                  "" => "<computed>"
    ebs_block_device.3935708772.snapshot_id:           "" => "<computed>"
    ebs_block_device.3935708772.volume_size:           "" => "8"
    ebs_block_device.3935708772.volume_type:           "" => "gp2"
    ephemeral_block_device.#:                          "" => "<computed>"
    instance_initiated_shutdown_behavior:              "" => "stop"
    instance_type:                                     "" => "t2.micro"
    key_name:                                          "" => "XXXXX"
    monitoring:                                        "" => "0"
    placement_group:                                   "" => "<computed>"
    private_dns:                                       "" => "<computed>"
    private_ip:                                        "" => "<computed>"
    public_dns:                                        "" => "<computed>"
    public_ip:                                         "" => "<computed>"
    root_block_device.#:                               "" => "<computed>"
    security_groups.#:                                 "" => "<computed>"
    source_dest_check:                                 "" => "1"
    subnet_id:                                         "" => "subnet-XXXXXXX"
    tags.#:                                            "" => "1"
    tags.Name:                                         "" => "server"
    tenancy:                                           "" => "<computed>"
    vpc_security_group_ids.#:                          "" => "1"
    vpc_security_group_ids.3045634505:                 "" => "sg-XXXXXXX"


Plan: 1 to add, 0 to change, 0 to destroy.

これで設定した項目がずらずらと表示されます。最終行には全体の変更内容が記述されていますね。1 to addということで、インスタンスがひとつ生成されることが確認できます。

Configurationの実行

terraform planで想定通りの実行内容が確認できたならば、実際に適用します。

$ terraform apply
aws_instance.server: Creating...

/* ...省略... */

aws_instance.server: Creation complete

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path: terraform.tfstate

Outputs:

  public ip = 52.69.86.51

これで、先ほどのterraform planで確認した設定内容がずらずらと表示されて、作成が始まっていることが確認できるかと思います。問題なければ、上記のように1 addedのように表示されて、インスタンスがひとつ生成されたことが確認できます。また、Outputには生成したインスタンスのパブリックIPが表示されます。せっかくなのでマネージメントコンソールからも確認しておきましょう。

f:id:watass:20151227200339p:plain

できてますね。これで後はパブリックIPの /twitterloginsample/public/ にアクセスすれば、いつものFuelPHPのWelcome画面が表示されます。

Terraformで作成したインスタンスの削除

これで一連の作業は終わりですが、せっかくなので、Terraformを使ってインスタンスを削除してみます。Terraformは適用した環境の状態をカレントディレクトリ配下のterraform.tfstateというファイルに保存します。これによって、先ほど使用したexample.tfを編集することで変更されたリソースの状態を認識し、その状態にできるように変更を加えます。それでは、example.tfを以下のように修正します。

example.tf

provider "aws" {
  region = "ap-northeast-1"
}

単に先ほど設定したresourceとoutputを削除しただけです。これが意味することは、先ほどTerraformで適用した追加リソースを削除するということになります。試しにterraform planで変更内容を確認してみましょう。

$ terraform plan
Refreshing Terraform state prior to plan...

aws_instance.server: Refreshing state... (ID: i-176720b2)

The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

- aws_instance.server


Plan: 0 to add, 0 to change, 1 to destroy.

上記のように、1 to destroyと表示されていますので、作成された"aws_instance"の"server"が削除されます。問題ありませんので適用します。

$ terraform apply
aws_instance.server: Refreshing state... (ID: i-176720b2)
aws_instance.server: Destroying...
aws_instance.server: Destruction complete

Apply complete! Resources: 0 added, 0 changed, 1 destroyed.

1 destoryedとなっているので削除に成功したようです。マネージメントコンソールから確認してみます。

f:id:watass:20151227200301p:plain

よいですね!

まとめ

  • PackerでAMIに対してプロビジョニング、ビルドができる。設定はTemplateファイルひとつで完結する。
  • TerraformでPackerで生成したAMIを使ってインスタンスの作成ができる。設定はConfigurationファイルひとつで完結する。
  • Terraformはリソースの状態を管理できるので、既にapplyしたConfigurationのリソースを削除して再度applyすればインスタンスの削除も自動化できる。

小さくまとまってて使いやすい

今回使用したPackerやTerraformはgolangで書かれているそうです。それぞれのツールは特定の目的に特化していてすごく扱いやすいですね。まだ登場したばかりのツールなので、採用するには不安も残るかもしれませんが、これほどシンプルにまとめられていると、いざ使えなくなった場合にも移行がしやすそうです。

現在、HashicorpではAtlasというサービスを展開し、Vagrant、Packer、Terrraform、Consulなどを連携する仕組みを構築しようとしているようですが、今回はミニマムに始めることを目的としていたので、使いませんでした。とはいえ、既にHashicorp製品はインフラ界隈でかなり強い影響力を持ち、様々なIT企業でも採用されている状態なので、どっぷりHashicorpのプラットフォームに乗っかってしまうのも一手かも知れません。

ベンダロックインの問題はAWSの登場辺りから騒がれている問題ですが、ベンダロックインを恐れて開発速度を落としてしまうよりも、便利なものはどんどん採用してもいいんじゃないかと。どこまで採用するかどうかは個人の技術的な好みにもよるとは思いますが、個人的にはシンプルなツールが好きなので、Hashicorp製品は結構好きですね。OttoやNomadなど常に新しいツールを展開してきているので、今後も動向は追っていきたいと思います。