このブログ記事では、Terraformを用いたリソースプロビジョニングの基礎を紹介します。

前回の記事では、Terraformとは何か、何のためのものか、そしてTerraformのインストールプロセスと標準的なデプロイメントワークフローについて説明しました。 そこで、この記事では、ワークフローの具体的な進め方と、簡単な例を使った基本的なリソースのプロビジョニングについて詳しく説明していきます。
リモートバックエンド
.tf
ファイル内のTerraformコードは主に3つのブロックで構成されています:terraformブロック、providerブロック、そしてresourceブロックです。
terraformブロックは、ステートファイル(.tfstate
拡張子)がホストされるバックエンドと、必要なプロバイダーのバージョンを定義します。
providerブロックはプロバイダーを設定し、resourceブロックはそれらのプロバイダーによって提供されるリソースを定義します。
以下のmain.tf
の例では、これらのブロックを使用して、デフォルトのローカルバックエンドでS3バケットとファイルロック用のDynamoDBテーブル(mutexロックのような)を設定しています。
# Terraform Block
terraform {
# ローカルバックエンドの使用に関しては`backend`ブロックを用いなくて良い
required_providers {
aws = {
source = "hashicorp/aws"
version = "-> 3.0"
}
}
}
# Provider Block (AWS CLIが設定されている前提)
provider "aws" {
region = "ap-northeast-1"
}
# Resource Block
# 構文: block_type resource_name user_defined_name {...params...}
# S3バケット "terraform_state"
resource "aws_s3_bucket" "terraform_state" {
bucket = "tf-state"
force_destroy = true
versioning {
enabled = true
}
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
}
# DyanmoDBのデーブル "terraform_locks"
resource "aws_dynamodb_table" "terraform_locks" {
name = "tf-state-locking"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Terraformは文字列、数値、真偽値、リストなどの基本的なデータ型をサポートしており、今後説明する他の言語機能も備えています。
上記のような構文を使用して、バックエンド(ローカル)、プロバイダーバージョン、プロバイダー(AWS)、および対応するリソース(S3バケットとDynamoDBテーブル)を定義できます。
リソースが定義されたら、terraform fmt --check
でフォーマットを確認し、前のコマンドから--check
フラグを削除してフォーマットを修正し、
terraform plan
で計画を立て、terraform apply
で設定を適用できます。AWS GUIを確認することで、
リソースの作成が成功したことを確認できます。
# Terraform Block
terraform {
backend "s3" {
bucket = "tf-state"
key = "tf-infra/terraform.tfstate" # .tfstateの保存先
region = "ap-northeast-1"
dynamodb_table = "tf-state-locking"
encrypt = true
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "-> 3.0"
}
}
}
先ほどでは、ローカルバックエンドでS3バケットとDynamoDBテーブルを定義しましたが、効果的なコラボレーションのために、
適切なファイルロックを備えたリモートバックエンドを設定してステートファイルをホストしたい場合がよくあります。
このようなリモートバックエンドを設定するには、backend
ブロック(上のハイライトされたコードブロック)を追加してファイルを編集し、
再度ファイルを適用します。Terraformは自動的に変更を検出し、ローカルバックエンドをすでに定義されたS3バケットに移行するよう促すので、
これを確認してリモートバックエンドを設定できます。これにより、他のリソース用の.tfstate
ファイルのホスティングを開始できます。
基本的なWebアーキテクチャの例
Webアプリケーションの最もシンプルなアーキテクチャは、リバースプロキシ(ロードバランサー)、リモートサーバー、そしてリレーショナルデータベースで構成されます。
そして2番目にシンプルなアーキテクチャでは、Application Load Balancer(ALB)、2つのEC2インスタンス、そしてマネージドリレーショナルデータベースサービス(RDS)を利用します。
ここでは、この2番目にシンプルなアーキテクチャを例として、TerraformでWebアプリケーション用の基本的なリソースをプロビジョニングする方法を学びます。
前のセクションと同様に、異なるバックエンドキー(例:webapp/terraform.tfstate
)を使用してterraformブロックとproviderブロックを設定することから始めることができます。
# EC2 インスタンス (Instance 1 & Instance 2)
resource "aws_instance" "instance_1" {
ami = "ami-0822295a729d2a28e" # Ubuntu 16.04 LTS ap-northeast-1のAmazon Machine Image (AMI)
instance_type = "t3.micro"
security_groups = [aws_security_group.instances.name] # のちに定義される"instances"セキュリティグループを参照
# 単純なindex.htmlを提供するPython HTTPサーバー
user_data = <<-EOF
#!/bin/bash
echo "This is Instance 1" > index.html
python3 -m http.server 8080 &
EOF
}
resource "aws_instance" "instance_2" {
ami = "ami-0822295a729d2a28e"
instance_type = "t3.micro"
security_groups = [aws_security_group.instances.name]
user_data = <<-EOF
#!/bin/bash
echo "This is Instance 2" > index.html
python3 -m http.server 8080 &
EOF
}
# EC2 セキュリティグループ
resource "aws_security_group" "instances" {
name = "instance-security-group"
}
# EC2 セキュリティグループルール
resource "aws_security_group_rule" "allow_http_inbound" {
type = "ingress"
security_group_id = aws_security_group.instances.id
from_port = 8080
to_port = 8080
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # allow all address
}
terraformブロックとproviderブロックを定義した後、EC2インスタンスからリソース設定の定義を開始できます。
ap-northeast-1
リージョンでUbuntu用のAmazon Machine Image(AMI)を参照し、
上記のようにポート8080でHTMLファイルを提供するt3.micro
インスタンスをインスタンス化できます。
(Ubuntu用のAMIはAmazon EC2 AMI Locatorで見つけることができます。)
以前に簡単に述べたように、上記のようにbashスクリプトをリソース設定に使用できますが、かなり制限があります(これが通常設定IaCツールを使用する理由です)。
「instances」と呼ばれるセキュリティグループも作成でき、任意のIPアドレスからの受信トラフィックを許可するルールを付加できます。
# デフォルトのVPC、Subnetを使用
# 事前に定義されたリソースの使用のため`data`ブロックを使用
data "aws_vpc" "default_vpc" {
default = true
}
data "aws_subnet_ids" "default_subnet" {
vpc_id = data.aws_vpc.default_vpc.id # 他のブロックにこうして参照かのう
}
# ALB
resource "aws_lb" "load_balancer" {
name = "web-app-lb"
load_balancer_type = "application" # ALB
subnets = data.aws_subnet_ids.default_subnet.ids
security_groups = [aws_security_group.alb.id]
}
# ALB セキュリティグループ
resource "aws_security_group" "alb" {
name = "alb-security-group"
}
# ALB セキュリティグループルール (Ingress and Egress)
resource "aws_security_group_rule" "allow_alb_http_inbound" {
type = "ingress"
security_group_id = aws_security_group.alb.id
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
resource "aws_security_group_rule" "allow_alb_all_outbound" {
type = "egress"
security_group_id = aws_security_group.alb.id
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
EC2インスタンスの定義に続いて、トラフィックをそれらにルーティングするロードバランサーを設定できます。
このアプローチでは、まずdata
ブロックを使用してデフォルトのVPCとサブネットの使用を指定し、
次にデフォルトサブネット内のALBとALB用のセキュリティグループを定義します。セキュリティグループは、
ポート80での任意のIPアドレスからのすべての受信トラフィックと、ポート0でのすべての送信トラフィックを許可します。
次に、ポート80で受信トラフィックを待機し、HTTPトラフィックをインスタンスを含むターゲットグループに転送するロードバランサーリスナーを定義する必要があります。
# ALB リスナー
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.load_balancer.arn
port = 80
protocol = "HTTP"
# Return a 404 page by default
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "404: page not found"
status_code = 404
}
}
}
# ALB リスナールール
resource "aws_lb_listener_rule" "instances" {
listener_arn = aws_lb_listener.http.arn
priority = 100
condition {
path_pattern {
values = ["*"]
}
}
action {
type = "forward"
target_group_arn = aws_lb_target_group.instances.arn
}
}
# ALB ターゲットグループ
resource "aws_lb_target_group" "instances" {
name = "example-target-group"
port = 8080
protocol = "HTTP"
vpc_id = data.aws_vpc.default_vpc.id
health_check {
path = "/"
protocol = "HTTP"
matcher = "200"
interval = 15
timeout = 3
healthy_threshold = 2
unhealthy_threshold = 2
}
}
# ALB ターゲットアタッチメント
resource "aws_lb_target_group_attachment" "instance_1" {
target_group_arn = aws_lb_target_group.instances.arn
target_id = aws_instance.instance_1.id
port = 8080
}
resource "aws_lb_target_group_attachment" "instance_2" {
target_group_arn = aws_lb_target_group.instances.arn
target_id = aws_instance.instance_2.id
port = 8080
}
上記のリスナーとターゲットグループの設定には、TLS終端や高度なルーティングは含まれていません。 これらは対応するパラメータを設定することで構成できます(詳細は記事の下部に引用されているAWSプロバイダーのドキュメントを確認できます)。 また、ALBを使用する代わりにNginxを実行するEC2インスタンスを使用するか、適切なネットワーキングを備えたKubernetesなどの他のサービスを利用してロードバランサーを定義することもできます。
resource "aws_db_instance" "db_instance" {
allocated_storage = 20
auto_minor_version_upgrade = true # 特定のマイナーバージョンを指定すべき
storage_type = "standard"
engine = "postgres"
engine_version = "12"
instance_class = "db.t4g.micro"
name = "mydb"
username = "foo"
password = "foobarbaz" # 直接的なパスワードの使用は避けるべき
skip_final_snapshot = true
}
最後に、上記のリソース設定を使用してPostgreSQLを実行するRDSインスタンスを定義できます。
本番環境では、特定のマイナーバージョンを指定し、パスワード(さらにはデータベース名とユーザー名も)の設定にシークレット管理メカニズム(今後の記事で説明します)を使用し、
データベースにアクセスするEC2上で実行されるアプリケーションと併用する必要があります。
すべてのリソースが定義されたら、terraform apply
を実行してそれらすべてを同時にプロビジョニングできます。
その後、しばらくしてからダッシュボードを確認することで、リソースの作成が成功したことを確認できます。
結論
この記事では、S3とDynamoDBテーブルを使用したリモートバックエンドの設定方法と、 シンプルで小規模なWebアプリケーション用の基本的なインフラストラクチャをプロビジョニングする方法について説明しました。 リソースのパラメータやAWSが提供する他のリソースの詳細については、下記に引用されているAWSプロバイダーのドキュメントと、 仕様を説明するAWS公式ウェブサイトを確認することをお勧めします。また、他のクラウドプロバイダーのリソースを利用するために、 他のプロバイダーのドキュメントもTerraform Registryから参照できます。
リソース
- DevOps Directive. 2022. Complete Terraform Course - From BEGINNER to PRO! (Learn Infrastructure as Code). YouTube.
- HashiCorp. n.d. AWS Provider Documentation. Terraform Registry.