使用Terraform从零开始构建微服务基础设施(AWS)
首先
我是空中衣櫥的工程師Ain。這篇文章是2023年空中衣櫥降临日历的第17天的文章。
Terraform是什么?
Terraform是一种使用代码来创建和管理基础架构资源的基础设施即代码(IaC)工具。
在Terraform中,有几个基本概念需要了解。
-
- Terraform state
-
- Terraform resource
-
- Terraform variable
- Terraform data source
接下来,我会简要解释上面提到的概念。
Terraform 状态
这是一个利用Terraform来追踪元数据,并提供实际基础架构资源与当前terraform源代码之间引用的工具。
以下的图像与上述图像相同。
Terraform state不仅适用于单独操作,对团队协作时尤为强大。简单来说,基于Terraform state,团队成员可以理解以下内容:
-
- 自分のローカルのTerraformソースは実際のインフラリソースとどのように「異なる」のか。
- 自分のローカルのTerraformソースは実際のインフラリソースとどのように「CONFLICT」のか。
Terraform state以JSON格式保存为terraform.tfstate文件。然而,应该将其存储在云环境中并进行正确的加密。
土地整治资源
资源是Terraform的基本组件,每个资源块描述一个或多个基础设施对象,如虚拟网络(Virtual-network)、计算实例(Compute-instances)等。
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.app_name}-vpc"
}
}
在上述的例子中,有一个名为”main”的aws_vpc。它包含以下信息:
-
- cidr_block
-
- tags
-
- dns_hostnames
- …
每个资源都具有自己的输出(这被认为是每个资源的公共属性)。
当创建 aws_ec2 实例资源时,会有以下基本的公共属性:
-
- arn
- public_ip
请参考以下链接了解更详细的内容:https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance#attribute-reference
Terraform 变量 (Terraform
与其他编程语言一样,Terraform也具有自己的变量定义。
Terraform中有两种基本的变量类型:
-
- Input変数
- Local変数
输入变量的定义如下形式。
variable app_name {
type = string
}
使用变量关键词来确定变量的类型。
这些变量可以作为某个模块的”参数”来使用。
她正在写一封电子邮件。 (She is writing an email.)
他正在看电视。 (He is watching TV.)
我正在洗衣服。 (I am doing laundry.)
他正在做作业。 (He is doing homework.)
他正在吃午饭。 (He is having lunch.)
他正在练习钢琴。 (He is practicing piano.)
我正在等朋友。 (I am waiting for a friend.)
他正在散步。 (He is taking a walk.)
她正在打电话。 (She is making a phone call.)
他正在喝咖啡。 (He is drinking coffee.)
// Module A
variable app_name {
type = string
}
在上述代码中,定义了一个名为A的模块,该模块需要一个名为 “app_name” 的参数。如果另一个模块要使用这个模块A,必须按照以下方式执行。
module "A" {
source = "./module_A"
app_name = "Sample App"
}
本地变量的定义通常以以下形式为例:
locals {
test_variable = "test"
}
这些本地变量仅在定义它们的模块内部有效。
基础设施代码化的数据源
Terraform数据源是用来允许使用Terraform未定义的资源,例如,引用系统内现有的基础设施资源。
根据个人经验,Terraform数据源对于以前需要手动设置基础架构的系统来说非常方便。
使用Terraform数据源,可以让我们能够引用这些“legacy resources”。
请帮我翻译下面的句子为中文,只需提供一种选项:
Words have the power to both destroy and heal. When words are both true and kind, they can change our world.
data "aws_ami" "example" {
most_recent = true
owners = ["self"]
tags = {
Name = "app-server"
Tested = "true"
}
}
以上的代码将参考现有的aws_ami。
微服务系统的基础设施架构
这次我们将部署三个微服务(A、B、C)。每个微服务将按照基本架构进行执行。
网络架构(虚拟私有云、公共子网、私有子网)
在这里,我们将分析图表中的每个组成要素。对于每个服务,我们将创建单独的VPC。
VPC中必不可少的是public_subnet和private_subnet。而为了让private_subnet内的资源能够访问外部互联网,需要设置nat_gateway并将其部署在public_subnet内。
数据库(在这里是AWS RDS)被部署在private_subnet内,但只接受以下两个输入:
-
- VPCの内部。
- Proxy (外部のインターネットから来れば)。
AWS ECS 将部署在 private_subnet 内,用于执行服务的服务器代码。
应用程序架构(ECS、负载均衡器)
在这里,把Application Load Balancer放在ECS之前,然后在ECS内部放置nginx负载均衡器,作为反向代理来保护在ECS服务内执行的服务器代码的角色。
查阅代码
请参考以下链接以查看完整的源代码:
https://github.com/tuananhhedspibk/NewAnigram-Infrastructure
建立文件夾
以模块为单位进行资源管理。
-
- network: vpc, public_subnet, private_subnet
-
- ecs_api: ecs_task_definition, ecs_service
-
- ecs_cluster: cluster
-
- proxy: database proxy
- rds: database
第一层外部是以下的main.tf文件:
module "network" {
source = "./network"
// ...
}
module "proxy" {
source = "./proxy"
// ...
}
module "rds" {
source = "./rds"
// ...
}
module "ecs_cluster" {
source = "./ecs_cluster"
// ...
}
module "ecs_api" {
source = "./ecs_api"
// ...
}
网络模块
在这个模块中,定义了以下内容:
-
- vpc
-
- public_subnet
-
- private_subnet
- nat_gateway
vpc是什么
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.app_name}-vpc"
}
}
在定义 VPC 时,最重要的是指定 cidr_block。cidr_block 被视为 VPC 的网络地址(虚拟地址)。
public_subnet 如下所示:
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id // id của vpc resource
count = length(var.public_subnets_cidr) // public_subnetの数
cidr_block = element(var.public_subnets_cidr, count.index) // cidr_block設定 - サブネットのネットワークアドレスつまりサブネット分けること
availability_zone = element(var.availability_zones, count.index)
map_public_ip_on_launch = true
tags = {
Name = "${var.app_name}-public-subnet-${element(var.availability_zones, count.index)}"
}
}
公共子网(public_subnet)是一种可以从全球互联网中访问的子网。因此,通过将map_public_ip_on_launch属性设置为true,确保该子网具有公共IP地址,以便从全球互联网中可见。
私有子网
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id // id của vpc resource
count = length(var.private_subnets_cidr) // private_subnetの数
cidr_block = element(var.private_subnets_cidr, count.index) // cidr_block設定 - サブネットのネットワークアドレスつまりサブネット分けること
availability_zone = element(var.availability_zones, count.index)
map_public_ip_on_launch = false
tags = {
Name = "${var.app_name}-public-subnet-${element(var.availability_zones, count.index)}"
}
}
私有子网是私有的,也就是说它在外部互联网上是不可见的,并且将map_public_ip_on_launch属性设置为false的私有子网不应分配public_ip地址。
对于nat_gateway的情况,
// NATにアサインするElasticIp定義
resource "aws_eip" "nat" {
vpc = true
depends_on = [aws_internet_gateway.main]
}
// NAT gateway
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = element(aws_subnet.public.*.id, 0) // nat_gatewayをpublic_subnetに配置すること
tags = {
Name = "${var.app_name}-nat"
}
}
NAT网关允许位于私有子网中的实例连接到外部互联网,但禁止外部互联网连接到私有子网中的实例。
请在此处详细查阅:https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html。
上述的nat_gateway构建过程需要以下步骤
-
- nat_gatewayのElasticIp設定する
- private_subnet内のインスタンスが外部インターネット接続するためnat_gatewayをpublic_subnetに配置する
代理模块
这个代理只是一台EC2实例,并且设置代理的真正目的是什么。
-
- RDSはprivate_subnet内に配置されるからエンジニアがRDS接続するため
- RDSに接続することを制限すること
当看到图像4时,我们可以想象出工程师通过SSH隧道连接到代理服务器,然后从代理服务器访问RDS的情景。
正因为如此,代理服务器被用作保护RDS的屏障。
使用中文进行同义改写:
代理服务器的设置是
resource "aws_security_group" "proxy" {
name = "${var.app_name}-proxy-sg"
description = "Allow ssh connect to db proxy"
vpc_id = var.vpc_id
// Inbound rule
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
// Outbound rule
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.app_name}-proxy-sg"
}
}
resource "aws_instance" "proxy" {
instance_type = "t2.micro"
ami = local.ami
subnet_id = var.subnet_id // public_subnet id
security_groups = [aws_security_group.proxy.id]
key_name = "key_pair_name"
tags = {
Name = "${var.app_name}-db-proxy"
}
}
为了限制通过SSH访问代理的访问权限,像上面的代码一样,设置代理安全组的入站规则,只允许端口22通过。
只需一个选项语义释义:由于Proxy只充当作为RDS之前的“缓冲区域”的角色,所以只需要一个微型实例就足够了。当然了,为了让Proxy能够从外部互联网访问,我们需要将其放置在public_subnet上。
RDS模块
我定义了一个RDS实例和包含该实例的集群。主要目的是存储数据(换句话说,这是一个服务的数据库)。
resource "aws_rds_cluster" "this" {
cluster_identifier = "${var.app_name}-mysql-cluster"
engine = local.rds_engine
engine_version = "8.0.mysql_aurora.3.02.0" // エンジン定義: mysql または postgreSQL
db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.default.name
db_subnet_group_name = aws_db_subnet_group.aurora_subnet_group.name
vpc_security_group_ids = [aws_security_group.this.id]
port = var.port
database_name = var.database_name // DB名
master_username = var.master_username
master_password = var.master_password
skip_final_snapshot = false
final_snapshot_identifier = "${var.app_name}-mysql-final-snapshot"
}
resource "aws_rds_cluster_instance" "this" {
identifier = "${var.app_name}-mysql-identifier"
cluster_identifier = aws_rds_cluster.this.id
db_subnet_group_name = aws_db_subnet_group.aurora_subnet_group.name
db_parameter_group_name = aws_db_parameter_group.default.name
engine = local.rds_engine
instance_class = "db.t3.medium" // rdsインスタンスのキャパシティ
}
基本上来说,我认为设置RDS实例不是特别复杂的工作。如需详细了解,请参考 https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster_instance。
在实际运营中,请注意以下一点:请不要使用master_username和master_password。严格来说,并不是绝对不能使用,但相反,请考虑以下替代方法:
-
- エンジニアごとに特定アカウントと紐付ける権限を指定する
- サービスも前に指定したアカウントでDBに接続する
这个任务的目的是在系统崩溃或发生事故时,进行原因调查并明确谁执行了什么SQL语句。
ECS集群和ECS API模块
将这两个模块合并成一个的原因是它们都与ECS相关。将这两个子模块分割为群集和API的主要目的只是为了将ecs_cluster的定义与ecs_service分离。
对于ecs集群来说
resource "aws_ecs_cluster" "this" {
name = "${var.app_name}"
}
The ECS API is
ECS API 是
resource "aws_ecs_service" "this" {
depends_on = [aws_lb_listener_rule.this]
name = var.app_name
desired_count = 1
launch_type = "FARGATE"
// fargate使用する目的はAWSが各サーバインスタンスを管理してくれる。
cluster = var.cluster_name
task_definition = aws_ecs_task_definition.this.arn // task_defintionに参照する
network_configuration {
subnets = var.subnet_ids
security_groups = [aws_security_group.this.id]
}
load_balancer {
container_name = "nginx"
container_port = local.port_nginx
target_group_arn = var.lb_target_group_arn
}
}
为了追踪,重新展示了ECS服务架构的一部分。
load_balancer {
container_name = "nginx"
container_port = local.port_nginx
target_group_arn = var.lb_target_group_arn
}
上述的代码创建了一个nginx反向代理。在这种情况下,所有对服务的请求都会在进入服务之前经过nginx。
请参考下图了解关于ECS的概要。
第6张图展示了ECS的操作方法。其中包括:
-
- ECRはサービスに対応するdockerイメージを保存する
- task_definitionは、サービスのブループリント(JSONファイルでパラメータやコンテナなどが定義されている)です。
基本上,如果您在这里使用ECS运营服务,需要按照以下步骤进行操作:
-
- 将服务容器化并将其转换为Docker镜像。
-
- 将此Docker镜像上传至注册表(这里使用ECR)。
-
- 从注册表构建任务定义。
- 从任务定义构建在ECS中运行的服务。
任务定义的设置是什么?
resource "aws_ecs_task_definition" "this" {
family = "newanigram-api"
container_definitions = data.template_file.task_definition.rendered // task_definition定義JSONファイルに参照する
cpu = "256"
memory = "512"
network_mode = "awsvpc"
task_role_arn = aws_iam_role.ecs_iam_role.arn
execution_role_arn = aws_iam_role.ecs_iam_role.arn
}
// task_definition定義JSONファイル
data "template_file" "task_definition" {
template = file("./ecs_api/task_definition.json")
vars = {
account_id = local.account_id
region = local.region
repository_api = "api"
}
}
任务定义的核心内容是什么?
[
{
"name": "api",
"image": "${account_id}.dkr.ecr.${region}.amazonaws.com/${repository_api}:${api_tag}",
"cpu": 0,
"memory": 128,
"portMappings": [
"containerPort": ${port_api},
"hostPort": ${port_api}
}
],
}
]
执行
要执行Terraform,可以使用terraform apply命令。在执行之前,可以预览即将创建、更改或删除的资源。
在AWS控制台上执行后,您将得到以下类似的结果。
虚拟专用网络
RDS是一种可扩展的关系数据库服务,可帮助您轻松管理和扩展数据库。
所有的配置信息都准确地反映在AWS控制台中(可以查看红框部分的详细信息)。
总结
那么,我将简要介绍如何使用Terraform在AWS上从零开始构建微服务的基础设施。
期待读者能够通过基本的微服务基础设施架构及使用terraform部署基础设施的方式,获得更直观的理解。
我非常期待下一篇文章。非常感谢。
另外,由于我们在进行工程师招聘活动,如果您对此有兴趣,请务必查看我们的空气衣柜!