2023-12-20

使用 Terraform 在 AWS Fargate 上部署 Rust 应用

Antonio Souza

在当今快速发展的技术环境中,基础设施即代码 (Infrastructure as Code, IaC) 已成为高效、可扩展和可维护的云基础设施部署的基石。IaC 涉及通过机器可读的脚本文件,而不是通过物理硬件配置或交互式配置工具来管理和配置计算基础设施。这实现了基础设施部署和管理的自动化,从而降低了人为错误的风险并提高了部署速度。

在本文中,我们将探讨如何使用 Terraform 在 AWS Fargate 上部署使用 loco 构建的 Rust 应用。我们将从创建一个新项目并选择 Rest API 模板开始:


```sh
$ cargo install loco
$ loco new
 ❯ App name? · myapp
? ❯ What would you like to build?
  lightweight-service (minimal, only controllers and views)
 Rest API (with DB and user auth)
  SaaS app (with DB and user auth)

前提条件

要在 AWS Fargate 上部署我们的应用,我们需要安装以下工具:

  • Docker - Docker 是一个容器化平台,允许您将应用程序及其所有依赖项打包到一个标准化的软件开发单元中。
  • Terraform - Terraform 是一款开源的基础设施即代码软件工具,使您能够安全且可预测地创建、更改和改进基础设施。
  • AWS CLI - AWS 命令行界面 (Command Line Interface, CLI) 是一个用于管理您的 AWS 服务的统一工具。

创建 Docker 镜像

要为我们的应用创建 Docker 镜像,我们将使用 loco CLI。cargo loco generate deployment 命令将为我们的应用创建一个 Docker 镜像。它还会为我们创建一个 Dockerfile,我们可以用它来构建镜像。

$ cargo loco generate deployment
? ❯ Choose your deployment ›
 Docker
  Shuttle

added: "dockerfile"
added: ".dockerignore"

现在,我们可以构建 Docker 镜像,该镜像将用于在 AWS Fargate 上部署我们的应用。

$ docker build -t myapp .

[+] Building 237.1s (16/16) FINISHED                                                                                                               docker:desktop-linux
 => [internal] load build definition from dockerfile                                                                                                               0.0s
 => => transferring dockerfile: 331B                                                                                                                               0.0s
 ...
 => => writing image sha256:07416ca8195e4026ab65bc567f990ea83141aa10890f8443deb8f54a8bae7f0a                                                                       0.0s
 => => naming to docker.io/library/myapp

设置 AWS

要在 AWS Fargate 上部署我们的应用,我们需要创建一个 AWS 账户并设置 AWS CLI。您可以在这里创建一个 AWS 账户。

您还需要安装 AWS CLI。您可以在这里找到有关如何执行此操作的说明。

最后,您需要创建一个 IAM 用户以与 AWS CLI 一起使用。您可以在这里找到有关如何执行此操作的说明。

现在,我们可以使用我们刚刚创建的 IAM 用户的凭证配置 AWS CLI。

$ aws configure
AWS Access Key ID [None]: <您的访问密钥 ID>
AWS Secret Access Key [None]: <您的秘密访问密钥>
Default region name [None]: <您的区域>
Default output format [None]: json

在 ECR 上创建仓库

要在 AWS Fargate 上部署我们的应用,我们需要在 ECR 上创建一个仓库。您可以通过运行以下命令来执行此操作:

$ aws ecr create-repository --repository-name myapp

{
    "repository": {
        "repositoryArn": "arn:aws:ecr:us-east-1:123456789012:repository/myapp",
        "registryId": "123456789012",
        "repositoryName": "myapp",
        "repositoryUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp",
        "createdAt": 1627981234.0,
        "imageTagMutability": "MUTABLE",
        "imageScanningConfiguration": {
            "scanOnPush": false
        }
    }
}

将 Docker 镜像推送到 ECR

现在,我们可以将 Docker 镜像推送到 ECR。您可以通过运行以下命令来执行此操作:

-1. 登录到 ECR

$ aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com

-2. 标记 Docker 镜像

$ docker tag myapp:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest

-3. 将 Docker 镜像推送到 ECR

$ docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest

创建 Terraform 的 main.tf 文件

这是主要的 Terraform 文件,将用于在 AWS Fargate 上部署我们的应用。它将创建以下资源:

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~> 4.0"
    }
    archive = {
      source = "hashicorp/archive"
      version = "~> 2.2.0"
    }
  }

  required_version = "~> 1.0"
}

# 配置 AWS Provider
provider "aws" {
  region = "us-east-1" // 更改为您的区域
  access_key = "<您的访问密钥>" // 更改为您的访问密钥
  secret_key = "your secret key" // 更改为您的秘密密钥
}

resource "aws_ecr_repository" "myapp" {
  name = "myapp"
}

resource "aws_ecs_cluster" "myapp_cluster" {
  name = "myapp_cluster"
}

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

resource "aws_ecs_task_definition" "myapp_task" {
  family                   = "myapp-task"
  container_definitions    = <<DEFINITION
  [
    {
      "name": "myapp-task",
      "image": "${aws_ecr_repository.myapp.repository_url}",
      "essential": true,
      "portMappings": [
        {
          "containerPort": 5150
        }
      ],
      "command": ["start"],
      "memory": 512,
      "cpu": 256,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-region": "us-east-2",
          "awslogs-group": "/ecs/myapp",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ]
  DEFINITION
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  memory                   = 512
  cpu                      = 256
  execution_role_arn       = aws_iam_role.ecsTaskExecutionRole.arn
}

resource "aws_iam_role" "ecsTaskExecutionRole" {
  name               = "ecsTaskExecutionRoleMyapp"
  assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
}

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

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

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

resource "aws_alb" "myapp" {
  name               = "myapp-lb"
  internal           = false
  load_balancer_type = "application"
  enable_deletion_protection = true

  subnets = [
    aws_subnet.public_d.id,
    aws_subnet.public_e.id,
  ]

  security_groups = [
    aws_security_group.http.id,
    aws_security_group.https.id,
    aws_security_group.egress_all.id,
  ]

  depends_on = [aws_internet_gateway.igw]
}


resource "aws_security_group" "load_balancer_security_group" {
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
resource "aws_lb_target_group" "myapp" {
  name        = "myapp-tg"
  port        = 5150
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = aws_vpc.myapp_vpc.id

  health_check {
    enabled = true
    path    = "/_health"
    matcher = "200,202"
  }

  depends_on = [aws_alb.myapp]
}

resource "aws_alb_listener" "myapp_http" {
  load_balancer_arn = aws_alb.myapp.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type =  "redirect"
    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

resource "aws_alb_listener" "myapp_https" {
  load_balancer_arn = aws_alb.myapp.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"

  certificate_arn = "<您的证书 ARN>" // 更改为您的证书 ARN

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.myapp.arn
  }
}

output "alb_url" {
  value = "https://${aws_alb.myapp.dns_name}"
}
resource "aws_ecs_service" "myapp" {
  name            = "myapp-service"
  cluster         = aws_ecs_cluster.myapp_cluster.id
  task_definition = aws_ecs_task_definition.myapp_task.arn
  launch_type     = "FARGATE"
  desired_count   = 1

  load_balancer {
    target_group_arn = aws_lb_target_group.myapp.arn
    container_name   = aws_ecs_task_definition.myapp_task.family
    container_port   = 5150
  }

  network_configuration {
    assign_public_ip = false

    security_groups = [
      aws_security_group.egress_all.id,
      aws_security_group.ingress_api.id,
    ]

    subnets = [
    aws_subnet.private_d.id,
    aws_subnet.private_e.id,
    ]
  }
}


resource "aws_security_group" "service_security_group" {
  ingress {
    from_port       = 0
    to_port         = 0
    protocol        = "-1"
    security_groups = ["${aws_security_group.load_balancer_security_group.id}"]
  }

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

此文件将创建以下资源:

  • 我们应用的 ECR 仓库
  • 我们应用的 ECS 集群
  • 我们应用的 ECS 任务定义
  • 我们应用的 ECS 服务

现在,我们需要创建一个 network.tf 文件来定义我们应用的网络配置。此文件将创建以下资源:

resource "aws_vpc" "myapp_vpc" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public_d" {
  vpc_id            = aws_vpc.myapp_vpc.id
  cidr_block        = "10.0.1.0/25"
  availability_zone = "us-east-2a"

  tags = {
    "Name" = "public | us-east-2a"
  }
}

resource "aws_subnet" "private_d" {
  vpc_id            = aws_vpc.myapp_vpc.id
  cidr_block        = "10.0.2.0/25"
  availability_zone = "us-east-2b"

  tags = {
    "Name" = "private | us-east-2b"
  }
}

resource "aws_subnet" "public_e" {
  vpc_id            = aws_vpc.myapp_vpc.id
  cidr_block        = "10.0.1.128/25"
  availability_zone = "us-east-2c"

  tags = {
    "Name" = "public | us-east-2c"
  }
}

resource "aws_subnet" "private_e" {
  vpc_id            = aws_vpc.myapp_vpc.id
  cidr_block        = "10.0.2.128/25"
  availability_zone = "us-east-2c"

  tags = {
    "Name" = "private | us-east-2c"
  }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.myapp_vpc.id
  tags = {
    "Name" = "public"
  }
}

resource "aws_route_table" "private" {
  vpc_id = aws_vpc.myapp_vpc.id
  tags = {
    "Name" = "private"
  }
}

resource "aws_route_table_association" "public_d_subnet" {
  subnet_id      = aws_subnet.public_d.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "private_d_subnet" {
  subnet_id      = aws_subnet.private_d.id
  route_table_id = aws_route_table.private.id
}

resource "aws_route_table_association" "public_e_subnet" {
  subnet_id      = aws_subnet.public_e.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "private_e_subnet" {
  subnet_id      = aws_subnet.private_e.id
  route_table_id = aws_route_table.private.id
}

resource "aws_eip" "nat" {
  vpc = true
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.myapp_vpc.id
}

resource "aws_nat_gateway" "ngw" {
  subnet_id     = aws_subnet.public_d.id
  allocation_id = aws_eip.nat.id

  depends_on = [aws_internet_gateway.igw]
}

resource "aws_route" "public_igw" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.igw.id
}

resource "aws_route" "private_ngw" {
  route_table_id         = aws_route_table.private.id
  destination_cidr_block = "0.0.0.0/0"
  nat_gateway_id         = aws_nat_gateway.ngw.id
}

resource "aws_security_group" "http" {
  name        = "http"
  description = "HTTP traffic" # HTTP 流量
  vpc_id      = aws_vpc.myapp_vpc.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "TCP"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_security_group" "https" {
  name        = "https"
  description = "HTTPS traffic" # HTTPS 流量
  vpc_id      = aws_vpc.myapp_vpc.id

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

resource "aws_security_group" "egress_all" {
  name        = "egress-all"
  description = "Allow outbound traffic" # 允许出站流量
  vpc_id      = aws_vpc.myapp_vpc.id

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

resource "aws_security_group" "ingress_api" {
  name        = "ingress-api"
  description = "Allow ingress to App" # 允许访问 App 的入站流量
  vpc_id      = aws_vpc.myapp_vpc.id

  ingress {
    from_port   = 5150
    to_port     = 5150
    protocol    = "TCP"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

网络配置将负责创建在 AWS Fargate 上部署我们的应用所需的所有网络基础设施。我建议您阅读 AWS Fargate 文档 以了解其工作原理,您也可以阅读 Terraform 文档关于 AWS FargateAWS VPC

所以,现在我们有了主要的 Terraform 文件和我们应用的网络配置文件。我们现在可以在 AWS Fargate 上部署我们的应用了。

在 AWS Fargate 上部署应用

要在 AWS Fargate 上部署我们的应用,我们需要运行以下命令:

-1. 初始化 Terraform

$ terraform init

-2. 规划部署

$ terraform plan

-3. 应用部署

$ terraform apply
```****

这些命令将创建我们在 AWS Fargate 上部署应用所需的所有资源。运行后,您将看到来自 `alb_url` 输出的 url。

```sh
Apply complete! Resources: 20 added, 0 changed, 0 destroyed.

Outputs:

alb_url = https://myapp-lb-1234567890.us-east-2.elb.amazonaws.com

现在,我们可以通过访问来自 alb_url 输出的 url 来访问我们的应用。

结论

在本文中,我们探讨了如何使用 Terraform 在 AWS Fargate 上部署使用 loco 构建的 Rust 应用。我们从创建一个新项目并选择 Rest API 模板开始。然后,我们为我们的应用创建了 Docker 镜像并将其推送到 ECR。最后,我们为我们的应用创建了主要的 Terraform 文件和网络配置文件,并在 AWS Fargate 上部署了它。

这种方法使我们能够以快速可靠的方式在 AWS Fargate 上部署我们的应用。它还使我们能够通过添加更多实例来轻松扩展我们的应用。