🙆‍♀️

【Terraform入門】EC2+RDS構成のAWS環境作成

2022/12/18に公開

はじめに

今回はAWSやAzureのインフラ構築ができるIaCツールであるTerraformを使い、
Mac環境でAWS上にEC2インスタンスx1台、RDSインスタンスx1台の構成を構築していきます。

想定読者

マネジメントコンソールを使ったEC2,RDSの構築経験済み
初めてTerraformを使う方

アジェンダ

  1. Terraformとは
  2. 構築イメージ
  3. 事前準備
  4. Terraformインストール
  5. ディレクトリとtfファイル作成
  6. リソース設定
  7. 構築
  8. 構築したリソースの削除

Terraformとは

HashiCorp社 が提供するIaC (Infracture as Code) を実現するためのツールです。
AWS、Azuru、GCPなどのクラウドプラットフォームに対応しています。
Terraform を用いると、インフラ環境をコードで管理しつつ、ターミナルからコマンドを実行するだけでクラウド上にインフラの構築が可能です。
類似する IaC ツールでは AWS の CloudFormation などが有名です。

構築イメージ

EC2インスタンスx1台、RDSインスタンスx1台の構成です。
具体的には、以下の通りです。

  • VPC
    • Public Subnet , Private Subnet
    • Internet Gateway
    • Route Table
    • Security Group
  • EC2
    • Instance
    • Elastic IP
  • RDS
    • Instance
    • DB Subnet Group

事前準備

必要な準備は、 IAMユーザーとアクセスキー作成、AWS CLIインストール、認証情報の設定の3点です。
この3点についてはこちらの記事を参考にしてください。

Terraformインストール

先ずは、tfenvをインストールします。
Mac環境であれば、Homebrewを使ってインストールが可能なので、
今回はHomebrewからインストールしていきます。

$ brew install tfenv  # tfenvインストールコマンド
$ tfenv -v            # バージョン確認
tfenv 3.0.0

次に、Terraformをインストールしていきます。
使用できるバージョンのリストを確認します。

$ tfenv list-remote  
1.3.2
1.3.1
1.3.0
・
・
・
0.2.0
0.1.1
0.1.0

指定したバージョンをローカルにインストールします。

$ tfenv install 1.3.2

インストールできたかどうか確認してみましょう。

$ terraform -v  
Terraform v1.3.2

無事にバージョンが表示されました!
これでTerraformのインストールが完了です。

tfファイルの作成

Terraformのインストールができたので、続いてはtfファイルを作成していきましょう。
先ずは、ファイルを配置するディレクトリを適当な場所に作成し、
ディレクトリに移動後、tfファイルを作成します。
ファイルの拡張子は「*.tf」で、任意の名前でOKです。
今回は、main.tfファイルのみ作成しますが、本番運用も想定して複数ファイルを作成することも可能です。

$ mkdir terraform-test  # ディレクトリ作成
$ cd terraform-test  # terraform-testディレクトリに移動
$ touch main.tf  # ファイル作成

リソース設定

ディレクトリとtfファイルができたので、さっそくtfファイルにコードを書き、AWSの構築にとりかかりましょう!

tfファイルはHCL(HashiCorp Configuration Language)というHashiCorp製の独自言語で記述します。
独自言語といってもHCLの構文はシンプルなため、繰り返しコードを書く内に慣れてくると思います。

プロバイダーの宣言

先ずは、どのプロバイダー(クラウド環境)を使用するのかを宣言しましょう。
main.tfファイルに以下のコードを記述します。

main.tf
provider "aws" {
  access_key = "****************"
  secret_key = "************************************"
  region     = "ap-northeast-1"
}

これでAWSを利用しますという宣言が出来ました。
・・・ただ、このコードをよく見ると、アクセスキーIDやシークレットアクセスキーが直接書き込まれてしまっています。
もしもこのまま git push なんてしてしまうとクレデンシャル情報が世界中から丸見えになってしまい、大変危険です。
そこで、次にクレデンシャル情報の切り出し方と、変数の扱い方を確認しましょう。

クレデンシャル情報と変数の扱い方について

クレデンシャル情報はいわゆる ID とパスワードのようなもので、tfファイルに直接記述してしまうのは非常に危険です。
そのためクレデンシャル情報はいくつかの方法を使って、tfファイルから切り出して管理します。
今回は、公式推奨方法である、terraform.tfvarsというファイルを作成し、その中にアクセスキー等の変数を記入する方法を採用します。

terraform.tfvarsファイルの作成

$ touch terraform.tfvars
terraform.tfvars
# リージョン
aws_region = "ap-northeast-1"

# アクセスキーIDとシークレットアクセスキー
aws_access_key        = "***************"
aws_secret_access_key = "****************************"

terraform.tfvarsファイルには、<変数名> = <値> という形式で記述します。
これで、main.tfファイルからは access_key = "${var.aws_access_key}" という形式でクレデンシャル情報を変数として読み込めるようになります。

変数定義

続いて、terraform.tfvars で指定した変数を main.tfファイルが読み込めるように、変数をvariableブロックで定義します。
variable "<変数名>" {} という形式で、空のブロックを与えて変数を宣言していきます。

main.tf
variable "aws_access_key" {} # 空の変数を定義
variable "aws_secret_access_key" {}
variable "aws_region" {}

それでは最後に、これまでの確認事項をまとめてプロバイダーの宣言を完成させましょう。

main.tf
variable "aws_access_key" {} 
variable "aws_secret_access_key" {}
variable "aws_region" {}

provider "aws" {
  access_key = "${var.aws_access_key}"
  secret_key = "${var.aws_secret_key}"
  region     = "${var.aws_region}"
}

VPC、Subnet、Route Table、Internet Gatewayの設定

それでは、いよいよ各種リソースの設定を定義していきます。
リソースは、resource "<リースの種類>" "<リソース名>" {設定項目名 = 設定値}といったresourceブロックで定義します。
リソースの種類は予めaws_*という名前でTerraformで定義されています。
例えば、VPCであればaws_vpc、RDSならaws_db_instanceです。
また、リソース名は任意の名前を設定可能です。

では、先ずはネットワーク環境(VPC、Subnet、Route Table、Internet Gateway)から設定していきます。
構成は以下の通りです。

VPC

Name tag CIDR block Tenancy
dev-env 10.0.0.0/16 default

Subnet

Name tag VPC AZ CIDR block
public-web dev-env ap-northeast-1a 10.0.0.0/24
private-db-1 dev-env ap-northeast-1a 10.0.1.0/24
private-db-2 dev-env ap-northeast-1c 10.0.2.0/24
main.tf
# -------------------
# VPCの設定
# -------------------
resource "aws_vpc" "dev_env" {
    cidr_block           = "10.0.0.0/16"
    instance_tenancy     = "default"  # ここで作成される EC2 インスタンスの物理ハードウェア分散方法を指定。通常は複数の AWS アカウントと共存する形式である共有型 (default) を指定
    enable_dns_support   = "true"  # VPC内でDNSによる名前解決を有効化するかを指定
    enable_dns_hostnames = "false"  # VPC内のパブリックIPアドレスを持つインスタンスがDNSホスト名を取得するかを指定
    tags = {
        Name = "dev-env"
    }
}

# -------------------
# Subnetの設定
# -------------------
# パブリックサブネットの作成
resource "aws_subnet" "public_web" {
    vpc_id                  = "${aws_vpc.dev_env.id}"
    cidr_block              = "10.0.0.0/24"
    map_public_ip_on_launch = true  # trueに設定することで、サブネット内で起動したインスタンスにパブリックIPアドレスを自動的に割り当てる
    availability_zone       = "ap-northeast-1a"
    tags = {
        Name = "public-web"
    }
}

# プライベートサブネットの作成
resource "aws_subnet" "private_db_1" {
    vpc_id                  = "${aws_vpc.dev_env.id}"
    cidr_block              = "10.0.1.0/24"
    map_public_ip_on_launch = false  # パブリックIPは不要なのでFalseに設定
    availability_zone       = "ap-northeast-1a"
    tags = {
        Name = "private-db-1"
    }
}

resource "aws_subnet" "private_db_2" {
    vpc_id                  = "${aws_vpc.dev_env.id}"
    cidr_block              = "10.0.2.0/24"
    map_public_ip_on_launch = false
    availability_zone       = "ap-northeast-1c"
    tags = {
        Name = "private-db-2"
    }
}

# -------------------
# DB Subnet Groupの設定
# -------------------
# RDS から VPC を利用するために設定します
resource "aws_db_subnet_group" "db_subnet" {
    name        = "db-subnet"
    description = "It is a DB subnet group on dev-env."
    subnet_ids  =["${aws_subnet.private_db_1.id}","${aws_subnet.private_db_2.id}"]
    tags = {
        Name = "db-subnet"
    }
}

# -------------------
# InternetGatewayの設定
# -------------------
resource "aws_internet_gateway" "dev_env_gw" {
    vpc_id     = "${aws_vpc.dev_env.id}"
    depends_on = [aws_vpc.dev_env]
    tags = {
        Name = "dev-env-gw"
    }
}

# -------------------
# RouteTableの設定
# -------------------
# パブリックルートテーブルの作成
resource "aws_route_table" "public_route" {
    vpc_id = "${aws_vpc.dev_env.id}"
    tags   = {
        Name = "public-route"
    }
}
 
# インターネットゲートウェイへのルートを設定
resource "aws_route" "public_route" {
    destination_cidr_block = "0.0.0.0/0"
    gateway_id             = "${aws_internet_gateway.dev_env_gw.id}"
    route_table_id         = "${aws_route_table.public_route.id}"
}

# ルートテーブルとパブリックサブネットの関連付け
# 関連付けをしていない場合、デフォルトルートテーブルが自動的に使われるますが、デフォルトはアンチパターンなので関連付けを忘れないよう注意してください
resource "aws_route_table_association" "public_a" {
    subnet_id      = "${aws_subnet.public_web.id}"
    route_table_id = "${aws_route_table.public_route.id}"
}

以上でネットワーク環境の定義は完了です。

ここまでに出てきたTerraformの記述方法について補足しておきます。
SubnetやInternet Gatewayの設定値で必要なのは、vpc_idです。
マネコンでは、先にVPCを作成するため既存のVPCにInternet Gatewayをアタッチできますが、
Terraformで構築する場合は、<リソースの種類>.<リソース名>.<属性名>と記述することで、
テンプレート内の他リソースの属性を参照することができます。
例えば、Subnetであれば下記のようになります。

resource "aws_vpc" "dev_env" {
    cidr_block           = "10.0.0.0/16"
    instance_tenancy     = "default"  
    enable_dns_support   = "true"  
    enable_dns_hostnames = "false"  
    tags = {
        Name = "dev-env"
    }
}

resource "aws_subnet" "public_web" {
    vpc_id = "${aws_vpc.dev_env.id}"  # dev-envのid属性を参照
}

EC2、Security Group(WEB用)の設定

WEBサーバーとWEBサーバー用のSecurity Groupの設定を定義していきます。
WEBサーバー用のSecurity Groupは、Internet(80番)とSSH(22番)、
WEBサーバーからインターネットへの出口の設定が必要です。
アウトバウンドルールについて、マネコンから作成の場合は自動的にALLTrafficルールが適用されますが、Terraformから作成時は明示的に指定します。

WEBサーバー用のセキュリティグループ

Name tag Group name VPC
web-sg web-sg dev-env

Inbound Rules

Type Protocol Port Source
SSH(22) TCP(6) 22 0.0.0.0/0
HTTP(80) TCP(6) 80 0.0.0.0/0

Outbound Rules

Type Protocol Port Source
ALL TCP(6) ALL 0.0.0.0/0
main.tf
# -------------------
# SecurityGroupの設定
# -------------------
#  WEBサーバーのセキュリティグループの作成
resource "aws_security_group" "web_sg" {
    name        = "web-sg"
    description = "it is a security group on http of dev-env"
    vpc_id      = "${aws_vpc.dev_env.id}"
    tags = {
        Name = "web-sg"
    }
}

# WEBサーバーのセキュリティグループのInbound Rules
resource "aws_security_group_rule" "ssh" {
    type              = "ingress"
    from_port         = 22
    to_port           = 22
    protocol          = "tcp"
    cidr_blocks       = ["0.0.0.0/0"]
    security_group_id = "${aws_security_group.web_sg.id}"
}

resource "aws_security_group_rule" "http" {
    type              = "ingress"
    from_port         = 80
    to_port           = 80
    protocol          = "tcp"
    cidr_blocks       = ["0.0.0.0/0"]
     security_group_id = "${aws_security_group.web_sg.id}"
}

# WEBサーバーのセキュリティグループのOutbound Rules
resource "aws_security_group_rule" "all" {
    type              = "egress"
    from_port         = 0
    to_port           = 0
    protocol          = -1  #protocol = "-1" は "all" と同等
    cidr_blocks       = ["0.0.0.0/0"]
    security_group_id = "${aws_security_group.web_sg.id}"
}

# -------------------
# WEBサーバーの設定
# -------------------
# EC2 Key Pairs
variable aws_key_name {}
variable public_key_path {}

resource "aws_key_pair" "key_pair" {
    key_name   = "${var.key_name}"
    public_key = file(var.public_key_path)
}

# EC2
resource "aws_instance" "webserver" {
     ami                         = "ami-072bfb8ae2c884cc4"
    instance_type               = "t2.micro"
    key_name                    = "${aws_key_pair.key_pair.key_name}" 
    vpc_security_group_ids      = ["${aws_security_group.web_sg.id}"]
    subnet_id                   = "${aws_subnet.public_web.id}"
    associate_public_ip_address = "true"
    root_block_device {
        volume_type = "gp2"
        volume_size = "20"
    }
    ebs_block_device {
        device_name = "/dev/sdf"
        volume_type = "gp2"
        volume_size = "10"
    }
    tags = {
        Name = "webserver"
    }
}

キーペアについて補足です。
キーペアはEC2に接続するために必要なため、事前に作成してください。
今回はssh-keygenコマンドでキーペアを作成し、aws_key_pairに登録しています。
terraform.tfvarsファイルには下記のように追記してください。

terraform.tfvars
aws_key_name    = "***" # キーペア名
public_key_path = "~/***/***/***" #パブリックキーへのパス

RDS、Security Group(DB用)の設定

続いて、DBサーバーとDBサーバー用のSecurity Groupの設定を定義していきます。
WEBサーバーからDBサーバーへの接続を許可してあげます。

DBサーバー用のセキュリティグループ

Name tag Group name VPC
db-sg db-sg dev-env

Inbound Rules

Type Protocol Port
MySQL(3306) TCP(6) 3306
main.tf
# -------------------
# SecurityGroupの設定
# -------------------
#  DBサーバーのセキュリティグループの作成
resource "aws_security_group" "db_sg" {
    name        = "db-sg"
    description = "It is a security group on db of dev-env"
    vpc_id      = aws_vpc.dev_env.id
    tags = {
       Name = "db-sg"
    }
}

# DBサーバーのセキュリティグループのInbound Rules
resource "aws_security_group_rule" "db" {
    type                     = "ingress"
    to_port                  = 3306
    protocol                 = "tcp"
    source_security_group_id = "${aws_security_group.web_sg.id}" # WEBサーバ用セキュリティグループからのアクセスを許可するために指定
    security_group_id        = "${aws_security_group.db_sg.id}"
}

# -------------------
# DBサーバーの設定
# -------------------
# DBのユーザー名とパスワード設定
variable "aws_db_username" {}
variable "aws_db_password" {}

# RDS
resource "aws_db_instance" "dbserver" {
    identifier              = "db"
    allocated_storage       = 5 # ストレージの大きさ
    engine                  = "mysql"
    engine_version          = "5.7"
    instance_class          = "db.t3.micro"
    storage_type            = "gp2"
    username                = "${var.aws_db_username}"
    password                = "${var.aws_db_password}
    backup_retention_period = 0 # バックアップの保存期間(日)を設定
    vpc_security_group_ids  = ["${aws_security_group.db_sg.id}"]
    db_subnet_group_name    = "${aws_db_subnet_group.db_subnet.name}"
    deletion_protection     = false 
    skip_final_snapshot     = true  
}

DBのユーザー名とパスワードは、クレデンシャル情報と同様variableで変数を定義します。
値は下記のようにterraform.tfvarsに記述してください。

terraform.tfvars
aws_db_username = "*****"
aws_db_password = "******************************"

アウトプット

例えば、EC2のパブリックIPアドレス等、環境構築後に各リソースに割り当てられた値を知りたい場合があります。
そんな時は、output "アウトプットする属性の説明" { value = "アウトプットする属性値"}と記述することで環境構築後にターミナルで各リソースの値を確認できます。
今回はwebserverのパブリックIPアドレスが出力されるように記述します。

main.tf
output "public ip of webserver" {
  value = "${aws_instance.webserver.public_ip}"
}

構築

リソース設定が完了しました!
いよいよAWSクラウド上にインフラを構築していきます!
Terraformでの基本的な構築は、
1.terrafom init
2.terraform plan
3.terraform apply
の3ステップです。

terraform init

terrafom initから解説していきます。

terrafom initは、ワークスペースを初期化するコマンドです。
Terraform を実行するためには、1番初めに ワークスペースを初期化することが必須となっています。
terraform initを実行すると、.tf ファイルで利用している plugin(aws provider など)のダウンロード処理などが走ります。
(最初に1度の実行でよいですが、複数回実行しても問題ありません)

では、ターミナルでterraform initを実行してみましょう。
成功するとTerraform has been successfully initialized!と表示されます。

$ terraform init
Initializing the backend...

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v4.34.0
・
・
・
Terraform has been successfully initialized!

terraform plan

terraform planはTerraformの実行計画を参照するためのコマンドです。
この時点ではまだインフラの構築はされませんが、どのようなリソースが作成・修正・削除されるのかを表示してくれます。
terraform planを実行すると、リソースの状態を表すterraform.tfstateという名前のJSONファイルが生成されます。

新規に作成されたリソースは「+」で表され、削除されたリソースには「ー」が表示されます。

$ terraform plan
Terraform used the selected providers to generate the following execution plan. Resource
actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_db_instance.dbserver will be created
  + resource "aws_db_instance" "dbserver" {
      + address                               = (known after apply)
      + allocated_storage                     = 5
      + apply_immediately                     = (known after apply)
      + arn                                   = (known after apply)
      + auto_minor_version_upgrade            = true
      + availability_zone                     = (known after apply)
      + backup_retention_period               = 0
      + backup_window                         = (known after apply)
      + ca_cert_identifier                    = (known after apply)
      + character_set_name                    = (known after apply)
・
・
・
Plan: 3 to add, 0 to change, 0 to destroy.

terraform apply

では、最後にterraform applyコマンドでAWS上にリソースを作成しましょう!
途中、Enter a valueと承認が求められますので、これに対して"yes"と入力しましょう。

数分間リソースの作成を待ち、Apply complete!と表示されれば完了です!
問題なく構築が完了すると、outputで設定したパブリックIPアドレスも出力されます。

$ terraform apply
Terraform used the selected providers to generate the following execution plan. Resource
actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:
・
・
・
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と入力してEnterキーを押下
・
・
・
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Outputs:

public_ip_of_webserver = "**.***.***.***"

構築したリソースの削除

Terraformで作成したリソースは、terraform destroyコマンドで一括削除が可能です。
構築した環境が必要ない場合はクリーンアップしておきましょう。

$ tf destroy
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:
・
・
・
Plan: 0 to add, 0 to change, 18 to destroy.

Changes to Outputs:
  - public_ip_of_webserver = "**.***.***.***" -> null

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と入力しEnterを押下
・
・
・
Destroy complete! Resources: 18 destroyed.

これで削除が完了しました!
作成したリソース情報を確認するterraform showコマンドを叩いても、何も表示されません。

$ tf show

$

お疲れ様でした!

Discussion