个人发展(基础设施)

首先

这次,我使用以下技术栈进行个人开发,所以我想将其留作备忘录。

    • Go(API)

 

    • Next.js・TypeScript(フロント)

 

    • AWS・Terraform(インフラ)

 

    github actions(CI/CD)

本文将简要提及使用AWS进行基础设施建设的相关工作。有关后端和前端的内容,请参阅以下部分。

后端方面

 

前台方面 (Front desk area)

 

Github代码库

应用程序侧

 

基础设施方面

 

系统概要图

image.png
    • s3

 

    • dynamodb

 

    • ECS

 

    • ECR

 

    • Fargate

 

    • VPC

 

    • Systems Manager

 

    • Lambda

 

    • API Gateway

 

    • RDS

 

    Load Balancer

其他使用的技术 de

Terraform是一种基础设施即代码(IaC)工具。

Terraform 是 HashiCorp 开发的一种开源的 “基础设施即代码” 工具之一。通过使用 Terraform,可以使用代码定义云资源和本地资源,并根据这些定义创建、更新和删除资源。

现在,我们正在用 Terraform(HCL)来描述和管理在 AWS 上构建的所有资源。

目前在市场上流行的 IaC 工具除了 Terraform 外,还有 “AWS CDK”、”Cloud Formation” 和 “Pulumi”。

它们各自具有以下特点:

    • マルチクラウド

Terraform
Pulumi

多言語対応

AWS CDK
Pulumi

有多种选择,每种选择都有各自的优缺点,但考虑到当前的普及率和多云适应能力,我们选择采用Terraform。

 

Github Actions – GitHub行动

GitHub Actions是一种工具,用于自动化GitHub库中的持续集成/持续部署(CI/CD)工作流程。它可以通过触发GitHub上特定的事件,如代码提交或拉取请求的创建,自动执行构建、测试和部署等任务。

 

Github操作和AWS集成

为了让GitHub Actions可以访问AWS资源,需要进行身份验证。因此,本次将使用OIDC进行AWS认证。

OIDC(OpenID Connect)是建立在OAuth 2.0协议之上的认证协议,它提供了ID令牌的形式,用于安全共享用户的认证信息。

OIDC认证的好处。

在解释OIDC的优点之前,如果想要在Github Actions中添加AWS身份验证而不使用OIDC,可以考虑以下方法。

    1. 创建IAM用户

 

    1. 生成认证信息(Access Key ID、Secret Access Key)

 

    将认证信息设置到GitHub Actions的Secrets中

只需按照上述的方式,两个服务很容易实现协作合作。

然而,基于安全风险和管理成本增加的考虑,这种方法并不被推荐。

另一方面,使用OIDC可以在不存放永久认证信息的情况下,仅通过发放符合OIDC规范的临时令牌来操作AWS资源。因此,不再需要在github上直接管理和运营AWS认证信息。

基于这些原因,我们决定采用OIDC认证。

以下是這次使用的OIDC認證步驟:

    1. 在AWS的管理控制台中创建ID提供者

 

    1. 创建用于Github Actions的IAM角色

 

    编辑策略,仅对特定存储库的特定分支进行认证。

tfstate文件的动态管理

在使用Terraform时,tfstate文件是表示Terraform基础设施当前状态的重要文件。如果有多个开发人员或CI/CD工具同时执行Terraform,则需要适当的管理来避免tfstate的一致性和竞争问题。
作为tfstate文件的管理方法,有一些选项,例如在本地保留或使用git进行管理,但由于这些方法都很难保证一致性并且存在较高的安全风险,因此不被推荐使用。

因此,本次将使用 AWS 的服务(如 s3 和 DynamoDB)来管理 tfstate 文件的方法。

使用S3的原因是为了管理tfstate文件,但是为了保持文件的一致性,需要使用DynamoDB。通过在S3中管理tfstate文件,可以让多个人处理tfstate文件。然而,这也带来了状态冲突的风险。
因此,Terraform提供了使用DynamoDB(对于S3)来保持tfstate文件一致性的功能。

这种方法在官方处也被推荐使用。

 

terraform {
  backend "s3" {
    bucket         = "recruit-info-service-tfstate"
    key            = "terraform.tfstate"
    region         = "ap-northeast-1"
    dynamodb_table = "recruit-info-service-tfstate-locking"
    encrypt        = true
  }
}

通过使用后端”s3″ ,Terraform可以在s3中管理tfstate文件。

使用Terraform构建AWS资源

我们将从这里开始介绍使用Terraform创建基础设施环境的实际资源。

网络建设

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"

  name = "aws-ecs-terraform"
  cidr = "10.0.0.0/16"

  azs             = ["${local.region}a", "${local.region}c"]
  public_subnets  = ["10.0.11.0/24", "10.0.12.0/24"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]

  public_subnet_names  = ["Public Subnet 1a", "Public Subnet 1c"]
  private_subnet_names = ["Private Subnet 1a", "Private Subnet 1c"]

  enable_dns_hostnames = true
  enable_dns_support   = true

  enable_nat_gateway = true
  single_nat_gateway = false
}

# RDSをaのAZに配置
resource "aws_subnet" "rds_subnet_a" {
  vpc_id                  = module.vpc.vpc_id
  cidr_block              = "10.0.5.0/24"
  availability_zone       = "${local.region}a"
  map_public_ip_on_launch = false

  tags = {
    Name                  = "${local.app} RDS Private Subnet 1a"
    "MapPublicIpOnLaunch" = "false"
    "Type"                = "rds"
  }
}

# RDSをcのAZに配置
resource "aws_subnet" "rds_subnet_c" {
  vpc_id                  = module.vpc.vpc_id
  cidr_block              = "10.0.6.0/24"
  availability_zone       = "${local.region}c"
  map_public_ip_on_launch = false

  tags = {
    Name                  = "${local.app} RDS Private Subnet 1c"
    "MapPublicIpOnLaunch" = "false"
    "Type"                = "rds"
  }
}

resource "aws_db_subnet_group" "my_db_subnet_group" {
  name       = "${local.app}-db-subnet-group"
  subnet_ids = [aws_subnet.rds_subnet_a.id, aws_subnet.rds_subnet_c.id]

  tags = {
    Name = "${local.app}-db-subnet-group"
  }
}

我們接下來將詳細解釋下面所列出的設定。

source = "terraform-aws-modules/vpc/aws"

通过使用Terraform的VPC模块来实现。

enable_dns_hostnames = true

在上述描述中,AWS的DNS服务器启用了名称解析。

enable_dns_support   = true

根据上述描述,VPC 内的资源将自动分配公共 DNS 主机名。

enable_nat_gateway = true
single_nat_gateway = false

根据以上描述,将在指定的每个可用区创建NAT网关。
与互联网网关不同,使用NAT网关时需要明确说明。

resource "aws_subnet" "rds_subnet_a" {
  vpc_id                  = module.vpc.vpc_id
  cidr_block              = "10.0.5.0/24"
  availability_zone       = "${local.region}a"
  map_public_ip_on_launch = false

  tags = {
    Name                  = "${local.app} RDS Private Subnet 1a"
    "MapPublicIpOnLaunch" = "false"
    "Type"                = "rds"
  }
}

通过上述方式,明确指定了设置RDS子网信息的操作。
将map_public_ip_on_launch设置为false时,默认情况下不会分配公有IP地址。
由于RDS不会直接公开在互联网上,所以将其设置为false。

resource "aws_db_subnet_group" "my_db_subnet_group" {
  name       = "${local.app}-db-subnet-group"
  subnet_ids = [aws_subnet.rds_subnet_a.id, aws_subnet.rds_subnet_c.id]

  tags = {
    Name = "${local.app}-db-subnet-group"
  }
}

上述是用于定义Terraform资源的RDS子网组。
为了将RDS实例放置在VPC的特定子网中,需要使用子网组来指定数据库将被放置在哪个子网中。在VPC中创建RDS实例时,必须使用子网组,否则无法实现。

以上,完成了为部署Fargate、ALB、RDS和Lambda(用于MySQL的AutoMigrate)所需的网络设置。

安全组

安全组是指控制在AWS虚拟私有云(VPC)内资源的网络流量的虚拟防火墙。

resource "aws_security_group" "alb" {
  name        = "${local.app}-alb"
  description = "For ALB."
  vpc_id      = module.vpc.vpc_id
  ingress {
    description = "Allow HTTP from ALL."
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    description = "Allow all to outbound."
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = {
    Name = "${local.app}-alb"
  }
}

resource "aws_security_group" "ecs" {
  name        = "${local.app}-ecs"
  description = "For ECS."
  vpc_id      = module.vpc.vpc_id
  egress {
    description = "Allow all to outbound."
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = {
    Name = "${local.app}-ecs"
  }
}

resource "aws_security_group_rule" "ecs_from_alb" {
  description              = "Allow 8080 from Security Group for ALB."
  type                     = "ingress"
  from_port                = 8080
  to_port                  = 8080
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.alb.id
  security_group_id        = aws_security_group.ecs.id
}

resource "aws_security_group" "rds" {
  name        = "${local.app}-rds"
  description = "For RDS."
  vpc_id      = module.vpc.vpc_id

  ingress {
    description     = "Allow MySQL from ECS security group"
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [aws_security_group.ecs.id]
  }

  egress {
    description = "Allow all outbound traffic"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${local.app}-rds"
  }
}

# For Lambda
resource "aws_security_group" "lambda_sg" {
  name        = "${local.app}-lambda"
  description = "For ${local.app} Lambda"
  vpc_id      = module.vpc.vpc_id

  egress {
    description = "Allow all to outbound."
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${local.app}-lambda"
  }
}

本次设定的安全组大致分为四个。

    1. ALB使用

 

    1. ECS使用

 

    1. RDS使用

 

    Lambda使用
aws_security_group "alb"

目标: 定义用于ALB的安全组
入口规则: 允许来自所有IP地址(0.0.0.0/0)的HTTP流量(端口80)
出口规则: 允许所有对外流量

aws_security_group "ecs"

目标:定义ECS使用的安全组
出口:允许所有对外流量

aws_security_group_rule "ecs_from_alb"

目的:为了允许从ALB到ECS的流量,定义一个安全组规则。
类型:用于指示流量方向。在这种情况下,是”ingress”,表示接收流量。
from_port和to_port:允许端口8080的流量。
协议:允许TCP协议的流量。
source_security_group_id:指定ALB安全组作为此规则的源。
security_group_id:指定适用于ECS安全组的此规则。

aws_security_group "rds"

目的:定义用于RDS的安全组。
入站:允许来自ECS安全组的MySQL(端口3306)流量。
出站:允许所有对外流量。

aws_security_group "lambda_sg"

目的:定义用于AWS Lambda的安全组
出站:允许所有对外流量。

负载均衡器

负载均衡器是一种网络设备,可将输入流量分散给多个目标(如EC2服务器和容器),从而提高系统的可用性和冗余性。

resource "aws_lb" "alb" {
  name               = "${local.app}-alb"
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = module.vpc.public_subnets
}

resource "aws_lb_listener" "alb_listener" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type = "fixed-response"

    fixed_response {
      content_type = "text/plain"
      message_body = "Fixed response content"
      status_code  = "200"
    }
  }
}

resource "aws_lb_listener_rule" "alb_listener_rule" {
  listener_arn = aws_lb_listener.alb_listener.arn

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.target_group.arn
  }

  condition {
    path_pattern {
      values = ["*"]
    }
  }
}

resource "aws_lb_target_group" "target_group" {
  name        = "${local.app}-tg"
  port        = 8080
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = module.vpc.vpc_id

  health_check {
    healthy_threshold = 3
    interval          = 30
    path              = "/health_checks"
    protocol          = "HTTP"
    timeout           = 5
  }
}

从现在开始,我们将详细解释上述设置的内容。

resource "aws_lb" "alb"
    • ここではALBのリソースを作成する

 

    • load_balancer_typeはロードバランサーのタイプを指定し、この場合は “application”とする

 

    • security_groupsはALBに関連付けるセキュリティグループを指定する

 

    subnetsはALBを配置するパブリックサブネットを指定する
aws_lb_listener "alb_listener"

ALB(应用程序负载均衡器)的监听器是一个具有特定IP地址和端口的组件,用于接收来自客户端的连接请求,并定义了当接收到客户端连接请求时的操作。

aws_lb_listener_rule "alb_listener_rule"

这里定义了ALB的侦听规则。
侦听规则是一组条件和操作,用于定义接收流量的路由方式。使用侦听规则可以根据特定条件将流量路由到不同的目标组。

aws_lb_target_group "target_group"

在这里,我们正在定义将与ALB关联的目标组。目标组是负载均衡器用于路由流量的一组资源,它们会监听特定的端口和协议,并负责执行健康检查的配置。

在目标群体的设定中,也包括了health_check的描述。

health_check {
  healthy_threshold = 3
  interval          = 30
  path              = "/health_checks"
  protocol          = "HTTP"
  timeout           = 5
}

定期性地检查已注册目标的健康状况。每30秒向路径发送一次请求,如果连续3次在5秒内收到响应,则判断容器已成功启动。如果健康检查失败,ALB将停止向目标组发送流量。

ECR (全欧共同宏观金融信息) 的意思是欧洲共同市场宏观金融信息。

为了部署,我们会使用Docker将API端的Go语言和前端端的Next.js应用打包成容器。为了进行部署,我们需要将在本地创建的容器镜像推送到一个用于存储和管理的注册表中。

这次我们使用Amazon ECR作为容器注册表。
以下是ECR的特点说明。

    1. 安全性:提供了安全的存储私有容器镜像的位置,并使用IAM来控制对资源的访问。

可扩展性:具有高度的可扩展性,可以存储大量的容器镜像,用户可以轻松地存储和获取数百万个镜像。

集成:无缝集成了AWS的容器管理服务,如Amazon ECS和AWS Fargate,使镜像部署变得更加容易。

镜像扫描:具有基于公开的脆弱性信息自动扫描Docker镜像的功能。

生命周期策略:可以设置自动删除旧镜像和不再需要的镜像的生命周期策略,以减少不必要的存储成本。

创建ECR时,使用Terraform将如下所示。

# GoのECRリポジトリ
resource "aws_ecr_repository" "go_ecr_repository" {
  name                 = "${local.app}-go"
  image_tag_mutability = "IMMUTABLE"
  force_delete         = true

  image_scanning_configuration {
    scan_on_push = true
  }
}

resource "aws_ecr_lifecycle_policy" "go_ecr_lifecycle_policy" {
  repository = aws_ecr_repository.go_ecr_repository.name

  policy = <<EOF
{
    "rules": [
        {
            "rulePriority": 1,
            "description": "Keep last 30 images for Go",
            "selection": {
                "tagStatus": "any",
                "countType": "imageCountMoreThan",
                "countNumber": 30
            },
            "action": {
                "type": "expire"
            }
        }
    ]
}
EOF
}

# Next.jsのECRリポジトリ
resource "aws_ecr_repository" "nextjs_ecr_repository" {
  name                 = "${local.app}-nextjs"
  image_tag_mutability = "IMMUTABLE"
  force_delete         = true

  image_scanning_configuration {
    scan_on_push = true
  }
}

resource "aws_ecr_lifecycle_policy" "nextjs_ecr_lifecycle_policy" {
  repository = aws_ecr_repository.nextjs_ecr_repository.name

  policy = <<EOF
{
    "rules": [
        {
            "rulePriority": 1,
            "description": "Keep last 30 images for Next.js",
            "selection": {
                "tagStatus": "any",
                "countType": "imageCountMoreThan",
                "countNumber": 30
            },
            "action": {
                "type": "expire"
            }
        }
    ]
}
EOF
}

我正在创建一个注册表来存储Go(API端)和Next.js的Docker镜像。

关于此次注册表的创建要点,列举以下两点。

    1. 禁止覆盖图像标签

 

    像push时进行弱点扫描的图像

关于问题1,涉及的部分如下所示。

image_tag_mutability = "IMMUTABLE"

image_tag_mutability 的设置控制着存储库中 Docker 镜像标签的可变性,有两个选项:MUTABLE 和 IMUUTABLE。

可更改:您可以将具有相同标签的新图像推送到代码库,并且可以重用相同标签来覆盖现有的图像

如果选择了这个设置,一旦给镜像打了标签,就无法覆盖它,如果试图使用相同的标签推送新的镜像,就会出现错误。

本次我们使用IMMUTABLE,并简要介绍其优点。

使用IMMUTABLE設定,可以保证与特定标签相关联的图像始终保持相同,避免在部署或回滚时产生混乱和误解。

安全性:能够防止意外覆写图像和推送恶意图像,保护仓库内容免受意外修改的影响。

监管和可追踪性:每个标签都是唯一的,并且无法更改,因此使用特定标签可以轻松准确地追踪部署的图像的版本和内容。

在部署过程中排除将相同的标签关联到不同的镜像上的可能性,可以提高部署的可靠性。

我读了下面的文章。

 

关于第二点,以下是。

image_scanning_configuration {
  scan_on_push = true
}

每当将图像推送到 ECR 时,都会自动触发脆弱性扫描的设置
通过设置此项,每当将新的容器映像推送到存储库时,该映像将自动进行脆弱性扫描,以防止部署可能包含安全风险的新映像。

Fargate上的ECS

首先,ECS 是一种全托管的容器编排服务,可以轻松部署、管理和扩展容器化的应用程序。

两种启动类型和特点

在使用ECS来操作应用程序时,有两种主要的执行环境。
我们将在下面解释这两种环境的特点,并对其优缺点以及本次选择进行说明。

在EC2起動类型中,在集群内的EC2实例上运行容器需要进行实例选择、扩展、补丁应用等管理任务。

【优势】
灵活性:可选择实例类型并进行操作系统级定制
成本:可通过预订实例或使用竞价实例来优化成本

缺点:
管理工作繁琐:需要管理EC2实例的生命周期和安全性。
扩展性:需要手动进行扩展或设置自动扩展。

Fargate启动类型是一种无服务器容器执行环境,无需管理实例。只需指定任务或服务的规格,基础设施将由AWS自动处理。

<优点>
管理成本: 不需要管理服务器、集群或应用补丁。
简单的可扩展性: 只需指定任务数量或服务部署,即可自动扩展。
安全性: 每个任务都有独立的隔离边界,增强了安全性。

【缺点】
成本:根据使用情况,可能比EC2实例类型更昂贵
灵活性限制:操作系统级别的定制受到限制

可以将上述内容归纳如下。
在这个使用案例中,

    1. 只有一名应用程序工程师进行开发

 

    没有操作系统级别的细节配置知识和技术能力

以此思考之后,我暂时决定在Fargate上使用ECS运行。

ECS架构组成元素

集群(Cluster)
集群是ECS资源的一个逻辑组,通过在集群内执行任务和服务,可以有效地管理和隔离资源。

服务(Service)
服务是维护和执行指定数量任务实例的管理实体。
例如,作为Web应用程序的后端,如果始终需要执行3个任务,可以使用服务来实现。
服务可以在任务失败或部署新版本时自动替换任务。

任务(Task)
任务是在ECS上运行的单位,由一个或多个Docker容器组成,并根据任务定义执行任务。

任务定义
任务定义是执行任务的“设计蓝图”
指定运行任务的Docker镜像,分配的CPU和内存大小,卷,环境变量,网络设置等,关于任务执行的详细信息。

如果使用Terraform来管理ECS,将会得到以下的结果。

resource "aws_ecs_task_definition" "ecs_task_definition" {
  family                   = local.app
  network_mode             = "awsvpc"
  cpu                      = 256
  memory                   = 512
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.ecs.arn
  task_role_arn            = aws_iam_role.ecs_task.arn
  container_definitions    = <<CONTAINERS
[
  {
    "name": "${local.app}",
    "image": "medpeer/health_check:latest",
    "portMappings": [
      {
        "containerPort": 8080
      }
      
    ],
    "healthCheck": {
      "command": ["CMD-SHELL", "curl -f http://localhost:8080/health_checks || exit 1"],
      "interval": 30,
      "timeout": 5,
      "retries": 3,
      "startPeriod": 10
    },
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "${aws_cloudwatch_log_group.cloudwatch_log_group.name}",
        "awslogs-region": "${local.region}",
        "awslogs-stream-prefix": "${local.app}"
      }
    },
    "environment": [
      {
        "name": "NGINX_PORT",
        "value": "8080"
      },
      {
        "name": "HEALTH_CHECK_PATH",
        "value": "/health_checks"
      }
    ]
  }
]
CONTAINERS
}

resource "aws_ecs_service" "ecs_service" {
  name            = local.app
  launch_type     = "FARGATE"
  cluster         = aws_ecs_cluster.ecs_cluster.id
  task_definition = aws_ecs_task_definition.ecs_task_definition.arn
  desired_count   = 2
  network_configuration {
    subnets         = module.vpc.private_subnets
    security_groups = [aws_security_group.ecs.id]
  }
}

resource "aws_ecs_cluster" "ecs_cluster" {
  name = local.app
}

resource "aws_ecs_task_definition" "nextjs_ecs_task_definition" {
  family                   = "${local.app}-nextjs"
  network_mode             = "awsvpc"
  cpu                      = 256
  memory                   = 512
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.ecs.arn
  task_role_arn            = aws_iam_role.ecs_task.arn

  container_definitions = <<CONTAINERS
[
  {
    "name": "${local.app}-nextjs",
    "image": "medpeer/nextjs_app:latest",
    "portMappings": [
      {
        "containerPort": 8080
      }
    ],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "${aws_cloudwatch_log_group.cloudwatch_log_group.name}",
        "awslogs-region": "${local.region}",
        "awslogs-stream-prefix": "${local.app}-nextjs"
      }
    },
    "environment": [
      {
        "name": "NGINX_PORT",
        "value": "8080"
      },
      {
        "name": "NEXTJS_ENV_VAR",
        "value": "Your value here"
      }
    ]
  }
]
CONTAINERS
}

resource "aws_ecs_service" "nextjs_ecs_service" {
  name            = "${local.app}-nextjs"
  launch_type     = "FARGATE"
  cluster         = aws_ecs_cluster.nextjs_ecs_cluster.id
  task_definition = aws_ecs_task_definition.nextjs_ecs_task_definition.arn
  desired_count   = 2
  network_configuration {
    subnets         = module.vpc.private_subnets
    security_groups = [aws_security_group.ecs.id]
  }
  load_balancer {
    target_group_arn = aws_lb_target_group.nextjs_target_group.arn
    container_name   = "${local.app}-nextjs"
    container_port   = 8080
  }
  depends_on = [aws_lb_listener_rule.alb_listener_rule]
}

resource "aws_ecs_cluster" "nextjs_ecs_cluster" {
  name = "${local.app}-nextjs"
}

简要解释这次的两种容器构成情况。

    • ALBに紐付けるコンテナはNext.jsを動かすコンテナのみ

 

    • コンテナ間でAPIのやり取りをする

 

    APIコンテナに関しては/health_checkに対して、ヘルスチェックを行う

在下面,我们展示了用于两个容器之间的HTTP通信的security_group。

resource "aws_security_group_rule" "ecs_from_nextjs" {
  description              = "Allow 8080 from next.js to API."
  type                     = "ingress"
  from_port                = 8080
  to_port                  = 8080
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.ecs.id
  security_group_id        = aws_security_group.ecs.id
}

根据以上描述,也会进行IAM的设置。

如果在Fargate上运行ECS,将需要分配以下两个角色。下面对它们分别进行简单解释。

任务执行角色用于ECS任务与AWS服务进行交互。
其中包括从ECR拉取Docker镜像的认证和将日志发送到CloudWatch Logs的权限。

任务卷轴
任务中的应用程序用于与AWS服务进行交互,并在任务执行期间由应用程序使用。

这次,没有将权限授予任务滚动条本身。
对于任务执行滚动条来说,需要赋予以下权限。

    1. 登录到 ECR 存储库并获取镜像的权限

 

    将日志输出到 CloudWatch 的日志组的权限

考虑到异常情况,如果使用Terraform来编写IAM配置,将会如下所示。

data "aws_iam_policy_document" "ecs_task_assume" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "ecs_task" {
  name               = "${local.app}-ecs-task"
  assume_role_policy = data.aws_iam_policy_document.ecs_task_assume.json
}

data "aws_iam_policy_document" "ecs_assume" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "ecs" {
  name               = "${local.app}-ecs"
  assume_role_policy = data.aws_iam_policy_document.ecs_assume.json
}

resource "aws_iam_role_policy_attachment" "ecs_basic" {
  role       = aws_iam_role.ecs.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

接下来,我们将使用Terraform创建输出容器运行日志到CloudWatch日志组的功能。

resource "aws_cloudwatch_log_group" "cloudwatch_log_group" {
  name              = "/aws/ecs/${local.app}"
  retention_in_days = 3
}

假设

retention_in_days = 3

这里指定了日志的保存期限(指定的天数是由官方规定的)。

我也在ecs.tf中指定了这个日志组。

RDS(MySQL)

RDS是用于在云上轻松、高效和可扩展地运营关系型数据库的服务。

如果总结一下这次的流程,就是指运行在Fargate上的ECS的Go应用程序发出的SQL查询被发送到RDS并由RDS处理并返回结果。
RDS和ECS之间的通信通过快速安全的VPC网络进行。

在启动RDS实例时,使用Terraform将其描述如下:

data "aws_ssm_parameter" "rds_password" {
  name = "/${local.app}/rds/password"
}

resource "aws_db_instance" "my_db_instance" {
  db_subnet_group_name = aws_db_subnet_group.my_db_subnet_group.name
  allocated_storage    = 20
  storage_type         = "gp2"
  engine               = "mysql"
  engine_version       = "5.7"
  instance_class       = "db.t2.micro"
  db_name              = "mydb"
  username             = "admin"
  password             = data.aws_ssm_parameter.rds_password.value
  parameter_group_name = "default.mysql5.7"
  skip_final_snapshot  = true

  vpc_security_group_ids = [aws_security_group.rds.id]

  tags = {
    Name = "${local.app} RDS Instance"
  }
}

从现在开始,我将详细地进行解释

allocated_storage    = 20
storage_type         = "gp2"
engine               = "mysql"
engine_version       = "5.7"
instance_class       = "db.t2.micro"

这部分指定了RDS实例的容量等。

data "aws_ssm_parameter" "rds_password" {
  name = "/${local.app}/rds/password"
}

这里正在获取存储在 SSM 参数存储中的 MySQL 密码信息。

SSM参数存储是一个安全的存储服务,用于安全存储敏感信息。数据会被加密后保存,并可以使用KMS密钥进行加密和解密。

 

通过处理这个问题,可以安全地处理作为机密信息的MySQL连接信息。

另外,要让 Terraform 访问 SSM,需要 IAM 权限。下面是本次设置的 IAM。

data "aws_iam_policy_document" "ssm_get_parameter" {
  statement {
    actions   = ["ssm:GetParameter"]
    resources = ["arn:aws:ssm:ap-northeast-1:113713103169:parameter/recruit-service-board/rds/*"]
  }
}

resource "aws_iam_policy" "ssm_get_parameter" {
  name        = "${local.app}-ssm-get-parameter"
  description = "Allows access to SSM GetParameter"
  policy      = data.aws_iam_policy_document.ssm_get_parameter.json
}

最后,我将解释以下内容。

skip_final_snapshot  = true

这个设置用于控制在删除RDS实例时是否获取最终的数据库快照。如果设置为true,则在删除实例时也不会获取最终的数据库快照。由于这会额外产生费用,所以我们明确地将其设置为true。

 

使用Github Actions进行CI/CD构建

这次我们在应用端和基础架构端分别建立了两个流水线。

基础设施部分

我們會使用Terraform將編寫的基礎架構資源同步到AWS上。然後,我們將使用GitHub Actions根據以下事件來觸發並啟動工作流程。

    • mainブランチに向けてPRが作成された時

 

    mainブランチにmerge or pushされた時
name: "Terraform"

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

env:
  OIDC_ARN: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/recruit-info-service-github-actions

permissions:
  id-token: write
  contents: read
  pull-requests: write

jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Assume Role
        uses: aws-actions/configure-aws-credentials@v1-node16
        with:
          role-to-assume: ${{ env.OIDC_ARN }}
          aws-region: ap-northeast-1

      - name: Terraform Format
        id: fmt
        run: terraform fmt -check

      - name: Terraform Init
        id: init
        run: terraform init

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color

      - name: Terraform Plan
        id: plan
        if: github.event_name == 'pull_request'
        run: terraform plan -no-color -input=false

      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve -input=false

我将详细解释本次工作流程的关键点。

- name: Terraform Format
    id: fmt
    run: terraform fmt -check

检查HCL文件中的代码是否已经被格式化。
如果格式不正确,工作流将失败。

- name: Terraform Init
    id: init
    run: terraform init

在这里进行了Terraform的初始化,并为后续的terraform命令的正确执行做准备。

- name: Terraform Validate
  id: validate
  run: terraform validate -no-color

进行语法检查以验证HCL描述是否正确,并对目录中的所有HCL文件执行语法检查,如果存在语法错误,则工作流失败。

- name: Terraform Plan
    id: plan
    if: github.event_name == 'pull_request'
    run: terraform plan -no-color -input=false

在这里,我们会在创建或更新pull request时,根据terraform的配置执行资源变更的模拟,并展示其结果。

- name: Terraform Apply
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    run: terraform apply -auto-approve -input=false

在这里,当对主分支进行推送时,将使用Terraform编写的基础设施资源应用到AWS上。

应用程序方面

在应用程序中,关于CI/CD有两种主要的方式:
1. 当合并到主分支时,访问AWS并更新各个资源的工作流程。

name: "APP Build and Deploy"

on:
  push:
    branches:
      - main

env:
  OIDC_ARN: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/recruit-info-service-github-actions
  ECR_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-1.amazonaws.com
  ECR_REPOSITORY: recruit-service-board
  APP: recruit-service-board
  NEXT_ECR_REPOSITORY: next-service-board
  NEXT_APP: next-service-board
  NEXT_DOCKERFILE: ./front/front.dockerfile

permissions:
  id-token: write
  contents: read

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - uses: actions/cache@v2
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-

      - name: Assume Role
        uses: aws-actions/configure-aws-credentials@v1-node16
        with:
          role-to-assume: ${{ env.OIDC_ARN }}
          aws-region: ap-northeast-1

      - name: Login to ECR
        uses: docker/login-action@v1
        with:
          registry: ${{ env.ECR_REGISTRY }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./app.dockerfile
          push: true
          tags: |
            ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max

      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache
          mv /tmp/.buildx-cache-new /tmp/.buildx-cache

        - name: Build and push Next.js
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ${{ env.NEXT_DOCKERFILE }}
          push: true
          tags: |
            ${{ env.ECR_REGISTRY }}/${{ env.NEXT_ECR_REPOSITORY }}:${{ github.sha }}
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache-next,mode=max

      - name: Move Next.js cache
        run: |
          rm -rf /tmp/.buildx-cache
          mv /tmp/.buildx-cache-next /tmp/.buildx-cache


      - name: Fill in the new image ID in the Amazon ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: ./aws/task-definition.json
          container-name: ${{ env.APP }}
          image: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
      
      - name: Fill in the new image ID in the Next.js Amazon ECS task definition
        id: task-def-next
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: ./aws/task-definition-next.json
          container-name: ${{ env.NEXT_APP }}
          image: ${{ env.ECR_REGISTRY }}/${{ env.NEXT_ECR_REPOSITORY }}:${{ github.sha }}


      - name: Trigger Lambda through API Gateway
        run: |
          response_body=$(curl -s ${{ secrets.API_GATEWAY_ENDPOINT }})
          response_code=$(curl -s -o /dev/null -w "%{http_code}" ${{ secrets.API_GATEWAY_ENDPOINT }})
          echo "Response Body: $response_body"
          echo "Response Code: $response_code"
          if [ "$response_code" -ne 200 ]; then
            echo "Failed to trigger Lambda through API Gateway. HTTP Response code: $response_code"
            exit 1
          fi

      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.APP }}
          cluster: ${{ env.APP }}
          wait-for-service-stability: true
        timeout-minutes: 5

      - name: Deploy Next.js Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def-next.outputs.task-definition }}
          service: ${{ env.NEXT_APP }}
          cluster: ${{ env.NEXT_APP }}
          wait-for-service-stability: true
        timeout-minutes: 5

关于上述的工作流程内容,简略提及。

    今回のワークフローが発火する条件として、直接mainにコードがpushされた時とbranchがmainmergeされた時となっている
on:
  push:
    branches:
      - main
    ここでは各種権限を設定している
permissions:
  id-token: write => (OIDC認証用)
  contents: read => (イメージビルドの際、リポジトリコンテンツ参照用)
    dockerコマンドの機能を拡張することができるBuildxプラグインを導入
- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v2
    ここでOIDC認証により、Github ActionsがAWSへアクセスできるようになる
- name: Assume Role
  uses: aws-actions/configure-aws-credentials@v1-node16
  with:
    role-to-assume: ${{ env.OIDC_ARN }}
    aws-region: ap-northeast-1
    ECRへのログインを行う
- name: Login to ECR
  uses: docker/login-action@v1
  with:
    registry: ${{ env.ECR_REGISTRY }}
    Docker イメージのビルド・プッシュ・キャッシュの生成を行う
- name: Build and push
    uses: docker/build-push-action@v5
    with:
        context: .
        file: ./app.dockerfile
        push: true
        tags: |
        ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
        cache-from: type=local,src=/tmp/.buildx-cache
        cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
    各デプロイでimageタグに置き換え、最新に保つ
- name: Fill in the new image ID in the Amazon ECS task definition
    id: task-def
    uses: aws-actions/amazon-ecs-render-task-definition@v1
    with:
        task-definition: ./aws/task-definition.json
        container-name: ${{ env.APP }}
        image: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
    deployのタイミングでRDSに対してmigrationを回す
- name: Trigger Lambda through API Gateway
        run: |
          response_body=$(curl -s ${{ secrets.API_GATEWAY_ENDPOINT }})
          response_code=$(curl -s -o /dev/null -w "%{http_code}" ${{ secrets.API_GATEWAY_ENDPOINT }})
          echo "Response Body: $response_body"
          echo "Response Code: $response_code"
          if [ "$response_code" -ne 200 ]; then
            echo "Failed to trigger Lambda through API Gateway. HTTP Response code: $response_code"
            exit 1
          fi

关于这一点,我会进行解释。
这个工作流的角色是“一旦部署,就对RDS进行迁移”
所以在这次的实现中,我们不得不采用以下的架构。

image.png
    CIでLambdaを起動するTriggerとなるAPI Gatewayにアクセスする
response_body=$(curl -s ${{ secrets.API_GATEWAY_ENDPOINT }})
response_code=$(curl -s -o /dev/null -w "%{http_code}" ${{ secrets.API_GATEWAY_ENDPOINT }})

(API Gateway与Lambda函数之间的通信是通过跨越VPC边界进行的,但实际流量并不经过VPC的外部网络,而是通过AWS的内部网络进行。因此,即使API Gateway是公开的,也可以直接触发VPC内的Lambda函数。)

    Lambdaには以下のようなGoのスクリプトのbinaryファイルとして実行しています
package main

import (
	"context"
	"log"
	"recruit-info-service/db"
	"recruit-info-service/model"

	"github.com/aws/aws-lambda-go/lambda"
)

func HandleRequest(ctx context.Context) (string, error) {
	log.Println("Starting the migration for the recruit info service DB...")
	dbConn := db.NewDB()
	defer log.Println("Successfully Migrated")
	defer db.CloseDB(dbConn)
	dbConn.AutoMigrate(
		&model.User{}, 
		&model.Company{}, 
		&model.Technology{}, 
		&model.CompanyTechnology{},
		&model.TechnologyTag{},
		&model.TechnologyTechnologyTag{},
		&model.Like{},
		&model.Comment{},
	)
	return "Migration completed successfully!", nil
}

func main() {
	lambda.Start(HandleRequest)
}

dbConn := db.NewDB()の段階で必要なDB情報をSSM parameter Storeに取りにいく

Lambda関数がRDSに対してmigrate処理をする

在此过程中,我们会额外定义以下两个安全组。

    • Lambdaのセキュリティグループの設定

RDSへの接続を許可するためのingressルール(RDSが使用しているポートへの接続を許可するルール)を追加する必要がある

ingress {
    description     = "Allow MySQL connection to RDS"
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [aws_security_group.rds.id]
  }
    • RDSのセキュリティグループの設定

Lambdaからの接続を許可するためのingressルールを追加する必要がある

ingress {
    description     = "Allow MySQL from Lambda security group"
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [aws_security_group.lambda_sg.id]
  }

可以通过Github Actions调用API Gateway端点,启动VPC内的Lambda,并执行对RDS的迁移操作。

    最新のイメージが指定されたタスク定義をデプロイ
- name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.APP }}
          cluster: ${{ env.APP }}
          wait-for-service-stability: true

设置 “wait-for-service-stability” 为 true,将等待直到服务稳定运行完成。

2. 对Go应用程序代码进行自动测试和Linter执行。

name: Go Lint and Test

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  lint-and-test:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v3

    - name: Set up Go
      uses: actions/setup-go@v2
      with:
        go-version: 1.x

    - name: Install dependencies
      run: go mod download

    - name: Run golangci-lint
      uses: golangci/golangci-lint-action@v3
      with:
        version: latest

    - name: Run Go Test
      run: go test ./tests/...
    golangcli-lintを使用してGoのコードにLinterを通す
- name: Run golangci-lint
      uses: golangci/golangci-lint-action@v3
      with:
        version: latest

 

    テストコードの自動実行
- name: Run Go Test
      run: go test ./tests/...

最终

这次我首次尝试使用“AWS ✖︎ Terraform ✖︎ Github Actions”这个技术堆栈来进行基础架构搭建。首先我想列举一些反省的事项。

image.png

尽管面临着诸多挑战,这个个人开发项目仍然有很多未解决的问题。但由于接触到了许多初次接触的技术,并且非常兴奋地投身其中,我认为这是一次非常宝贵的经历。

广告
将在 10 秒后关闭
bannerAds