发布于 2026-01-06 3 阅读
0

使用 Fargate、RDS 和 Terraform 在 AWS 上部署 Hasura

使用 Fargate、RDS 和 Terraform 在 AWS 上部署 Hasura

Hasura是一个非常棒的 Postgres GraphQL 网关。在 Heroku 上部署 Hasura 非常简单,但如果您希望将其部署到 AWS 并实现全自动部署,本文将指导您了解一种可行的方法。

在 AWS 上部署时,强烈建议跨多个可用区 (AZ) 进行部署,这样可以确保如果一个可用区发生故障,您的服务只会受到短暂中断,而不是一直停机直到可用区恢复。

本次部署中使用的组件包括:

  • Postgres RDS 数据库部署在“多可用区”环境中
  • Hasura 已在 Fargate 的多个可用区部署。
  • ALB 在 Hasura 任务之间进行负载均衡
  • 由 ACM 颁发的用于保护 ALB 流量的证书。
  • 将 RDS、ECS 和 ALB 的日志记录到 CloudWatch Logs 中。

这就是我们将要建造的架构:

ECS Fargate Hasura RDS 图

您可以使用 Cloudformation 来构建它,但我出于各种原因选择了 Terraform,其中最重要的是它能够执行以下操作terraform plan

顺便说一句,如果您刚开始使用 Fargate,建议先在 Web 管理控制台中进行尝试,它会自动处理下面很多复杂的操作,例如创建服务角色、IAM 权限、日志组等等。当您想要实现自动化操作时,再回来深入研究下面的细节。

在 AWS 账户中配置 ECS 资源之前,必须先AWSServiceRoleForECS在该账户中创建 IAM 角色。如果您已在 Web 控制台中手动创建了集群,则系统会自动为您创建 IAM 角色。如果您想使用 Terraform 管理 ECS 资源,可以将其导入到 Terraform 配置中。

需要注意的是,AWSServiceRoleForECS每个账户只能存在一个服务角色(它不支持服务角色后缀),因此,如果您在一个 AWS 账户中部署多个 Hasura 堆栈,则服务角色的 Terraform 需要独立于主堆栈存在。

创建角色的方式如下



# Service role allowing AWS to manage resources required for ECS
resource "aws_iam_service_linked_role" "ecs_service" {
  aws_service_name = "ecs.amazonaws.com"
}


Enter fullscreen mode Exit fullscreen mode

在深入探讨基础设施组件之前,需要考虑一些变量。



# Which region to deploy to
variable "region" { }
# Which domain to use. Service will be deployed at hasura.domain
variable "domain" { }
# The access key to secure hasura with. For admin access
variable "hasura_access_key" { }
# The secret shared HMAC key for JWT authentication
variable "hasura_jwt_hmac_key" { }
# User name for RDS
variable "rds_username" { }
# Password for RDS
variable "rds_password" { }
# The DB name in the RDS instance. Note that this cannot contain -'s
variable "rds_db_name" { }
# The size of RDS instance, eg db.t2.micro
variable "rds_instance" { }
# How many AZ's to create in the VPC
variable "az_count" { default = 2 }
# Whether to deploy RDS and ECS in multi AZ mode or not
variable "multi_az" { default = true }


Enter fullscreen mode Exit fullscreen mode

接下来,我们将为 ALB 创建证书。如果您需要定期删除并重新创建堆栈(例如在开发环境中),那么最好在单独的 Terraform 堆栈中创建证书,这样每次都会被删除和重新创建。新的 AWS 账户默认每年只能创建 20 个证书,因此很容易意外超出此限制。虽然可以申请增加证书数量,但根据我的经验,通常需要一到两天的时间才能完成。

如果您使用 Route 53,您可以自动验证 ACM 证书,这是实现全自动工作流程的最简单方法。或者,如果 Terraform 支持您的 DNS 提供商,您可以让它在 DNS 记录中添加相应的记录。

创建证书:



resource "aws_acm_certificate" "hasura" {
  domain_name       = "hasura.${var.domain}"
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}


Enter fullscreen mode Exit fullscreen mode

验证证书



data "aws_route53_zone" "hasura" {
  name         = "${var.domain}."
}

resource "aws_route53_record" "hasura_validation" {
    depends_on = ["aws_acm_certificate.hasura"]
    name = "${lookup(aws_acm_certificate.hasura.domain_validation_options[0], "resource_record_name")}"
    type = "${lookup(aws_acm_certificate.hasura.domain_validation_options[0], "resource_record_type")}"
    zone_id = "${data.aws_route53_zone.hasura.zone_id}"
    records = ["${lookup(aws_acm_certificate.hasura.domain_validation_options[0], "resource_record_value")}"]
    ttl = 300
}

resource "aws_acm_certificate_validation" "hasura" {
    certificate_arn = "${aws_acm_certificate.hasura.arn}"
    validation_record_fqdns = ["${aws_route53_record.hasura_validation.*.fqdn}" ]
}



Enter fullscreen mode Exit fullscreen mode

好了,现在我们可以继续推进基础设施主体部分的建设了。

首先,我们需要一个 VPC 来部署这套基础设施。我们将创建一个私有子网用于 RDS,一个公有子网用于 ECS。ECS 任务已放置在公有子网中,以便它们可以从 Docker Hub 获取 Hasura 镜像。如果将它们放置在私有子网中,则需要添加 NAT 网关才能使它们能够拉取镜像。




### VPC

# Fetch AZs in the current region
data "aws_availability_zones" "available" {}

resource "aws_vpc" "hasura" {
  cidr_block = "172.17.0.0/16"
}

# Create var.az_count private subnets for RDS, each in a different AZ
resource "aws_subnet" "hasura_rds" {
  count             = "${var.az_count}"
  cidr_block        = "${cidrsubnet(aws_vpc.hasura.cidr_block, 8, count.index)}"
  availability_zone = "${data.aws_availability_zones.available.names[count.index]}"
  vpc_id            = "${aws_vpc.hasura.id}"
}

# Create var.az_count public subnets for Hasura, each in a different AZ
resource "aws_subnet" "hasura_ecs" {
  count                   = "${var.az_count}"
  cidr_block              = "${cidrsubnet(aws_vpc.hasura.cidr_block, 8, var.az_count + count.index)}"
  availability_zone       = "${data.aws_availability_zones.available.names[count.index]}"
  vpc_id                  = "${aws_vpc.hasura.id}"
  map_public_ip_on_launch = true
}

# IGW for the public subnet
resource "aws_internet_gateway" "hasura" {
  vpc_id = "${aws_vpc.hasura.id}"
}

# Route the public subnet traffic through the IGW
resource "aws_route" "internet_access" {
  route_table_id         = "${aws_vpc.hasura.main_route_table_id}"
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = "${aws_internet_gateway.hasura.id}"
}




Enter fullscreen mode Exit fullscreen mode

现在创建一些安全组,以便 ALB 可以与 ECS 通信,ECS 任务可以与 RDS 通信:



# Security Groups

# Internet to ALB
resource "aws_security_group" "hasura_alb" {
  name        = "hasura-alb"
  description = "Allow access on port 443 only to ALB"
  vpc_id      = "${aws_vpc.hasura.id}"

  ingress {
    protocol    = "tcp"
    from_port   = 443
    to_port     = 443
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port = 0
    to_port   = 0
    protocol  = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
# ALB TO ECS

resource "aws_security_group" "hasura_ecs" {
  name        = "hasura-tasks"
  description = "allow inbound access from the ALB only"
  vpc_id      = "${aws_vpc.hasura.id}"

  ingress {
    protocol        = "tcp"
    from_port       = "8080"
    to_port         = "8080"
    security_groups = ["${aws_security_group.hasura_alb.id}"]
  }

  egress {
    protocol    = "-1"
    from_port   = 0
    to_port     = 0
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# ECS to RDS
resource "aws_security_group" "hasura_rds" {
  name        = "hasura-rds"
  description = "allow inbound access from the hasura tasks only"
  vpc_id      = "${aws_vpc.hasura.id}"

  ingress {
    protocol        = "tcp"
    from_port       = "5432"
    to_port         = "5432"
    security_groups = ["${aws_security_group.hasura_ecs.id}"]
  }

  egress {
    protocol    = "-1"
    from_port   = 0
    to_port     = 0
    cidr_blocks = ["0.0.0.0/0"]
  }
}



Enter fullscreen mode Exit fullscreen mode

现在我们可以创建 RDS 实例了。它需要一个“子网组”来放置该实例,我们将使用hasura_rds上面创建的子网。



resource "aws_db_subnet_group" "hasura" {
  name       = "hasura"
  subnet_ids = ["${aws_subnet.hasura_rds.*.id}"]
}


Enter fullscreen mode Exit fullscreen mode

然后创建 RDS 实例本身



resource "aws_db_instance" "hasura" {
    name                        = "${var.rds_db_name}"
    identifier                  = "hasura"
    username                    = "${var.rds_username}"
    password                    = "${var.rds_password}"
    port                        = "5432"
    engine                      = "postgres"
    engine_version              = "10.5"
    instance_class              = "${var.rds_instance}"
    allocated_storage           = "10"
    storage_encrypted           = false
    vpc_security_group_ids      = ["${aws_security_group.hasura_rds.id}"]
    db_subnet_group_name        = "${aws_db_subnet_group.hasura.name}"
    parameter_group_name        = "default.postgres10"
    multi_az                    = "${var.multi_az}"
    storage_type                = "gp2"
    publicly_accessible         = false
    # snapshot_identifier       = "hasura"
    allow_major_version_upgrade = false
    auto_minor_version_upgrade  = false
    apply_immediately           = true
    maintenance_window          = "sun:02:00-sun:04:00"
    skip_final_snapshot         = false
    copy_tags_to_snapshot       = true
    backup_retention_period     = 7
    backup_window               = "04:00-06:00"
    final_snapshot_identifier   = "hasura"
}


Enter fullscreen mode Exit fullscreen mode

根据上述配置,hasura将创建一个名为 `<instance_name>` 的新 RDS 实例。可以使用 Terraform 从现有快照恢复 RDS 实例。您可以通过取消注释相应的# snapshot_identifier行来实现。但是,我建议您在从快照创建实例之前先阅读此问题。简而言之,如果您从快照创建实例,则必须snapshot_identifier在模板的后续运行中包含 `<instance_name>`,否则它将删除并重新创建该实例。

接下来是 ECS / Fargate...

创建 ECS 集群



resource "aws_ecs_cluster" "hasura" {
  name = "hasura-cluster"
}


Enter fullscreen mode Exit fullscreen mode

在创建 Hasura 服务之前,我们先来创建一个日志记录位置。



resource "aws_cloudwatch_log_group" "hasura" {
  name = "/ecs/hasura"
}


Enter fullscreen mode Exit fullscreen mode

创建日志组很简单,但允许 ECS 任务将日志记录到该日志组则像大多数 IAM 事项一样,稍微复杂一些!



data "aws_iam_policy_document" "hasura_log_publishing" {
  statement {
    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents",
      "logs:PutLogEventsBatch",
    ]
    resources = ["arn:aws:logs:${var.region}:*:log-group:/ecs/hasura:*"]
  }
}

resource "aws_iam_policy" "hasura_log_publishing" {
  name        = "hasura-log-pub"
  path        = "/"
  description = "Allow publishing to cloudwach"

  policy = "${data.aws_iam_policy_document.hasura_log_publishing.json}"
}

data "aws_iam_policy_document" "hasura_assume_role_policy" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "hasura_role" {
  name               = "hasura-role"
  path               = "/system/"
  assume_role_policy = "${data.aws_iam_policy_document.hasura_assume_role_policy.json}"
}


resource "aws_iam_role_policy_attachment" "hasura_role_log_publishing" {
  role = "${aws_iam_role.hasura_role.name}"
  policy_arn = "${aws_iam_policy.hasura_log_publishing.arn}"
}



Enter fullscreen mode Exit fullscreen mode

然后创建任务定义。在这里,您可以调整实例大小,并配置传递给 Docker 容器的环境属性。这里我们配置实例以进行 JWT 身份验证。

将定义更新image为您想要运行的版本。您需要更新CORS应用程序名称的设置,或者将其完全删除。




resource "aws_ecs_task_definition" "hasura" {
  family                   = "hasura"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "256"
  memory                   = "512"
  execution_role_arn       = "${aws_iam_role.hasura_role.arn}"

  container_definitions = <<DEFINITION
    [
      {
        "image": "hasura/graphql-engine:v1.0.0-alpha34",
        "name": "hasura",
        "networkMode": "awsvpc",
        "portMappings": [
          {
            "containerPort": 8080,
            "hostPort": 8080
          }
        ],
        "logConfiguration": {
          "logDriver": "awslogs",
          "options": {
            "awslogs-group": "/ecs/hasura",
            "awslogs-region": "${var.region}",
            "awslogs-stream-prefix": "ecs"
          }
        },
        "environment": [
          {
            "name": "HASURA_GRAPHQL_ACCESS_KEY",
            "value": "${var.hasura_access_key}"
          },
          {
            "name": "HASURA_GRAPHQL_DATABASE_URL",
            "value": "postgres://${var.rds_username}:${var.rds_password}@${aws_db_instance.hasura.endpoint}/${var.rds_db_name}"
          },
          {
            "name": "HASURA_GRAPHQL_ENABLE_CONSOLE",
            "value": "true"
          },
          {
            "name": "HASURA_GRAPHQL_CORS_DOMAIN",
            "value": "https://app.${var.domain}:443"
          },

          {
            "name": "HASURA_GRAPHQL_PG_CONNECTIONS",
            "value": "100"
          },
          {
            "name": "HASURA_GRAPHQL_JWT_SECRET",
            "value": "{\"type\":\"HS256\", \"key\": \"${var.hasura_jwt_hmac_key}\"}"
          }
        ]
      }
    ]
DEFINITION

}



Enter fullscreen mode Exit fullscreen mode

现在创建 ECS 服务。如果已将该multi_az属性设置为 true,它将启动 2 个任务。它会自动将任务均匀分配到服务中配置的子网(即两个可用区)上。



resource "aws_ecs_service" "hasura" {
  depends_on      = ["aws_ecs_task_definition.hasura", "aws_cloudwatch_log_group.hasura"]
  name            = "hasura-service"
  cluster         = "${aws_ecs_cluster.hasura.id}"
  task_definition = "${aws_ecs_task_definition.hasura.arn}"
  desired_count   = "${var.multi_az == true ? "2" : "1"}"
  launch_type     = "FARGATE"

  network_configuration {
    assign_public_ip  = true
    security_groups   = ["${aws_security_group.hasura_ecs.id}"]
    subnets           = ["${aws_subnet.hasura_ecs.*.id}"]
  }

  load_balancer {
    target_group_arn = "${aws_alb_target_group.hasura.id}"
    container_name   = "hasura"
    container_port   = "8080"
  }

  depends_on = [
    "aws_alb_listener.hasura",
  ]
}


Enter fullscreen mode Exit fullscreen mode

现在我们有了 ECS 服务和 RDS 数据库,我们只需要一些公共访问权限,这将由 ALB 提供。

首先,为 ALB 创建一个日志记录位置(如果需要日志记录)。可以先使用 S3 存储桶。您可以添加所需的任何生命周期策略,但请记住,存储桶名称是全局唯一的。



resource "aws_s3_bucket" "hasura" {
  bucket = "hasura-${var.region}"
  acl    = "private"
}


Enter fullscreen mode Exit fullscreen mode

添加 IAM 策略以允许 ALB 向其写入日志。请记得更新存储桶名称。



data "aws_elb_service_account" "main" {}

resource "aws_s3_bucket_policy" "hasura" {
  bucket = "${aws_s3_bucket.hasura.id}"

  policy = <<POLICY
{
  "Id": "hasuraALBWrite",
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "hasuraALBWrite",
      "Action": [
        "s3:PutObject"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::hasura-${var.region}/alb/*",
      "Principal": {
        "AWS": [
          "${data.aws_elb_service_account.main.arn}"
        ]
      }
    }
  ]
}
POLICY
}



Enter fullscreen mode Exit fullscreen mode

如果您已将 ACM 证书放在单独的 Terraform 堆栈中,则需要将其导入。



data "aws_acm_certificate" "hasura" {
  domain   = "hasura.${var.domain}"
  types       = ["AMAZON_ISSUED"] 
  most_recent = true
  statuses = ["ISSUED"]
}


Enter fullscreen mode Exit fullscreen mode

创建ALB本身。



resource "aws_alb" "hasura" {
  name            = "hasura-alb"
  subnets         = ["${aws_subnet.hasura_ecs.*.id}"]
  security_groups = ["${aws_security_group.hasura_alb.id}"]

  access_logs {
    bucket = "${aws_s3_bucket.hasura.id}"
    prefix = "alb"
    enabled = true
  }
}


Enter fullscreen mode Exit fullscreen mode

然后创建目标组。ECS 会在任务停止/启动时将其注册到此目标组中。



resource "aws_alb_target_group" "hasura" {
  name        = "hasura-alb"
  port        = 8080
  protocol    = "HTTP"
  vpc_id      = "${aws_vpc.hasura.id}"
  target_type = "ip"
  health_check {
    path = "/"
    matcher = "302"
  }
}


Enter fullscreen mode Exit fullscreen mode

然后创建监听器。如果已导入,请设置其certificate_arn值。"${data.aws_acm_certificate.hasura.arn}"



resource "aws_alb_listener" "hasura" {
  load_balancer_arn = "${aws_alb.hasura.id}"
  port              = "443"
  protocol          = "HTTPS"
  certificate_arn   = "${aws_acm_certificate.hasura.arn}"

  default_action {
    target_group_arn = "${aws_alb_target_group.hasura.id}"
    type             = "forward"
  }
}


Enter fullscreen mode Exit fullscreen mode

最后,创建一个 53 号公路记录,指向您的 ALB。



resource "aws_route53_record" "hasura" {
  zone_id = "${data.aws_route53_zone.hasura.zone_id}"
  name    = "hasura.${var.domain}"
  type    = "A"

  alias {
    name                   = "${aws_alb.hasura.dns_name}"
    zone_id                = "${aws_alb.hasura.zone_id}"
    evaluate_target_health = true
  }
}



Enter fullscreen mode Exit fullscreen mode

Terraform 配置完成!现在应该可以试运行了!

该堆栈应以空模式和监听的 Hasura 实例启动。https://hasura.domain

祝你好运,欢迎随时给我留言,或者你也可以在 Hasura Discord 上找到我,我的用户名是@elgordino 。


@rayraegah将这篇文章改编成了一个完整的 Terraform 模块。如果您想部署它,可以访问这里查看:https://github.com/Rayraegah/terraform-aws-hasura

文章来源:https://dev.to/lineup-ninja/deploying-hasura-on-aws-with-fargate-rds-and-terraform-4gk7