LoginSignup
1
1

More than 3 years have passed since last update.

第1回「EC2」@Baby Step Terraform for AWS

Last updated at Posted at 2019-06-26

お題

英語的におかしいであろう表題のことは置いといて、ある(小さな)アプリをAWS上で動かそうとした時にTerraformを使って少しずつ目的のものに近づけていくことを試みる。
Terraformのテンプレートファイルの記述も、最初は愚直に、問題に直面しつつ徐々に改修していく想定。
世の中の記事や書籍では最初からベストプラクティスを踏まえた設計をすることが多い。
でも、理解のためには、まず目的とする最低限の書き方で書いて、そこから「今、こうなってるからこうした方がいい」、「次にこれらを追加する想定だから、こうしておいた方がいい」といった改善をしてベストプラクティスに近づいた方がいいと思う。

以下、構築予定。ただし、今回は1のEC2上でWebアプリを動かすとこのみ。

  1. アプリ(※)をEC2上で動かしてみる。
  2. マネージドなDBに接続しにいく。
  3. デフォルトのネットワークを使うのではなくvpcsubnetを定義する。
  4. マルチAZ(アベイラビリティゾーン)化とロードバランサーを導入する。 (5. 画像ファイルをS3に置いてCloudFront経由でアクセスさせる。) ※
  5. アプリ(※)をDocker化してECSで動かすようにする。
  6. 最後はCI/CD。GitHubにアプリのソースをプッシュしたら自動でビルド・デプロイが走るようにする。
  7. Route 53使って独自ドメインでアクセスできるようにする。

※Goで適当に作ったアプリ(http://【デプロイ先ホスト】/helloにアクセスすると「hello」と吐くだけ)
https://github.com/sky0621/go-experiment/tree/af129be808476b56e06073775a92fb5d4cec3980

Terraformのテンプレートファイル(*.tf)の書き方やModule化など、最初は愚直なやり方で行い、ちょっとずつベストプラクティス要素を取り入れていく。

極力、無料枠内でやりくりしたいところだけど、立てるリソース如何ではなんとも。。。
https://aws.amazon.com/jp/free/?all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=categories%2312monthsfree%7Ccategories%23alwaysfree

今回の最終的なイメージ

step01n.png

PlantUMLAWS-PlantUMLで作成
【参考】https://qiita.com/sky0621/items/f27ca0638635119e6721

お断り

  • AWS自体の説明は薄いので「AWS触ったこともない」人に理解してもらえる内容にはなっていないです。
  • 上記に関連して、AWSのアカウントは保持済み、AWSのCredentialはローカルにある前提でterraformコマンド叩いています。
  • この記事の内容で実際のサービスを本番運用しているわけではないので、このまま真似してプロダクションレディになる保証はないです。

開発環境

# OS - Linux(Ubuntu)

$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.2 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.2 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic

# IDE - Visual Studio Code

Version: 1.35.1
Commit: c7d83e57cd18f18026a8162d042843bda1bcf21f
Date: 2019-06-12T14:27:31.086Z
Electron: 3.1.8
Chrome: 66.0.3359.181
Node.js: 10.2.0
V8: 6.6.346.32
OS: Linux x64 4.15.0-47-generic
vscode-terraform Plugin
Name: Terraform
Id: mauve.terraform
Description: Syntax highlighting, linting, formatting, and validation for Hashicorp's Terraform
Version: 1.3.12
Publisher: Mikael Olenfalk
VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=mauve.terraform

# Terraform

$ terraform version
Terraform v0.12.2

# tfenv

$ tfenv
tfenv 0.6.0

実践

1. ミニマムアプリのEC2デプロイ

アプリはGolang製でLinux(Ubuntu)環境でビルドしたバイナリ(go-experiment)がGitHub(※)に上がっている前提。
https://github.com/sky0621/go-experiment/tree/af129be808476b56e06073775a92fb5d4cec3980

Projectのディレクトリ構成

EC2インスタンス1つ立てるだけということもあり、Projectルート直下に1ファイルのみ。

$ tree 
.
└── main.tf

0 directories, 1 file

テンプレートファイルの中身チェック

main.tf
resource "aws_instance" "go-app-server" {
  ami                    = "ami-0f9ae750e8274075b" # https://aws.amazon.com/jp/amazon-linux-2/
  instance_type          = "t2.micro"              # https://aws.amazon.com/jp/ec2/instance-types/

  user_data = <<EOF
    #!/bin/bash
    sudo yum -y install git
    git clone https://github.com/sky0621/go-experiment.git
    cd go-experiment
    ./go-experiment
EOF
}

aws_instance

EC2のインスタンス生成を行う。
https://www.terraform.io/docs/providers/aws/r/instance.html

ami

Amazonマシンイメージは、Amazon Linux2を採用。

instance_type

インスタンスタイプの種類はさまざま。
https://aws.amazon.com/jp/ec2/instance-types/
今回は無料枠の対象であるt2.microを採用。
https://aws.amazon.com/jp/free/?all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=categories%23featured

user_data

ここでGitHub経由でgo-experimentアプリをEC2インスタンス内に格納して起動。

ただ、ここでエラーが起きた場合、どうやって検知できるんだろう・・・。

AWS環境へ反映

フォーマッターにかけて、

$ terraform fmt
main.tf

バリデーションチェックして、

$ terraform validate
Success! The configuration is valid.

プランチェックして、

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

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:

  # aws_instance.name will be created
  + resource "aws_instance" "name" {
      + ami                          = "ami-0f9ae750e8274075b"
      + arn                          = (known after apply)
         〜〜 省略 〜〜
      + instance_state               = (known after apply)
      + instance_type                = "t2.micro"
      + ipv6_address_count           = (known after apply)
         〜〜 省略 〜〜
      + tenancy                      = (known after apply)
      + user_data                    = "3a280d56e8e7e3aec19bad9a8f5b696867b8dd39"
      + volume_tags                  = (known after apply)
      + vpc_security_group_ids       = (known after apply)

 〜〜 省略 〜〜

いざ、実行。

$ terraform apply

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

 〜〜 省略 〜〜

  Enter a value: yes

aws_instance.name: Creating...
aws_instance.name: Still creating... [10s elapsed]
aws_instance.name: Still creating... [20s elapsed]
aws_instance.name: Still creating... [30s elapsed]
aws_instance.name: Creation complete after 32s [id=i-0018f388ca8a7774f]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

EC2インスタンスは?

screenshot-ap-northeast-1.console.aws.amazon.com-2019-06-23-11-24-22-157.png

出来てる。
じゃあ、もうアプリにつながるかというと、

Screenshot from 2019-06-23 19-08-09.png

つながらない。

セキュリティグループの定義

EC2インスタンス立てただけでは80番ポートが開いてないので、穴あけ作業が必要。

テンプレートファイルに追記

main.tf
resource "aws_instance" "go-app-server" {
  ami                    = "ami-0f9ae750e8274075b" # https://aws.amazon.com/jp/amazon-linux-2/
  instance_type          = "t2.micro"              # https://aws.amazon.com/jp/ec2/instance-types/
  vpc_security_group_ids = [aws_security_group.go-app-security-group.id]

 〜〜 省略 〜〜
}

resource "aws_security_group" "go-app-security-group" {
  name = "go-app-security-group"

  ingress {
      from_port = 80
      to_port = 80
      protocol = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
      from_port = 0
      to_port = 0
      protocol = "-1"
      cidr_blocks = ["0.0.0.0/0"]
  }
}

output "go-app-public-dns" {
  value = aws_instance.go-app-server.public_dns
}

vpc_security_group_ids

作成したセキュリティグループとこのEC2インスタンスを紐付ける。
https://www.terraform.io/docs/providers/aws/r/instance.html#vpc_security_group_ids

aws_security_group

紐付けたインスタンスへの”入り”と”出”を制御する。
https://www.terraform.io/docs/providers/aws/r/security_group.html

ingress

紐付けるサーバへの”IN"を制御。今回はtcp:80番ポートを開ける。特にアクセス元は絞らない。

egress

紐付けるサーバの”OUT"を制御。protocol-1ないしallにすると全ポート開放になるらしい。
https://www.terraform.io/docs/providers/aws/r/security_group_rule.html

再度、AWS環境へ反映

Terraformコマンド使うくだりは同じなので省略。
applyが終わり、再度、ブラウザで見てみると、

Screenshot from 2019-06-24 07-58-08.png

セキュリティグループの設定でポート「80」番を開けたので、今度は表示された。

この段階でのTerraform関連のソース

1の後のStepupフェーズ

おさらい

現在のTerraformテンプレートファイルは下記。

main.tf
resource "aws_instance" "web-server" {
  ami                    = "ami-0f9ae750e8274075b" # https://aws.amazon.com/jp/amazon-linux-2/
  instance_type          = "t2.micro"              # https://aws.amazon.com/jp/ec2/instance-types/
  vpc_security_group_ids = [aws_security_group.web-server-security-group.id]

  user_data = <<EOF
    #!/bin/bash
    sudo yum -y install git
    git clone https://github.com/sky0621/go-experiment.git
    cd go-experiment
    ./go-experiment
EOF
}

resource "aws_security_group" "web-server-security-group" {
  name = "web-server-security-group"

  ingress {
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

output "web-server-public-dns" {
  value = aws_instance.web-server.public_dns
}

プラクティス

この分量のテンプレートファイルなら一読して理解不可能な複雑さはない。
ただ、少なくともこの後、ネットワークを構成したり、マネージドなDB、S3、ECSというようにAWSのリソースをふんだんに使い出すことになる。
そうなると、main.tfだけでは読み解きづらくなるので、現時点でファイルを分けておく。

outputs

本家HashiCorpのGitHub上の事例でも、outputvariable(当記事ではまだ使ってない)は専用のファイルを設けている。
Subnetの事例

上記にならって以下のようにファイル分けしておく。
以下の通り。

main.tf
resource "aws_instance" "web-server" {
  ami                    = "ami-0f9ae750e8274075b" # https://aws.amazon.com/jp/amazon-linux-2/
  instance_type          = "t2.micro"              # https://aws.amazon.com/jp/ec2/instance-types/
  vpc_security_group_ids = ["${aws_security_group.web-server-security-group.id}"]

  user_data = <<EOF
    #!/bin/bash
    sudo yum -y install git
    git clone https://github.com/sky0621/go-experiment.git
    cd go-experiment
    ./go-experiment
EOF
}

resource "aws_security_group" "web-server-security-group" {
  name = "web-server-security-group"

  ingress {
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
outputs.tf
output "web-server-public-dns" {
  value = "${aws_instance.web-server.public_dns}"
}

※ちなみに、これまで単に「aws_instance.web-server.public_dns」と書いていた部分を今回「"${aws_instance.web-server.public_dns}"」というように修正した。
修正前の時点でも動作はしていたものの、Visual Studio CodeのTerraformプラグイン上では警告が出ていた。
今回の修正により警告がなくなった。

AWSリソース別のファイル化

これも本家HashiCorpの事例や他の記事でもよく見るように、AWSリソース別にファイル化することが多い様子。
あわせて、何度も使われる(であろう)リソースはModule化も行うらしいけど、今回はまだそこまではしない。

今回やるのは、EC2インスタンスリソース分とセキュリティグループ分の別ファイル化。
以下の通り。

ec2.tf
resource "aws_instance" "web-server" {
  ami                    = "ami-0f9ae750e8274075b" # https://aws.amazon.com/jp/amazon-linux-2/
  instance_type          = "t2.micro"              # https://aws.amazon.com/jp/ec2/instance-types/
  vpc_security_group_ids = ["${aws_security_group.web-server-security-group.id}"]

  user_data = <<EOF
    #!/bin/bash
    sudo yum -y install git
    git clone https://github.com/sky0621/go-experiment.git
    cd go-experiment
    ./go-experiment
EOF
}
security_group.tf
resource "aws_security_group" "web-server-security-group" {
  name = "web-server-security-group"

  ingress {
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

user_dataの別ファイル化

EC2インスタンス起動時に実行するコマンドをuser_dataに指定しているが、これはいわゆるセットアップスクリプトなので別ファイルとして管理したい。
Terraformでは、ファイル操作や文字列操作ができる(テンプレートファイル内に書ける)組み込み関数が用意されているので、それを利用。

web-server-setup.sh
#!/bin/bash
sudo yum -y install git
git clone https://github.com/sky0621/go-experiment.git
cd go-experiment
./go-experiment
ec2.tf
resource "aws_instance" "web-server" {
  ami                    = "ami-0f9ae750e8274075b" # https://aws.amazon.com/jp/amazon-linux-2/
  instance_type          = "t2.micro"              # https://aws.amazon.com/jp/ec2/instance-types/
  vpc_security_group_ids = ["${aws_security_group.web-server-security-group.id}"]

  user_data = file("./web-server-setup.sh")
}

ファイル分割後の構成

$ tree
.
├── ec2.tf
├── outputs.tf
├── security_group.tf
├── terraform.tfstate
└── web-server-setup.sh

terraform.tfstateはAWS適用後の状態を保持するファイル(terraform apply実行により自動作成される)

Providerの設定

リージョン指定の謎

これまでのテンプレートファイルで作られるのはAWSの各種リソース。
ただ、Terraformが扱うのはAWSに限ったことではない。GCPだってAzureだって扱える。
じゃあ、何をもってAWSのリソースを使うと判断できるのか。
テンプレートファイルで「aws_instance」といった、AWSを示すリソースを指定しているのだから、当然と言えば当然なんだけど。
ここで1つ疑問。
今のところ、テンプレートファイルでリージョンの指定はしていない。
なのに、EC2インスタンスは下記のように東京リージョンに作られている。
screenshot-ap-northeast-1.console.aws.amazon.com-2019-06-25-16-32-25-813.png
これは、実はAWSのクライアントツールをセットアップした時に各種クレデンシャル情報に加え、デフォルトのリージョンを下記のように環境変数にセットしたためと思われる。

$ env | grep AWS
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AWS_DEFAULT_REGION=ap-northeast-1
AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxx

試しに、以下のようにリージョンを「us-east-2(=オハイオ)」に変えてterraform applyを実行すると、

$ env | grep AWS
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AWS_DEFAULT_REGION=us-east-2
AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxx

Screenshot from 2019-06-26 06-38-49.png

となる。

リージョンの明示的な定義

上記のようにTerraformコマンドの実行環境に依存するリージョンを明示的に指定するには「provider」を定義する。
https://www.terraform.io/docs/configuration/providers.html
これも、専用のテンプレートファイルとする。
ちなみに、前述の「outputs.tf」もそうだったけど、「variables.tf」や今回のproviderが「providers.tf」になるようにファイル名には複数形を用いられる(ことが多い?)。
ただし、「vpc.tf」や「security_group.tf」のように各サービスの場合は単数形を用いている様子。

providers.tf
provider "aws" {
  region = "ap-northeast-1"
}

上記のようにしておくと、環境変数として下記のように「us-east-2」がセットされていても、

$ env | grep AWS
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AWS_DEFAULT_REGION=us-east-2
AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxx

下のように「ap-northeast-1」リージョンでインスタンスが作られる。

Screenshot from 2019-06-26 08-18-20.png

その他プロバイダー設定

プロバイダーバージョン

プロバイダーとしてはAWSを用いる。そのプロバイダーにはバージョンがある。
今どんなバージョンが存在するかは下記参照。
https://github.com/terraform-providers/terraform-provider-aws/blob/master/CHANGELOG.md

セマンティックバージョニングを採用しているので、現時点の安定版バージョンの中でバグフィックス起因のアップデートのみ許容する指定にする。
https://www.terraform.io/docs/configuration/providers.html#version-provider-versions

provider "aws" {
  version = "~> 2.16.0"
  region = "ap-northeast-1"
}

これ以外に指定可能な設定については下記参照。
https://www.terraform.io/docs/providers/aws/index.html#argument-reference

Terraform自体の設定

プロバイダーのバージョンも大事だけど、そもそもTerraform自体のバージョンも明示的に固定すべき。
これは以下のように、また別ファイル化しておく。

terraform.tf
terraform {
  required_version = "~> 0.12.0"
}

ファイル分割後の構成

$ tree
.
├── ec2.tf
├── outputs.tf
├── providers.tf
├── security_group.tf
├── terraform.tf
└── web-server-setup.sh

※あえて *.tfstate ファイルは表示上、外した。

上記以外の設定については下記参照。
https://www.terraform.io/docs/configuration/terraform.html

この段階でのTerraform関連のソース

今回は、まだ6ファイルだけなので、中身を全て再掲しておく。

以下はEC2インスタンスを立てて外部からアクセスするのに必要な定義。

ec2.tf
# https://www.terraform.io/docs/providers/aws/r/instance.html
resource "aws_instance" "web-server" {
  ami                    = "ami-0f9ae750e8274075b" # https://aws.amazon.com/jp/amazon-linux-2/
  instance_type          = "t2.micro"              # https://aws.amazon.com/jp/ec2/instance-types/
  vpc_security_group_ids = ["${aws_security_group.web-server-security-group.id}"]

  user_data = file("./web-server-setup.sh")
}
web-server-setup.sh
#!/bin/bash
sudo yum -y install git
git clone https://github.com/sky0621/go-experiment.git
cd go-experiment
./go-experiment
security_group.tf
# https://www.terraform.io/docs/providers/aws/r/security_group.html
resource "aws_security_group" "web-server-security-group" {
  name = "web-server-security-group"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

↓は、単にterraform applyした時にコンソールに出力してほしい内容。

outputs.tf

output "web-server-public-dns" {
  value = "${aws_instance.web-server.public_dns}"
}

以降は、Terraform自体やAWSプロバイダーの定義。

terraform.tf
terraform {
  required_version = "~> 0.12.0"
}
providers.tf
provider "aws" {
  version = "~> 2.16.0"
  region = "ap-northeast-1"
}
1
1
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
1
1