如何使用Fauna构建PythonRESTAPI并将其部署到DigitalOcean应用平台
介绍
许多开发人员没有时间或经验为其应用程序设置和管理基础架构。 为了赶上最后期限并降低成本,开发人员需要找到能够让他们尽可能快速高效地将应用程序部署到云端的解决方案,从而专注于编写代码并向客户交付新功能。 DigitalOcean 的 App Platform 和 Fauna 共同提供了这种能力。
DigitalOcean App Platform 是一种平台即服务 (PaaS),它抽象了运行您的应用程序的基础设施。 它还允许您通过将代码推送到 Git 分支来部署应用程序。
Fauna 是一个强大的数据层,适用于任何规模的应用程序。 正如您将在本教程中看到的,使用 Fauna,您可以快速启动并运行数据库,而无需担心数据库操作。
这两个解决方案一起让您专注于您的应用程序,而不是管理您的基础设施。
在本教程中,您将使用 Flask 框架编写一个最小的 REST API,将 Fauna 与 Python 集成。 然后,您将从 Git 存储库将 API 部署到 DigitalOcean 的应用平台。API 将包括:
- 用于在
Users
集合中创建用户的公共/signup
POST 端点。 - 一个公共的
/login
POST 端点,用于对Users
集合中的文档进行身份验证。 - 一个私有的
/things
GET 端点,用于从Things
集合中获取 Fauna 文档列表。
完成的 Python 项目可在 这个 Github 存储库 上找到。
先决条件
在开始本教程之前,您需要:
- 一个动物区系帐户。
- DigitalOcean 帐户 配置了付款方式。
- 一个 Github 帐户,能够将您的项目部署到 App Platform。
- Python 3 和
pip
安装在您的开发机器上。 按照 如何为 Python 3 安装和设置本地编程环境进行设置。 - Git 安装在本地机器上。 您可以按照教程 如何为开源做贡献:Git 入门 在您的计算机上安装和设置 Git。
- 一个文本编辑器。 您可以使用 Visual Studio Code 或您喜欢的文本编辑器。
第 1 步 — 设置动物数据库
在第一步中,您将配置一个 Fauna 数据库并为 API 创建集合。 Fauna 是 基于文档的数据库 而不是传统的基于表的关系数据库。 Fauna 将您的数据存储在文档和集合中,它们是文档组。
要创建集合,您将使用 Fauna 的本地查询语言 FQL 执行查询。 FQL 是一种表达力强且功能强大的查询语言,可让您访问 Fauna 的全部功能。
要开始,登录到 Fauna 的仪表板。 登录后,点击顶部的【X31X】创建数据库【X50X】按钮。
在 New Database 表单中,使用 PYTHON_API
作为数据库名称:
保留 使用演示数据预填充 未选中。 按保存按钮。
创建数据库后,您将看到数据库的主页部分:
您现在要创建两个集合:
Users
集合将存储带有身份验证信息的文档。Things
集合用于存储一些模拟数据以测试您的 API。
要创建这些集合,您将在仪表板的 shell 中执行一些 FQL 查询。 从左侧的主仪表板菜单访问 shell:
在 shell 的底部面板中编写以下 FQL 查询,使用 CreateCollection 函数创建一个名为 Things
的集合:
CreateCollection({name: "Things"})
按 运行查询 按钮。 您将在 shell 的顶部面板中获得与此类似的结果:
{ ref: Collection("Things"), ts: 1614805457170000, history_days: 30, name: "Things" }
结果显示四个字段:
ref
是对集合本身的引用。ts
是其创建的时间戳,以微秒为单位。history_days
是 Fauna 将保留文档更改的更改时间。name
是集合名称。
接下来,使用以下查询创建 Users
集合:
CreateCollection({name: "Users"})
现在这两个集合都已就位,您将创建您的第一个文档。
Fauna 中的文档有点类似于 JSON 对象。 文档可以存储字符串、数字和数组,但它们也可以使用 Fauna 数据类型。 常见的 Fauna 类型是 Ref,它表示对集合中文档的引用。
Create 函数在指定集合中创建一个新文档。 运行以下查询以在 Things
集合中创建一个包含两个字段的文档:
Create( Collection("Things"), { data: { name: "Banana", color: "Yellow" } } )
运行该查询后,Fauna 将返回创建的文档:
{ ref: Ref(Collection("Things"), "292079274901373446"), ts: 1614807352895000, data: { name: "Banana", color: "Yellow" } }
结果显示以下字段:
Ref
类型的ref
是对 ID 为292079274901373446
的Things
集合中此文档的引用。 请注意,您的文档将具有不同的 ID。ts
是其创建的时间戳,以微秒为单位。data
是文档的实际内容。
此结果与您在创建集合时得到的结果相似。 这是因为 Fauna 中的所有实体(集合、索引、角色等)实际上都存储为文档。
要读取文档,请使用接受文档引用的 Get 函数。 使用文档的参考运行 Get
查询:
Get(Ref(Collection("Things"), "292079274901373446"))
结果是完整的文档:
{ ref: Ref(Collection("Things"), "292079274901373446"), ts: 1614807352895000, data: { name: "Banana", color: "Yellow" } }
要获取存储在集合中的文档的所有引用,请将 Documents 函数与 Paginate 函数一起使用:
Paginate(Documents(Collection("Things")))
此查询返回一个包含引用数组的页面:
{ data: [Ref(Collection("Things"), "292079274901373446")] }
要获取实际文档而不是引用,请使用 Map 遍历引用。 然后使用 Lambda (匿名函数)遍历引用数组和 Get
每个引用:
Map( Paginate(Documents(Collection("Things"))), Lambda("ref", Get(Var("ref"))) )
结果是一个包含完整文档的数组:
{ data: [ { ref: Ref(Collection("Things"), "292079274901373446"), ts: 1614807352895000, data: { name: "Banana", color: "Yellow" } } ] }
您现在要创建 Users_by_username
索引。 您通常使用 Fauna 中的索引来对数据进行分类、过滤和排序,但您也可以将它们用于其他目的,例如强制执行唯一约束。
Users_by_username
索引将通过用户的 username
查找用户,并强制执行唯一约束以防止两个文档具有相同的 username
。
在 shell 中执行此代码以创建索引:
CreateIndex({ name: "Users_by_username", source: Collection("Users"), terms: [{ field: ["data", "username"] }], unique: true })
CreateIndex 函数将使用配置的设置创建索引:
name
是索引的名称。source
是索引将从中索引数据的集合(或多个集合)。terms
是您在使用它来查找文档时将传递给该索引的搜索/过滤条件。unique
表示索引值将是唯一的。 在此示例中,Users
集合中文档的username
属性将被强制为唯一。
要测试索引,请通过在 Fauna shell 中运行以下代码,在 Users
集合中创建一个新文档:
Create( Collection("Users"), { data: { username: "sammy" } } )
您将看到如下结果:
{ ref: Ref(Collection("Users"), "292085174927098368"), ts: 1614812979580000, data: { username: "sammy" } }
现在尝试创建一个具有相同 username
值的文档:
Create( Collection("Users"), { data: { username: "sammy" } } )
您现在会收到一个错误:
Error: [ { "position": [ "create" ], "code": "instance not unique", "description": "document is not unique." } ]
现在索引已经到位,您可以查询它并获取单个文档。 在 shell 中运行此代码以使用索引获取 sammy
用户:
Get( Match( Index("Users_by_username"), "sammy" ) )
以下是它的工作原理:
- Index 返回对
Users_by_username
索引的引用。 - Match 返回对匹配文档的引用(具有
username
且值为sammy
的文档)。 Get
获取由Match
返回的引用,并获取实际文档。
此查询的结果将是:
{ ref: Ref(Collection("Users"), "292085174927098368"), ts: 1614812979580000, data: { username: "sammy" } }
通过将其引用传递给 Delete 函数来删除此测试文档:
Delete(Ref(Collection("Users"), "292085174927098368"))
接下来,您将为 Fauna 配置安全设置,以便您可以从代码连接到它。
第 2 步 — 配置服务器密钥和授权规则
在此步骤中,您将创建一个服务器密钥,您的 Python 应用程序将使用该密钥与 Fauna 进行通信。 然后,您将配置访问权限。
要创建密钥,请使用左侧的主菜单转到 Fauna 仪表板的 Security 部分。 一到那里:
- 按 新密钥 按钮。
- 选择 Server 角色。
- 按保存。
保存后,仪表板将向您显示密钥的秘密。 将秘密保存在安全的地方,永远不要将其提交到您的 Git 存储库。
Warning:Server 角色是无所不能的,任何拥有此机密的人都可以完全访问您的数据库。 顾名思义,这是受信任的服务器应用程序通常使用的角色,尽管也可以创建具有有限权限的自定义角色的密钥。 当您创建生产应用程序时,您会希望扮演一个更具限制性的角色。
默认情况下,Fauna 中的所有内容都是私有的,因此您现在将创建一个新角色以允许登录用户从 Things
集合中读取文档。
在仪表板的 Security 部分,转到 Roles,并创建一个名为 User
的新自定义角色。
在 Collections 下拉列表中,添加 Things
集合并按下 Read 权限,使其显示绿色复选标记:
在保存角色之前,转到 Membership 选项卡并将 Users
集合添加到角色:
您现在可以通过按下 保存 按钮来保存您的 User
自定义角色。
现在,任何从 Users
集合中的文档登录的用户都可以从 Things
集合中读取任何文档。
有了身份验证和授权,现在让我们创建将与 Fauna 对话的 Python API。
第 3 步 — 构建 Python 应用程序
在这一步中,您将使用 Flask 框架构建一个小型 REST API,并使用 Python 编写 FQL 查询,使用 Fauna 驱动程序连接到您的 Fauna 数据库。
首先,创建一个项目文件夹并从您的终端访问它。
首先安装 Flask:
pip install flask
然后安装 Fauna Python 驱动程序:
pip install faunadb
在您的项目文件夹中,创建文件 main.py
并将以下代码添加到文件中,其中添加了必要的导入、FAUNA_SECRET
环境变量和 Flask 应用程序的基本配置:
主文件
import os FAUNA_SECRET = os.environ.get('FAUNA_SECRET') import flask from flask import request import faunadb from faunadb import query as q from faunadb.client import FaunaClient app = flask.Flask(__name__) app.config["DEBUG"] = True
FAUNA_SECRET
环境变量将携带您之前创建的服务器密码。 为了能够在本地或云中运行此应用程序,需要注入此变量。 没有它,应用程序将无法连接到 Fauna。 您将在启动应用程序时提供此环境变量。
现在将 /signup
路由添加到 main.py
文件。 这将在 Users
集合中创建新文档:
主文件
@app.route('/signup', methods=['POST']) def signup(): body = request.json client = FaunaClient(secret=FAUNA_SECRET) try: result = client.query( q.create( q.collection("Users"), { "data": { "username": body["username"] }, "credentials": { "password": body["password"] } } ) ) return { "userId": result['ref'].id() } except faunadb.errors.BadRequest as exception: error = exception.errors[0] return { "code": error.code, "description": error.description }, 409
请注意,Fauna 客户端正在使用服务器密钥对每个请求进行实例化:
主文件
... client = FaunaClient(secret=FAUNA_SECRET) ...
一旦用户登录,API 将代表每个用户使用不同的秘密执行查询,这就是为什么在每个请求上实例化客户端是有意义的。
与其他数据库不同,Fauna 客户端不维护持久连接。 在外界看来,Fauna 的行为就像一个 API; 每个查询都是一个 HTTP 请求。
客户端准备就绪后,将执行 FQL 查询,在 Users
集合中创建一个新文档。 每个 Fauna 驱动程序都将惯用语法转换为 FQL 语句。 在此路线中,您添加了以下查询:
主文件
... q.create( q.collection("Users"), { "data": { "user": json["user"] }, "credentials": { "password": json["password"] } } ) ...
这是这个查询在原生 FQL 中的样子:
Create( Collection("Users"), { "data": { "user": "sammy" }, "credentials": { "password": "secretpassword" } } )
除了文档数据之外,您还添加了带有用户密码的 credentials
配置。 文档的这一部分是完全私密的。 之后您将永远无法读取文档的凭据。 在使用 Fauna 的身份验证系统时,不可能错误地暴露用户的密码。
最后,如果已经有相同用户名的用户,则会引发 faunadb.errors.BadRequest
异常,并向客户端返回带有错误信息的 409
响应。
接下来,在 main.py
文件中添加 /login
路由来验证用户和密码。 这遵循与前一个示例类似的模式; 您使用 Fauna 连接执行查询,如果身份验证失败,则引发 faunadb.errors.BadRequest
异常并返回带有错误信息的 401
响应。 将此代码添加到 main.py
:
主文件
@app.route('/login', methods=['POST']) def login(): body = request.json client = FaunaClient(secret=FAUNA_SECRET) try: result = client.query( q.login( q.match( q.index("Users_by_username"), body["username"] ), {"password": body["password"]} ) ) return { "secret": result['secret'] } except faunadb.errors.BadRequest as exception: error = exception.errors[0] return { "code": error.code, "description": error.description }, 401
这是用于使用 Fauna 对用户进行身份验证的 FQL 查询:
主文件
q.login( q.match( q.index("Users_by_username"), body["username"] ), {"password": body["password"]} )
这是这个查询在原生 FQL 中的样子:
Login( Match( Index("Users_by_username"), "sammy" ), {"password": "secretpassword"} )
Match
使用我们之前创建的 Users_by_username
索引返回对文档的引用。
如果提供的密码与引用的文档匹配,Login
将创建一个新令牌并返回具有以下键的字典:
ref
引用新文档的令牌。ts
带有交易的时间戳。instance
引用用于进行身份验证的文档。secret
带有令牌的秘密,将用于对 Fauna 进行进一步查询。
如果您在 Fauna 仪表板的 shell 中运行该 FQL 查询,您将看到类似于以下内容的内容:
{ ref: Ref(Ref("tokens"), "292001047221633538"), ts: 1614732749110000, instance: Ref(Collection("Users"), "291901454585692675"), secret: "fnEEDWVnxbACAgQNBIxMIAIIKq1E5xvPPdGwQ_zUFH4F5Dl0neg" }
根据项目的安全要求,您必须决定如何处理令牌的秘密。 如果此 API 旨在供浏览器使用,您可能会在安全 cookie 或加密的 JSON Web 令牌 (JWT) 中返回秘密。 或者,您可以将其作为会话数据存储在其他地方,例如 Redis 实例。 出于本演示的目的,您在 HTTP 响应的正文中返回它:
最后,将这段代码添加到 main.py
中,这将启动 Flask 应用程序:
主文件
app.run(host=os.getenv('IP', '0.0.0.0'), port=int(os.getenv('PORT', 8080)))
指定 0.0.0.0
地址很重要。 一旦部署到云端,该应用程序将在 Docker 容器中运行。 如果它在 127.0.0.1
上运行,它将无法接收来自远程客户端的请求,这是 Flask 应用程序的默认地址。
这是到目前为止完整的 main.py
文件:
主文件
import os FAUNA_SECRET = os.environ.get('FAUNA_SECRET') import flask from flask import request import faunadb from faunadb import query as q from faunadb.client import FaunaClient app = flask.Flask(__name__) app.config["DEBUG"] = True @app.route('/signup', methods=['POST']) def signup(): body = request.json client = FaunaClient(secret=FAUNA_SECRET) try: result = client.query( q.create( q.collection("Users"), { "data": { "username": body["username"] }, "credentials": { "password": body["password"] } } ) ) return { "userId": result['ref'].id() } except faunadb.errors.BadRequest as exception: error = exception.errors[0] return { "code": error.code, "description": error.description }, 409 @app.route('/login', methods=['POST']) def login(): body = request.json client = FaunaClient(secret=FAUNA_SECRET) try: result = client.query( q.login( q.match( q.index("Users_by_username"), body["username"] ), {"password": body["password"]} ) ) return { "secret": result['secret'] } except faunadb.errors.BadRequest as exception: error = exception.errors[0] return { "code": error.code, "description": error.description }, 401 app.run(host=os.getenv('IP', '0.0.0.0'), port=int(os.getenv('PORT', 8080)))
保存文件。
要从终端本地启动此服务器,请使用以下命令和 FAUNA_SECRET
环境变量以及您在创建服务器密钥时获得的密钥:
FAUNA_SECRET=your_fauna_server_secret python main.py
触发该命令后,Flask 将显示警告,通知您它正在与开发 WSGI 服务器一起运行。 这对于本演示来说很好,因此您可以放心地忽略此警告。
通过使用 curl
命令发出 HTTP 请求来测试您的 API。 打开一个新的终端窗口并运行以下命令:
使用以下命令创建用户:
curl -i -d '{"user":"sammy", "password": "secretpassword"}' -H 'Content-Type: application/json' -X POST http://0.0.0.0:8080/signup
您将看到以下响应,表明用户创建成功:
HTTP/1.0 200 OK Content-Type: application/json Content-Length: 37 Server: Werkzeug/1.0.1 Python/3.9.2 Date: Thu, 04 Mar 2021 01:00:47 GMT { "userId": "292092166117786112" }
现在使用以下命令验证该用户:
curl -i -d '{"user":"sammy", "password": "secretpassword"}' -H 'Content-Type: application/json' -X POST http://0.0.0.0:8080/login
你会得到这个成功的响应:
HTTP/1.0 200 OK Content-Type: application/json Content-Length: 70 Server: Werkzeug/1.0.1 Python/3.9.2 Date: Thu, 04 Mar 2021 01:01:19 GMT { "secret": "fnEEDbhO3jACAAQNBIxMIAIIOlDxujk-VJShnnhkZkCUPKIHxbc" }
关闭运行 curl
命令的终端窗口,然后切换回运行 Python 服务器的终端。 按 CTRL+C
停止服务器。
现在应用程序正在运行,我们将添加一个需要对用户进行身份验证的私有端点。
第 4 步 — 添加私有端点
在此步骤中,您将向 API 添加一个私有端点,这将要求首先对用户进行身份验证。
首先,在 main.py
文件中新建一条路由。 此路由将响应 /things
端点。 将它放在使用 app.run()
方法启动服务器的行上方:
主文件
@app.route('/things', methods=['GET']) def things():
接下来,在 /things
路由中,实例化 Fauna 客户端:
主文件
userSecret = request.headers.get('fauna-user-secret') client = FaunaClient(secret=userSecret)
该路由不使用服务器密码,而是使用来自 fauna-user-secret
HTTP 标头的用户密码,该标头用于实例化 Fauna 客户端。 通过使用用户的机密而不是服务器机密,FQL 查询现在将受制于我们之前在仪表板中配置的授权规则。
然后将此 try
块添加到路由以执行查询:
主文件
try: result = client.query( q.map_( q.lambda_("ref", q.get(q.var("ref"))), q.paginate(q.documents(q.collection("Things"))) ) ) things = map( lambda doc: { "id": doc["ref"].id(), "name": doc["data"]["name"], "color": doc["data"]["color"] }, result["data"] ) return { "things": list(things) }
这将执行 FQL 查询并将 Fauna 响应解析为可序列化类型,然后在 HTTP 响应正文中作为 JSON 字符串返回。
最后,将这个 except
块添加到路由中:
主文件
except faunadb.errors.Unauthorized as exception: error = exception.errors[0] return { "code": error.code, "description": error.description }, 401
如果请求不包含有效密钥,则会引发 faunadb.errors.Unauthorized
异常并返回带有错误信息的 401 响应。
这是 /things
路线的完整代码:
主文件
@app.route('/things', methods=['GET']) def things(): userSecret = request.headers.get('fauna-user-secret') client = FaunaClient(secret=userSecret) try: result = client.query( q.map_( q.lambda_("ref", q.get(q.var("ref"))), q.paginate(q.documents(q.collection("Things"))) ) ) things = map( lambda doc: { "id": doc["ref"].id(), "name": doc["data"]["name"], "color": doc["data"]["color"] }, result["data"] ) return { "things": list(things) } except faunadb.errors.Unauthorized as exception: error = exception.errors[0] return { "code": error.code, "description": error.description }, 401
保存文件并再次运行您的服务器:
FAUNA_SECRET=your_fauna_server_secret python main.py
要测试此端点,首先通过使用有效凭据进行身份验证来获取密钥。 打开一个新的终端窗口并执行这个 curl
命令:
curl -i -d '{"username":"sammy", "password": "secretpassword"}' -H 'Content-Type: application/json' -X POST http://0.0.0.0:8080/login
此命令返回成功响应,尽管 secret
的值会有所不同:
HTTP/1.0 200 OK Content-Type: application/json Content-Length: 70 Server: Werkzeug/1.0.1 Python/3.9.2 Date: Thu, 04 Mar 2021 01:01:19 GMT { "secret": "fnEEDb...." }
现在 hen 使用秘密向 /things
发出 GET 请求:
curl -i -H 'fauna-user-secret: fnEEDb...' -X GET http://0.0.0.0:8080/things
你会得到另一个成功的响应:
HTTP/1.0 200 OK Content-Type: application/json Content-Length: 118 Server: Werkzeug/1.0.1 Python/3.9.2 Date: Thu, 04 Mar 2021 01:14:49 GMT { "things": [ { "color": "Yellow", "id": "292079274901373446", "name": "Banana" } ] }
关闭运行 curl
命令的终端窗口。 返回到您的服务器正在运行的窗口并使用 CTRL+C
停止服务器。
现在您有了一个可以工作的应用程序,您就可以部署它了。
第 4 步 — 部署到 DigitalOcean
在本教程的最后一步中,您将在 App Platform 上创建一个应用程序并从 GitHub 存储库部署它。
在将项目推送到 Git 存储库之前,请确保在项目文件夹中运行以下命令:
pip freeze > requirements.txt
这将创建一个 requirements.txt
文件,其中包含部署应用程序后需要安装的依赖项列表。
现在将您的项目目录初始化为 Git 存储库:
git init
现在执行以下命令将文件添加到您的存储库:
git add .
这将添加当前目录中的所有文件。
添加文件后,进行初始提交:
git commit -m "Initial version of the site"
您的文件将提交。
打开浏览器并导航到 GitHub,使用您的个人资料登录,然后创建一个名为 sharkopedia
的新存储库。 创建一个没有 README
或许可证文件的空存储库。
创建存储库后,返回命令行将本地文件推送到 GitHub。
首先,将 GitHub 添加为远程存储库:
git remote add origin https://github.com/your_username/sharkopedia
接下来,重命名默认分支 main
,以匹配 GitHub 的期望:
git branch -M main
最后,将你的 main
分支推送到 GitHub 的 main
分支:
git push -u origin main
您的文件将被传输。 您现在已准备好部署您的应用程序。
注意:为了能够在App Platform上创建应用程序,您首先需要将付款方式添加到您的DigitalOcean帐户。
该应用程序将在一个每月花费 5 美元的容器上运行,尽管测试它只需要几美分。 完成后不要忘记删除应用程序,否则您将继续被收费。
转到 DigitalOcean 仪表板的 Apps 部分,然后单击 Launch Your App:
选择部署源。 您需要授权 DigitalOcean 才能阅读您的 Github 存储库。 授权访问后,选择包含您的 Python 项目的存储库和包含您要部署的应用程序版本的分支:
此时,App Platform 将确定您的项目使用 Python,并让您配置一些应用程序选项:
设置以下选项
- 确保 Type 是 Web Service。
- 使用您的服务器密码创建一个
FAUNA_SECRET
环境变量。 - 将 运行命令 设置为
python main.py
。 - 将 HTTP 端口 设置为
8080
。
接下来,为您的应用输入一个名称并选择一个部署区域:
接下来,选择每月 5 美元的 Basic 计划和 Basic Size:
之后,向下滚动并单击 Launch Your App。
完成应用程序的配置后,将创建一个容器并将其与您的应用程序一起部署。 首次初始化需要几分钟,但后续部署会快得多。
在应用程序的仪表板中,您将看到一个绿色复选标记,表示部署过程已成功完成:
您现在将能够对提供的应用程序域执行 HTTP 请求。 在终端中执行以下命令,将 your_app_name
替换为您的实际应用名称,以返回 sammy
用户的新密钥:
curl -i -d '{"user":"sammy", "password": "secretpassword"}' -H 'Content-Type: application/json' -X POST https://your_app_name.ondigitalocean.app/login
您将收到类似于以下内容的响应:
HTTP/1.0 200 OK Content-Type: application/json Content-Length: 70 Server: Werkzeug/1.0.1 Python/3.9.2 Date: Thu, 04 Mar 2021 01:01:19 GMT { "secret": "fnAADbhO3jACEEQNBIxMIAOOIlDxujk-VJShnnhkZkCUPKIskdjfh" }
您的应用程序现在已在 Digital Ocean 上启动并运行。
结论
在本教程中,您使用 Fauna 作为数据层创建了一个 Python REST API,并将其部署到 DigitalOcean 应用平台。
要继续了解 Fauna 并深入了解 FQL,请查看 Fauna 文档。