前回の記事では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とは
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圧縮をダウンロードしてきます。
ダウンロードできたら、 /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圧縮をダウンロードしてきます。
ダウンロードできたら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を作成し、インスタンスを削除するという一連の作業が自動で完了しました。マネージメントコンソールから確認してみましょう。
ちゃんと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という値を割り当てます。 |
他の項目については公式ドキュメントを確認してください。
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が表示されます。せっかくなのでマネージメントコンソールからも確認しておきましょう。
できてますね。これで後はパブリック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となっているので削除に成功したようです。マネージメントコンソールから確認してみます。
よいですね!
まとめ
小さくまとまってて使いやすい
今回使用したPackerやTerraformはgolangで書かれているそうです。それぞれのツールは特定の目的に特化していてすごく扱いやすいですね。まだ登場したばかりのツールなので、採用するには不安も残るかもしれませんが、これほどシンプルにまとめられていると、いざ使えなくなった場合にも移行がしやすそうです。
現在、HashicorpではAtlasというサービスを展開し、Vagrant、Packer、Terrraform、Consulなどを連携する仕組みを構築しようとしているようですが、今回はミニマムに始めることを目的としていたので、使いませんでした。とはいえ、既にHashicorp製品はインフラ界隈でかなり強い影響力を持ち、様々なIT企業でも採用されている状態なので、どっぷりHashicorpのプラットフォームに乗っかってしまうのも一手かも知れません。
ベンダロックインの問題はAWSの登場辺りから騒がれている問題ですが、ベンダロックインを恐れて開発速度を落としてしまうよりも、便利なものはどんどん採用してもいいんじゃないかと。どこまで採用するかどうかは個人の技術的な好みにもよるとは思いますが、個人的にはシンプルなツールが好きなので、Hashicorp製品は結構好きですね。OttoやNomadなど常に新しいツールを展開してきているので、今後も動向は追っていきたいと思います。