我尝试使用Terraform批量管理AWS的安全组
我现在正在从事的服务基础设施改革真的非常具有挑战性…
当我开始重新审视安全组(SG)时,发现一个资源上可能会附加多个类似的安全组…所花费的时间比我想象的更长。
只要只是为了服务正常运行,原本的状态就没有问题,但是我们也需要考虑到安全性并进行管理。因此,我们决定与团队成员讨论并将基础设施进行编码化。
我个人本来不是基础设施工程师,但我决定借此机会尝试引入Terraform。
将基础设施即代码 (IaC) 的目标和其必要性用中文进行释义。
-
- 操作ミスによるリソース作成ミスの削減
-
- 今後のテスト環境構築における一貫性の確保
- バージョン管理による変更履歴の追跡と、問題発生時の迅速なロールバックの実現
选择Terraform和其优势的原因
-
- 他のツール(例: CloudFormation)と比較した際の、Terraformのコード(HCL)の高い可読性
-
- 手動で作成した既存インフラのコード化が容易(参考:https://zenn.dev/yuta28/articles/iac-existing-infrastructure)
- 将来的にGCPリソース(BigQuery)を導入する可能性への対応能力
引导步骤 bù
我将按顺序解释Terraform的安装、安全组的实施和应用等事项。
安装Terraform
在MacOS上,你可以从Homebrew进行安装。
$ brew install terraform
创建新项目
创建一个新的Terraform项目(例如:terraform-project)。
目录结构参考以下文章。
目录结构
目录结构如下。
-- terraform-project/
-- environments/
-- dev/
-- backend.tf
-- main.tf
-- stg/
-- backend.tf
-- main.tf
-- prod/
-- backend.tf
-- main.tf
-- modules/
-- <service-name>/
-- main.tf
-- variables.tf
-- outputs.tf
-- provider.tf
-- README.md
-- security_group/
-- main.tf
-- variables.tf
-- outputs.tf
-- provider.tf
-- README.md
-- ...other…
-- docs/
-- architecrture.drowio
-- architecrture.png
-
- 环境/:存储各个环境(dev, stg, prod)的配置。不同的环境可能具有不同的配置(例如在dev环境中使用小型EC2实例,在prod环境中使用大型实例)。
environments/{environment}/backend.tf:backend.tf包含了Terraform后端配置。它管理着Terraform状态文件(.tfstate)的保存位置。
environments/{environment}/main.tf:定义每个环境使用的资源和模块。主要进行模块的调用和配置的引用,而不是具体资源的配置。
modules/:存储Terraform模块。模块是可重用的Terraform代码块,可以使用模块来整理代码,而不是多次编写相同的代码。
modules/{service-name}/main.tf:对特定服务相关的Terraform资源进行配置。
modules/{service-name}/variables.tf:定义模块中使用的变量。
modules/{service-name}/outputs.tf:定义模块输出的值。
modules/{service-name}/provider.tf:存储提供者的配置。通常包括提供者的版本信息以及在整个项目中共享的提供者配置。
modules/{service-name}/README.md:描述模块以及使用方法的文档。
docs/:管理架构图的drowio和png格式文档。
创建安全组(SG)规则
作为SG创建的示例,考虑将Lambda部署在一个私有子网中,而将RDS Proxy部署在另一个私有子网中。
Lambda的SG
在这些连接中,Lambda具有RDS代理,VPC接口终端和通过公共子网的NAT网关访问互联网的出站流量。为了这些连接,首先我们将aws_security_group定义为lambda_sg,然后关联所需的aws_security_group_rule。
在AWS中,当我们在aws_security_group中附加SG资源时,需要指定vpc_id。这个VPC已经手动创建并且其配置由Git进行管理。因此,我们不应该硬编码vpc_id,而是应该从SSM参数存储中读取。
在这个Lambda项目中,由于不需要入站规则,我们只需创建出站规则(type = “egress”)。通过将每个aws_security_group_rule的security_group_id设置为aws_security_group.lambda_sg.id,可以指定将规则附加到哪个安全组。此外,如果已确定连接目标,则将source_security_group_id指定为连接目标安全组的ID。
以下是一个反映了这些设置的Terraform代码。
data "aws_ssm_parameter" "lambda_vpc_id" {
name = "/vpc/id/lambda"
}
resource "aws_security_group" "lambda_sg" {
name = "lambda-sg"
description = "Security group for Lambda"
vpc_id = data.aws_ssm_parameter.lambda_vpc_id.value
}
resource "aws_security_group_rule" "lambda_sg_egress_vpc" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
security_group_id = aws_security_group.lambda_sg.id
source_security_group_id = aws_security_group.vpc_endpoint_sg.id
}
resource "aws_security_group_rule" "lambda_sg_egress_rds_proxy" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
security_group_id = aws_security_group.lambda_sg.id
source_security_group_id = aws_security_group.rds_proxy_sg.id
}
resource "aws_security_group_rule" "lambda_sg_egress_https" {
type = "egress"
from_port = 443
to_port = 443
protocol = "tcp"
security_group_id = aws_security_group.lambda_sg.id
cidr_blocks = ["0.0.0.0/0"]
}
出于安全考虑,在实际项目中,应尽可能应用访问限制,并仅开放所需最小端口和协议。
RDS Proxy的安全组
RDS Proxy具有Lambda发出的入站流量和RDS的出站流量。
与前面提到的Lambda示例一样,首先我们将aws_security_group定义为rds_proxy_sg。然后,我们将关联所需的aws_security_group_rule。
这个RDS Proxy需要一个规则来允许Lambda的连接(入口)和RDS的连接(出口)。入口规则(type = “ingress”)允许Lambda的连接,出口规则(type = “egress”)允许连接到RDS。
将每个`aws_security_group_rule`的`security_group_id`设置为`aws_security_group.rds_proxy_sg.id`,并指定`source_security_group_id`为相应连接源SG的ID。
以下是应用这些设置的Terraform代码。
resource "aws_security_group" "rds_proxy_sg" {
name = "rds-proxy-sg"
description = "Security group for RDS Proxy"
vpc_id = data.aws_ssm_parameter.lambda_vpc_id.value
}
resource "aws_security_group_rule" "rds_proxy_sg_ingress" {
type = "ingress"
from_port = 3306
to_port = 3306
protocol = "tcp"
security_group_id = aws_security_group.rds_proxy_sg.id
source_security_group_id = aws_security_group.lambda_sg.id
}
resource "aws_security_group_rule" "rds_proxy_sg_egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
security_group_id = aws_security_group.rds_proxy_sg.id
source_security_group_id = aws_security_group.rds_sg.id
}
设置地区信息
请在这个文件中指定要创建的SG的区域。
provider "aws" {
region = "ap-northeast-1"
}
设定状态文件的保存路径
Terraform的状态文件(.tfstate)是保存基础架构当前状态的重要文件。这个文件的存储位置由./environments/{environment}/backend.tf来管理。通常建议将状态文件存储在远程存储中,并通过使用远程后端来实现团队成员之间的状态共享,以及从状态文件的丢失或损坏中进行恢复变得容易。
以下是一个使用S3作为后端存储的例子。在这个例子中,我们将状态文件存储在名为terraform-state-1234的S3存储桶中。S3存储桶的名称在全球AWS账户范围内必须是唯一的。另外,您需要提前创建好这个存储桶。
terraform {
backend "s3" {
bucket = "terraform-state-1234" // bucket名は全AWSアカウントで一意
key = "dev/terraform.tfstate"
region = "ap-northeast-1"
}
}
通过这个设置,当执行terraform apply时,状态文件会自动保存到指定的S3存储桶中。同样地,执行terraform init时,Terraform会从指定的后端获取状态文件。
在各种环境中使用的模块定义
在设置dev环境的安全组(SG)时,可以在./environments/dev/main.tf中加载创建的模块。
module "security_group" {
source = "../../modules/security_group"
}
读取变量(此次不需要)
这次我们从SSM参数存储中读取lambda_vpc_id, 但也可以从environments/{environment}/main.tf中将其作为变量导入。
首先,在variables.tf文件中定义变量。
variable "lambda_vpc_id" {
description = "The ID of the lambda-vpc where the security group will be created"
type = string
}
然后,在`./environments/{environment}/main.tf`文件中指定变量的值。
module "security_group" {
source = "../../modules/security_group"
lambda_vpc_id = "vpc-123456"
}
使用这种方式,可以将变量读取为var.lambda_vpc_id。
resource "aws_security_group" "lambda_sg" {
name = "lambda-sg"
description = "Security group for Lambda"
vpc_id = var.lambda_vpc_id
}
此次不需要输出值
通过输出在./modules/security_group/main.tf中创建的资源的ID等信息,可以从其他模块的资源中引用这些值。
这是输出的示例。
output "vpc_endpoint_sg_id" {
value = aws_security_group.vpc_endpoint_sg.id
description = "The ID of vpc-endpoint-sg"
}
确认和应用差异的方法
在Terraform中,您可以确认资源在变更前后的具体改变。在这个阶段,您可以在实际创建或修改资源之前,查看Terraform提出的计划。
在./environments/{environment}的目录中执行以下命令。
$ terraform plan
当执行此命令时,Terraform会比较当前状态和定义的设置,并显示应用的更改。这样可以防止意外的修改。
如果确认所做的更改没有问题,那么执行以下命令来应用这些更改。
$ terraform apply
Terraform通过此apply命令将实际资源应用于计划变更。此过程完全自动化,能大大减少手动操作错误。
通过提出并应用接近期望状态的变更,Terraform可以准确地了解当前基础设施的状态并简化其管理。
最后
在创建安全组的过程中,我发现了一些规则设置错误,但是由于使用了Terraform,修复这些错误非常容易。
作为下一步,我考虑引入GitHub Actions工作流,在创建PR时自动执行terraform plan。另外,我也计划在推送到Git时进行安全检查的工作流构建。通过这些自动化,我希望能进一步加强安全性并提高工作效率。