AWSの環境をTerraformを使って構築してみた



AWSで使い捨ての環境を構築する際は、AWS CloudFormationを利用することが多いと思います。
検証環境をテンプレートとして残しておいたり、マネジメントコンソールやAWS CLIから簡単に構築/削除を行うことができます。

AWS公式のツール以外だと、Terraformが選択肢の1つとして挙げられます。

## Terraformって何？

HashiCorp社が提供するTerraformは、インフラ構築や設定などをコード(テンプレートファイル)を使って自動化するツールです。（Infrastructure as Code）</br>
AWS以外にも数多くのプロバイダ(クラウドサービスやツール)に対応しています。

この記事の載時点では最新バージョン0.14.5までリリースされています。(2021年 1月時点)
Terraformの開発は非常に活発であり、最新バージョンのリリースもかなり頻度が高いです。
今後、Infrastructure as Codeの分野でますます注目を浴びそうです。
https://www.terraform.io/

## 構築内容

今回はシンプルな環境を構築します。作成するawsリソースは以下の通りです。<br/>
    - VPC
    - Internet Gateway
    - Subnet

## インストール

Mac環境でのインストールはHomebrewを利用します。<br>
`$ brew install terraform`
`$ terraform version`

## テンプレートファイルの作成

Terraformのインストールを終えたら、AWSの環境構築を行っていきます。
はじめにワークディレクトリを作成して、その中にTerraformのテンプレートファイルを作成します。<br/>
下記のようなディレクトリ構成にします。<br/>

terraform
  ├ env
  ├ main.tf
  ├ modules
  └ variables.tf

基本的にはmain.tfにコードを書いて行きます。

## 変数の宣言

terraformでは変数が使用できます。使用方法は何通りかありますが、ここではファイルから値を渡す方法を用います。
  envディレクトリの配下に、<任意の名前>.tfvarsというファイルを作成し以下のように記述します。

   env
    └ dev.tfvars


    # プロジェクト名
    name = "Example"

    # 環境名
    env = "dev"

    # profile
    profile = "default"

    # vpc
    vpc_cidr_block = "10.100.0.0/16"


awsのアクセスキーやシークレットキーを直接記述するのは危険なので、代わりにawsのクレデンシャルを参照させます。
変数：profileには使用するクレデンシャルの名前をセットします。


## プロバイダーの設定

テンプレートを書く際にまず必要となるのはプロバイダーの設定です。
Terraformは複数のプロバイダーに対応しているため、「どのプロバイダーを使用するか？」を始めに宣言する必要があります。<br/>
main.tfに記述します。

terraform
    └ main.tf


    #####################################
    # 初期設定
    #####################################
    terraform {
      required_version = ">= 0.12.0"
      required_providers {
        aws = "~> 3.0"
      }
      backend "s3"{}
    }

    provider "aws" {
      profile = var.profile
      region  = "ap-northeast-1"
    }

    locals {
      az_names = slice(data.aws_availability_zones.available.names, 0, var.az_cnt)
      name     = "project-${var.env}"
    }




    #####################################
    # Availability Zones
    #####################################
    data "aws_availability_zones" "available" {
      state = "available"
    }
 
providerブロックの中でvar.profileという値が使用されています。
localsブロックの中でもvar.envという値が使用されています。
これは、.tfvarsで宣言した変数になります。**var.<変数名>**という感じです。
main.tfでこのような変数を使用する場合は、variables.tfの中で型の宣言をする必要があります。

terraform
    └ variables.tf


      variable "env" {
        type        = string
        description = "Current state of project:dev, stage, prod..."
      }

      variable "profile" {
        type        = string
        description = "The aws profile name."
      }
      

## moduleの使い方

terraformでコードを書いていくと、コードがとても長くなっていくので、
モジュールとして分けてしまいます。
使用する意味合いとしては、VPCの部分、EC2の部分などとリソースごとに分けることで可読性を高めることができます。

moduleを使用する際は、main.tfにmoduleブロックを記述し、moduleのなかで使用する変数を宣言します。

terraform
    └ main.tf


    #####################################
    # VPC
    #####################################
    module "vpc" {
      source = "./modules/vpc"

      name            = local.name
      vpc_cidr_block  = var.vpc_cidr_block
      az_names        = local.az_names
    }
    

ourceにはmoduleとして使用する.tfファイルのパスを指定します。(後ほどファイルを作成します。)
**local.<変数名>**はlocalsブロック内で宣言した変数を示しています。

次に、modulesディレクトリの下にvpcディレクトリを作成し、その中にmain.tfとvariables.tfを作成します。

modules
└ vpc
      ├ main.tf
      └ variables.tf

先ほどのmoduleブロック内で宣言した変数の型を、modules/vpc/variables.tfにて宣言します。


    variable "name" {
      type        = string
      description = "The cidr_block."
    }

    variable "vpc_cidr_block" {
      type        = string
      description = "The VPC's CIDR block."
    }

    variable "az_names" {
      type        = list(string)
      description = "The AZ names"
    }

  次にmodules/vpc/main.tfにコードを書いていきます。ここではproviderの宣言は必要ありません。


    locals {
      az_cnt = length(var.az_names)
    }

    #####################################
    # VPC
    #####################################
    resource "aws_vpc" "this" {
      cidr_block           = var.vpc_cidr_block
      enable_dns_support   = true
      enable_dns_hostnames = true

      tags = {
        Name = "${var.name}-vpc"
      }
    }

    #####################################
    # Internet Gateway
    #####################################
    resource "aws_internet_gateway" "this" {
     vpc_id = aws_vpc.this.id

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

    #####################################
    # Publiuc Subnet
    #####################################
    resource "aws_subnet" "public" {
      count             = local.az_cnt
      vpc_id            = aws_vpc.this.id

      cidr_block        = cidrsubnet(var.vpc_cidr_block, 8, count.index)
      availability_zone = var.az_names[count.index]

      tags = {
        Name = "${var.name}-subnet-public-${var.az_names[count.index]}"
      }
    }
    

## dry runの使用方法

実際に記述したterraformコードをデプロイする前にdry runを行うことで、
作成されるリソースや現時点との差分を確認することができます。
dry runする場合はターミナルで以下のコマンドを実行します。


    terraform plan -var-file=./env/dev.tfvars



    An execution plan has been generated and is shown below.
    Resource actions are indicated with the following symbols:
      + create

    Terraform will perform the following actions:

    # module.vpc.aws_internet_gateway.this will be created
    + resource "aws_internet_gateway" "this" {
        + arn      = (known after apply)
        + id       = (known after apply)
        + owner_id = (known after apply)
        + tags     = {
            + "Name" = "project-dev-igw"
          }
        + vpc_id   = (known after apply)
      }

    # module.vpc.aws_subnet.public[0] will be created
    + resource "aws_subnet" "public" {
        + arn                             = (known after apply)
        + assign_ipv6_address_on_creation = false
        + availability_zone               = "ap-northeast-1a"
        + availability_zone_id            = (known after apply)
        + cidr_block                      = "10.100.0.0/24"
        + id                              = (known after apply)
        + ipv6_cidr_block_association_id  = (known after apply)
        + map_public_ip_on_launch         = false
        + owner_id                        = (known after apply)
        + tags                            = {
            + "Name" = "project-dev-subnet-public-ap-northeast-1a"
          }
        + vpc_id                          = (known after apply)
      }

    # module.vpc.aws_subnet.public[1] will be created
    + resource "aws_subnet" "public" {
        + arn                             = (known after apply)
        + assign_ipv6_address_on_creation = false
        + availability_zone               = "ap-northeast-1c"
        + availability_zone_id            = (known after apply)
        + cidr_block                      = "10.100.1.0/24"
        + id                              = (known after apply)
        + ipv6_cidr_block_association_id  = (known after apply)
        + map_public_ip_on_launch         = false
        + owner_id                        = (known after apply)
        + tags                            = {
            + "Name" = "project-dev-subnet-public-ap-northeast-1c"
          }
        + vpc_id                          = (known after apply)
      }

    # module.vpc.aws_vpc.this will be created
    + resource "aws_vpc" "this" {
        + arn                              = (known after apply)
        + assign_generated_ipv6_cidr_block = false
        + cidr_block                       = "10.100.0.0/16"
        + default_network_acl_id           = (known after apply)
        + default_route_table_id           = (known after apply)
        + default_security_group_id        = (known after apply)
        + dhcp_options_id                  = (known after apply)
        + enable_classiclink               = (known after apply)
        + enable_classiclink_dns_support   = (known after apply)
        + enable_dns_hostnames             = true
        + enable_dns_support               = true
        + id                               = (known after apply)
        + instance_tenancy                 = "default"
        + ipv6_association_id              = (known after apply)
        + ipv6_cidr_block                  = (known after apply)
        + main_route_table_id              = (known after apply)
        + owner_id                         = (known after apply)
        + tags                             = {
            + "Name" = "project-dev-vpc"
          }
      }

    Plan: 4 to add, 0 to change, 0 to destroy.
    ------------------------------------------------------------------------

    Note: You didn't specify an "-out" parameter to save this plan, so Terraform
    can't guarantee that exactly these actions will be performed if
    "terraform apply" is subsequently run.
    

## デプロイ

dry runを実行して何も問題がなければいよいよデプロイをします。
デプロイするにはターミナルで下記コマンドを実行します。

    
      terraform apply -var-file=./env/dev.tfvars

      #出力された内容でデプロイするか聞かれるので'yes'と入力する
      Do you want to perform these actions?
      Terraform will perform the actions described above.
      Only 'yes' will be accepted to approve.

      Enter a value: yes

      # 実行ログが下に出力される
    

## 環境削除

  デプロイした環境を削除する場合は、ターミナルで下記コマンドを実行します。

    
      terraform destroy -var-file=./env/dev.tfvars

      #出力された内容を削除するか聞かれるので'yes'と入力する
      Do you really want to destroy all resources?
      Terraform will destroy all your managed infrastructure, as shown above.
      There is no undo. Only 'yes' will be accepted to confirm.

      Enter a value: yes

      # 実行ログが下に出力される
    

## 使ってみた感想

コマンド叩くだけでインフラの構築が簡単にできてしまうことに驚きました。
テンプレートを用意してしまえば、使い捨ての検証環境をバンバン立てられちゃいますね！
構築するリソースをモジュールに分けることで、インフラの全体像が把握しやすくなると感じました！
コードだからバージョン管理もしやすそうです。

ドキュメントにはとてもお世話になりました。不明点があったらとりあえずドキュメントを見ましょう。
https://registry.terraform.io/providers/hashicorp/aws/latest/docs