お題
英語的におかしいであろう表題のことは置いといて、ある(小さな)アプリをAWS上で動かそうとした時にTerraformを使って少しずつ目的のものに近づけていくことを試みる。
Terraformのテンプレートファイルの記述も、最初は愚直に、問題に直面しつつ徐々に改修していく想定。
世の中の記事や書籍では最初からベストプラクティスを踏まえた設計をすることが多い。
でも、理解のためには、まず目的とする最低限の書き方で書いて、そこから「今、こうなってるからこうした方がいい」、「次にこれらを追加する想定だから、こうしておいた方がいい」といった改善をしてベストプラクティスに近づいた方がいいと思う。
以下、構築予定。ただし、今回は1のEC2
上でWebアプリを動かすとこのみ。
- アプリ(※)を
EC2
上で動かしてみる。 - マネージドなDBに接続しにいく。
- デフォルトのネットワークを使うのではなく
vpc
、subnet
を定義する。 - マルチAZ(アベイラビリティゾーン)化とロードバランサーを導入する。
(5. 画像ファイルを
S3
に置いてCloudFront
経由でアクセスさせる。) ※ - アプリ(※)をDocker化して
ECS
で動かすようにする。 - 最後はCI/CD。GitHubにアプリのソースをプッシュしたら自動でビルド・デプロイが走るようにする。
-
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
今回の最終的なイメージ
PlantUML+AWS-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
テンプレートファイルの中身チェック
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インスタンスは?
出来てる。
じゃあ、もうアプリにつながるかというと、
つながらない。
セキュリティグループの定義
EC2インスタンス立てただけでは80
番ポートが開いてないので、穴あけ作業が必要。
テンプレートファイルに追記
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が終わり、再度、ブラウザで見てみると、
セキュリティグループの設定でポート「80
」番を開けたので、今度は表示された。
この段階でのTerraform関連のソース
1の後のStepupフェーズ
おさらい
現在のTerraformテンプレートファイルは下記。
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上の事例でも、output
やvariable
(当記事ではまだ使ってない)は専用のファイルを設けている。
※Subnetの事例
上記にならって以下のようにファイル分けしておく。
以下の通り。
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}"
}
※ちなみに、これまで単に「aws_instance.web-server.public_dns
」と書いていた部分を今回「"${aws_instance.web-server.public_dns}"
」というように修正した。
修正前の時点でも動作はしていたものの、Visual Studio CodeのTerraformプラグイン上では警告が出ていた。
今回の修正により警告がなくなった。
AWSリソース別のファイル化
これも本家HashiCorpの事例や他の記事でもよく見るように、AWSリソース別にファイル化することが多い様子。
あわせて、何度も使われる(であろう)リソースはModule化も行うらしいけど、今回はまだそこまではしない。
今回やるのは、EC2インスタンスリソース分とセキュリティグループ分の別ファイル化。
以下の通り。
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"]
}
}
user_dataの別ファイル化
EC2インスタンス起動時に実行するコマンドをuser_data
に指定しているが、これはいわゆるセットアップスクリプトなので別ファイルとして管理したい。
Terraformでは、ファイル操作や文字列操作ができる(テンプレートファイル内に書ける)組み込み関数が用意されているので、それを利用。
#!/bin/bash
sudo yum -y install git
git clone https://github.com/sky0621/go-experiment.git
cd go-experiment
./go-experiment
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インスタンスは下記のように東京リージョンに作られている。
これは、実は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
となる。
リージョンの明示的な定義
上記のようにTerraformコマンドの実行環境に依存するリージョンを明示的に指定するには「provider
」を定義する。
https://www.terraform.io/docs/configuration/providers.html
これも、専用のテンプレートファイルとする。
ちなみに、前述の「outputs.tf
」もそうだったけど、「variables.tf
」や今回のprovider
が「providers.tf
」になるようにファイル名には複数形を用いられる(ことが多い?)。
ただし、「vpc.tf
」や「security_group.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
」リージョンでインスタンスが作られる。
その他プロバイダー設定
プロバイダーバージョン
プロバイダーとしては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 {
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
インスタンスを立てて外部からアクセスするのに必要な定義。
# 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")
}
#!/bin/bash
sudo yum -y install git
git clone https://github.com/sky0621/go-experiment.git
cd go-experiment
./go-experiment
# 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
した時にコンソールに出力してほしい内容。
output "web-server-public-dns" {
value = "${aws_instance.web-server.public_dns}"
}
以降は、Terraform自体やAWSプロバイダーの定義。
terraform {
required_version = "~> 0.12.0"
}
provider "aws" {
version = "~> 2.16.0"
region = "ap-northeast-1"
}