LoginSignup
8
5

Terraform(0.12)でVPCからEC2とRDSを作る一例

Last updated at Posted at 2020-01-26

この記事で作るもの

  • 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へのデプロイは、これらの単位でおこなわれます。
デプロイの順番はnetworksecuritygroupdbec2とします。

またモジュールについては、以下の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 initterraform 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

それぞれ、以下のように定義しましょう。

provider.tf
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"ということになります。

backend.tf
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とセキュリティグループを作成する必要があるのでnetworksecuritygroupのコンポーネントを記載します。

components/ec2/backend.tf
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

components/network/main.tf
resource "aws_vpc" "default" {
  cidr_block = "10.1.0.0/16"
  enable_dns_support = true
  enable_dns_hostnames = true

  tags = {
    Name = "sample-${terraform.workspace}"
  }
}

パブリックルートテーブル

components/network/main.tf
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.default.id

  tags = {
    Name = "sample-public-route-table-${terraform.workspace}"
  }
}

プライベートルートテーブル

components/network/main.tf
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}"
  }
}

インターネットゲートウェイ

components/network/main.tf
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"
}

パブリックサブネット

components/network/main.tf
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
}

プライベートサブネット

components/network/main.tf
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に記載しておきます

components/network/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

modules/securitygroup/variables.tf
variable "name" {}
variable "vpc_id" {}
variable "port" {}

variable "cider_blocks" {
  type = list(string)
}
modules/securitygroup/main.tf
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
}
modules/securitygroup/outputs.tf
output "security_group_id" {
  value = aws_security_group.default.id
}

コンポーネントからは以下のようにしてモジュールを呼び出します。
今回設定するインバウンドはSSH(Port=22)です。自身のIPからのみSSHできるように設定しましょう。

components

components/securitygroup/main.tf
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.tfterraform_remote_stateとして記載していることが条件です。

RDS(Aurora)の作成

RDSでは以下の項目を作成します。

  • Auroraモニタリング用IAMロール
  • クラスターパラメーターグループ
  • DBパラメーターグループ
  • DBサブネットグループ
  • Auroraセキュリティグループ
  • Auroraクラスター
  • Auroraインスタンス

Auroraモニタリング用IAMロール

IAMロールの作成もモジュール化してみます。

modules/iam_role/variables.tf
variable "name" {}
variable "policy" {}
variable "identifier" {}
modules/iam_role/main.tf
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
}
modules/iam_role/outputs.tf
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となります。

components/db/main.tf
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に変更し、タイムゾーンを東京にしてみます。
その他、要件に従って適切なパラメータを設定しましょう。

components/db/main.tf
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パラメーターグループ

components/db/main.tf
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.tfterraform_remote_stateとして記載していることが条件です。

components/db/main.tf
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インスタンスがあればアクセスができるという想定です。

components/db/main.tf
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欄に注目してください。次回の変更からはマスターパスワードの変更は無視するような設定になっています。

components/db/main.tf
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つで中〜大スペック
といった具合です。
これらの変数は以下に定義しておきます。

envronments/staging/terraform.tfvars
# Auroraスペック
instance_class = "db.t3.small"

# Auroraインスタンス数
cluster_instance_count = 1
envronments/production/terraform.tfvars
# Auroraスペック
instance_class = "db.r5.large"

# Auroraインスタンス数
cluster_instance_count = 2
components/db/main.tf
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は鍵の出力先を指定します。

modules/key_pair/variables.tf
variable "public_key_file" {}
variable "private_key_file" {}
variable "key_name" {}
modules/key_pair/main.tf
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"
}
modules/key_pair/output.tf
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
}

コンポーネント側の処理です。

components/ec2/main.tf
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を直書きしていますが
要件に従って自由に選んでください。

components/ec2/main.tf
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"

networksecuritygroupdbec2の順であれば滞りなくデプロイできるかと思います。

以上

8
5
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
8
5