我尝试使用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
    1. 环境/:存储各个环境(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时进行安全检查的工作流构建。通过这些自动化,我希望能进一步加强安全性并提高工作效率。

广告
将在 10 秒后关闭
bannerAds