この記事で作るもの
- VPC
- RDS(Aurora)
- EC2(踏み台用途)
- EC2キーペア
RDSはVPCのプライベートサブネットに配置する想定です。そのためローカルから接続することができません。
RDSの設定でパブリックアクセシビリティ
をONにすればアクセスできますが、
セキュリティ上、踏み台サーバー(EC2)を配置することが良いと思われます。
また構築するEC2は、通常のアプリケーションサーバーとしての用途に置き換えても問題ないと思います。
Terraformのバージョンは0.12.19
となります。
前提条件
- awscliが使えること
$ brew install awscli
$ aws --version
aws-cli/1.17.0 Python/3.8.1 Darwin/19.2.0 botocore/1.14.0
- Terraform(0.12.19)が使えること
$ wget https://releases.hashicorp.com/terraform/0.12.19/terraform_0.12.19_darwin_amd64.zip
$ unzip terraform_0.12.19_darwin_amd64.zip -d /usr/local/bin/
$ terraform -v
Terraform v0.12.19
ディレクトリ構成
.
├── README.md
├── components
│ ├── db
│ │ ├── backend.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ ├── provider.tf
│ │ └── variables.tf
│ ├── ec2
│ │ ├── backend.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ ├── provider.tf
│ │ └── variables.tf
│ ├── network
│ │ ├── backend.tf
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ ├── provider.tf
│ │ └── variables.tf
│ └── securitygroup
│ ├── backend.tf
│ ├── main.tf
│ ├── outputs.tf
│ ├── provider.tf
│ └── variables.tf
├── environments
│ ├── production
│ │ └── terraform.tfvars
│ └── staging
│ └── terraform.tfvars
└── modules
├── iam_role
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
├── key_pair
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
└── securitygroup
├── main.tf
├── outputs.tf
└── variables.tf
今回、以下の4つのコンポーネントを用意しました。
- db
- ec2
- network
- securitygroup
AWSへのデプロイは、これらの単位でおこなわれます。
デプロイの順番はnetwork
→securitygroup
→db
→ec2
とします。
またモジュールについては、以下の3つを用意しました。
- iam_role
- key_pair
- securitygroup
これらは、作成する度に定義を使い回せるようにモジュール化しました。
なお、環境はステージング
と本番
とし、terraform workspace
によって環境を切り分ける仕様とします。
事前準備
初期化
まず、自身のAWS認証情報をawscliより入力してください。
$ aws configure --profile { 自身で決めたProfile名 }
AWS Access Key ID [None]: { 自身のアクセスキー }
AWS Secret Access Key [None]: { 自身のシークレットアクセスキー }
Default region name [None]: ap-northeast-1
Default output format [None]:
Profile名はTerraformの定義で使います。default
は避けましょう。
S3バケットの作成
事前にS3バケットを手動で作成しておきます。
このS3バケットは、Terraformの状態を格納しておく大事なバケットです。
このバケットに格納されている情報をもとに、TerraformがAWSリソースを構築していきます。
コンソール画面で作ってもいいですし、cliでやってもいいでしょう
aws s3 --profile { 自身で決めたProfile名 } mb s3://{ 自身で決めたバケット名 }
workspaceの作成
各コンポーネントに移動し、terraform init
とterraform workspace new {環境名}
を実施しましょう。
$ cd ./components/db/
$ terraform init
$ terraform workspace new staging
$ terraform workspace new production
$ terraform workspace select staging
4つのコンポーネントすべてに適用します。
provider.tfの記述
以下のコンポーネントそれぞれにprovider.tf
を用意します。
- db
- ec2
- network
- securitygroup
それぞれ、以下のように定義しましょう。
variable "profile" {
default = "{ 自身で決めたProfile名 }"
}
provider "aws" {
version = "= 2.45.0"
region = "ap-northeast-1"
profile = var.profile
}
backend.tfの記述
以下のコンポーネントそれぞれにbackend.tf
を用意します。
- db
- ec2
- network
- securitygroup
component名には上記の文字列を入れましょう。db
なら
key = "db/terraform.tfstate"
ということになります。
terraform {
required_version = "= 0.12.19"
backend "s3" {
region = "ap-northeast-1"
encrypt = true
bucket = "{ 先ほど作成したS3バケット }"
key = "{ component名 }/terraform.tfstate"
profile = "{ 自身で決めたProfile名 }"
}
}
なお、今回はコンポーネントごとに状態を分けているため、このままでは各コンポーネントのリソース情報にアクセスすることができません。
例えば、EC2を作りたいのにどのVPCを指定すればいいのか分からない…といった具合です。
こういうケースでは、backend.tf
で依存関係のあるコンポーネントをdata
として定義してあげます。
EC2ではVPCとセキュリティグループを作成する必要があるのでnetwork
とsecuritygroup
のコンポーネントを記載します。
terraform {
required_version = "= 0.12.19"
backend "s3" {
region = "ap-northeast-1"
encrypt = true
bucket = "{ 先ほど作成したS3バケット }"
key = "bastion/terraform.tfstate"
profile = "{ 自身で決めたProfile名 }"
}
}
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "{ 先ほど作成したS3バケット }"
key = "env:/${terraform.workspace}/network/terraform.tfstate"
region = "ap-northeast-1"
profile = "{ 自身で決めたProfile名 }"
}
}
data "terraform_remote_state" "securitygroup" {
backend = "s3"
config = {
bucket = "{ 先ほど作成したS3バケット }"
key = "env:/${terraform.workspace}/securitygroup/terraform.tfstate"
region = "ap-northeast-1"
profile = "{ 自身で決めたProfile名 }"
}
}
dbコンポーネント
はVPCを指定する必要があるのでnetwork
を記載します
securitygroupコンポーネント
もVPCを指定する必要があるのでnetwork
を記載します
※厳密にはDBもセキュリティグループは必要ですが、後述しますが今後修正することはない定義になるので今回はdbコンポーネントに作っちゃいます
VPCの作成
以下を作成します
- VPC
- パブリックルートテーブル
- プライベートルートテーブル (2つ) ※一応将来的にNATGatewayを分けられるようにしてる
- インターネットゲートウェイ
- パブリックサブネット (2つ)
- プライベートサブネット (2つ)
VPC
resource "aws_vpc" "default" {
cidr_block = "10.1.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "sample-${terraform.workspace}"
}
}
パブリックルートテーブル
resource "aws_route_table" "public" {
vpc_id = aws_vpc.default.id
tags = {
Name = "sample-public-route-table-${terraform.workspace}"
}
}
プライベートルートテーブル
resource "aws_route_table" "private_0" {
vpc_id = aws_vpc.default.id
tags = {
Name = "sample-private-route-table-0-${terraform.workspace}"
}
}
resource "aws_route_table" "private_1" {
vpc_id = aws_vpc.default.id
tags = {
Name = "sample-private-route-table-1-${terraform.workspace}"
}
}
インターネットゲートウェイ
resource "aws_internet_gateway" "default" {
vpc_id = aws_vpc.default.id
tags = {
Name = "sample-internet-gateway-${terraform.workspace}"
}
}
resource "aws_route" "public" {
route_table_id = aws_route_table.public.id
gateway_id = aws_internet_gateway.default.id
destination_cidr_block = "0.0.0.0/0"
}
パブリックサブネット
resource "aws_subnet" "public_0" {
vpc_id = aws_vpc.default.id
cidr_block = "10.1.0.0/24"
map_public_ip_on_launch = true
availability_zone = "ap-northeast-1c"
tags = {
Name = "sample-public-subnet-0-${terraform.workspace}"
}
}
resource "aws_subnet" "public_1" {
vpc_id = aws_vpc.default.id
cidr_block = "10.1.1.0/24"
map_public_ip_on_launch = true
availability_zone = "ap-northeast-1d"
tags = {
Name = "sample-public-subnet-1-${terraform.workspace}"
}
}
resource "aws_route_table_association" "public_0" {
subnet_id = aws_subnet.public_0.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "public_1" {
subnet_id = aws_subnet.public_1.id
route_table_id = aws_route_table.public.id
}
プライベートサブネット
resource "aws_subnet" "private_0" {
vpc_id = aws_vpc.default.id
cidr_block = "10.1.10.0/24"
map_public_ip_on_launch = false
availability_zone = "ap-northeast-1c"
tags = {
Name = "sample-private-subnet-0-${terraform.workspace}"
}
}
resource "aws_subnet" "private_1" {
vpc_id = aws_vpc.default.id
cidr_block = "10.1.11.0/24"
map_public_ip_on_launch = false
availability_zone = "ap-northeast-1d"
tags = {
Name = "sample-private-subnet-1-${terraform.workspace}"
}
}
resource "aws_route_table_association" "private_0" {
subnet_id = aws_subnet.private_0.id
route_table_id = aws_route_table.private_0.id
}
resource "aws_route_table_association" "private_1" {
subnet_id = aws_subnet.private_1.id
route_table_id = aws_route_table.private_0.id
}
あとで別のコンポーネントから参照するリソースをoutputs.tf
に記載しておきます
output "sample_vpc_id" {
value = aws_vpc.default.id
}
output "sample_vpc_public_subnet_0_id" {
value = aws_subnet.public_0.id
}
output "sample_vpc_public_subnet_1_id" {
value = aws_subnet.public_1.id
}
output "sample_vpc_private_subnet_0_id" {
value = aws_subnet.private_0.id
}
output "sample_vpc_private_subnet_1_id" {
value = aws_subnet.private_1.id
}
output "sample_vpc_cider_block" {
value = aws_vpc.default.cidr_block
}
セキュリティグループの作成
セキュリティグループ
セキュリティグループの作成はモジュール化してみます。
modules
variable "name" {}
variable "vpc_id" {}
variable "port" {}
variable "cider_blocks" {
type = list(string)
}
resource "aws_security_group" "default" {
name = var.name
vpc_id = var.vpc_id
tags = {
Name = var.name
}
}
resource "aws_security_group_rule" "ingress" {
type = "ingress"
from_port = var.port
to_port = var.port
protocol = "tcp"
cidr_blocks = var.cider_blocks
security_group_id = aws_security_group.default.id
}
resource "aws_security_group_rule" "egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.default.id
}
output "security_group_id" {
value = aws_security_group.default.id
}
コンポーネントからは以下のようにしてモジュールを呼び出します。
今回設定するインバウンドはSSH(Port=22)です。自身のIPからのみSSHできるように設定しましょう。
components
module "ec2" {
source = "../../modules/securitygroup"
name = "ec2-sg-${terraform.workspace}"
vpc_id = data.terraform_remote_state.network.outputs.sample_vpc_id
port = 22
cider_blocks = var.ec2_access_ip
}
vpc_idは、networkコンポーネント
からリソースの情報を引っ張るところに注意しましょう。
先ほど作成したcomponents/network/outputs.tf
に記載があること、
components/securitygroup/backend.tf
にterraform_remote_state
として記載していることが条件です。
RDS(Aurora)の作成
RDSでは以下の項目を作成します。
- Auroraモニタリング用IAMロール
- クラスターパラメーターグループ
- DBパラメーターグループ
- DBサブネットグループ
- Auroraセキュリティグループ
- Auroraクラスター
- Auroraインスタンス
Auroraモニタリング用IAMロール
IAMロールの作成もモジュール化してみます。
variable "name" {}
variable "policy" {}
variable "identifier" {}
resource "aws_iam_role" "default" {
name = var.name
assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
data "aws_iam_policy_document" "assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = [var.identifier]
}
}
}
resource "aws_iam_policy" "default" {
name = var.name
policy = var.policy
}
resource "aws_iam_role_policy_attachment" "default" {
policy_arn = aws_iam_policy.default.arn
role = aws_iam_role.default.name
}
output "iam_role_arn" {
value = aws_iam_role.default.arn
}
output "iam_role_name" {
value = aws_iam_role.default.name
}
モジュールができたらコンポーネントから呼んでみましょう
AmazonRDSEnhancedMonitoringRole
というポリシーが既にAWS公式で存在します。
こちらを適用しましょう。
identifier
は、それぞれのサービスごとに固定値が存在しますので注意しましょう。
今回はmonitoring.rds.amazonaws.com
となります。
data "aws_iam_policy" "aurora_monitoring_policy" {
arn = "arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole"
}
data "aws_iam_policy_document" "aurora_monitoring" {
source_json = data.aws_iam_policy.aurora_monitoring_policy.policy
}
module "aurora_monitoring_role" {
source = "../../modules/iam_role"
name = "aurora_monitoring_role"
identifier = "monitoring.rds.amazonaws.com"
policy = data.aws_iam_policy_document.aurora_monitoring.json
}
クラスターパラメーターグループ
今回はcharset関連をutf8mb4
に変更し、タイムゾーンを東京にしてみます。
その他、要件に従って適切なパラメータを設定しましょう。
resource "aws_rds_cluster_parameter_group" "sample" {
name = "sample-cluster-paameter-group-${terraform.workspace}"
family = "aurora-mysql5.7"
parameter {
name = "character_set_client"
value = "utf8mb4"
}
parameter {
name = "character_set_connection"
value = "utf8mb4"
}
parameter {
name = "character_set_database"
value = "utf8mb4"
}
parameter {
name = "character_set_results"
value = "utf8mb4"
}
parameter {
name = "character_set_server"
value = "utf8mb4"
}
parameter {
name = "time_zone"
value = "Asia/Tokyo"
}
}
DBパラメーターグループ
resource "aws_db_parameter_group" "sample" {
name = "sample-db-paameter-group-${terraform.workspace}"
family = "aurora-mysql5.7"
}
DBサブネットグループ
サブネットのIDは、networkコンポーネント
からリソースの情報を引っ張るところに注意しましょう。
先ほど作成したcomponents/network/outputs.tf
に記載があること、
components/db/backend.tf
にterraform_remote_state
として記載していることが条件です。
resource "aws_db_subnet_group" "sample" {
name = "sample-db-subnet-group-${terraform.workspace}"
subnet_ids = [
data.terraform_remote_state.network.outputs.sample_vpc_private_subnet_0_id,
data.terraform_remote_state.network.outputs.sample_vpc_private_subnet_1_id,
]
}
Auroraセキュリティグループ
本来はsecuritygroupコンポーネント
で作るものかもしれませんが
生涯固定のインバウンド設定になるので、dbコンポーネント
で作ってもいいかなと思います。
セキュリティグループ作成用のモジュールを作ってない場合は、当記事を少し戻って確認してください。
今回、Port=3306をVPC内(CiderBlock)の指定でアクセス可能とします。
プライベートサブネットのため外からアクセスができませんが、
同じVPC内にパブリックサブネットに配置したEC2インスタンスがあればアクセスができるという想定です。
module "aurora_sg" {
source = "../../modules/securitygroup"
name = "sample-db-${terraform.workspace}"
vpc_id = data.terraform_remote_state.network.outputs.sample_vpc_id
port = 3306
cider_blocks = [data.terraform_remote_state.network.outputs.sample_vpc_cider_block]
}
Auroraクラスター
クラスターについて気をつけるべき点はマスターパスワードです。
当記事の方法では、一旦適当なパスワードを設定しておき、後でコンソール画面から手動で変更します。
lifecycle
欄に注目してください。次回の変更からはマスターパスワードの変更は無視するような設定になっています。
resource "aws_rds_cluster" "sample" {
cluster_identifier = "sample-${terraform.workspace}"
master_username = "sample"
master_password = "initial_password" # 手動で変更すること
database_name = "sample"
backup_retention_period = 7
preferred_backup_window = "09:30-10:00" # UTC
preferred_maintenance_window = "wed:10:30-wed:11:00" # UTC
engine = "aurora-mysql"
engine_version = "5.7.mysql_aurora.2.07.1"
port = 3306
vpc_security_group_ids = [module.aurora_sg.security_group_id]
db_subnet_group_name = aws_db_subnet_group.sample.name
db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.sample.name
storage_encrypted = true
deletion_protection = var.deletion_protection
enabled_cloudwatch_logs_exports = ["audit", "error", "general", "slowquery"]
skip_final_snapshot = false
final_snapshot_identifier = "sample-${terraform.workspace}-final-snapshot"
lifecycle {
ignore_changes = [master_password]
}
}
Auroraインスタンス
なお今回、インスタンス数とインスタンスタイプについては、ステージングと本番で差異が発生すると想定させていただきました。
ステージングはインスタンス数1つで小スペック
本番はインスタンス数2つで中〜大スペック
といった具合です。
これらの変数は以下に定義しておきます。
# Auroraスペック
instance_class = "db.t3.small"
# Auroraインスタンス数
cluster_instance_count = 1
# Auroraスペック
instance_class = "db.r5.large"
# Auroraインスタンス数
cluster_instance_count = 2
resource "aws_rds_cluster_instance" "sample" {
count = var.cluster_instance_count
identifier = "sample-${terraform.workspace}-${count.index}"
cluster_identifier = aws_rds_cluster.sample.id
instance_class = var.instance_class
db_subnet_group_name = aws_db_subnet_group.sample.name
db_parameter_group_name = aws_db_parameter_group.sample.name
monitoring_role_arn = module.aurora_monitoring_role.iam_role_arn
monitoring_interval = 60
engine = "aurora-mysql"
engine_version = "5.7.mysql_aurora.2.07.1"
ca_cert_identifier = "rds-ca-2019"
# 変更をすぐに適用する場合
# apply_immediately = true
}
EC2の作成
当記事では踏み台サーバーの用途でEC2を作成しますが、
アプリケーション用のサーバーとして作っても問題はありません。その場合、適切なセキュリティグループを作成するように心がけましょう。
ec2コンポーネント
で作成するリソースは以下です。
- キーペア
- インスタンス
キーペア
キーペアはモジュール化します。
ファイルのパーミッションに注意しましょう。
SSH接続するときに怒られます。
例) file_permission = "0400"
変数のpublic_key_file
およびprivate_key_file
は鍵の出力先を指定します。
variable "public_key_file" {}
variable "private_key_file" {}
variable "key_name" {}
resource "tls_private_key" "keygen" {
algorithm = "RSA"
rsa_bits = 4096
}
resource "local_file" "private_key_pem" {
filename = var.private_key_file
content = tls_private_key.keygen.private_key_pem
file_permission = "0400"
}
resource "local_file" "public_key_openssh" {
filename = var.public_key_file
content = tls_private_key.keygen.public_key_openssh
file_permission = "0600"
}
output "private_key_file" {
value = var.private_key_file
}
output "private_key_pem" {
value = tls_private_key.keygen.private_key_pem
}
output "public_key_file" {
value = var.public_key_file
}
output "public_key_openssh" {
value = tls_private_key.keygen.public_key_openssh
}
resource "aws_key_pair" "key_pair" {
key_name = var.key_name
public_key = tls_private_key.keygen.public_key_openssh
}
output "key_name" {
value = aws_key_pair.key_pair.key_name
}
コンポーネント側の処理です。
module "ec2_key" {
source = "../../modules/key_pair"
public_key_file = "./ec2-${terraform.workspace}.id_rsa.pub"
private_key_file = "./ec2-${terraform.workspace}.id_rsa"
key_name = "ec2-${terraform.workspace}"
}
インスタンス
先ほど作成したキーペアを指定しましょう。
なお、AMIはAmazon Linux2
にしています。当記事ではAMIのIDを直書きしていますが
要件に従って自由に選んでください。
resource "aws_instance" "ec2" {
ami = "ami-011facbea5ec0363b"
instance_type = "t3.nano"
availability_zone = "ap-northeast-1c"
ebs_optimized = false
vpc_security_group_ids = [data.terraform_remote_state.securitygroup.outputs.ec2_security_group_id]
key_name = module.ec2_key.key_name
subnet_id = data.terraform_remote_state.network.outputs.sample_vpc_public_subnet_0_id
associate_public_ip_address = true
tags = {
Name = "sample-${terraform.workspace}"
}
}
デプロイ
いよいよデプロイです!
デプロイコマンド実行時に、環境変数のファイルを指定するのを忘れないでください。
また、workspace
には気をつけましょう。
ステージングにデプロイするときはterraform workspace select staging
しましょう。
$ cd ./components/network/
$ terraform workspace select staging
$ terraform plan -var-file="../../environments/$(terraform workspace show)/terraform.tfvars"
$ terraform apply -var-file="../../environments/$(terraform workspace show)/terraform.tfvars"
network
→securitygroup
→db
→ec2
の順であれば滞りなくデプロイできるかと思います。
以上