使用 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 中。
这就是我们将要建造的架构:
您可以使用 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"
}
在深入探讨基础设施组件之前,需要考虑一些变量。
# 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 }
接下来,我们将为 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
}
}
验证证书
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}" ]
}
好了,现在我们可以继续推进基础设施主体部分的建设了。
首先,我们需要一个 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}"
}
现在创建一些安全组,以便 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"]
}
}
现在我们可以创建 RDS 实例了。它需要一个“子网组”来放置该实例,我们将使用hasura_rds上面创建的子网。
resource "aws_db_subnet_group" "hasura" {
name = "hasura"
subnet_ids = ["${aws_subnet.hasura_rds.*.id}"]
}
然后创建 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"
}
根据上述配置,hasura将创建一个名为 `<instance_name>` 的新 RDS 实例。可以使用 Terraform 从现有快照恢复 RDS 实例。您可以通过取消注释相应的# snapshot_identifier行来实现。但是,我建议您在从快照创建实例之前先阅读此问题。简而言之,如果您从快照创建实例,则必须snapshot_identifier在模板的后续运行中包含 `<instance_name>`,否则它将删除并重新创建该实例。
接下来是 ECS / Fargate...
创建 ECS 集群
resource "aws_ecs_cluster" "hasura" {
name = "hasura-cluster"
}
在创建 Hasura 服务之前,我们先来创建一个日志记录位置。
resource "aws_cloudwatch_log_group" "hasura" {
name = "/ecs/hasura"
}
创建日志组很简单,但允许 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}"
}
然后创建任务定义。在这里,您可以调整实例大小,并配置传递给 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
}
现在创建 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",
]
}
现在我们有了 ECS 服务和 RDS 数据库,我们只需要一些公共访问权限,这将由 ALB 提供。
首先,为 ALB 创建一个日志记录位置(如果需要日志记录)。可以先使用 S3 存储桶。您可以添加所需的任何生命周期策略,但请记住,存储桶名称是全局唯一的。
resource "aws_s3_bucket" "hasura" {
bucket = "hasura-${var.region}"
acl = "private"
}
添加 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
}
如果您已将 ACM 证书放在单独的 Terraform 堆栈中,则需要将其导入。
data "aws_acm_certificate" "hasura" {
domain = "hasura.${var.domain}"
types = ["AMAZON_ISSUED"]
most_recent = true
statuses = ["ISSUED"]
}
创建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
}
}
然后创建目标组。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"
}
}
然后创建监听器。如果已导入,请设置其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"
}
}
最后,创建一个 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
}
}
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
