虚拟专用云(Virtual Private Cloud ,VPC)在云计算技术中扮演了重要角色。VPC能让两个云实例之间进行私有通信,将网络流量与其他互联网用户隔离开,从而提高安全性。换句话说,作为一种虚拟网络环境,VPC能在公有云基础设施中创建一个隔离且私密的网络空间,为用户提供一个安全、灵活、可定制的网络环境,帮助他们构建和管理各种类型的应用和服务。
也就是说,就像是“套娃”一样,我们可以借助这种技术,在公共互联网的基础上构建一个规模更小,更私密的专用“Internet”来。想想是不是还挺激动?
请收下这份教程,祝你玩得愉快!
背景
下文我们将通过一个仪表板为例进行介绍。为此我们将部署两个应用程序,每个应用程序由一个应用服务器和一个数据库服务器组成(共四个服务器)。第一个应用程序和对应的数据库服务器采用正常方式来部署,第二组则配置为在VPC中运行。
每个应用的前端都使用Qwik构建,并使用Tailwind进行样式设计。服务器端由Qwik City(Qwik的官方元框架)提供支持,并在Node.js上运行,托管在共享的Linode VPS上。这些应用还使用PM2进行进程管理,并使用Caddy作为反向代理和SSL供应商。数据存储在PostgreSQL数据库中,该数据库也在共享的Linode VPS上运行。应用程序使用Drizzle与数据库进行交互,Drizzle是JavaScript的对象关系映射器(ORM)。
两个应用程序的整个基础架构都由Terraform管理,具体来说,使用了Terraform的Linode提供程序进行管理。初次接触Linode的童鞋对此可能感觉有些陌生,但只要学会了操作方法,就能快速轻松地进行基础架构的配置和销毁。
如果感兴趣,大家也可以在这里找到下文涉及的所有代码:http://github.com/AustinGil/linode-vpc-demo
Demo
正如上文所述,本例将部署两个相同的应用程序。这方面并没有什么特别需要注意的,相应的内容如下图所示。
整个技术栈没有什么特别之处。之所以选择这些工具也没什么特别的原因,大家完全可以结合自己的需求和习惯选择自己觉得最适合的工具。
真正有趣的部分在于基础架构。
首先看看一号应用程序,它基本上由两个托管在Akamai云中的服务器组成,一个用于应用程序,另一个用于数据库。当用户加载应用程序时,应用程序服务器将从数据库中提取数据,构建HTML,并将结果返回给用户。
这里的问题在于数据库连接的配置。某些情况下,我们可能会部署一个数据库服务器,但并不能事先知道需要允许哪些IP地址的设备(如应用服务器)访问。此时很多人惯用的做法是简单粗暴地直接允许具备正确凭据的任何计算机连接。但这会带来安全隐患,因为攻击者有可能借此连接到数据库并窃取敏感数据。
虽然攻击者仍然需要知道数据库主机、端口、用户名和密码才能获取访问权限,因此看起来这个隐患其实也并不是很大。毕竟刚才也说了,这种做法在业内其实很常见。但其实还有更好的做法!
如果已经知道每台需要访问的计算机的IP地址,那么一种比较好的解决方案是设置防火墙或VLAN。但如果基础架构更加动态,服务器会频繁启动和关闭,IP地址列表的维护工作可能会显得非常麻烦。这就到了VPC的用武之地。我们可以将服务器配置到VPC中,并允只允许位于同一个VPC网络中的计算机相互自由通信。
这就是二号应用程序的设置方式。用户可以连接到应用服务器,该服务器位于VPC中,但允许处理来自公共互联网的流量。该应用服务器可以连接到数据库,数据库也位于同一个VPC中,并且只允许来自同一网络的访问。然后,应用服务器就可以照常从数据库获取数据,构建HTML,并将页面返回给用户。
对于普通用户,两个应用的访问体验没有任何差异。浏览器可以很好地加载任何一个应用的界面。对普通用户来说,VPC使用与否都不会对自己产生一丝一毫的影响。
然而对于攻击者来说,体验就截然不同了。即便攻击者设法获取了数据库访问凭据,他们也无法连接,因为VPC的网络是被隔离的。此时,VPC充当了虚拟防火墙的角色,确保只有位于同一个网络中的设备才能访问数据库。(这个概念有时也被称为“分割[segmentation]”)
Talk is cheap,show you the证据
通过加入emoji图标的示意图来展示技术概念,这固然是一种可爱的做法,但为了更有说服力,最好还是能给出实证。因此我们将尝试着使用数据库客户端DBeaver直接连接两个数据库,然后看看会发生什么。
对于一号数据库,我们设置了一个Postgres连接:使用从Akamai仪表板获取的主机IP地址以及在Terraform脚本中设置的端口、用户名和密码。发现连接可以按预期工作。
对于二号数据库,我们只需要更改IP地址,因为所有数据库配置都是由相同Terraform脚本处理的,唯一的区别是:二号数据库服务器被放在与二号应用服务器相同的VPC中,并且配置为只允许来自同一网络内的计算机访问。
毫不意外,当我们尝试连接时,尽管提供了所有正确信息,但依然收到了一个错误:
错误信息中完全没有提到任何关于VPC的内容。它只是说我们的IP地址不在配置文件的允许访问列表中。这是合理的。如果需要,我们可以明确地添加自己的IP地址并获得对数据库的访问权限,但这不是重点。
这里的重点是:我们并没有明确将任何IP地址添加到Postgres的允许列表中。然而应用服务器能够正常连接,但其他所有人都被VPC阻止了。
接下来,上代码
为了便于大家自行实验和练习,我们提供了部署上述应用程序的Terraform代码。相关文件请访问:http://github.com/AustinGil/linode-vpc-demo/blob/main/terraform/terraform.tf
需要注意的是,我们已经尽最大努力让这个Terraform文件对其他人可重用。这需要更多的变量和基于tfvars文件的配置设置:http://github.com/AustinGil/linode-vpc-demo/blob/main/terraform/terraform.tfvars.example。
接下来一起看看其中的关键部分。
1.配置Terraform提供程序
首先,由于使用了Linode Terraform提供程序,因此有必要了解这是如何设置的:
terraform {
required_providers {
linode = {
source = "linode/linode"
version = "2.13.0"
}
}
}
variable "LINODE_TOKEN" {}
provider "linode" {
token = var.LINODE_TOKEN
}
这部分设置了提供程序以及Terraform要求我们提供的变量,或者大家也可以使用tfvars文件提供。
2.设置VPC和VPC子网
接下来需要设置实际的VPC资源以及子网资源。
resource "linode_vpc" "vpc" {
label = "${local.app_name}-vpc"
region = var.REGION
}
resource "linode_vpc_subnet" "vpc_subnet" {
vpc_id = linode_vpc.vpc.id
label = "${local.app_name}-vpc-subnet"
ipv4 = "${var.VPC_SUBNET_IP}"
}
服务器只能添加到同一地区的VPC中。在撰写本文时,Akamai云平台支持VPC的地区有13个。最新信息请参阅Akamai官网。
有三个IPv4地址范围是专门保留可用于私有网络(如VPC)的:
- 10.0.0.0 – 10.255.255.255
- 172.16.0.0 – 172.31.255.255
- 192.168.0.0 – 192.168.255.255
因此在配置子网时,我们必须从这三个选项中选择一个,并且必须使用CIDR格式来指定想使用的IP范围。这里我们使用了10.0.0.0/24。
私有网络中的每台服务器都将拥有该范围内的一个IPv4地址。
3. 设置应用服务器
为了让Terraform部署我们的应用服务器,我们使用了linode_instance资源,此外还使用了stackscript资源来创建一个可重复使用的部署脚本,用于安装和配置软件。这就像一个位于Akamai云仪表板中的Bash脚本,我们可以直接将其用于新的服务器。
相关代码就不放在这里了,但需要通过NVM安装Node.js 20,安装PM2,克隆项目代码库,运行应用,并设置Caddy。StackScript的内容请自行参阅源代码,下文想简单说说Terraform的部分。
resource "linode_instance" "application1" {
depends_on = [
linode_instance.database1
]
image = "linode/ubuntu20.04"
type = "g6-nanode-1"
label = "${local.app_name}-application1"
group = "${local.app_name}-group"
region = var.REGION
authorized_keys = [ linode_sshkey.ssh_key.ssh_key ]
stackscript_id = linode_stackscript.configure_app_server.id
stackscript_data = {
"GIT_REPO" = var.GIT_REPO,
"START_COMMAND" = var.START_COMMAND,
"DOMAIN" = var.DOMAIN1,
"NODE_PORT" = var.NODE_PORT,
"DB_HOST" = linode_instance.database1.ip_address,
"DB_PORT" = var.DB_PORT,
"DB_NAME" = var.DB_NAME,
"DB_USER" = var.DB_USER,
"DB_PASS" = var.DB_PASS,
}
}
resource "linode_instance" "application2" {
depends_on = [
linode_instance.database2
]
image = "linode/ubuntu20.04"
type = "g6-nanode-1"
label = "${local.app_name}-application2"
group = "${local.app_name}-group"
region = var.REGION
authorized_keys = [ linode_sshkey.ssh_key.ssh_key ]
stackscript_id = linode_stackscript.configure_app_server.id
stackscript_data = {
"GIT_REPO" = var.GIT_REPO,
"START_COMMAND" = var.START_COMMAND,
"DOMAIN" = var.DOMAIN2,
"NODE_PORT" = var.NODE_PORT,
"DB_HOST" = var.DB_PRIVATE_IP,
"DB_PORT" = var.DB_PORT,
"DB_NAME" = var.DB_NAME,
"DB_USER" = var.DB_USER,
"DB_PASS" = var.DB_PASS,
}
interface {
purpose = "public"
}
interface {
purpose = "vpc"
subnet_id = linode_vpc_subnet.vpc_subnet.id
}
}
这两个资源的配置过程几乎相同,只有一些重要的事情需要注意:
- 二号应用程序包含了将应用添加到VPC的相关配置。
- StackScript 需要数据库的IP地址。一号应用程序使用一号数据库的公共IP地址(linode_instance.database1.ip_address),二号应用程序则使用了一个变量(var.DB_PRIVATE_IP)。这个变量稍后会用到,它是分配给VPC中运行的二号数据库的私有IP地址。该地址也可以手动分配,所以我们将其设置为10.0.0.3。
另外还要注意,应用服务器要部署到与VPC相同的地区,原因如上文所述。
4. 设置数据库服务器
数据库也使用linode_instance和linode_stackscript资源进行设置。同样,StackScript的内容就直接略过了,感兴趣的同学请直接参阅代码。接下来我们需要安装Postgres,设置数据库和凭据,并提供一些配置选项。
resource "linode_instance" "database1" {
image = "linode/ubuntu20.04"
type = "g6-nanode-1"
label = "${local.app_name}-db1"
group = "${local.app_name}-group"
region = var.REGION
authorized_keys = [ linode_sshkey.ssh_key.ssh_key ]
stackscript_id = linode_stackscript.configure_db_server.id
stackscript_data = {
"DB_NAME" = var.DB_NAME,
"DB_USER" = var.DB_USER,
"DB_PASS" = var.DB_PASS,
"PG_HBA_ENTRY" = "host all all all md5"
}
}
resource "linode_instance" "database2" {
image = "linode/ubuntu20.04"
type = "g6-nanode-1"
label = "${local.app_name}-db2"
group = "${local.app_name}-group"
region = var.REGION
authorized_keys = [ linode_sshkey.ssh_key.ssh_key ]
stackscript_id = linode_stackscript.configure_db_server.id
stackscript_data = {
"DB_NAME" = var.DB_NAME,
"DB_USER" = var.DB_USER,
"DB_PASS" = var.DB_PASS,
"PG_HBA_ENTRY" = "host all all samenet md5"
}
interface {
purpose = "public"
}
interface {
purpose = "vpc"
subnet_id = linode_vpc_subnet.vpc_subnet.id
ipv4 {
vpc = var.DB_PRIVATE_IP
}
}
}
与应用程序服务器一样,这两个数据库服务器大致相同,但有一些关键区别:
- 第二个数据库包含将自己添加到VPC的配置。
- 客户端身份验证文件(pg_hba.conf)中会被写入不同的设置。一号数据库允许所有的互联网连接("host all all all md5"),而二号数据库只允许来自相同网络的访问("host all all samenet md5")。
另外需要注意,在配置VPC设置时,我们明确分配了服务器的私有IP地址(var.DB_PRIVATE_IP)。这与应用程序服务器所获得的静态值相同,因此可以从VPC内部连接到数据库。
总结
希望本文能让大家了解到VPC是什么,有什么用,以及何时应该考虑使用。这就像拥有了自己的小型私有互联网。严格意义上来说,VPC并不是为了替代VLAN或防火墙,但它是现有安全实践很好的补充。