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

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

目次

はじめに
構成図
Terraformファイル構成
 - terraform.tf
 - provider.tf
VPC関連の定義
 - variables.tf
 - vpc.tf
AWSマネジメントコンソール上での作業
次回予告

はじめに

以前にAWS CloudFormationを使用してVPCの構築やAmazon EC2、Amazon S3の構築検証をご紹介いたしました。

今回はTerraformを使用してAWS上にWebサーバーの構築まで行いたいと思います。
Terraformで構築するAWSの構成を記載します。

構成図

Application Load Balancerを使用して、Webサーバーはプライベートサブネットに配置します。
VPN等の用意が無いと通常はプライベートサブネットに配置したEC2インスタンスへはssh接続が出来ないのですが、実用を考慮してSSMのSession ManagerでEC2へローカルから接続が出来るようにIAM Policyの設定もTerraformで後ほど書く予定です。それでは、Terraformのファイル構成を記載します。

Terraformファイル構成

terraform/
├─vars/
│  └─terraform.tfvars
├─acm.tf
├─ec2.tf
├─elb.tf
├─iam.tf
├─provider.tf
├─route53.tf
├─securitygroup.tf
├─terraform.tf
├─variables.tf
└─vpc.tf

複数のAWSサービスをTerraformで構築をしていきたいと思いますので、何回かに記事を分けて記載させていただきますので、予めご了承ください。まずは基本設定となる「terraform.tf」を書いていきます。

terraform.tf
terraform {
  required_version = "~> 1.1"

  backend "s3" {
    region  = "ap-northeast-1"
    bucket  = "itport-terraform-state"
    key     = "terraform.tfstate"
    profile = "itport"
  }
}

Terraformではリソースの作成をするとステータスを管理するファイル「.tfstate」ファイルが生成されますが、Git等でこのファイルが分散されると不整合が発生するため、バックエンドという仕組みでstateファイルを一貫した場所で管理できるように上記のような書き方で定義ができます。バックエンドはAmazon S3をサポートしているので、今回はAmazon S3にterraform.tfstateを配置するように書いております。6行目と8行目は適宜変更ください。bucketはバケット名、profileは自身のローカルのcredentialsに設定したprofileの名前を指定します。

~/.aws/credentialsの例
[itport]
aws_access_key_id = <YOUR_AWS_ACCESS_KEY>
aws_secret_access_key = <YOUR_AWS_SECRET_ACCESS_KEY>


バックエンドとなるAmazon S3のバケットをまずは事前に作成しておく必要があるためAWS-CLIでバケットを作成します。

aws s3api --profile itport create-bucket --acl private --bucket itport-terraform-state --create-bucket-configuration LocationConstraint=ap-northeast-1

次にAWSとしてTerraformを定義していくためにproviderを定義します。

provider.tf
provider "aws" {
  profile = var.profile
  region  = var.region
  
  default_tags {
    tags = {
      Terraform      = var.tag_terraform
      Env            = var.env
      SystemName     = var.tag_system_name
    }
  }
}

上記providier.tf内のprofileプロパティは変数からprofile名を取得するようにしています。そしてdefault_tagsはTerraformで作成されるリソース全てにデフォルトで設定するタグが定義できます。これも変数から値を入れるようにしており、変数の値はterraform apply時に直接指定するか、.tfvarsファイルで変数の値を書いていきます。apply時に毎回変数を書くのは大変なので後ほど/vars/terraform.tfvarsにて変数の値を書いていく予定です。

VPC関連の定義

続いてVPCの定義を作成していきますが、変数を使いたいので先に「variables.tf」に変数を用意します。

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

続いてVPC関連のリソースを定義します。

vpc.tf
# ------------------------------------------------------ #
# VPC
# ------------------------------------------------------ #
resource "aws_vpc" "vpc" {
  cidr_block                       = var.vpc_cidr
  enable_dns_support               = "true"
  enable_dns_hostnames             = "true"
  assign_generated_ipv6_cidr_block = "false"
  tags = {
    Name = "${var.name_prefix}-vpc"
  }
}

# ------------------------------------------------------ #
# Private Subnet
# ------------------------------------------------------ #
resource "aws_subnet" "private_a_1" {
  vpc_id                          = aws_vpc.vpc.id
  cidr_block                      = var.private_subnet_cidr_a_1
  assign_ipv6_address_on_creation = "false"
  map_public_ip_on_launch         = "true"
  availability_zone               = "ap-northeast-1a"

  tags = {
    Name = "${var.name_prefix}-private-subnet-a-1"
  }
}

resource "aws_subnet" "private_c_1" {
  vpc_id                          = aws_vpc.vpc.id
  cidr_block                      = var.private_subnet_cidr_c_1
  assign_ipv6_address_on_creation = "false"
  map_public_ip_on_launch         = "true"
  availability_zone               = "ap-northeast-1c"

  tags = {
    Name = "${var.name_prefix}-private-subnet-c-1"
  }
}

# ------------------------------------------------------ #
# Public Subnet
# ------------------------------------------------------ #
resource "aws_subnet" "public_a_1" {
  vpc_id                          = aws_vpc.vpc.id
  cidr_block                      = var.public_subnet_cidr_a_1
  assign_ipv6_address_on_creation = "false"
  map_public_ip_on_launch         = "true"
  availability_zone               = "ap-northeast-1a"

  tags = {
    Name = "${var.name_prefix}-public-subnet-a-1"
  }
}

resource "aws_subnet" "public_c_1" {
  vpc_id                          = aws_vpc.vpc.id
  cidr_block                      = var.public_subnet_cidr_c_1
  assign_ipv6_address_on_creation = "false"
  map_public_ip_on_launch         = "true"
  availability_zone               = "ap-northeast-1c"

  tags = {
    Name = "${var.name_prefix}-public-subnet-c-1"
  }
}

# ------------------------------------------------------ #
# Internet Gateway
# ------------------------------------------------------ #
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.vpc.id

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

# ------------------------------------------------------ #
# Elastic IP
# ------------------------------------------------------ #
resource "aws_eip" "ngw" {
  vpc = true

  tags = {
    Name = "${var.name_prefix}-eip-ngw"
  }
}

# ------------------------------------------------------ #
# Nat Gateway
# ------------------------------------------------------ #
resource "aws_nat_gateway" "ngw_a" {
  allocation_id = aws_eip.ngw.id
  subnet_id     = aws_subnet.public_a_1.id

  tags = {
    Name = "${var.name_prefix}-ngw-a"
  }

  depends_on = [aws_internet_gateway.igw]
}

# ------------------------------------------------------ #
# Route Table
# ------------------------------------------------------ #
# Public
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.name_prefix}-rtb-public"
  }
}

resource "aws_route" "igw" {
  destination_cidr_block = "0.0.0.0/0"
  route_table_id         = aws_route_table.public.id
  gateway_id             = aws_internet_gateway.igw.id
}

resource "aws_route_table_association" "public_a" {
  subnet_id      = aws_subnet.public_a_1.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "public_c" {
  subnet_id      = aws_subnet.public_c_1.id
  route_table_id = aws_route_table.public.id
}

# Private
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.name_prefix}-rtb-private"
  }
}

resource "aws_route" "ngw" {
  destination_cidr_block = "0.0.0.0/0"
  route_table_id         = aws_route_table.private.id
  nat_gateway_id         = aws_nat_gateway.ngw_a.id
}

resource "aws_route_table_association" "private_a" {
  subnet_id      = aws_subnet.private_a_1.id
  route_table_id = aws_route_table.private.id
}

resource "aws_route_table_association" "private_c" {
  subnet_id      = aws_subnet.private_c_1.id
  route_table_id = aws_route_table.private.id
}

ネットワーク系のコンポーネントをまとめて書きました。上記の定義で以下のようなVPCが構築されます。

vpc.tfの構成

Nat Gatewayを配置しない場合は、プライベートサブネットに配置するEC2インスタンスがyumリポジトリにアクセス出来なくなるため、その場合はAmazon S3にアクセスできるようにゲートウェイ型のVPCエンドポイントを用意する必要があります。

AWSマネジメントコンソール上での作業

EC2インスタンスをTerraformで作成する予定のため、事前にAWSマネジメントコンソール上でキーペアを作成しておく必要があります。EC2 > キーペアよりキーペアの作成をします。

次回予告

今回はここまでです。結局AWS CloudFormationと何が違うの?という疑問があるかと思いますが確かに実現できることは似ています。AWS CloudFormationはYaml形式で書きましたが、Terraformではモデルを定義していくような書き方になります。開発者の方はTerraformの方が馴染みがあり書きやすいのではないかと個人的には感じました。ただしAWS CloudFormationもAWS Cloud Development Kit(AWS CDK)を利用することでプログラミングをするような感覚で作成できるようです。

また、インフラ構成を更新する際の差分検出方法にもかなりの違いがあり、TerraformではAWSマネジメントコンソール等から手動で変更した内容もterraform apply時にtfファイルに定義した内容に合わせて修正をしてくれますが、AWS CloudFormationは「Drift detect」で差分の検知は出来るものの、スタックを更新しても修正自体はしてくれないため手作業での修正が必要です。

そして、Terraformの一番のメリットはAWSに限らないという点です。逆にデメリットとしてはTerraform自体のバージョンを管理することも考慮する必要がありますので、それらのポイントを加味してツール選定をするのが良いかと思いました。

次回は、ELBやEC2インスタンスの定義を作成していきたいと思います。