0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS初学者がTerraformでAWS環境の構築に挑戦するよ

0
Posted at

はじめに

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.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

作成するリソースの設定ファイル。

main.tf
#リージョン指定
provider "aws" {
  region = "ap-northeast-1"
}

リソースを作成するリージョンを指定。

main.tf
#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の作成。

main.tf
#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)で、他所で定義されているリソースの属性を参照できる
main.tf
#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の値を取得する
main.tf
#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"
  }
}

ルートテーブルの作成。
プライベートサブネットはインターネットに接続しない設定のため、インターネットゲートウェイとの紐付けを行っていない。

main.tf
#ルートテーブルとサブネットを関連付け
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を取得する。
main.tf
#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"」は、すべてのプロトコルを指定するの意味。
    なお、アウトバンドルールを設定しないと、外部との通信ができないので要注意。
main.tf
#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は機密事項なので、変数として設定している。

main.tf
#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のみ許可し、アウトバンドは全ての通信を許可している。

main.tf
#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
    リスナーの設定。
main.tf
#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つを指定している。

main.tf
#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は外への通信はしないので、アウトバンドルールは設定していない。

main.tf
#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"
  }
}

データベースインスタンスの設定。
データベースのユーザーネームとパスワードは、機密事項なので変数として設定。

main.tf
#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使用率がしきい値を超えた場合に、指定したメールアドレスに通知が来るように設定。
通知が届くメールアドレスをコードに直接書くことを避けるため、変数として設定。

main.tf
#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の設定。

main.tf
#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を紐付けている。

main.tf
#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 "変数名" 。変数名は、同じディレクトリ内で一意である必要がある。

variables.tf
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は、どのルートテーブルと紐付けるかを判断するためのもの。

variables.tf
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

リソースを作成した後に出力する情報を設定するファイル。

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に全てのリソースの設定を書きましたが、やはり長くなってしまいました。次回は機能ごとにファイルを分割するなど、リソースの設定ファイルを分けることに挑戦してみたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?