使用 Fargate、Vapor 和 MongoDB Atlas 部署到 AWS
本指南说明了如何在 AWS 上部署服务器端 Swift 工作负载。该工作负载是一个用于跟踪待办事项列表的 REST API。它使用 Vapor 框架来编写 API 方法。这些方法在 MongoDB Atlas 云数据库中存储和检索数据。Vapor 应用程序被容器化,并使用 AWS Copilot 工具包部署到 AWS Fargate 上的 AWS。
架构
- Amazon API Gateway 接收 API 请求
- API Gateway 通过 AWS Cloud Map 管理的内部 DNS 在 AWS Fargate 中定位您的应用程序容器
- API Gateway 将请求转发到容器
- 容器运行 Vapor 框架,并具有 GET 和 POST 项目的方法
- Vapor 在 MongoDB Atlas 云数据库中存储和检索项目,该数据库在 MongoDB 管理的 AWS 账户中运行
先决条件
要构建此示例应用程序,您需要
- AWS 账户
- MongoDB Atlas 数据库
- AWS Copilot - 用于在 AWS 上创建容器化工作负载的命令行工具
- Docker Desktop - 用于将您的 Swift 代码编译成 Docker 镜像
- Vapor - 用于编写 REST 服务代码
- AWS 命令行界面 (AWS CLI) - 安装 CLI 并使用您 AWS 账户的凭证进行配置
步骤 1:创建数据库
如果您是 MongoDB Atlas 的新手,请按照此入门指南操作。您需要创建以下项目
- Atlas 账户
- 集群
- 数据库用户名/密码
- 数据库
- 集合
在后续步骤中,您将为这些项目提供值以配置应用程序。
步骤 2:初始化新的 Vapor 项目
为您的项目创建一个文件夹。
mkdir todo-app && cd todo-app
初始化一个名为 api 的 Vapor 项目。
vapor new api -n
步骤 3:添加项目依赖项
Vapor 初始化一个 Package.swift 文件用于项目依赖项。您的项目需要一个额外的库,MongoDBVapor。将 MongoDBVapor 库添加到项目和 Package.swift 文件的目标依赖项中。
更新后的文件应如下所示
api/Package.swift
// swift-tools-version:5.6
import PackageDescription
let package = Package(
name: "api",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/vapor/vapor", .upToNextMajor(from: "4.7.0")),
.package(url: "https://github.com/mongodb/mongodb-vapor", .upToNextMajor(from: "1.1.0"))
],
targets: [
.target(
name: "App",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "MongoDBVapor", package: "mongodb-vapor")
],
swiftSettings: [
.unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
]
),
.executableTarget(name: "Run", dependencies: [.target(name: "App")]),
.testTarget(name: "AppTests", dependencies: [
.target(name: "App"),
.product(name: "XCTVapor", package: "vapor"),
])
]
)
步骤 4:更新 Dockerfile
您将 Swift 服务器代码作为 Docker 镜像部署到 AWS Fargate。Vapor 为您的应用程序生成一个初始 Dockerfile。您的应用程序需要对这个 Dockerfile 进行一些修改
- 从 Amazon ECR Public Gallery 容器存储库中拉取 build 和 run 镜像
- 在 build 镜像中安装 libssl-dev
- 在 run 镜像中安装 libxml2 和 curl
将 Vapor 生成的 Dockerfile 的内容替换为以下代码
api/Dockerfile
# ================================
# Build image
# ================================
FROM public.ecr.aws/docker/library/swift:5.6.2-focal as build
# Install OS updates
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
&& apt-get -q update \
&& apt-get -q dist-upgrade -y \
&& apt-get -y install libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# Set up a build area
WORKDIR /build
# First just resolve dependencies.
# This creates a cached layer that can be reused
# as long as your Package.swift/Package.resolved
# files do not change.
COPY ./Package.* ./
RUN swift package resolve
# Copy entire repo into container
COPY . .
# Build everything, with optimizations
RUN swift build -c release --static-swift-stdlib
# Switch to the staging area
WORKDIR /staging
# Copy main executable to staging area
RUN cp "$(swift build --package-path /build -c release --show-bin-path)/Run" ./
# Copy resources bundled by SwiftPM to staging area
RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \;
# Copy any resources from the public directory and views directory if the directories exist
# Ensure that by default, neither the directory nor any of its contents are writable.
RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true
RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true
# ================================
# Run image
# ================================
FROM public.ecr.aws/ubuntu/ubuntu:focal
# Make sure all system packages are up to date, and install only essential packages.
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
&& apt-get -q update \
&& apt-get -q dist-upgrade -y \
&& apt-get -q install -y \
ca-certificates \
tzdata \
curl \
libxml2 \
&& rm -r /var/lib/apt/lists/*
# Create a vapor user and group with /app as its home directory
RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor
# Switch to the new home directory
WORKDIR /app
# Copy built executable and any staged resources from builder
COPY --from=build --chown=vapor:vapor /staging /app
# Ensure all further commands run as the vapor user
USER vapor:vapor
# Let Docker bind to port 8080
EXPOSE 8080
# Start the Vapor service when the image is run, default to listening on 8080 in production environment
ENTRYPOINT ["./Run"]
CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
步骤 5:更新 Vapor 源代码
Vapor 还生成了编写 API 代码所需的示例文件。您必须自定义这些文件,使其代码公开您的待办事项列表 API 方法并与您的 MongoDB 数据库交互。
configure.swift 文件初始化一个应用程序范围的连接池,用于连接到您的 MongoDB 数据库。它在运行时从环境变量中检索到您的 MongoDB 数据库的连接字符串。
将该文件的内容替换为以下代码
api/Sources/App/configure.swift
import MongoDBVapor
import Vapor
public func configure(_ app: Application) throws {
let MONGODB_URI = Environment.get("MONGODB_URI") ?? ""
try app.mongoDB.configure(MONGODB_URI)
ContentConfiguration.global.use(encoder: ExtendedJSONEncoder(), for: .json)
ContentConfiguration.global.use(decoder: ExtendedJSONDecoder(), for: .json)
try routes(app)
}
routes.swift 文件定义了您的 API 的方法。这些方法包括一个 POST Item 方法用于插入新项目,以及一个 GET Items 方法用于检索所有现有项目的列表。请参阅代码中的注释以了解每个部分中发生的情况。
将该文件的内容替换为以下代码
api/Sources/App/routes.swift
import Vapor
import MongoDBVapor
// define the structure of a ToDoItem
struct ToDoItem: Content {
var _id: BSONObjectID?
let name: String
var createdOn: Date?
}
// import the MongoDB database and collection names from environment variables
let MONGODB_DATABASE = Environment.get("MONGODB_DATABASE") ?? ""
let MONGODB_COLLECTION = Environment.get("MONGODB_COLLECTION") ?? ""
// define an extension to the Vapor Request object to interact with the database and collection
extension Request {
var todoCollection: MongoCollection<ToDoItem> {
self.application.mongoDB.client.db(MONGODB_DATABASE).collection(MONGODB_COLLECTION, withType: ToDoItem.self)
}
}
// define the api routes
func routes(_ app: Application) throws {
// an base level route used for container healthchecks
app.get { req in
return "OK"
}
// GET items returns a JSON array of all items in the database
app.get("items") { req async throws -> [ToDoItem] in
try await req.todoCollection.find().toArray()
}
// POST item inserts a new item into the database and returns the item as JSON
app.post("item") { req async throws -> ToDoItem in
var item = try req.content.decode(ToDoItem.self)
item.createdOn = Date()
let response = try await req.todoCollection.insertOne(item)
item._id = response?.insertedID.objectIDValue
return item
}
}
main.swift 文件定义了应用程序的启动和关闭代码。更改代码以包含一个 defer 语句,以便在应用程序结束时关闭与 MongoDB 数据库的连接。
将该文件的内容替换为以下代码
api/Sources/Run/main.swift
import App
import Vapor
import MongoDBVapor
var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
let app = Application(env)
try configure(app)
// shutdown and cleanup the MongoDB connection when the application terminates
defer {
app.mongoDB.cleanup()
cleanupMongoSwift()
app.shutdown()
}
try app.run()
步骤 6:初始化 AWS Copilot
AWS Copilot 是一个用于在 AWS 中生成容器化应用程序的命令行实用程序。您可以使用 Copilot 构建 Vapor 代码并将其作为容器部署在 Fargate 中。Copilot 还为您的 MongoDB 连接字符串值创建并跟踪 AWS Systems Manager 密钥参数。您将此值存储为密钥,因为它包含数据库的用户名和密码。您永远不想将其存储在源代码中。最后,Copilot 创建一个 API Gateway 以公开 API 的公共端点。
初始化一个新的 Copilot 应用程序。
copilot app init todo
添加一个新的 Copilot 后端服务。该服务引用您的 Vapor 项目的 Dockerfile,以获取有关如何构建容器的说明。
copilot svc init --name api --svc-type "Backend Service" --dockerfile ./api/Dockerfile
为您的应用程序创建一个 Copilot 环境。环境通常与阶段对齐,例如 dev、test 或 prod。出现提示时,选择您使用 AWS CLI 配置的 AWS 凭证配置文件。
copilot env init --name dev --app todo --default-config
部署 dev 环境
copilot env deploy --name dev
步骤 7:为数据库凭证创建 Copilot 密钥
您的应用程序需要凭证才能向 MongoDB Atlas 数据库进行身份验证。您永远不应将此敏感信息存储在源代码中。创建一个 Copilot 密钥 来存储凭证。这会将您的 MongoDB 集群的连接字符串存储在 AWS Systems Manager 密钥参数中。
从 MongoDB Atlas 网站确定连接字符串。在集群页面上选择连接按钮,然后选择连接您的应用程序。
选择Swift version 1.2.0作为驱动程序,并复制显示的连接字符串。它看起来像这样
mongodb+srv://username:<password>@mycluster.mongodb.net/?retryWrites=true&w=majority
连接字符串包含您的数据库用户名和密码占位符。将 <password> 部分替换为您的数据库密码。然后创建一个名为 MONGODB_URI 的新 Copilot 密钥,并在提示输入值时保存您的连接字符串。
copilot secret init --app todo --name MONGODB_URI
Fargate 在运行时将密钥值作为环境变量注入到您的容器中。在上面的步骤 5 中,您在 api/Sources/App/configure.swift 文件中提取了此值,并使用它来配置您的 MongoDB 连接。
步骤 8:配置后端服务
Copilot 为您的应用程序生成一个 manifest.yml 文件,该文件定义了您的服务的属性,例如 Docker 镜像、网络、密钥和环境变量。更改 Copilot 生成的 manifest 文件以添加以下属性
- 为容器镜像配置运行状况检查
- 添加对 MONGODB_URI 密钥的引用
- 将服务网络配置为私有
- 为 MONGODB_DATABASE 和 MONGODB_COLLECTION 添加环境变量
要实施这些更改,请将 manifest.yml 文件的内容替换为以下代码。更新 MONGODB_DATABASE 和 MONGODB_COLLECTION 的值,以反映您在 MongoDB Atlas 中为此应用程序创建的数据库和集群的名称。
如果您在 Mac M1/M2 机器上构建此解决方案,请取消注释 manifest.yml 文件中的 platform 属性以指定 ARM 构建。默认值为 linux/x86_64。
copilot/api/manifest.yml
# The manifest for the "api" service.
# Read the full specification for the "Backend Service" type at:
# https://aws.github.io/copilot-cli/docs/manifest/backend-service/
# Your service name will be used in naming your resources like log groups, ECS services, etc.
name: api
type: Backend Service
# Your service is reachable at "http://api.${COPILOT_SERVICE_DISCOVERY_ENDPOINT}:8080" but is not public.
# Configuration for your containers and service.
image:
# Docker build arguments. For additional overrides: https://aws.github.io/copilot-cli/docs/manifest/backend-service/#image-build
build: api/Dockerfile
# Port exposed through your container to route traffic to it.
port: 8080
healthcheck:
command: ["CMD-SHELL", "curl -f https://127.0.0.1:8080 || exit 1"]
interval: 10s
retries: 2
timeout: 5s
start_period: 0s
# Mac M1/M2 users - uncomment the following platform line
# the default platform is linux/x86_64
# platform: linux/arm64
cpu: 256 # Number of CPU units for the task.
memory: 512 # Amount of memory in MiB used by the task.
count: 2 # Number of tasks that should be running in your service.
exec: true # Enable running commands in your container.
# define the network as private. this will place Fargate in private subnets
network:
vpc:
placement: private
# Optional fields for more advanced use-cases.
#
# Pass environment variables as key value pairs.
variables:
MONGODB_DATABASE: home
MONGODB_COLLECTION: todolist
# Pass secrets from AWS Systems Manager (SSM) Parameter Store.
secrets:
MONGODB_URI: /copilot/${COPILOT_APPLICATION_NAME}/${COPILOT_ENVIRONMENT_NAME}/secrets/MONGODB_URI
# You can override any of the values defined above by environment.
#environments:
# test:
# count: 2 # Number of tasks to run for the "test" environment.
# deployment: # The deployment strategy for the "test" environment.
# rolling: 'recreate' # Stops existing tasks before new ones are started for faster deployments.
步骤 9:为您的 API Gateway 创建 Copilot 插件服务
Copilot 不具备为您的应用程序添加 API Gateway 的功能。但是,您可以使用 Copilot “插件”为您的应用程序添加额外的 AWS 资源。
通过在您的 Copilot 服务文件夹下创建一个 addons 文件夹,并创建一个 CloudFormation yaml 模板来定义您希望创建的服务,从而定义插件。
为插件创建一个文件夹
mkdir -p copilot/api/addons
创建一个文件来定义 API Gateway
touch copilot/api/addons/apigateway.yml
创建一个文件,将参数从主服务传递到插件服务中
touch copilot/api/addons/addons.parameters.yml
将以下代码复制到 addons.parameters.yml 文件中。它将 Cloud Map 服务的 ID 传递到插件堆栈中。
copilot/api/addons/addons.parameters.yml
Parameters:
DiscoveryServiceARN: !GetAtt DiscoveryService.Arn
将以下代码复制到 addons/apigateway.yml 文件中。它使用 DiscoveryServiceARN 创建一个 API Gateway,以便与 Copilot 为您的 Fargate 容器创建的 Cloud Map 服务集成。
copilot/api/addons/apigateway.yml
Parameters:
App:
Type: String
Description: Your application's name.
Env:
Type: String
Description: The environment name your service, job, or workflow is being deployed to.
Name:
Type: String
Description: The name of the service, job, or workflow being deployed.
DiscoveryServiceARN:
Type: String
Description: The ARN of the Cloud Map discovery service.
Resources:
ApiVpcLink:
Type: AWS::ApiGatewayV2::VpcLink
Properties:
Name: !Sub "${App}-${Env}-${Name}"
SubnetIds:
!Split [",", Fn::ImportValue: !Sub "${App}-${Env}-PrivateSubnets"]
SecurityGroupIds:
- Fn::ImportValue: !Sub "${App}-${Env}-EnvironmentSecurityGroup"
ApiGatewayV2Api:
Type: "AWS::ApiGatewayV2::Api"
Properties:
Name: !Sub "${Name}.${Env}.${App}.api"
ProtocolType: "HTTP"
CorsConfiguration:
AllowHeaders:
- "*"
AllowMethods:
- "*"
AllowOrigins:
- "*"
ApiGatewayV2Stage:
Type: "AWS::ApiGatewayV2::Stage"
Properties:
StageName: "$default"
ApiId: !Ref ApiGatewayV2Api
AutoDeploy: true
ApiGatewayV2Integration:
Type: "AWS::ApiGatewayV2::Integration"
Properties:
ApiId: !Ref ApiGatewayV2Api
ConnectionId: !Ref ApiVpcLink
ConnectionType: "VPC_LINK"
IntegrationMethod: "ANY"
IntegrationType: "HTTP_PROXY"
IntegrationUri: !Sub "${DiscoveryServiceARN}"
TimeoutInMillis: 30000
PayloadFormatVersion: "1.0"
ApiGatewayV2Route:
Type: "AWS::ApiGatewayV2::Route"
Properties:
ApiId: !Ref ApiGatewayV2Api
RouteKey: "$default"
Target: !Sub "integrations/${ApiGatewayV2Integration}"
步骤 10:部署 Copilot 服务
在部署服务时,Copilot 执行以下操作:
- 构建您的 Vapor Docker 镜像
- 将镜像部署到您 AWS 账户中的 Amazon Elastic Container Registry (ECR)
- 在您的 AWS 账户中创建并部署 AWS CloudFormation 模板。CloudFormation 创建在您的应用程序中定义的所有服务。
copilot svc deploy --name api --app todo --env dev
步骤 11:配置 MongoDB Atlas 网络访问
MongoDB Atlas 使用 IP 访问列表来限制对数据库的访问,仅限于特定的源 IP 地址列表。在您的应用程序中,来自容器的流量源自您应用程序网络中 NAT 网关的公有 IP 地址。您必须配置 MongoDB Atlas 以允许来自这些 IP 地址的流量。
要获取 NAT 网关的 IP 地址,请运行以下 AWS CLI 命令:
aws ec2 describe-nat-gateways --filter "Name=tag-key, Values=copilot-application" --query 'NatGateways[?State == `available`].NatGatewayAddresses[].PublicIp' --output table
输出
---------------------
|DescribeNatGateways|
+-------------------+
| 1.1.1.1 |
| 2.2.2.2 |
+-------------------+
使用这些 IP 地址在您的 MongoDB Atlas 账户中为每个地址创建一个网络访问规则。
步骤 12:使用您的 API
要获取 API 的终端节点,请使用以下 AWS CLI 命令:
aws apigatewayv2 get-apis --query 'Items[?Name==`api.dev.todo.api`].ApiEndpoint' --output table
输出
------------------------------------------------------------
| GetApis |
+----------------------------------------------------------+
| https://[your-api-endpoint] |
+----------------------------------------------------------+
使用 cURL 或诸如 Postman 之类的工具与您的 API 交互
添加待办事项列表项
curl --request POST 'https://[your-api-endpoint]/item' --header 'Content-Type: application/json' --data-raw '{"name": "my todo item"}'
检索待办事项列表项
curl https://[your-api-endpoint]/items
清理
完成应用程序后,使用 Copilot 删除它。这将删除在您的 AWS 账户中创建的所有服务。
copilot app delete --name todo