(第3回)TerraformでAWS上にWebサーバーを構築する

(第3回)TerraformでAWS上にWebサーバーを構築する

目次

前回の振り返り
EC2リソース定義の作成
ec2.tf
Elastic IPを追加する際のリソース定義
ELBリソース定義の作成
elb.tf
変数定義の追加と.tfvarsの用意
variables.tf
vars/terraform.tfvars
Terraformの実行
終わりに

前回の振り返り

前回はTerraformで IAMロール、セキュリティグループ、Route53、Certificate Managerのリソースを作成しました。

今回はいよいよラストのEC2インスタンスとALBを用意してWebサイトを公開します。

terraform/
├─vars/
│  └─terraform.tfvars ... ★今回★
├─acm.tf              ... 第2回で作成済
├─ec2.tf              ... ★今回★
├─elb.tf              ... ★今回★
├─iam.tf              ... 第2回で作成済
├─provider.tf         ... 第1回で作成済
├─route53.tf          ... 第2回で作成済
├─securitygroup.tf    ... 第2回で作成済
├─terraform.tf        ... 第1回で作成済
├─variables.tf        ... 第1回で作成済
└─vpc.tf              ... 第1回で作成済

EC2リソース定義の作成

WebサーバーとしてEC2インスタンスを定義しますが、KMSで鍵を作成しEBSを暗号化するようにしています。
AMIはAmazon Linux2の5.10系の最新ビルドが選択されるようにData Sourcesを使ってAMI IDを取得します。

ec2.tf
# AWSアカウントIDを取得
data "aws_caller_identity" "current" {}
locals {
  account_id = data.aws_caller_identity.current.account_id
}

# Amazon Linux 2の最新のAMI IDを取得
data "aws_ami" "amzn2" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "state"
    values = ["available"]
  }

  filter {
    name   = "name"
    values = ["amzn2-ami-kernel-5.10-hvm*"]
  }
}

# EBS用の暗号鍵の作成
# KMS Key
resource "aws_kms_key" "ec2_kms_key" {
  description             = "EC2 KMS Key"
  deletion_window_in_days = 10
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        "Sid" : "Enable IAM User Permissions",
        "Effect" : "Allow",
        "Principal" : {
          "AWS" : "arn:aws:iam::${local.account_id}:root"
        },
        "Action" : "kms:*",
        "Resource" : "*"
      }
    ]
  })
}

resource "aws_kms_alias" "ec2_kms_key" {
  name          = "alias/ebs-kms-key"
  target_key_id = aws_kms_key.ec2_kms_key.key_id
}

# EC2 Web Instance
resource "aws_instance" "web" {
  ami                         = data.aws_ami.amzn2.id
  instance_type               = var.ec2_instance_type
  iam_instance_profile        = aws_iam_instance_profile.ec2_profile.name
  key_name                    = var.ec2_keypair
  subnet_id                   = aws_subnet.private_a_1.id
  monitoring                  = false
  vpc_security_group_ids      = ["${aws_security_group.web_sg.id}"]
  hibernation                 = false
  tenancy                     = "default"
  associate_public_ip_address = false
  user_data                   = <<EOF
    #!/bin/sh
    amazon-linux-extras install -y nginx1
    systemctl start nginx
    systemctl enable nginx
  EOF

  credit_specification {
    cpu_credits = "standard"
  }

  root_block_device {
    delete_on_termination = true
    encrypted             = true
    kms_key_id            = aws_kms_key.ec2_kms_key.arn
    volume_size           = var.ec2_volume_size
    volume_type           = "gp3"
    tags = {
      Name = "${var.name_prefix}-web"
    }
  }

  lifecycle {
    ignore_changes = [
      associate_public_ip_address
    ]
  }

  tags = {
    Name = "${var.name_prefix}-web"
  }
}

※ インターネット経由でインスタンスにssh接続する場合は、インスタンスの配置サブネットをパブリックサブネットに変更し、Elastic IPを追加する必要があります。

Elastic IPを追加する際のリソース定義
# Elastic IP
resource "aws_eip" "ec2" {
  instance = aws_instance.web.id
  vpc      = true
  tags = {
    Name = "${var.name_prefix}-eip-ec2"
  }
}

ELBリソース定義の作成

Application Load Balancer(ALB)、HTTPSとHTTPの各リスナー、ターゲットグループとターゲットグループをALBにアタッチするためのターゲットグループアタッチメントを記載します。
ALBにアタッチするセキュリティグループは第2回の記事で事前に用意しています。

elb.tf
# Application load balancer
resource "aws_lb" "alb" {
  name               = "${var.name_prefix}-alb"
  internal           = false
  subnets            = [aws_subnet.public_a_1.id, aws_subnet.public_c_1.id]
  security_groups    = [aws_security_group.alb_sg.id]
  load_balancer_type = "application"
  idle_timeout       = 60

  tags = {
    Name = "${var.name_prefix}-alb"
  }
}

# HTTPS Listener
resource "aws_lb_listener" "alb_https" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS-1-2-Ext-2018-06"
  certificate_arn   = aws_acm_certificate.cert.id

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.alb_tg.arn
  }

  tags = {
    Name = "${var.name_prefix}-alb-https-listener"
  }
}

# HTTP Listener
resource "aws_lb_listener" "alb_http" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.alb_tg.arn
  }

  tags = {
    Name = "${var.name_prefix}-alb-http-listener"
  }
}

# Target Group
resource "aws_lb_target_group" "alb_tg" {
  name     = "${var.name_prefix}-alb-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = aws_vpc.vpc.id

  health_check {
    healthy_threshold   = 5
    unhealthy_threshold = 2
    matcher             = 200
    timeout             = 5
    path                = "/"
    interval            = 30
  }

  tags = {
    Name = "${var.name_prefix}-alb-tg"
  }
}

# Target Group Attachment
resource "aws_lb_target_group_attachment" "alb_tg_attach" {
  target_group_arn = aws_lb_target_group.alb_tg.arn
  target_id        = aws_instance.web.id
  port             = 80
}

変数定義の追加と.tfvarsの用意

追加したec2とelbの定義を一部変数化したため variables.tf に変数を追加し terraform apply の際に手で入力せずに済むように terraform.tfvars で変数の値を定義しておきます。

variables.tf(第1回から追記
# -------------------------------------- #
# General
# -------------------------------------- #
# Prefix
variable "name_prefix" {
  type        = string
  description = "system code prefix"
}

# Environment
variable "env" {
  type        = string
  description = "tag"
}

# Tags
variable "tag_terraform" {
  type        = string
  default     = "true"
  description = "tag"
}

variable "tag_system_name" {
  type        = string
  description = "tag"
}

# Profile Name
variable "profile" {
  type        = string
  description = "aws cli profile name"
}

variable "region" {
  type        = string
  default     = "ap-northeast-1"
  description = "AWS region in which resources will get deployed. Defaults to Tokyo."
}

# -------------------------------------- #
# VPC
# -------------------------------------- #
variable "vpc_cidr" {
  type        = string
  description = "VPC CIDR"
}

variable "private_subnet_cidr_a_1" {
  type        = string
  description = "Private Subnets CIDR"
}

variable "private_subnet_cidr_c_1" {
  type        = string
  description = "Private Subnets CIDR"
}

variable "public_subnet_cidr_a_1" {
  type        = string
  description = "Public Subnets CIDR"
}

variable "public_subnet_cidr_c_1" {
  type        = string
  description = "Public Subnets CIDR"
}

# -------------------------------------- #
# Route53
# -------------------------------------- #
variable "dns_zone" {
  type        = string
  description = "dns zone name"
}

# -------------------------------------- #
# EC2
# -------------------------------------- #
variable "ec2_instance_type" {
  type        = string
  description = "EC2 Instance Type"
}

variable "ec2_volume_size" {
  type        = string
  description = "EBS Volume Size"
}

variable "ec2_keypair" {
  type = string
  description = "EC2 Key pair name"
}

# -------------------------------------- #
# ELB ヘルスチェックするパス
# -------------------------------------- #
variable "elb_healthcheck_path" {
    type = string
    default = "/"
    description = "health check path"
}

変数に対して値を入れておきたいため .tfvars を用意し以下のような内容を入力します。
※ わざわざ変数化する理由としては開発、本番等のリソースを複数環境にデプロイをする際に一部設定を変えたい際などに変数化しておいたりしています。もしくは、複数のリソースに同じ値を入れておきたい場合などに使ったりします。(その場合は locals で定義しても良いかと思います)

vars/terraform.tfvars
# General
name_prefix = "itport" # 各リソースのNameタグの接頭辞に使用する
env         = "dev"    # リソースタグに記載する

# Shared Resource Tags
tag_terraform   = "true"   # Terraformを使用して構築されたリソースか分かるようにする
tag_system_name = "itport" # SystemNameタグに使用する

# Network
region  = "ap-northeast-1"
profile = "itport"

# VPC CIDR
vpc_cidr = "10.0.0.0/22"

# Subnets CIDR
private_subnet_cidr_a_1 = "10.0.0.0/27"
private_subnet_cidr_c_1 = "10.0.0.32/27"
public_subnet_cidr_a_1  = "10.0.1.0/27"
public_subnet_cidr_c_1  = "10.0.1.32/27"

# EC2
ec2_instance_type = "t3.small"
ec2_keypair       = "itport"
ec2_volume_size   = 50

# Route53
dns_zone = "作成したいゾーン名"

Terraformの実行

それではTerraformを実行していきます。planを行い必ずどういうリソースが作られるのか確認をします。

# 初回はワークスペースを初期化する
terraform init

# 実行計画を確認
terraform plan -var-file .\vars\terraform.tfvars

# 実行
terraform apply -var-file .\vars\terraform.tfvars

※ Windows で実行しています。MacやLinuxの場合は.tfvarsのパスをバックスラッシュではなくスラッシュにします。
※ ACM証明書発行時のDNS検証が成功しないと後続のALBのHTTPSリスナーの作成が失敗します。利用するドメインの権威DNSをRoute53に変えるようにする必要があります。
※ 初回のapply時はdestroyされるリソースはないと思いますが、2回目以降は再作成(destroy & add)されてしまう変更が含まれないか必ずチェックします。例えばEC2インスタンスのAMI IDを変えようとした場合など、マネジメントコンソールで変更出来ないような属性を変えようとすると削除して作成という挙動になることがあります。そのため最初に terraform plan で確認をしておくのがベストです。

terraform plan 時に表示される構築概要が表示されている内容の例
Plan: 34 to add, 0 to change, 0 to destroy.

終わりに

TerraformでAWS上にWebサーバーを構築する方法を最後まで見てくださりありがとうございます!
もちろんではございますが、私の書き方が全てではございません。

複数のEC2インスタンスを用意する場合はモジュール化をして簡略して書けるようにすることも可能ですし、今回全てのファイルを同階層に配置しましたが、影響範囲を少なくするためにコンポーネント単位でフォルダーを分けても良いかと思います。(Terraformの実行はカレントのフォルダー直下に配置された .tf を元に実行される)

そして、なるべくシンプルに記述することを心掛けていきましたが保守性等を考慮してどう書いていくかは、毎回割と悩むところです。例えばAMI IDに関しても Data Sources を使用したため、今回の書き方だと都度最新のIDを取得してしまうため、時間の経過と共に最新のIDが変わるためDestroyされる危険があります。
ですので、あえてAMI IDを直接書いてしまうのも手かと思います。

今後も、より良い書き方が出来るよう精進したいと思います。では!