はじめに
RaiseTechの課題で、CloudFormationで構築した環境を、Terraformで再現しました。
自分の理解を深めるために作成した環境のアウトプットをします。
参考
- 公式ドキュメント
- AWSプロバイダ公式ドキュメント
- ディレクトリ構成について
CloudFormationで構築した環境
ディレクトリ構成
ディレクトリ構成は下記のとおりです。
.
├── main.tf # リソースの作成
├── outputs.tf # 出力情報の定義
├── terraform.tf # プロバイダーの設定
├── terraform.tfvars # 変数の値
└── variables.tf # 変数の定義
今回は学習用で複雑な環境ではないので、単一のディレクトリ構成にしています。
管理しやすいように、目的によってファイルを分けています。
各ファイルの説明
Terraformはブロックと呼ばれるまとまりで設定を書いていきます。
今回使用するブロックは下記のとおりです。
terraform block
- インストールするプロバイダやTerraformのバージョンなどTerraform自体の設定
required_providers block
- プロバイダ名やバージョンを設定
provider block
- リソースを作成するリージョンなど、プロバイダが管理するすべてのリソースに適用されるオプションを設定
resource block
- 作成するリソースを設定
data block
- プロバイダから取得するリソースに関するデータの設定
variable block
- 変数の設定
output block
- 出力情報の設定
terraform.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.28.0"
}
}
required_version = "~> 1.14.3"
}
Terraform本体の設定ファイル。
Terraformのバージョン、使用するプロバイダとそのバージョンを指定。
- source = "hashicorp/aws"
プロバイダ名の指定。AWSの場合は「hashicorp/aws」となる。
プロバイダとは、特定のクラウドやサービスとのやり取りをするための専用パーツ。
Terraform本体をリモコンに例えると、プロバイダは、そのリモコンが何を操作できるかを決めるもの。 - version = "~> 6.28.0"
プロバイダのバージョンの設定。「~>」で「6.28.0以上6.29未満」の意味。パッチバージョンのアップデートのみ許可する設定 - required_version = "~> 1.14.3"
Terraform本体のバージョンの設定。「~>」で「1.14.3以上1.15未満」の意味。パッチバージョンのアップデートのみ許可する設定
バージョン番号の構成
TerraformはバージョンをX.Y.Zの形式で表す。
Xはメジャーバージョン、Yはマイナーバージョン、Zはパッチバージョン。
パッチバージョンをアップデートすると、バグが修正される。
マイナーバージョンをアップデートすると、新機能が追加されるが、既存の機能とは概ね互換性を保っている状態。
メジャーバージョンをアップデートすると、互換性のない変更が行われるため注意が必要。
main.tf
作成するリソースの設定ファイル。
#リージョン指定
provider "aws" {
region = "ap-northeast-1"
}
リソースを作成するリージョンを指定。
#VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
instance_tenancy = "default"
tags = {
Name = "terraform-study-vpc"
}
}
VPCの作成。
- resource blockでは、リソースタイプとリソース名を最初に宣言する必要がある
- 「aws_vpc」がリソースタイプ、「main」がリソース名
- リソース名にリソースタイプを含めることは推奨されていない
#Internet GateWay
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "terraform-study-ig"
}
}
インターネットゲートウェイの作成。
- vpc_id = aws_vpc.main.id
インターネットゲートウェイを作成するVPCのIDを指定
リソースタイプ(aws_vpc).リソース名(main).属性名(id)で、他所で定義されているリソースの属性を参照できる
#Subnet
resource "aws_subnet" "public_private" {
vpc_id = aws_vpc.main.id
for_each = var.terraform_subnets
availability_zone = each.value.zone
cidr_block = each.value.cidr
map_public_ip_on_launch = each.value.launch
tags = {
Name = each.value.name
}
}
サブネットの作成。
今回は、パブリックサブネットを2つ、プライベートサブネットを2つ、合計4つのサブネットを作成する。そのため、for_eachを使用し、1回コードを書くだけで4つのサブネットを作成できるようにしている。
for_each
・定義した値の数だけ繰り返し処理をすることができる機能
・値の定義の方法はmap型またはset型
・map型:名前(キー)と内容(値)がセットになった辞書のようなデータ型
・set型:重複することのないユニークな値を持つ配列のデータ型
each.value
・for_eachを使用する際に、Terraformが自動的に用意してくれる変数の一つ
・データの値を参照する
- for_each = var.terraform_subnets
variables.tfで定義した変数を参照し、キーごとに値を取得していく
var."変数名"で、variableで定義した変数を参照できる - availability_zone = each.value.zone
terraform_subnetsのzoneの値を取得する - cidr_block = each.value.cidr
terraform_subnetsのcidrの値を取得する - map_public_ip_on_launch = each.value.launch
terraform_subnetsのlaunchの値を取得する
#Route Table
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
tags = {
Name = "terraform-study-routetable-public"
}
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
tags = {
Name = "terraform-study-routetable-private"
}
}
ルートテーブルの作成。
プライベートサブネットはインターネットに接続しない設定のため、インターネットゲートウェイとの紐付けを行っていない。
#ルートテーブルとサブネットを関連付け
resource "aws_route_table_association" "public_private" {
for_each = var.terraform_subnets # サブネットと同じ数だけループを回す
# each.key は "public-1a" などが入る
subnet_id = aws_subnet.public_private[each.key].id # 作成したサブネットの ID を取得
# 三項演算子でルートテーブルを切り替える
# 条件式 ? 真の場合の値(true_value) : 偽の場合の値(false_value)
route_table_id = each.value.is_public ? aws_route_table.public.id : aws_route_table.private.id
}
ルートテーブルとサブネットの紐付け。
for_eachを使い、サブネットと同じ数だけ処理を回すようにしている。
each.key
・for_eachを使用する際に、Terraformが自動的に用意してくれる変数の一つ
・データの見出しを参照する
- subnet_id = aws_subnet.public_private[each.key].id
サブネットのIDを取得。 - route_table_id = each.value.is_public ? aws_route_table.public.id : aws_route_table.private.id
ルートテーブルのIDを取得。
三項演算子を使い、紐付けるルートテーブルを切り替えるようにしている。「each.value.is_public」が「true」の場合、パブリックサブネット用のルートテーブルIDを取得し、「each.value.is_public」が「false」の場合、プライベートサブネット用のルートテーブルのIDを取得する。
#EC2 Security Group
resource "aws_security_group" "ec2_sg" {
name = "Security Group for EC2"
description = "EC2-SG"
vpc_id = aws_vpc.main.id
tags = {
Name = "terraform-study-ec2-sg"
}
}
resource "aws_vpc_security_group_ingress_rule" "allow_ssh" {
security_group_id = aws_security_group.ec2_sg.id
cidr_ipv4 = var.CidrIp_From_Internet
from_port = 22
to_port = 22
ip_protocol = "tcp"
}
resource "aws_vpc_security_group_ingress_rule" "allow_springboot" {
security_group_id = aws_security_group.ec2_sg.id
from_port = 8080
to_port = 8080
ip_protocol = "tcp"
referenced_security_group_id = aws_security_group.alb_sg.id #ALB用のセキュリティグループ
}
resource "aws_vpc_security_group_egress_rule" "allow_all_ec2" {
security_group_id = aws_security_group.ec2_sg.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
}
EC2用のセキリュティグループの作成。
セキュリティグループを作成するときは、お互いがお互いを参照する循環参照を防ぐために、セキリュティグループ本体とルールを別々に作成して紐付けることが推奨されている。
参考:aws_security_group
- aws_security_group
セキリュティグループ本体の設定 - aws_vpc_security_group_ingress_rule
インバウンドルールの設定。SSHとALBからの接続のみを許可。
SSH接続をする際に許可するIPアドレスは変数として設定。 - aws_vpc_security_group_egress_rule
アウトバンドルールの設定。
「ip_protocol = "-1"」は、すべてのプロトコルを指定するの意味。
なお、アウトバンドルールを設定しないと、外部との通信ができないので要注意。
#EC2
data "aws_ssm_parameter" "amazonlinux_2" {
name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}
resource "aws_instance" "web" {
availability_zone = "ap-northeast-1a"
ami = data.aws_ssm_parameter.amazonlinux_2.value
disable_api_termination = false
instance_type = "t3.micro"
key_name = var.key_pair_name
monitoring = false
subnet_id = aws_subnet.public_private["public-1a"].id
vpc_security_group_ids = [aws_security_group.ec2_sg.id]
tags = {
Name = "terraform-study-ec2"
}
}
EC2インスタンスの設定。
aws_ssm_parameterで、最新のAmazon Linux 2023を取得。
key_nameは機密事項なので、変数として設定している。
#ALB Security Group
resource "aws_security_group" "alb_sg" {
name = "Security Group for ALB"
description = "ALB-SG"
vpc_id = aws_vpc.main.id
tags = {
Name = "terraform-study-alb-sg"
}
}
resource "aws_vpc_security_group_ingress_rule" "allow_http" {
security_group_id = aws_security_group.alb_sg.id
cidr_ipv4 = "0.0.0.0/0"
from_port = 80
to_port = 80
ip_protocol = "tcp"
}
resource "aws_vpc_security_group_egress_rule" "allow_all_alb" {
security_group_id = aws_security_group.alb_sg.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
}
ALB用のセキュリティグループの設定。
インバウンドはHTTPのみ許可し、アウトバンドは全ての通信を許可している。
#ALB
resource "aws_lb" "main" {
name = "aws-study-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb_sg.id]
subnets = [aws_subnet.public_private["public-1a"].id, aws_subnet.public_private["public-1c"].id]
ip_address_type = "ipv4"
tags = {
Name = "terraform-study-alb"
}
}
#ALB Target Group
resource "aws_lb_target_group" "web" {
name = "aws-study-alb-tg"
target_type = "instance"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.main.id
health_check {
enabled = true
interval = 30
path = "/"
port = "traffic-port"
protocol = "HTTP"
timeout = 5
healthy_threshold = 5
unhealthy_threshold = 2
matcher = "200,300,301"
}
tags = {
Name = "terraform-study-alb-tg"
}
}
#ALBとTargetを紐付ける
resource "aws_lb_target_group_attachment" "web" {
target_group_arn = aws_lb_target_group.web.arn
target_id = aws_instance.web.id
port = 8080
}
#ALB Listener
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.web.arn
}
}
ALBの設定。
- aws_lb
ALB本体の設定。 - aws_lb_target_group
ターゲットグループの設定。 - aws_lb_target_group_attachment
ALB本体とターゲットグループを紐付け。 - aws_lb_listener
リスナーの設定。
#RDS Subnet Group
resource "aws_db_subnet_group" "this" {
name = "terraform-study-db-subnet-group"
subnet_ids = [aws_subnet.public_private["private-1a"].id, aws_subnet.public_private["private-1c"].id]
tags = {
Name = "terraform-study-db-subnet-group"
}
}
サブネットグループの設定。
RDSはプライベートサブネットに設置するので、プライベートサブネット2つを指定している。
#RDS Security Group
resource "aws_security_group" "rds_sg" {
name = "Security Group for RDS"
description = "RDS-SG"
vpc_id = aws_vpc.main.id
tags = {
Name = "terraform-study-rds-sg"
}
}
resource "aws_vpc_security_group_ingress_rule" "allow_ec2_sg" {
security_group_id = aws_security_group.rds_sg.id
referenced_security_group_id = aws_security_group.ec2_sg.id
from_port = 3306
to_port = 3306
ip_protocol = "tcp"
}
RDS用のセキュリティグループの設定。
インバウンドはEC2のみからの接続を許可。
RDSは外への通信はしないので、アウトバンドルールは設定していない。
#RDS
resource "aws_db_instance" "main" {
allocated_storage = 20
allow_major_version_upgrade = false
auto_minor_version_upgrade = true
availability_zone = "ap-northeast-1a"
backup_retention_period = 1
db_name = "rdsstudy"
db_subnet_group_name = aws_db_subnet_group.this.name
engine = "mysql"
engine_version = "8.0.43"
instance_class = "db.t4g.micro"
username = var.RDS_Master_User_Name
password = var.RDS_Master_User_Password
publicly_accessible = false
storage_type = "gp2"
vpc_security_group_ids = [aws_security_group.rds_sg.id]
skip_final_snapshot = true
tags = {
Name = "terraform-study-rds"
}
}
データベースインスタンスの設定。
データベースのユーザーネームとパスワードは、機密事項なので変数として設定。
#SNS Topic
resource "aws_sns_topic" "cpu_alarm" {
display_name = "EC2 Monitoring Notifications"
name = "EC2-CPU-Alarm-Topic"
}
#SNS Subscription
resource "aws_sns_topic_subscription" "email" {
topic_arn = aws_sns_topic.cpu_alarm.arn
protocol = "email"
endpoint = var.My_Email_Address
}
SNSに関する設定。
EC2のCPU使用率がしきい値を超えた場合に、指定したメールアドレスに通知が来るように設定。
通知が届くメールアドレスをコードに直接書くことを避けるため、変数として設定。
#Cloud Watch Alarm
resource "aws_cloudwatch_metric_alarm" "ALERT_EC2_CPUUtilization" {
alarm_name = "EC2-CPUUtilization-Alarm"
alarm_description = "Alarm when CPU usage exceeds 70%"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 300
statistic = "Average"
threshold = 70
unit = "Percent"
dimensions = {
InstanceId = aws_instance.web.id
}
actions_enabled = true
alarm_actions = [aws_sns_topic.cpu_alarm.arn]
}
CloudWatch Alarmの設定。
#WAF
resource "aws_wafv2_web_acl" "web_application" {
name = "terraform-study-alb-waf"
scope = "REGIONAL"
default_action {
allow {}
}
rule {
name = "AWS-AWSManagedRulesCommonRuleSet"
priority = 1
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWS-AWSManagedRulesCommonRuleSet"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "aws-study-alb-waf"
sampled_requests_enabled = true
}
}
#ALBにWAFを関連付ける
resource "aws_wafv2_web_acl_association" "alb_waf_attach" {
resource_arn = aws_lb.main.arn
web_acl_arn = aws_wafv2_web_acl.web_application.arn
}
WAFの設定。
ALBの前にWAFを設置するので、ALBとWAFを紐付けている。
#WAF Log
resource "aws_cloudwatch_log_group" "web_application_waf" {
name = "aws-waf-logs-alb-alc"
retention_in_days = 1
}
resource "aws_wafv2_web_acl_logging_configuration" "web_application" {
log_destination_configs = [aws_cloudwatch_log_group.web_application_waf.arn]
resource_arn = aws_wafv2_web_acl.web_application.arn
}
WAFのログをCloudWatchへ書き込むための設定。
仕様で、ロググループ名は必ず「aws-waf-logs-」で始まる必要があるので要注意。
variables.tf
変数の設定ファイル。デフォルト値が設定されていればその値が、設定されていなければ任意の値の入力が求められる。
定義の仕方は、variable "変数名" 。変数名は、同じディレクトリ内で一意である必要がある。
variable "terraform_subnets" {
type = map(object({
cidr = string
zone = string
launch = bool # 文字列 "true" ではなく 真偽値 bool
name = string
is_public = bool # 文字列 "true" ではなく 真偽値 bool
}))
default = {
public-1a = {
cidr = "10.0.1.0/24"
zone = "ap-northeast-1a"
launch = true
name = "terraform-study-public-subnet1"
is_public = true
}
public-1c = {
cidr = "10.0.3.0/24"
zone = "ap-northeast-1c"
launch = true
name = "terraform-study-public-subnet2"
is_public = true
}
private-1a = {
cidr = "10.0.2.0/24"
zone = "ap-northeast-1a"
launch = false
name = "terraform-study-private-subnet1"
is_public = false
}
private-1c = {
cidr = "10.0.4.0/24"
zone = "ap-northeast-1c"
launch = false
name = "terraform-study-private-subnet2"
is_public = false
}
}
}
サブネット作成のための変数。
type = mapでmap型を指定。各キーにどんな値が入るのかをobjectを使い指定。
stringは文字列、boolは真偽値のtrue/falseが値として入る設定。
is_publicは、どのルートテーブルと紐付けるかを判断するためのもの。
variable "key_pair_name" {
description = "Name of an existing EC2 KeyPair to enable SSH access to the instance."
type = string
}
variable "CidrIp_From_Internet" {
description = "CIDR IP range for allowing access from the internet"
type = string
validation {
condition = can(regex("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/32)$", var.CidrIp_From_Internet))
error_message = "IPアドレスは必ず 'x.x.x.x/32' の形式で入力してください。0.0.0.0/0は許可されていません。"
}
}
variable "RDS_Master_User_Name" {
description = "RDS Master User Name"
type = string
}
variable "RDS_Master_User_Password" {
description = "RDS Master User Password"
type = string
sensitive = true
}
variable "My_Email_Address" {
description = "Enter the email address for SNS subscription"
type = string
}
コードに直接書くことが憚られるものを変数として設定している。
terraform.tfvars
variavles.tfに渡す値を設定したファイル。
機密事項なので中身はお見せできません。
outputs.tf
リソースを作成した後に出力する情報を設定するファイル。
#あるととても便利
output "EC2_Instance" {
description = "Instance Id of the web server"
value = aws_instance.web.id
}
output "alb_dns_name" {
description = "DNS name of the ALB"
value = aws_lb.main.dns_name
}
output "RDS_Instance" {
description = "Endpoint of the RDS Instance"
value = aws_db_instance.main.endpoint
}
output "Sns_Topic_EC2" {
description = "ARN of the SNS Topic"
value = aws_sns_topic.cpu_alarm.arn
}
#あると便利
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "Web_ACL" {
description = "ARN of the WebACL"
value = aws_wafv2_web_acl.web_application.arn
}
定義の仕方は、output "出力名"。
出力名は、同じディレクトリ内で一意である必要がある。
終わりに
サブネットを作る際に、CloudFormationは同じコードを4回書かないといけませんでしたが、Terraformでは1回書くだけで済んだので、そこが感動ポイントでした。コードがすっきりした!
今回はmain.tfに全てのリソースの設定を書きましたが、やはり長くなってしまいました。次回は機能ごとにファイルを分割するなど、リソースの設定ファイルを分けることに挑戦してみたいと思います。