本稿は、社内で行われた @reoring 主催のTerraformハンズオンに参加したときの作業ログです。一応、ハンズオンを再現しやすいように書いたつもりですが、TerraformもAWSも疎いため誤りがあったらお教えください。
読者対象者
- プログラマーでTerraformでAWSの環境構築を自動化してみたい人。
- ちなみに、筆者
- AWS: 管理画面でEC2立てたり、S3バケット作ったりはできるが、VPSに逃げがち。
- プログラミング歴: PHP15年。他にGo言語などもやったことある。
- インフラ: 小規模なウェブアプリをホスティングするために、アプリに関係がある周辺のインフラ知識だけつまみ食いしたレベル。
- ちなみに、筆者
ハンズオンで得るもの
- TerraformでAWS上にEC2インスタンスを建てられるようになる。(hello worldレベル)
- Terraformコードを再利用性・保守性があるものにするモジュールの作り方が分かるようになる。
Terraformハンズオン
では、Terraformハンズオンをはじめよう。
必要ツールをインストールする
$ brew install tfenv awscli packer
# 特定のバージョンのterraformをインストールする
$ tfenv install 0.12.7
開発環境
Terraform初心者はシンタックスハイライトや構文チェックしてくれるエディタがあったほうがいいので、下記のとおり環境をととのえておいたほうが断然良い。
- IntelliJ IDEA Community(無料)をインストールしておく。
- IntelliJを起動したら、HashiCorp Terraform/HCL Language supportプラグインをインストールする。
- Preferences→Pluginsを開き、Marketplaceタブを選択→「terraform」で検索→「HashiCorp Terraform/HCL Language support」の「Install」をクリックする。
教材をダウンロード
$ git clone git@github.com:reoring/terraform-handson.git
$ cd terraform-handson
- ハンズオンで使った資料: terraformハンズオン - Qiita
- 本稿を使って追体験するだけなら、こちらの資料を読む必要はありません。
AWSプロファイルに認証情報を入れる
自分のIAMの認証情報を入れる。(ハンズオンでは、主催者がハンズオン用に用意したものを使った)
$ aws configure --profile terraform-hello-world
AWS Access Key ID [None]: **************
AWS Secret Access Key [None]: **********************************
Default region name [None]: ap-southeast-1
Default output format [None]:
profile
は好きな名前をつけていい。ここではterraform-hello-world
にした。後々使うので覚えておくこと。
※実験のためシンガポールリージョン(ap-southeast-1)を指定しています。
認証情報が入ったか確認する:
$ cat ~/.aws/credentials
[terraform-hello-world]
aws_access_key_id = ***************
aws_secret_access_key = *********************************
packerでdocker入りのamazon linux2をAMIをビルドする
まずPackerのディレクトリにいく。
$ cd amazonlinux2-with-docker
シンガポールリージョンにAMIを作りたいので、Packerの設定を変更しておく:
"variables": {
- "aws_region": "ap-northeast-1",
+ "aws_region": "ap-southeast-1",
"aws_profile": "{{env `AWS_PROFILE`}}"
},
下記のコマンドを実行すると、EC2上でビルドが走り、AMIが作られる:
$ AWS_PROFILE=terraform-hello-world packer build amazon-linux2-docker.json
2分くらいかかります。ビルドが一旦始まれば、AWS EC2コンソール(管理画面)上にもAMIが表示されるようになります。
terraformを初期化する
$ cd ../terraform-ec2
$ terraform init
このinitコマンドは、実行場所配下のtfファイルを読み込み、.terraformというディレクトリを作成する。
.terraform
└── plugins
└── darwin_amd64
├── lock.json
└── terraform-provider-aws_v2.26.0_x4
これは依存するプロバイダ(=AWSなどのIaaS)を扱う上で必要なプラグインをダウンロードしたもの。
variables.tfのAWSプロファイル名を自分のものに変更する
variable "aws_profile" {
type = string
description = "AWSのプロファイル名"
- default = "sandbox"
+ default = "terraform-hello-world"
}
これをしないとエラーになります:
Error: error validating provider credentials: error calling sts:GetCallerIdentity: NoCredentialProviders: no valid providers in chain. Deprecated.
For verbose messaging see aws.Config.CredentialsChainVerboseErrors
on main.tf line 5, in provider "aws":
5: provider "aws" {
terraformのplanを確認する
自分のIPアドレスを調べておく。
$ curl https://httpbin.org/ip
{
"origin": "117.102.178.110, 117.102.178.110"
}
VPCの接続元IP制限はどうするか聞かれるので上で調べた自分のIP + "/32"
を入力する。32はネットマスクでそのホスト1つだけという意味。
$ terraform plan
var.admin_ip
Enter a value: 117.102.178.110/32
トラブルシューティング
SSHの公開鍵がローカルにない
Error: Error in function call
on key-pair.tf line 3, in resource "aws_key_pair" "master-key":
3: public_key = file(var.path_to_public_key)
|----------------
| var.path_to_public_key is "~/.ssh/id_rsa.pub"
Call to function "file" failed: no file exists at /Users/suin/.ssh/id_rsa.pub.
このエラーが出たら鍵のパスを調整する:
variable "path_to_public_key" {
type = string
- default = "~/.ssh/id_rsa.pub"
+ default = "~/.ssh/id_myrsa.pub"
}
※なお、ECDSAの鍵は使えなかった。
planの内容
-
+
が追加されるもの
Terraform will perform the following actions:
# aws_instance.web will be created
+ resource "aws_instance" "web" {
+ ami = "ami-0ebd006a05952cc33"
+ arn = (known after apply)
+ associate_public_ip_address = true
+ availability_zone = (known after apply)
+ cpu_core_count = (known after apply)
+ cpu_threads_per_core = (known after apply)
- 上の
var.admin_ip
で設定した117.102.178.110/32
はセキュリティグループのingressの設定に現れる。 - ingressはincommingな通信のこと。
-
117.102.178.110/32
からは繋がるという意味。
+ ingress = [
+ {
+ cidr_blocks = [
+ "117.102.178.110/32",
]
なぜvar.admin_ip
が聞かれたのか?
variables.tfにデフォルト値がないため。
variable "admin_ip" {
type = string
// ここに default = "117.102.178.110/32" と書くと聞かれなくなる
}
または、terraform.tfvars
に値をセットすることもできる。
admin_ip = "117.102.178.110/32"
terraform.tfvars
というファイル名は特殊で、そのファイルがあるとplan
コマンドに-var-file terraform.tfvars
オプションをセットしたのと同じ意味になる。それ以外のファイル名は-var-file
プションを使ってファイル名を指定する。
# production専用のtfvarsを作った場合の例
$ terraform plan -var-file terraform.production.tfvars
そもそもplan
とは?
tfファイルと現在のAWS環境を突き合わせて、実行計画を見せてくれるもの。plan
はAWS環境を参照するだけで変更は加えない。
plan
の出力結果のうち次のサマリーの部分がとても重要。
Plan: 3 to add, 0 to change, 0 to destroy.
changeやdestoryが出ている場合は、よくチェックする必要がある。名前を変えただけなのに、AWS的にはdestoryしてからaddするといったオペレーションがあったりする。例えば、セキュリティグループの名称など。セキュリティグループの再作成は一瞬と言っても、瞬断するなどの副作用があったりする。そういう副作用がある場合は、メンテナンス時間にやらなければならなかったりするので、サマリーはよく見る習慣をつける。
実行する
terraform apply
を実行して
$ terraform apply
...[略]...
Plan: 3 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value:
yes
と入力することで、反映が始まる。
しばらくすると実行結果が表示される:
...[略]...
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
instance_ip = 18.138.255.66
このinstance_ip
は反映によって作られたEC2インスタンスのグローバルIPアドレスになる。
Outputsに出す情報は、outputs.tfで設定されたものになる。
output "instance_ip" {
value = aws_instance.web.public_ip
}
SSHでログインできるか確認してみよう:
ssh -i ~/.ssh/id_rsa ec2-user@18.138.255.66
破壊する
applyと反対に作った環境を無かったことにするにはdestoryコマンドを実行する。
terraform destory
main.tfのコードリーディング
main.tfを読み解き、HCL言語を理解する。
// メタ情報
terraform {
required_version = "= 0.12.7" // terraform 0.12.7でしかこの設定実行できませんよという意味
}
// ここからAWSの設定
provider "aws" {
region = var.aws_region
profile = var.aws_profile
}
// EC2インスタンス作成に仕様するAMIの情報をAWSからとってくる宣言。
// data "データタイプ" "変数名" の書式で定義していく。とってきたデータは変数名に代入される。
// JavaScriptで言ったら const amazonLinux2 = awsAmi({ most_recent: true... }) みたいなイメージ。
data "aws_ami" "amazon-linux2" {
// とってくるAMIの条件設定
most_recent = true // 最新のAMI1件
owners = ["self"]
// AMI名で絞り込み
filter {
name = "name"
values = ["docker-amazon-linux2-*"]
}
// 仮想化タイプで絞り込む
filter {
name = "virtualization-type"
values = ["hvm"]
} // あえてSQLでいうと、次のようなイメージ:
// SELECT * FROM aws_ami
// WHERE name LIKE "docker-amazon-linux2-%"
// AND virtualization-type = "hvm"
// ORDER BY created_at DESC LIMIT 1;
// 他に指定できる条件は公式サイト参照: https://www.terraform.io/docs/providers/aws/d/ami.html#attributes-reference
}
resource "aws_instance" "web" {
// インスタンスの設定をお好みでここに書く
ami = data.aws_ami.amazon-linux2.image_id // 10行目で定義したデータを参照
instance_type = "t2.micro"
associate_public_ip_address = true
key_name = aws_key_pair.master-key.key_name
vpc_security_group_ids = [aws_security_group.web.id]
// 他に指定できる設定値は公式サイト参照: https://www.terraform.io/docs/providers/aws/d/instance.html
}
モジュール化にチャレンジ
リソースの定義をモジュール化して、コードの再利用にチャレンジする。
再利用可能にするリソース定義
main.tfの下記の部分をモジュール化して再利用できるようにする。
// main.tfより抜粋
resource "aws_instance" "web" {
ami = data.aws_ami.amazon-linux2.image_id
instance_type = "t2.micro"
associate_public_ip_address = true
key_name = aws_key_pair.master-key.key_name
vpc_security_group_ids = [aws_security_group.web.id]
}
再利用可能な状態になると、下記のように同じようなEC2インスタンスをコピペ少なめで量産できるようになる。
module "web" {
source = "./modules/instance"
ami_id = data.aws_ami.amazon-linux2.image_id
instance_type = "t2.micro"
enable_public_ip = true
key_name = aws_key_pair.master-key.key_name
security_group_id = aws_security_group.web.id
}
module "web2" {
source = "./modules/instance"
ami_id = data.aws_ami.amazon-linux2.image_id
instance_type = "t2.micro"
enable_public_ip = true
key_name = aws_key_pair.master-key.key_name
security_group_id = aws_security_group.web.id
}
module "web3" {
source = "./modules/instance"
ami_id = data.aws_ami.amazon-linux2.image_id
instance_type = "t2.micro"
enable_public_ip = true
key_name = aws_key_pair.master-key.key_name
security_group_id = aws_security_group.web.id
}
モジュール化をやってみよう
まずmodules/instanceというディレクトリをほり、そこに3つのtfファイルを作る:
mkdir -p modules/instance
touch modules/instance/{main,outputs,variables}.tf
こんな構造になる。
├── main.tf
├── modules ... 今作ったディレクトリ
│ └── instance
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
ファイルの内容は次のようにする:
resource "aws_instance" "ec2_instance" {
ami = var.ami_id
instance_type = var.instance_type
associate_public_ip_address = var.enable_public_ip
key_name = var.key_name
vpc_security_group_ids = [var.security_group_id]
}
variable "security_group_id" {
type = string
}
variable "key_name" {
type = string
}
variable "enable_public_ip" {
type = bool
}
variable "instance_type" {
type = string
}
variable "ami_id" {
type = string
}
output "instance_ip" {
value = aws_instance.ec2_instance.public_ip
}
これでモジュール化が完了。
このモジュールを使うかたちにmain.tfを修正する
// コメントアウト
//resource "aws_instance" "web" {
// ami = data.aws_ami.amazon-linux2.image_id
// instance_type = "t2.micro"
//
// associate_public_ip_address = true
// key_name = aws_key_pair.master-key.key_name
//
// vpc_security_group_ids = [aws_security_group.web.id]
//}
module "web" {
source = "./modules/instance"
ami_id = data.aws_ami.amazon-linux2.image_id
instance_type = "t2.micro"
enable_public_ip = true
key_name = aws_key_pair.master-key.key_name
security_group_id = aws_security_group.web.id
}
次に、outputs.tfも修正する
output "instance_ip" {
- value = aws_instance.web.public_ip
+ value = module.web.instance_ip
}
以上でモジュール化のチャレンジはおわり。
最後に、terraform init
をするとモジュールが使えるようになる。
terraform init
公開されているサードパーティ製
Cloud Posseにはサードパーティ製モジュールがたくさん公開されている。これを使うもよし、コードリーディングして技を盗むもあり。
Q&A
main.tfが長くなったら?
ファイルを分ける。例えば、セキュリティグループのリソース設定を切り出して、security-group.tfを作る。main.tfからリンク(JavaScriptやGo言語のimport
のようなこと)する必要はない。ディレクトリ配下のtfファイルがすべて読まれる。
CloudFormationの違いは?
TerraformはいろんなIaaSに対応している、CloudFormationはAWSにしか対応していない。
CloudFormationはJSONやYAMLをひたすら書く感じ。TerraformはHCL言語という書きやすいDSLで書く。
CloudFormationは手続き型プログラミング。Terraformは宣言型プログラミング。
HCLって?
HCLはHashiCorp Configuration Languageというプログラミング言語。Terraformのtfファイルもこの言語で記述する。
HCLには2つのバージョンがあり、HCL1とHCL2がある。HCL2は最近リリースされたもののため、ネット上にはHCL1の情報が多いことには要注意。
HCL2はHCL1の下位互換性がある。
terraform.tfstateはgitで管理する?
gitでは管理しない。secretは含まれないがインフラ構成の実態など機密情報が含まれているため、公開は避けたほうがいい。特にグループでインフラを保守するときは、backendというTerraformの仕組みを使って、terraform.tfstateをS3に同期してグループで共有するようにしておく。
tfファイルのlintをするには?
terraform validate
コマンドで設定ファイルの静的チェックが行える。
tfファイルの自動整形をするには?
terraform fmt
コマンドで設定ファイルのコードが自動整形できる。go fmt
のようなイメージ。
このハンズオンではPackerでAMIを作ったけど、Packerは必要なの?
もちろん、AWSが提供しているAMIを使ってもいい。その場合、Packerは不要。
所感
- 思っていたよりTerraformは簡単だった。(たぶんひとりでやると難しく、教わると簡単というタイプのツールだと感じた)
- AWSは別途覚えないとならないが、Terraform自体はプログラマならすぐ覚えられる言語だと思った。
- TerraformのエコシステムにはGoの香りを感じた。go getとterraform getとか。
- @reoring ありがとう😌