使用Terraform(0.12)从VPC创建EC2和RDS的示例

利用这篇文章创作的东西

    • VPC

 

    • RDS(Aurora)

 

    • EC2(踏み台用途)

 

    EC2キーペア

预计将RDS部署在VPC的私有子网中,因此无法从本地访问。
如果在RDS的配置中将公共可访问性打开,则可以进行访问,但出于安全考虑,最好部署一台跳板服务器(EC2)。
此外,我认为构建的EC2也可以替代常规的应用服务器。

Terraform版本为0.12.19。

前提条件 tí

    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

这次,我们准备了以下四个组件。

    • db

 

    • ec2

 

    • network

 

    securitygroup

AWS部署是按照这些单位进行的。
部署顺序为network→securitygroup→db→ec2。

同样,关于模块,我准备了以下三个选项。

    • iam_role

 

    • key_pair

 

    securitygroup

这些已经被模块化,以便每次创建时可以重复使用定义。

而且,我们将环境分为测试环境和生产环境,并通过terraform工作区来进行环境划分。

事前准备

初始化

首先,请从awscli中输入您自己的AWS认证信息。

$ 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]: 

在Terraform的定义中使用的名称为Profile名。应避免使用默认的名称。

创建S3存储桶

事先手动创建S3存储桶。
此存储桶是用于保存Terraform状态的重要存储桶。
Terraform将根据存储在该存储桶中的信息来构建AWS资源。

可以在控制台界面上完成,也可以使用命令行界面完成。

aws s3 --profile { 自身で決めたProfile名 } mb s3://{ 自身で決めたバケット名 }

创建工作空间

请前往每个组件并执行terraform init和terraform workspace new {环境名称}。

$ cd ./components/db/
$ terraform init
$ terraform workspace new staging
$ terraform workspace new production
$ terraform workspace select staging

适用于所有四个组件。

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 中将具有依赖关系的组件定义为数据。

在EC2上需要创建VPC和安全组,因此需要列出网络和安全组组件。

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
}

创建安全组

安全组

尝试将安全组创建模块化。

模块

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(端口=22)。让我们将其设置为只允许从自己的IP地址SSH访问。

组件

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
}

请注意从network组件中拉取资源信息的vpc_id。
条件是它在先前创建的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
}

当模块创建好后,让我们从组件中试着调用一下。
AWS官方已经存在名为AmazonRDSEnhancedMonitoringRole的策略。
让我们应用它。
注意,每个服务都有一个固定的标识符,请注意。
本次的标识符是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"
  }
}

数据库参数组

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,
  ]
}

奥罗拉安全集团

也许本来应该用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]
}

极光集群

在集群中,需要注意的是主密码。
根据本文的方法,首先设置一个随意的密码,然后在控制台界面上手动更改。
请注意生命周期栏。从下次更改开始,将忽略主密码的更改设置。

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スペック
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}"
  }
}

部署

终于要部署了!在执行部署命令时,请不要忘记指定环境变量文件。此外,请注意工作空间。当要部署到暂存环境时,请选择terraform工作空间为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"

如果以网络→安全组→数据库→EC2的顺序进行部署,我认为应该可以顺利完成。

以上 – In Chinese, this term typically means “above” or “the above.” However, without additional context, it is challenging to provide a more accurate paraphrase.

bannerAds