
TerraformでAWSリソースを作成してみる #482
Terraformでインフラをいじる機会が増えてきたので一度体系的に学びたく、以下の書籍を参考に勉強しています。
とても理解しやすく書かれており、良書だと思います。
ここで学んだことを少しずつアウトプットしていきます。
Terraformとは
Terraformは、IaC (Infrastructure as Code) を実現する手段の1つで、大きな括りではプロビジョニングツールという分類になります。プロビジョニングツールはサーバ、DB、キャッシュ、ロードバランサー、キュー、監視、サブネット設定、ファイフォール設定、SSL証明書など、インフラに関するほとんどあらゆるものを作成できます。
また、IaCツールの中でも手続き型と宣言型という2つのスタイルがあり、Terraformは宣言型に該当します。両者の違いは、前者は実行する内容をコード化するのに対し、後者は最終的な状態をコード化します。
つまり、手続き型のツールを何度も実行すると同じ内容でいくつもリソースが作成されますが、宣言型では前回から変更箇所がなければ何もしません。チームで長く管理することを考えると、宣言型の方が扱いやすそうですね。
Terraformはどう動くのか
TerraformはHashicorpによって作られ、Go言語で書かれたオープンソースツールです。AWS、Azure、Google Cloud、DigitalOcean、OpenStackなどに対応していますが、これは各プロバイダに対してAPIコールを行うことで実現されています。
APIで各プロバイダにコマンドを送信し、プロバイダのエージェントが各サーバー上でそのコマンドを実行してくれるため、自分のローカルにエージェントを追加でインストールする必要はありません。
基本文法
最も基本的な文法についてメモしておきます。
リソース
作成するリソースは以下のように定義できます。
resource "<プロバイダ>_<タイプ>" "名前" {
[設定...]
}
例えばAWSのEC2インスタンスを作成する場合は以下のようになります。
resource "aws_instance" "example" {
ami = "ami-0fb653ca2d3203ac1"
instance_type = "t2.micro"
vpc_security_group_ids = [ aws_security_group.my_instance.id ]
# インスタンスの最初の起動時に実行されるスクリプト
user_data = <<-EOF
#!/bin/bash
echo "Hello, World" > index.html
nohup busybox httpd -f -p ${var.server_port} &
EOF
# user_dataを変更してapplyした際、元のインスタンスをターミネートして全く新しいインスタンスを起動するための設定
# user_dataは最初の起動時にしか実行されないため
user_data_replace_on_change = true
tags = {
Name = "terraform-example"
}
}
入力変数
他のプログラミング言語と同様に、Terraformでも変数に値を格納して使うことができます。
variable "名前" {
[設定...]
}
例えばサーバーのポート番号を格納する場合は以下です。
variable "server_port" {
description = "The port the server will use for HTTP requests"
type = number
default = 8080
}
出力変数
これはterraform applyでリソースを生成した時に表示されたり、Terraformモジュールを使っている場合の、値の引き渡しに使えます。(例えば、1つのモジュールで生成されたリソースのIDやARNを、別のモジュールで利用したい場合など。モジュールについては後日まとめます。)
output "名前" {
value = <値>
[設定...]
}
例えばELBを使っていて、そのDNS名を出力したい場合は以下です。
output "alb_dns_name" {
value = aws_lb.my_example.dns_name
description = "The domain name of the load balancer"
}
データソース
Terraformを動かすたびにプロバイダ(AWSなど)から取得する読み出し専用の情報です。これを定義しても何もリソースは作成されません。プロバイダのAPIでデータ取得し、それをTerraformコード内で使えるようにするだけです。
data "<プロバイダ>_<タイプ>" "名前" {
[CONFIG...]
}
例えばデフォルトVPCのデータを取得する時は以下です。
data "aws_vpc" "default" {
default = true
}
以下のようにして取り出せます
data.aws_vpc.default.id
これを使って、重ねてVPC内のサブネットのデータを取得する時は以下です。
data "aws_subnets" "default" {
filter {
name = "vpc-id"
values = [data.aws_vpc.default.id]
}
}
基本コマンド
コードが書けたら、以下の手順でコマンドを実行してインフラを作成していけます。
$ terraform init
$ terraform plan
$ terraform apply
$ terraform destroy
terraform init
Terraformがコードをスキャンし、どのプロバイダを使うのか判断し、そのプロバイダに関するコードをダウンロードするためのコマンドです。デフォルトでは、プロバイダのコードは「.terraform」フォルダにダウンロードされ(.gitignoreすることを推奨)、.terraform.lock.hclファイルにもその情報を持ちます。
このコマンドは最初に実行する必要があり、何度実行しても問題ありません。
terraform plan
実際に変更を加える前にTerraformが何をする予定なのかを確認できます。インフラを触る前に最終確認ができます。ただ、ここで事前確認できる内容には限界があるみたいで、planでは無問題だったのに次のapplyでコケる、という事象がよくあります。
terraform apply
これを実行すると実際にリソースが作成・変更されます。前述の通り、planでは上手くいってもここでコケることがあります。例えば私は以下に直面しました。
AutoScaling groupの設定で、サンプルでは以下のようになっており、かつplanは無事に通っていました。
resource "aws_launch_configuration" "my_example" {
image_id = "0fb653ca2d3203ac1"
instance_type = "t2.micro"
security_groups = [ aws_security_group.my_instance.id ]
user_data = <<-EOF
#!/bin/bash
echo "Hello, World" > index.html
nohup busybox httpd -f -p ${var.server_port} &
EOF
# Autoscaling Groupがある起動設定を使った場合に必須
lifecycle {
create_before_destroy = true
}
}
ただしこれでは以下のエラーが出ました。
│ Error: creating Auto Scaling Launch Configuration (terraform-20240811124644967400000001): operation error EC2: DescribeImages, https response error StatusCode: 400, RequestID: b574174d-de24-4a77-8f1d-ca2562f56952, api error InvalidAMIID.Malformed: Invalid id: "0fb653ca2d3203ac1" (expecting "ami-...")
│
│ with aws_launch_configuration.my_example,
│ on main.tf line 36, in resource "aws_launch_configuration" "my_example":
│ 36: resource "aws_launch_configuration" "my_example" {
どうもimage_tagの所に「ami-」を付ける必要があったみたいです。
以下で通過しました。
resource "aws_launch_configuration" "my_example" {
image_id = "ami-0fb653ca2d3203ac1"
instance_type = "t2.micro"
security_groups = [ aws_security_group.my_instance.id ]
user_data = <<-EOF
#!/bin/bash
echo "Hello, World" > index.html
nohup busybox httpd -f -p ${var.server_port} &
EOF
# Autoscaling Groupがある起動設定を使った場合に必須
lifecycle {
create_before_destroy = true
}
}
他にもELBのセキュリティグループの設定で、以下のようにしているとエラーが出ました。これは凡ミスですが、planでは検出されませんでした。
resource "aws_security_group" "alb" {
name = "terraform-example-alb"
# インバウンドHTTPリクエストを許可
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# アウトバウンドHTTPリクエストを許可
egress {
from_port = 80
to_port = 80
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
エラーの内容は以下の通りで、egressでprotocolを「-1 (なんでもOK)」にするならportは指定するな、という内容です。
│ Error: updating Security Group (sg-0ac573eac927d0d12) egress rules: updating rules: from_port (80) and to_port (80) must both be 0 to use the 'ALL' "-1" protocol!
│
│ with aws_security_group.alb,
│ on main.tf line 72, in resource "aws_security_group" "alb":
│ 72: resource "aws_security_group" "alb" {
│
ポートを0にしたら通りました。
# アウトバウンドHTTPリクエストを許可
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
ここまでやって、その後に再度実行すると以下のエラーが出ました。
│ Error: waiting for Auto Scaling Group (terraform-20240811125414427700000002) capacity satisfied: scaling activity (fa064506-040f-9f76-9710-266ba715c4f9): Failed: Access denied when attempting to assume role arn:aws:iam::345042987984:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling. Validating load balancer configuration failed.
│
│ with aws_autoscaling_group.my_example,
│ on main.tf line 53, in resource "aws_autoscaling_group" "my_example":
│ 53: resource "aws_autoscaling_group" "my_example" {
ただしこれは原因が分からずで、、一度後述のdestroyを実行して、翌日に再度terraform applyしたら出ませんでした。再実行すると上手くいくケースもあるのかな。
terraform destroy
最後にdestroyコマンドです。実務でこれを使うことはほぼ無いと思います。これはここで作成したリソースを全て削除するコマンドです。所謂「前に戻る」機能は無いので、とてもシンプルに全削除してくれます。
チュートリアルで色々と試している時はとても便利です。
ここまでお読みいただきありがとうございました!!