原文:
zh.annas-archive.org/md5/349aae26397953b4cb6aac827cad2f26
译者:飞龙
第五章:通过 GitHub Copilot 和 Copilot Chat 管理数据
本章内容涵盖
-
将我们的数据持久化到关系型数据库
-
使用 Apache Kafka 进行数据流式传输
-
融合事件驱动原则
-
使用 Spark 分析我们的数据以监控位置
上一章为我们的信息技术资产管理系统奠定了基础。然而,没有数据,这个应用程序将无法满足我们的要求。数据是每个应用程序的命脉。这正是本章的主题:我们可以使用生成式 AI 来创建数据、流式传输数据、转换数据、对数据做出反应并从数据中学习的各种方式。
敏锐的人可能已经注意到在上一章中,我们的数据访问模式是无法正常工作的,因为它是不完整的。本章的开头部分将解决这个问题。之后,我们将设置我们的数据库,修复访问这些数据的类,并加载一些示例数据,以便在本章的其余部分使用。
5.1 积累我们的数据集
我们的第一个任务将是构建一个大量的数据集,以帮助我们在本章剩余的实验中。首先,我们将使用 GitHub Copilot 生成一千行资产信息。然而,我们很快会发现,这可能不是最适合这项任务的工具。使用这些工具的一个关键驱动因素是发现的概念:测试它们的边界,推动它们,有时候,反击它们。但旅程往往是快乐的源泉。一旦我们找到了这个边缘,我们将被介绍一个新的、以前从未见过的工具:GitHub Copilot Chat。最后,一旦我们创建了我们的资产列表,我们将再次使用 GitHub Copilot Chat 为这些资产添加位置信息。
在构建我们的初始数据集之前,我们需要让数据库运行起来。Docker 让这个任务变得微不足道,让我们能够快速启动一个空的 Postgres(或其他 RDBMS/NoSQL 服务器),几乎不费吹灰之力。你忘记了执行此操作的命令吗?别担心,我们可以问 Copilot。打开一个名为 data/initial_data_laod.sql 的新文件,并在你的新生成的 SQL 文件顶部输入以下提示。
片段 5.1 提示 GitHub Copilot 为我们提供一个 Docker 命令
-- Question: What is the command to run a docker container, running PostgreSQL for a database called itam_db. I would like to specify the password for this database.
Copilot 将逐渐揭示 Docker 命令:--Answer: docker run --name itam_db -e POSTGRES_PASSWORD=postgres -d -p 5432:5432 postgres.
一旦你在终端或命令行运行这个命令,我们就可以构建出我们的数据集。你应该能够连接到本地运行的数据库。你应该注意到里面有一个名为 itam_db 的数据库在运行。然而,这个数据库没有模式、表或数据。让我们首先设置一个新的模式。
在我们的 initial_data_file.sql 中,我们将添加一个提示,让 Copilot 起草模式创建命令。执行此命令将允许您从数据库客户端应用程序(例如 DataGrip、SQuirreL、pdAdmin,甚至使用 Docker exec 命令 docker exec -i itam_db psql -U postgres -c "create schema itam"
)中创建名为 itam 的新模式。
提示让 Copilot 创建新模式
-- create a schema called itam
create schema itam;
接下来,我们应该为应用程序中使用的用户添加一个用户。此用户将能够对我们的数据执行CRUD(创建、读取、更新、删除)操作,但将无法影响数据库表或存储过程的结构。
注意
以双破折号开头的行是 SQL 中的注释。从 Copilot 的角度来看,注释这些行是可选的,因为它将生成没有注释的解决方案;这样做可以更轻松地将代码直接复制粘贴到我们选择的数据库工具中。
在此期间,我们还将添加一个管理帐户,以执行我们的读写用户无法执行的操作,例如创建或删除表。
提示创建新用户
-- create two users called itam_user and itam_admin. itam_user should be able to read from and write to the tables in the itam schema. itam_admin should be able to read from and write to the tables in the itam schema and create and drop tables in the itam schema.
create user itam_user with password 'itam_user';
create user itam_admin with password 'itam_admin';
grant usage on schema itam to itam_user;
grant usage on schema itam to itam_admin;
grant select, insert, update, delete on all tables in schema itam to itam_user;
grant select, insert, update, delete on all tables in schema itam to itam_admin;
grant usage, drop on schema itam to itam_admin;
接下来,我们将把此模式的所有权授予 itam_admin 帐户。转移此所有权将确保只有此帐户可以更改表结构:数据定义。
将模式所有权转移给管理员帐户的提示
-- grant ownership of the itam schema to itam_admin
alter schema itam owner to itam_admin;
配置完成、帐户创建和系统的崇拜已经完成,我们可以开始专注于数据。我们将从添加参考数据开始,即支持资产的数据:折旧策略。这些数据的性质更加静态;它们的变化频率较低,甚至根本不变。接下来,我们将定义并存储这些策略。
提示创建折旧策略表
-- create a table called depreciation_strategy in the itam schema. the table should have the following columns: id (int), name (varchar), and description (varchar). the table should have a primary key on id.
-- id needs to be in quotes because it is a reserved word in postgresql
-- there are two values for depreciation_strategy: straight line and double declining balance
create table itam.depreciation_strategy (
"id" int primary key,
"name" varchar,
"description" varchar
);
我们将使用序列作为此表的主键。虽然对于一个不会很大并且我们可以手动输入已知值的表而言,这并不是严格必要的,但是添加此序列将允许我们与 Copilot 更多地合作并让它提出一些建议。此外,询问 Copilot 并在文本文件中获得 Copilot 的回答是有趣的。
提示为折旧策略表的主键创建序列
-- create a sequence called depreciation_strategy_seq, which should start at 1 and increment by 1 and should be used as the primary key for the depreciation_strategy table.
create sequence itam.depreciation_strategy_seq start 1 increment 1;
自然地,有了我们手中的序列,我们需要知道如何将序列与depreciation_stategy
表的主键列关联起来。幸运的是,Copilot 有答案。
询问 Copilot 如何将序列与主键关联
-- question: how do I make the sequence the primary key for the depreciation_strategy table?
-- answer: use the following command
alter table itam.depreciation_strategy alter column "id" set default nextval('itam.depreciation_strategy_seq'::regclass);
最后,我们将通过将以下静态条目插入表格来完成此表格。目前我们只使用两种折旧策略:直线法和双倍余额递减法。
将静态条目添加到折旧策略表
insert into depreciation_strategy (id, name, description) values (1, 'straight line', 'straight line');
insert into depreciation_strategy (id, name, description) values (2, 'double declining balance', 'double declining balance');
接下来,我们将转向资金细节表。这些信息告诉我们如何为我们的设备进行融资,再销售价值,并对资产在其有用生命周期结束后应采取的措施进行说明。我们在折旧策略中所做的步骤顺序将与此相同,唯一的区别是我们不会添加静态条目,因为这些数据直接与个体资产相关。我们将定义表,创建序列,并将该序列应用于表,作为主键的功能。
列表 5.6 资金详情表的完整代码列表
-- create a table called funding_details in the itam schema. the table should have the following columns: id (int), name (varchar),depreciation_strategy_id (int) and depreciation_rate (float). the table should have a primary key on id.
-- depreciation_stategy_id is a foreign key to the depreciation_strategy table.
-- id needs to be in quotes because it is a reserved word in postgresql
create table itam.funding_details (
"id" int primary key,
"name" varchar,
"depreciation_strategy_id" int,
"depreciation_rate" float
);
-- create a sequence called funding_details_seq, which should start at 1 and increment by 1 and should be used as the primary key for the funding_details table.
create sequence itam.funding_details_seq start 1 increment 1;
alter table itam.funding_details alter column "id" set default nextval('itam.funding_details_seq'::regclass);
我们将定义和生成的最后信息是资产本身。这个列表也是冗余的,但出于完整性考虑已包括在内。最后,我们创建表,创建序列,并将其用作主键。
列表 5.7 资产表的完整代码列表
-- create a table called assets in the itam schema. the table should have the following columns:
-- id (int), name (varchar), status (varchar), category (varchar), cost (float), useful_life (int), salvage_value (float), purchase_date (date), funding_details_id (int). The table should have a primary key on id and a foreign key on funding_details_id.
-- id needs to be in quotes because it is a reserved word in postgresql
-- the table should have a sequence called assets_id_seq, which should start at 1 and increment by 1 and should be used as the primary key for the assets table.
create table itam.assets (
"id" int primary key,
"name" varchar,
"status" varchar,
"category" varchar,
"cost" float,
"useful_life" int,
"salvage_value" float,
"purchase_date" date,
"funding_details_id" int
);
-- create a sequence called assets_seq, which should start at 1 and increment by 1 and should be used as the primary key for the assets table.
create sequence itam.assets_seq start 1 increment 1;
alter table itam.assets alter column "id" set default nextval('itam.assets_seq'::regclass);
在定义和创建表之后,我们现在将专注于创建数据。在我们的文本文件中,我们使用参数指示 Copilot 我们正在寻找的数据集。Copilot 可能会尝试帮助您概述围绕新数据集的属性。
列表 5.8 为资产表创建数据集
-- Generate a dataset of assets for an ITAM system. The dataset should include the following columns: id (int), name (varchar), status (varchar), category (varchar), cost (float), useful_life (int), salvage_value (float), purchase_date (date), funding_details_id (int). The dataset should have 1000 rows, sorted by id. Each row should have the following characteristics:
-- - id should be a unique integer and sequential starting at 1.
-- - name should be a random string of characters between 1 and 50 characters long.
-- - status should be a random selection from the following valid asset statuses: in use, in storage, disposed of, in repair, in transit, other.
-- - category should be a random selection from the following valid categories: hardware, software, other.
-- - cost should be a random float between 0 and 100000.
-- - useful_life should be a random int between 1 and 10.
-- - salvage_value should be a random float greater than 0 but less than the cost of the asset.
-- - purchase_date should be a random date between 1/1/2019 and 12/31/2022.
-- - funding_details_id should be a random integer either 1 or 2.
-- The dataset should be saved as a CSV file named assets.csv in the data directory. The file should have a header row and the columns should have the following data types: id (int), name (varchar), status (varchar), category (varchar), cost (float), useful_life (float), salvage_value (float), funding_details_id (int)
Copilot 提供的解决方案是新颖的。它使用 Postgres 内置功能构建了一个大系列,这意味着这不是一个可移植的解决方案。然而,考虑到这是我们将要使用的数据库,这是一个足够合适的解决方案。生成的数据集经过了精心制作。如果我们使用 Python 并要求 Copilot 帮助编写一个用于加载到 Postgres 的文件的脚本,我们可能会得到更好的结果。然而,鉴于这个数据集只是用来玩应用程序,目前我们不需要过于担心数据质量。尽管在现实世界中,数据质量是至关重要的。
列表 5.9 Copilot 的响应:基于一系列构建的插入语句
insert into itam.assets (id, name, status, category, cost, useful_life, salvage_value, purchase_date, funding_details_id)
select
id,
name,
status,
category,
cost,
useful_life,
salvage_value,
purchase_date,
funding_details_id
from (
select
row_number() over (order by random()) as id,
md5(random()::text) as name,
case
when random() < 0.2 then 'in use'
when random() < 0.4 then 'in storage'
when random() < 0.6 then 'disposed of'
when random() < 0.8 then 'in repair'
when random() < 0.9 then 'in transit'
else 'other'
end as status,
case
when random() < 0.5 then 'hardware'
when random() < 0.9 then 'software'
else 'other'
end as category,
random() * 100000 as cost,
(random() * 100)::int as useful_life,
random() * (random() * 100000) as salvage_value,
-- generate a random date between 1/1/2019 and 12/31/2022
-- this does not work please fix
-- '2019-01-01'::date + random() * ('2022-12-31'::date - '2019-01-01'::date) as purchase_date,
'2019-01-01'::date + (random() * (DATE '2022-12-31' - DATE '2019-01-01')::integer)::integer as purchase_date
case
when random() < 0.5 then 1
else 2
end as funding_details_id
from generate_series(1, 1000)
) as assets;
如果我们仅仅转向 ChatGPT,我们可以得到如何创建这样的数据集的第二意见。ChatGPT 建议使用 Python 库faker
。faker
包用于生成虚假数据,例如常见的英文姓名。numpy
用于生成成本、有用生命和残值的随机浮点值。pandas
用于在DataFrame
(表)中管理数据。此外,我们可以使用df.to_csv('assets.csv', index=False)
方法将DataFrame
保存到 CSV 文件中。
列表 5.10 ChatGPT 建议使用 Faker 来生成虚假数据集
import pandas as pd
import numpy as np
import random
from faker import Faker
# Create Faker object
fake = Faker()
# Define our valid statuses, categories, funding_details and depreciation_strategies
valid_statuses = ['in use', 'in storage', 'disposed', 'in repair', 'in transit', 'other']
valid_categories = ['hardware']
valid_funding_details = ['purchased']
valid_depreciation_strategies = ['straight line']
# Generate the data
data = {
'id': list(range(1, 1001)),
'name': [fake.first_name() for _ in range(1000)],
'status': [random.choice(valid_statuses) for _ in range(1000)],
'category': [random.choice(valid_categories) for _ in range(1000)],
'cost': np.random.uniform(0, 100000, 1000),
'useful_life': np.random.uniform(1, 10, 1000),
'salvage_value': np.random.uniform(0, 10000, 1000),
'funding_details': [random.choice(valid_funding_details) for _ in range(1000)],
'depreciation_strategy': [random.choice(valid_depreciation_strategies) for _ in range(1000)],
}
# Create DataFrame
df = pd.DataFrame(data)
# Ensure the DataFrame is sorted by id
df = df.sort_values('id')
# Show the DataFrame
print(df)
对于这些资产的每一项,我们也需要资金细节;它们是如何融资的(在这种情况下购买),以及折旧细节。毫不奇怪,我们从 Copilot 得到了类似的解决方案:使用与我们为资产使用的类似提示生成一系列条目。我们需要确保对于每个资产标识符(1-1000),我们都有相应的资金细节条目。否则,在运行我们的代码时会出现空指针的风险。
清单 5.11 创建funding_details
表的数据集
-- Generate a dataset of funding details for an ITAM system. The dataset should include the following columns: id (int), name (varchar), depreciation_strategy_id (int), depreciation_rate (float). The dataset should have 1000 rows, sorted by id.
-- Each row should have the following characteristics:
-- - id should be a unique integer and sequential starting at 1.
-- - name should be a random string of characters between 1 and 10 characters long.
-- - depreciation_strategy_id should be a random integer either 1 or 2.
-- - depreciation_rate should be a random float between 0 and .4.
insert into itam.funding_details (id, name, depreciation_strategy_id, depreciation_rate)
select
id,
name,
depreciation_strategy_id,
depreciation_rate
from (
select
row_number() over (order by random()) as id,
md5(random()::text) as name,
case
when random() < 0.5 then 1
else 2
end as depreciation_strategy_id,
random() * 0.4 as depreciation_rate
from generate_series(1, 1000)
) as funding_details;
有了在数据库中生成和存储的数据集,我们应该能够通过 REST API 连接我们应用程序的其余部分来存储和显示资产。然而,由于我们在构建阶段之前已经剥离了所有的 SQLAlchemy 元数据(请参阅上一章),我们需要一种不同的方法来将这些元数据与我们的适配器进行连接。
有了这个,我们已经达到了 Copilot 的能力边缘。我们对接下来会发生什么感到困惑;我们如何解决我们最近的困境。尽管诱人,我们不能放弃回家。因此,现在是时候介绍 Copilot 产品套件的最新成员了:Copilot Chat。Copilot Chat 是嵌入式的 GPT-4 模型,可以集成到你的 IDE 中(目前仅支持 Visual Studio Code)。我们将打开聊天对话框,询问如何保持我们的业务模型清晰,但仍然使用 SQLAlchemy 的 ORM(对象关系模型)功能。
图 5.1 GitHub Copilot Chat 对我们最近困境的解决方案
Copilot Chat 建议我们创建一个单独的数据访问层。这种方法与我们迄今为止使用的端口和适配器方法非常契合。此外,Copilot Chat 建议将这些类建模得与领域类类似,但包括 ORM 功能正常工作所需的元数据。生成的代码见清单 5.12。
清单 5.12 领域类之外的 ORM 支持。
Base = declarative_base()
class FundingDetailsModel(Base):
__tablename__ = 'funding_details'
id = Column(Integer, primary_key=True)
depreciation_rate = Column(Float)
depreciation_strategy_id = Column(Integer)
def get_depreciation_strategy(self) -> DepreciationStrategy:
if self.depreciation_strategy_id is 1:
return StraightLineDepreciationStrategy()
else:
return DoubleDecliningDepreciationStrategy()
class AssetModel(Base):
__tablename__ = 'assets'
id = Column(Integer, primary_key=True)
name = Column(String)
status = Column(String)
category = Column(String)
cost = Column(Float)
useful_life = Column(Float)
salvage_value = Column(Float)
purchase_date = Column(Date)
funding_details_id = Column(Integer, ForeignKey('funding_details.id'))
funding_details = relationship('FundingDetailsModel')
现在,外部模型类已经创建,我们必须将这些 ORM 实例映射到我们的领域模型,然后返回给系统核心。虽然对于这样一个简单的应用程序来说,这似乎是过度设计的代码,但这给了我们很大的灵活性,使我们的领域模型可以进行复杂的操作。例如,我们的模型可以执行 CRUD 之外的复杂操作。如果我们将我们的领域模型身份限制在数据访问层中使用的模型上,我们将受到这些操作的限制。
接下来,我们将使用 Copilot 和 Copilot Chat 探索将事件驱动思想纳入我们的应用程序中。事件驱动的概念将允许我们实时跟踪我们的 IT 资产:它们的位置、状态和市场价值,例如。
5.2 使用 Kafka 实时监控我们的资产
我们将实时监控我们的资产,以激励我们探索将生成式人工智能与事件驱动架构结合使用的探索。我们应该认为,信息安全资产管理系统外部的某些系统在我们的资产从一个位置移动到另一个位置时会触发事件。
要深入了解 ITAM 事件,我们需要配置一些额外的服务。在这种情况下,我们将使用 Apache Kafka。Apache Kafka 是一个分布式流平台,用于构建实时数据管道和流应用程序。它被设计用于处理来自多个来源的数据流,并将它们传送到多个消费者,有效地充当我们实时数据的中间人。
首先,我们将询问 Copilot Chat 如何在本地使用 Docker 运行 Kafka。Apache Kafka 有一个不应该存在的声誉,即安装和配置很困难。在 Docker 中运行将允许我们回避这个争议。使用 Copilot Chat,我们可以生成一个 docker compose 文件。然而,通常情况下,版本非常旧,甚至不支持一些硬件。图示 5.13 是从 Confluent(提供 Kafka 商业支持的公司)官方 GitHub 存储库中提取的更新的列表。请注意,docker-compose 文件的内容包括 Kafka 和 Zookeeper。Zookeeper 是 Kafka 用来管理和协调集群内代理的分布式协调服务,至少目前是这样。未来版本的目标是消除对 Zookeeper 的依赖。
图示 5.13 Docker-Compose 文件,用于启动带有 Zookeeper 的 Kafka
version: '2.1'
services:
zookeeper:
image: confluentinc/cp-zookeeper:7.3.2
container_name: zookeeper
ports:
- "2181:2181"
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_SERVER_ID: 1
ZOOKEEPER_SERVERS: zoo1:2888:3888
kafka:
image: confluentinc/cp-kafka:7.3.2
hostname: kafka
container_name: kafka
ports:
- "9092:9092"
- "29092:29092"
- "9999:9999"
environment:
KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:19092,EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9092,DOCKER://host.docker.internal:29092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,DOCKER:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
KAFKA_BROKER_ID: 1
KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO"
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
KAFKA_JMX_PORT: 9999
KAFKA_JMX_HOSTNAME: ${DOCKER_HOST_IP:-127.0.0.1}
KAFKA_AUTHORIZER_CLASS_NAME: kafka.security.authorizer.AclAuthorizer
KAFKA_ALLOW_EVERYONE_IF_NO_ACL_FOUND: "true"
depends_on:
- zookeeper
现在我们的新 Kafka 实例正在运行,我们现在需要一个消费者,它将从主题asset_location
(我们很快将创建的主题)中拉取更新的位置,并在数据库中更新库存。同样,我们可以请求 Copilot Chat 为我们提供如何做到这一点的建议。
图示 5.14 提示 Copilot Chat 如何使用 Python 订阅主题
In our ITAM project, we would like a Python consumer for the assets_location Kafka topic. Once it receives a new updated location, it should update the domain model to reflect the new location. How should we do this?
Copilot Chat 生成的代码创建一个消费者,监听主题,并使用对AssetManager
类的引用来更新受影响资产的位置。
图示 5.15 Copilot Chat 代码,用于监控资产位置
from kafka import KafkaConsumer
from asset_manager import AssetManager
consumer = KafkaConsumer(
'asset_location',
bootstrap_servers=['localhost:9092'],
auto_offset_reset='earliest',
enable_auto_commit=True,
group_id='my-group',
value_deserializer=lambda x: tuple(map(float, x.decode('utf-8').split(',')))
)
asset_manager = AssetManager()
for message in consumer:
asset_id, latitude, longitude = message.value
asset_manager.update_asset_location(asset_id, (latitude, longitude))
但是,适配器应该知道AssetManager
吗?虽然AssetManager
不直接位于领域模型内部,但它是一个至关重要的中介。适配器和AssetManager
之间的这种耦合程度可能会违反六边形架构模型中的六边形。让我们让 Copilot Chat 来发表意见。我们可以问它:“考虑到这个项目使用了六边形架构,让适配器知道AssetManager
是一个好主意吗?”
图示 5.2 GitHub Copilot Chat 承认它实际上并不完美
看起来 Copilot Chat 认为这确实会被视为适配器的适当承包责任的违约。我们可以向我们的AssetManager
类添加一个新的端口,它可以使用 Kakfa 端口。然而,让我们看看 Copilot Chat 是否有其他建议:“我们不希望 Kafka 消费者直接与AssetManager
进行交互。有什么方法可以实现这一点?” Copilot Chat 建议我们应用Mediator
模式,这类似于一个端口和适配器的组合。
中介者模式
Mediator 设计模式是一种行为模式,通过将对象之间的交互封装在一个中介对象中,促进对象之间的松耦合。中介对象充当一个中心枢纽,促进对象之间的通信,而无需对象知道彼此。
让我们首先看一下 Copilot Chat 生成的 Mediator 类。该类将位于AssetManager
和 Kafka 消费者之间。
图 5.16 Mediator 类,它将调和 AssetManager 和 Kafka 消费者之间的交互
class AssetLocationMediator:
def __init__(self):
self.handlers = {}
def register_handler(self, event_type, handler):
if event_type not in self.handlers:
self.handlers[event_type] = []
self.handlers[event_type].append(handler)
def publish(self, event):
event_type = type(event)
if event_type in self.handlers:
for handler in self.handlers[event_type]:
handler(event)
这个实现与消息队列或消息发布者十分相似,这正是关键所在:解耦。
注意
有慧眼的读者可能会注意到我们在资产和硬件类型的资产之间的区别上有一点疏忽。在原始领域模型中,只有硬件有位置。通常,人们不会认为软件有位置。当然,您可以说软件安装在它的位置上,但人们是否会认为这个论点说服力有待商榷。不过,随着项目的继续进行,领域模型变平的原因是为了简化,因为在持久性层中的多态结构是一个令人分心的复杂主题。
现在我们有了一个将 Kafka 消费者与 AssetManager 解耦的策略,我们应该更新 Kafka 消费者以利用它。我们需要在构造函数中将中介者传递给类。这样,AssetManager 和消费者将可以访问相同的实例,并且消息可以自由地来来往往;或者,在这种情况下,流程将是单向的。您应该注意,我们打算在这个主题上读取和写入 JSON,因此我们需要让我们的值反序列化器理解这一点。
图 5.17 将中介者整合到 Kafka 消费者类中
from kafka import KafkaConsumer
from itam.domain.events.asset_location_updated import AssetLocationUpdated
import json
class AssetLocationKafkaConsumer:
def __init__(self, mediator):
self.mediator = mediator
self.consumer = KafkaConsumer(
'asset_location',
bootstrap_servers=['localhost:9092'],
enable_auto_commit=True,
group_id='itam-group',
value_deserializer=lambda m: json.loads(m.decode('utf-8'))
)
def poll(self):
print("Polling for asset location updates")
for message in self.consumer:
asset_id = message.value['asset_id']
latitude = message.value['latitude']
longitude = message.value['longitude']
timestamp = message.value['timestamp']
event = AssetLocationUpdated(asset_id, latitude, longitude, timestamp)
self.mediator.publish(event)
接下来,我们将检查AssetManager
类需要的更改,以包含跟踪这些位置的功能。您应该注意,要使该项目完全运行起来,您需要修改AssetManager
、SQLAlchemyAssetRepository
和Asset
类,并在数据库中创建一个名为itam.asset_locations
的新表。完整且更新的源代码可在本书的 GitHub 存储库中找到。现在,我们将专注于为我们的系统流动的事件所需的更改,并在需要时使用存储库作为参考。
图 5.3 AssetManager
需要另外一个构造函数参数和一个方法来处理其位置对象的更新
对于AssetManager
类,有两个必需的更改:首先,我们需要将AssetLocationMediator
添加到构造函数中,并注册它来处理AssetLocationUpdated
事件。其次,我们需要添加一个处理此事件的方法。在这种情况下,我们称此方法为update_asset_location
。删节后的代码如下。
图 5.18 更新后的AssetManager
类的构造函数和事件处理程序
from itam.infrastructure.mediators.asset_location_mediator import
class AssetManager:
def __init__(self, base_repository: BaseRepository[Asset], mediator: AssetLocationMediator):
self._repository = base_repository
self.mediator = mediator
self.mediator.register_handler(AssetLocationUpdated, self.update_asset_location)
def update_asset_location(self, event: AssetLocationUpdated) -> None:
asset = self.read(event.asset_id)
asset.add_location(event.latitude, event.longitude, event.timestamp)
#self.update(asset)
print(f"Asset {asset.id} location updated to {event.latitude}, {event.longitude} at {event.timestamp}")
Asset
类的add_location
方法仅仅是将新的位置追加到位置列表的末尾。更复杂的领域模型可能会包括一个current_location
属性,将其余部分归类为历史位置列表;然而,鉴于我们试图使我们的事件在系统中流动,使事情保持简单是明智的。
我们待办事项清单上只有一项最终项目:创建主题。如何创建主题呢?这是一个很好的问题。幸运的是,我们运行的 Docker 容器中有我们需要的所有工具。所以,让我们登录到我们的 Kafka Docker 实例中。我们使用以下命令(假设您的 Docker 实例命名为 kafka):docker exec -it kafka /bin/bash
.
应该首先检查的是是否已经创建了任何主题。我们可以使用以下命令来执行:kafka-topics --list --bootstrap-server localhost:9092.
此命令将列出在此 Kafka 集群上运行的所有现有主题。正如您所见,没有任何主题。
需要一个主题,让我们来创建它。您可以使用以下命令:kafka-topics --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic asset_location.
如果您重新运行kafka-topics --list
命令,您将看到新主题。我们在创建主题命令中包含的分区和复制因子说明告诉 Kafka 我们希望有一个分区和一个复制因子。如果我们是为生产环境或测试以外的任何目的设置这个,我们可能希望它们大于那个数,以确保数据的可用性。
表 5.1 Kafka 控制台命令摘要
动作 | 命令 |
---|---|
创建 |
kafka-topics --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic asset_location
|
读取 |
---|
kafka-console-consumer --broker-list localhost:9092 --topic asset_location –from-beginning
|
写入 |
---|
kafka-console-producer --broker-list localhost:9092 --topic asset_location
|
删除 |
---|
kafka-topics --delete --topic asset_location --bootstrap-server localhost:9092
|
列出 |
---|
kafka-topics --list --bootstrap-server localhost:9092
|
现在是有趣的部分,观察应用程序的运行情况。Kafka 自带一个控制台生产者,允许我们从标准输入向 Kafka 发布消息。要做到这一点,我们应该使用以下命令启动控制台生产者:kafka-console-producer --broker-list localhost:9092 --topic asset_location
。您将进入一个交互式会话,可以在每行发布一条消息。让我们发布一些消息,模拟我们的资产在芝加哥附近移动的情况。
列表 5.19 Kafka 控制台生产者的条目
{"asset_id": 1, "latitude": 41.8781, "longitude": -87.6298, "timestamp": "2022-01-01T00:00:00Z"}
{"asset_id": 1, "latitude": 41.9000, "longitude": -87.6244, "timestamp": "2022-01-01T00:10:00Z"}
{"asset_id": 1, "latitude": 41.8676, "longitude": -87.6270, "timestamp": "2022-01-01T00:20:00Z"}
{"asset_id": 1, "latitude": 41.8788, "longitude": -87.6359, "timestamp": "2022-01-01T00:30:00Z"}
{"asset_id": 1, "latitude": 41.8740, "longitude": -87.6298, "timestamp": "2022-01-01T00:40:00Z"}
{"asset_id": 1, "latitude": 41.8800, "longitude": -87.6300, "timestamp": "2022-01-01T00:50:00Z"}
{"asset_id": 1, "latitude": 41.8775, "longitude": -87.6244, "timestamp": "2022-01-01T01:00:00Z"}
{"asset_id": 1, "latitude": 41.8745, "longitude": -87.6328, "timestamp": "2022-01-01T01:10:00Z"}
{"asset_id": 1, "latitude": 41.8825, "longitude": -87.6298, "timestamp": "2022-01-01T01:20:00Z"}
{"asset_id": 1, "latitude": 41.8800, "longitude": -87.6250, "timestamp": "2022-01-01T01:30:00Z"}
当您输入这些消息时,您应该看到应用程序的输出,指示位置已经更新。
为了完整起见,还有一个命令需要您注意:在输入这些消息时,您可能会犯错。一个无效的消息可能会导致您的消费者出现问题。一个可能的解决方案是删除主题。删除主题可能听起来很夸张,但这将解决问题。因此,这就是那个命令:kafka-topics --delete --topic asset_location --bootstrap-server localhost:9092.
在本节中,我们已经添加了实时跟踪我们的 资产
在 Apache Kafka 中的位置变化的能力。在本章的最后一节中,我们将使用 Copilot Chat 扩展能力,通过实时监控我们的 资产
,尝试确定它们是否位于它们应该位的位置。同样,我们将探讨使用 Spark 和 Kafka 一起完成此分析。完成后,我们将感谢我们的信息安全团队,他们担心我们的核心业务和知识产权过多存在于这些 资产
中。
5.3 使用 Apache Spark 进行分析、学习和跟踪
资产的实时跟踪是一个业务关键功能。您的 IT 资产包含着敏感的商业数据、客户名单、销售数据、利润和损失(PnL)预测以及销售策略,以及许多其他项目。一次丢失的资产可能是公司的生存危机。因此,对于许多信息安全专业人员来说,仔细的管理和监控是首要任务。在本节中,我们旨在使他们的工作变得更加轻松。现代数据平台使实时跟踪资产并在出现可疑情况时发送通知变得微不足道。让我们开始吧。
Apache Spark 是一个强大的、开源的数据处理引擎,以速度、易用性和复杂的分析而闻名。它旨在提供一个比 MapReduce 更好的处理大数据集的替代方案,并且可以处理批处理和实时分析。Spark 提供了 Scala、Java、Python 和 R 的 API,并且具有用于 SQL 查询的内置模块。其核心数据结构,弹性分布式数据集(RDD),支持容错操作,并允许数据在计算机集群上并行处理。
Spark 还包括几个库来扩展其功能,包括用于机器学习的 MLlib,用于处理实时数据流的 Spark Streaming,以及用于处理结构化数据的 Spark SQL 和 DataFrames。这些工具使其非常适合从机器学习到实时数据流和批处理的任务。其内存处理能力使 Spark 比其前身快得多,使其成为大数据处理的热门选择。
首先,我们将要求 Copilot Chat 推荐一种使用 Apache Spark 跟踪我们资产的策略。
清单 5.20 请求 Copilot Chat 如何最好地实时跟踪我们的资产
Let's imagine that our company is in Chicago. We want to create a class called AssetLocationSparkAdapter that will continuously stream AssetLocation json messages from a Kafka topic called asset_location. An AssetLocation has the following attributes asset_id: int, latitude: float, longitude: float, and timestamp: datetime. The AssetLocations are stored in a Postgres database in a table called itam.asset_locations. AssetLocationSparkAdapter should calculate if the new AssetLocation is more than 25 miles from Chicago. If it is, then it should write a message to the console. This should be using the latest version of Spark. How would we do this?
Copilot Chat 生成了应该放入 infrastructure 包中名为 asset_location_spark_adapter.py 的文件的类。幸运的是,它还为每一行包括了注释,因此你应该会发现生成的代码很容易跟踪。导入语句包括 Spark 库以及 geopy。
清单 5.21 运行 Spark 所需的导入项
from pyspark.sql import SparkSession
from pyspark.sql.functions import from_json, col, expr
from pyspark.sql.types import StructType, StructField, IntegerType, DoubleType, TimestampType
from geopy.distance import distance
课程以一个过于臃肿的构造函数开始,该构造函数定义了 Spark 在将 JSON 转换为 DataFrame 时将使用的模式。
注意
如定义的 AssetLocationSparkAdapter
是一个阻塞进程。因此,在 Spark 进程被终止之前,你的 FastAPI 应用程序将不会“完全”启动。你希望这是一个独立的进程,或者你需要引入一个异步框架来使这两个进程同时运行。
接下来,它将启动一个本地 Spark 实例/会话,允许 Spark 连接到 Kafka 主题并持续地流式处理记录。
清单 5.22 负责处理 Kafka 主题并生成通知的 AssessLocationSparkAdapter
class AssetLocationSparkAdapter:
def __init__(self):
# Define the schema for the incoming JSON data
self.schema = StructType([
StructField("asset_id", IntegerType()),
StructField("latitude", DoubleType()),
StructField("longitude", DoubleType()),
StructField("timestamp", TimestampType())
])
# Create a SparkSession
self.spark = SparkSession.builder \
.appName("AssetLocationSparkAdapter") \
.getOrCreate()
# Create a streaming DataFrame from the asset_location topic
self.df = self.spark \
.readStream \
.format("kafka") \
.option("kafka.bootstrap.servers", "localhost:9092") \
.option("subscribe", "asset_location") \
.option("startingOffsets", "earliest") \
.load() \
.selectExpr("CAST(value AS STRING)")
# Parse the incoming JSON data
self.parsed_stream = self.df \
.select(from_json(col("value"), self.schema).alias("data")) \
.select("data.*")
AssetLocationSparkAdapter
类的最后一部分将计算资产当前位置到芝加哥的距离。如果差距大于 25 英里,则将结果集发送到控制台。此外,它还提供了一个方法来启动和停止适配器。
清单 5.23 AssessLocationSparkAdapter
计算当前资产
位置与芝加哥的距离
# Calculate the distance between the current location and Chicago for each asset
self.distance = self.parsed_stream \
.withColumn("distance", expr("calculate_distance(latitude, longitude, 41.8781, -87.6298)")) \
.select(col("asset_id"), col("timestamp"), col("distance")) \
.filter(col("distance") > 25)
# Write the results to the console
self.query = self.distance \
.writeStream \
.outputMode("append") \
.format("console") \
.start()
def run(self):
# Start the streaming query
self.query.awaitTermination()
def stop(self):
# Stop the streaming query and SparkSession
self.query.stop()
self.spark.stop()
calculate_distance 方法接受资产位置的经度和纬度,并使用 geopy.distance 函数确定距离芝加哥的距离。
清单 5.24 Spark 用于计算芝加哥和你的资产
之间距离的函数
def calculate_distance(lat1, lon1, lat2, lon2):
return distance((lat1, lon1), (lat2, lon2)).miles
在这种情况下,Copilot Chat 生成的代码存在一些问题,阻止其在本地运行。在本地运行并遇到这些问题后,搜索 Stack Overflow,你会发现代码的两个主要问题的解决方案:缺少用于本地运行的环境变量,以及未能注册您的 UDF(用户定义的函数)。幸运的是,你不需要进行测试和研究,因为清单 5.23 中提供了解决方案。
清单 5.25 在本地运行应用程序所需的编辑
os.environ['PYSPARK_SUBMIT_ARGS'] = '--packages org.apache.spark:spark-streaming-kafka-0-10_2.12:3.2.0,org.apache.spark:spark-sql-kafka-0-10_2.12:3.2.0 pyspark-shell'
class AssetLocationSparkAdapter:
def __init__(self):
# Create a SparkSession
self.spark = SparkSession.builder \
.appName("AssetLocationSparkAdapter") \
.getOrCreate()
self.spark.udf.register("calculate_distance", calculate_distance)
最后,要运行你的 Spark 应用程序,你需要在 main.py
中的 main
函数中更新以下代码。
列表 5.26 对 main
函数的更新
if __name__ == "__main__":
adapter = AssetLocationSparkAdapter()
adapter.run()
当你将资产位置输入到 Kafka 控制台生产者中时,如果距离芝加哥市中心超过二十五英里,你会注意到条目被写入到控制台中。更新类以将这些结果输出到 Twilio 的短信 API 或类似 SendGrid 的电子邮件服务是微不足道的。
列表 5.27 你的资产位置的流式输出
+--------+-------------------+------------------+
|asset_id| timestamp| distance|
+--------+-------------------+------------------+
| 1|2021-12-31 20:30:00| 712.8314662207446|
+--------+-------------------+------------------+
恭喜,你正在实时追踪你的资产,并在公司资源离开的时候发送实时警报。
5.4 总结
-
GitHub Copilot Chat 是一款创新性工具,结合了 ChatGPT 的全面语言理解和 Copilot 的便捷功能。这是编程辅助领域的一个值得注意的发展,特别是在实时提供详细且上下文相关的建议方面,促进了更高效的编码体验。
-
中介者设计模式是一种独特的行为模式,它促进了对象之间高度解耦,从而增强了代码的模块化。通过在一个中介者对象中包含对象之间的交互,对象可以间接地进行通信,从而减少了依赖性,促进了代码的可重用性和易修改性。
-
Apache Kafka 是一个健壮的、分布式的流平台,专为创建实时数据管道和流应用而设计。它可以有效处理来自多种来源的数据流,并将其传输给各种消费者,使其成为处理大量实时或准实时数据的理想解决方案。需要记住的是,Kafka 优化了追加式、不可变数据,而不适用于需要记录更新或删除,或复杂查询的用例。
-
Apache Spark 是一款性能卓越的、分布式的数据处理引擎,以其速度、易用性和高级分析功能而闻名。它非常适用于需要实时数据处理或对大量数据集进行操作的场景。然而,对于诸如基本分析或简单聚合等较简单的任务,传统的关系型数据库可能是更合适的选择。
-
尽管生成式人工智能迅速发展,但并非无懈可击。仔细审查所有生成的输出以确保其符合你的特定要求和质量标准至关重要。虽然生成式人工智能不能替代深入的领域知识或编码专业知识,但它通过提供有价值的见解和减少在例行任务上花费的时间来显著提高生产力。
第六章:测试、评估和解释大型语言模型
本章内容包括
-
轻松草拟单元测试
-
生成集成测试
-
确定代码质量和覆盖率
-
评估软件复杂性
-
翻译代码和文本
本章将探讨软件工程的一个关键方面:测试。测试软件的行为有多个重要目的。首先,它有助于识别可能会影响软件功能、可用性或性能的错误和问题。此外,它确保软件符合所需的质量标准。通过进行全面的测试,我们可以验证软件是否满足指定的要求,正如预期的那样工作,并产生预期的结果。通过全面的测试,开发人员可以评估软件在各种平台和环境中的可靠性、准确性、效率、安全性和兼容性。在开发过程的早期检测和解决软件缺陷可以节省大量的时间和成本。
当我们完成测试的制定后,我们将评估代码的质量。你将了解到几个有助于评估软件质量和复杂性的度量标准。此外,如果我们需要对代码的目的有更清晰的了解,或者是首次审核代码,我们将寻求解释以确保全面理解。
6.1 测试,测试…一、二、三种类型
测试在软件工程中扮演着重要的角色,因此我们将详细探讨各种类型的测试。这包括单元测试、集成测试和行为测试。首先,我们将利用 Copilot Chat 来帮助我们创建一个单元测试。
单元测试
单元测试是一种专注于测试单个组件或代码单元的测试类型,以确保它们在独立环境中的正确运行。通常由开发人员执行这种测试,以帮助识别特定软件单元中的错误和问题。
6.1.1 单元测试
在本节中,我们将创建单元测试来测试我们的软件组件。Python 的有几个用于单元测试的测试框架。每个框架都有其独特的特点,适用于不同的场景。在我们的 AI 工具提供的建议基础上,我们将简要介绍每个框架,然后选择一个特定的框架。
第一个框架是unittest
:这是 Python 用于创建单元测试的标准库。它与 Python 捆绑在一起,无需单独安装。unittest
提供了丰富的断言集,并非常适合编写简单到复杂的测试用例,但是代码量可能会相当庞大。unittest
适合编写基本的单元测试,特别是如果您不想在项目中引入其他依赖项时。在任何需要独立于系统其余部分确认代码功能的情况下,它都非常有用。
接下来,让我们来看一下pytest
:pytest
是一个流行的第三方库,用于单元测试,尽管它足够灵活,可以处理更多不仅仅是单元测试。它比unittest
需要更少的样板代码,并且具有强大的功能,例如设置和拆卸的 fixture,参数化测试,以及运行unittest
和 nose 测试套件的能力。pytest
非常适合简单和复杂的单元测试用例。它也适用于功能和集成测试。如果你重视简单和易用性,并且你的项目不限于仅使用 Python 标准库,那么pytest
是一个绝佳的选择。
接下来,我们有nose2
,它是被废弃的"nose
"测试框架的继承者。它扩展了unittest
,使测试变得更容易。它以其测试发现功能而闻名,该功能可以自动找到您项目中的测试,因此您无需手动列出它们。nose2
适用于大型项目,其中测试发现可以节省时间。与pytest
一样,它可以运行unittest
测试套件,因此如果您正在从unittest
迁移但希望保留现有的测试,它也是一个不错的选择。
最后,还有doctest
。这是 Python 标准库的另一个模块。它不像其他模块那样是一个功能齐全的测试框架,但它允许你直接在文档字符串中编写测试。doctest
最适合简单的情况,你想演示如何使用一个函数或模块,并确认示例代码是否按预期工作。这是确保你的文档与代码保持最新的好方法。
Copilot Chat 建议我们使用unittest
,所以我们会这样做。你可以自己尝试询问它,并看看它是否会得出不同的结论,这取决于你如何提问。
首先,我们将通过创建从unittest.TestCase
类继承的类来定义测试用例。在编写我们的第一个单元测试时,我们将发现一个相当隐匿的错误,这个错误迄今为止一直使我们束手无策。我们马上就要解决这个问题了。最后,我们将测试DepartmentStatisticsVisitor
类的正确性。
我们从提示开始,请 Copilot Chat 创建一个DepartmentStatisticsVisitor
的单元测试。回复将提醒我们上下文的重要性。Copilot Chat,考虑到我们开始了一个新的聊天会话,将尝试创建这个单元测试,但它缺乏我们任何类的定义。
列表 6.1:为DepartmentStatisticsVisitor
定义单元测试的第一次尝试
import unittest
from department_visitor import DepartmentStatisticsVisitor
class TestDepartmentStatisticsVisitor(unittest.TestCase):
def test_visit_department(self):
visitor = DepartmentStatisticsVisitor()
department = {'name': 'Sales', 'employees': [{'name': 'John', 'salary': 50000}, {'name': 'Jane', 'salary': 60000}]}
visitor.visit_department(department)
self.assertEqual(visitor.total_salaries, 110000)
self.assertEqual(visitor.num_employees, 2)
self.assertEqual(visitor.avg_salary, 55000)
我们应该继续将定义输入到 Copilot Chat 中,以便它可以完善测试用例。我们可以告诉它更多关于访问者类的信息,如下面的代码片段所示。
The DepartmentStatisticsVisitor class visits to determine the total asset depreciation based off of the asset allocation for each department
有了这些更新的信息,Copilot Chat 即将更好地估计我们的单元测试应该如何运行。
列表 6.1:为DepartmentStatisticsVisitor
定义单元测试的第一次尝试
import unittest
from department_visitor import DepartmentStatisticsVisitor, Asset
class TestDepartmentStatisticsVisitor(unittest.TestCase):
def test_visit(self):
visitor = DepartmentStatisticsVisitor()
asset1 = Asset(1000, 500, 0.5)
asset2 = Asset(2000, 1000, 0.3)
visitor.visit(asset1)
visitor.visit(asset2)
self.assertEqual(visitor.get_total_cost(), 3000)
self.assertEqual(visitor.get_total_depreciation(), 1500)
self.assertEqual(visitor.get_total_allocation(), 0.8)
单元测试接近所需的内容;但是,Asset 的定义是错误的。我们应该从我们的 asset.py 类中输入正确的定义。Copilot 聊天将继续完善单元测试。
第 6.2 节 在 Copilot 聊天了解如何定义 Asset
后,对 DepartmentStatisticsVisitor n
定义单元测试的下一次尝试。
import unittest
from datetime import date
from typing import List
from dataclasses import dataclass
@dataclass
class Location:
name: str
address: str
@dataclass
class Asset:
id: int
name: str
status: str
category: str
cost: float
useful_life: int
salvage_value: float
purchase_date: date
locations: List[Location]
class TestAsset(unittest.TestCase):
def test_asset(self):
location1 = Location('Office', '123 Main St')
location2 = Location('Warehouse', '456 Elm St')
asset = Asset(1, 'Computer', 'In use', 'Hardware', 1000.0, 3, 100.0, date.today(), [location1, location2])
self.assertEqual(asset.id, 1)
self.assertEqual(asset.name, 'Computer')
self.assertEqual(asset.status, 'In use')
self.assertEqual(asset.category, 'Hardware')
self.assertEqual(asset.cost, 1000.0)
self.assertEqual(asset.useful_life, 3)
self.assertEqual(asset.salvage_value, 100.0)
self.assertEqual(asset.purchase_date, date.today())
self.assertEqual(asset.locations, [location1, location2])
这更接近了,我们还应该提醒 Copilot 聊天,Asset
类还有一个名为 FundingDetails
的类。一旦我们提供了 FundingDetails
的定义,并告知 Copilot 聊天它应该是 Asset
的一部分,我们就更接近了。
第 6.3 节 第三次尝试编写一个相当接近的单元测试。
import unittest
from department_visitor import DepartmentStatisticsVisitor, Asset, FundingDetails
from datetime import date
from typing import List, Dict
from dataclasses import dataclass
@dataclass
class Location:
name: str
address: str
class TestDepartmentStatisticsVisitor(unittest.TestCase):
def test_visit(self):
visitor = DepartmentStatisticsVisitor()
location1 = Location('Office', '123 Main St')
location2 = Location('Warehouse', '456 Elm St')
asset1 = Asset(1, 'Computer', 'In use', 'Hardware', 1000.0, 3, 100.0, date.today(), [location1, location2])
asset2 = Asset(2, 'Printer', 'In use', 'Hardware', 500.0, 2, 50.0, date.today(), [location1])
funding_details1 = FundingDetails(asset1, 'Straight Line', 0.33, { 'IT': 0.5, 'HR': 0.5 })
funding_details2 = FundingDetails(asset2, 'Double Declining Balance', 0.25, { 'IT': 0.3, 'HR': 0.7 })
visitor.visit(asset1, funding_details1)
visitor.visit(asset2, funding_details2)
self.assertEqual(visitor.get_total_cost(), 1500.0)
self.assertEqual(visitor.get_total_depreciation(), 550.0)
self.assertEqual(visitor.get_total_allocation(), 0.8)
唯一需要手动更新的是导入语句。
注意
让这个练习成为对使用大型语言模型的警示,强迫它们在高度专注的任务上表现得很具有挑战性,甚至可能不值得付出这种努力。在现实世界中,没有人会责怪您放弃提示工程,而回到只是编写出这个测试的代码。然而,通过一些坚持,您可能能够建立一个模板库,用于构建一套类似形状的类的单元测试。另一个额外的注意是 Copilot 聊天可以生成编辑器窗口中文件的测试,如果您指示它“为我的代码生成一个单元测试”,但是,它将模拟所有不直接属于正在测试的类的对象/属性。根据您尝试测试的内容,此功能的效用可能值得怀疑。
当我们尝试运行此测试时,我们发现 visitor、asset、funding details 和 depreciation strategy 之间存在循环依赖。循环依赖是指两个或多个模块或组件直接或间接地彼此依赖的情况。在我们的情况下,当 Python 尝试实例化 Asset
时,它会加载 FundingDetails
的定义。
我们通过摆脱对 FundingDetails
类的直接实例化或引用来修复这个问题。
第 6.4 节 更新后的 Asset
类,不再直接引用 FundingDetails
类。
@dataclass
class Asset():
id: int
name: str
status: str
category: str
cost: float
useful_life: int
salvage_value: float
purchase_date: date
locations: List[Location]
funding_details: None or 'itam.domain.funding_details.FundingDetails'
我们需要对 FundingDetails
类执行相同的操作。它不应该直接引用 DepreciationStrategy
类。
第 6.5 节 更新后的 FundingDetails
类,不再直接引用 DepreciationStrategy
类。
@dataclass
class FundingDetails:
depreciation_rate: float
department_allocations: Dict[Department, float]
depreciation_strategy: DepreciationStrategy or 'itam.domain.depreciation_strategy.DepreciationStrategy'
asset: None or 'itam.domain.asset.Asset'
正如我们所见,我们能够使用 Copilot 聊天创建一个单元测试。然而,如果我们没有使用 Copilot,可能会更容易地创建它。这个工具非常擅长提供何时以及如何测试您的代码的指导,但是实施(至少目前)还有待改进。
在现实世界中,我们将继续添加单元测试来建立一个实质性的测试体系。你可能会问,什么样的测试是实质性的?我们马上就会探讨这个问题。但是,我们应该首先把注意力转向下一种类型的测试:集成测试。
集成测试
集成测试涉及测试软件的不同组件或模块之间的交互,以确保它们能够无缝地配合工作。它验证集成系统是否按预期功能,并检测模块之间的任何不一致或通信问题。
6.1.2 集成测试
在本节中,我们将开发一个集成测试,以便测试端到端的系统。幸运的是,fastapi
自带了自己的测试客户端,这将帮助我们创建这个测试。
我们首先将 AssetController
的定义复制到 Copilot Chat 窗口中。然后我们可以询问 Copilot Chat 如何为这个控制器创建集成测试。鉴于我们在定义中包含了路由,Copilot Chat 应该能够为我们提供准确的集成测试。我们需要指定我们将使用 InMemoryAssetRepository
类,或者在生成测试后修复它。
列表 6.6 AssetController
生成的 Copilot Chat 集成测试
from fastapi.testclient import TestClient
from itam.infrastructure.adapters.memory_asset_repository import MemoryAssetRepository
from itam.infrastructure.ports.asset_rest_port import AssetRestPort
from itam.interfaces.asset_controller import AssetController
def test_get_assets():
# Create an instance of the InMemoryAssetRepository
asset_repository = InMemoryAssetRepository()
# Add some assets to the repository
asset_repository.create(Asset(name="Asset 1", unit_cost=1000))
asset_repository.create(Asset(name="Asset 2", unit_cost=2000))
# Create an instance of the AssetRestPort using the MemoryAssetRepository
asset_rest_port = AssetRestPort(asset_repository)
# Create an instance of the AssetController using the AssetRestPort
asset_controller = AssetController(asset_rest_port)
# Create a TestClient instance using the AssetController's router
client = TestClient(asset_controller.get_router())
# Send a GET request to the /assets endpoint
response = client.get("/assets")
# Check that the response status code is 200 OK
assert response.status_code == 200
# Check that the response body contains the expected assets
assert response.json() == [
{"id": 1, "name": "Asset 1", "unit_cost": 1000},
{"id": 2, "name": "Asset 2", "unit_cost": 2000},
现在我们将注意力转向我们要检查的最后一种测试类型:行为测试。
行为测试
行为测试是一种侧重于系统整体行为的测试类型,从最终用户的角度来看待。行为测试通常用于测试系统的功能,并确保它符合为其定义的要求和规范。
6.1.3 行为测试
从某种意义上说,所有测试都是行为测试,因为测试验证系统的行为。然而,行为测试在某些方面是独特的。让我们总结一下到目前为止我们遇到的不同类型的测试,并将其与行为测试进行对比。
单元测试是一种侧重于测试系统的单个单元或组件的测试类型,通常使用自动化测试。单元测试旨在测试单个函数或方法的功能,并确保它们在各种条件下表现正确。
另一方面,集成测试是一种侧重于测试系统的不同组件或单元之间的交互的测试类型。集成测试通常用于测试不同组件或单元之间的接口,并确保它们正确地配合工作。集成测试可以手动执行或使用自动化测试,并且通常涉及测试系统的不同组件或单元之间的交互,而不是整个系统。
行为测试侧重于根据用户故事或场景定义软件的行为。这些场景以一种特定的格式写入,称为“给定-当-那么”(GWT),用于驱动开发过程。GWT 格式描述了特定场景的前提条件(给定)、操作(当)和预期结果(那么)。
随着我们在测试中的进展,我们可能会发现一些行为或组件在我们的测试中很难设置。此外,我们可能会发现难以隔离特定对象或模块的行为,并测试不同对象之间的交互。为了解决这个限制,我们可以使用一个模拟对象。
模拟对象
模拟对象是一种以受控方式模拟真实对象行为的测试替身。它们也可以用来模拟难以通过真实对象复制的错误条件或边缘情况。模拟对象可以手动创建,使用诸如unittest.mock
或pytest-mock
等模拟库。这些库提供了用于创建和配置模拟对象的函数和类。模拟对象可以配置为在调用其方法时返回特定值或引发特定异常。它们还可以用于记录对其方法的调用,以便您可以验证是否正确地使用了正确的参数调用了正确的方法。
我们将在提示中引入模拟对象,以创建AssetManager
的行为测试。
列表 6.7 设置AssetManager
行为测试的提示
We have a class called AssetManager. AssetManager's constructor takes two class BaseRepository and AssetLocationMediator. The BaseRepository is a repository in which Assets are stored. We want to use InMemoryAssetRepository, which is a subclass of BaseRepository that stores the Assets in memory. Use a mock object for AssetLocationMediator. The AssetManager stores Assets using the method create and gets all of the Assets using the method get_assets. An asset has 10 required positional arguments: 'id', 'name', 'status', 'category', 'cost', 'useful_life', 'salvage_value', 'purchase_date', 'locations', and 'funding_details'. Use named parameters to make it clear which param is going where. Create a BDD script that will add two Assets to the AssetManager, confirming that all Assets cost $4000.00\. This should be implemented in Python. Take it step by step, telling us the tools and libraries you will use.
Copilot Chat 将告诉我们需要创建一个名为asset_manager.feature
的新文件(称为特性文件)。在这个文件中,我们将定义AssetManager
的属性(以及任何支持类)。例如,在特性文件中定义了一个场景,将两个资产添加到AssetManager
,确认所有资产的总成本为$4000.00,并模拟AssetLocationMediator
类。
列表 6.8 根据 Copilot Chat 定义的asset_manager.feature
文件
Feature: Asset Manager
As an IT Asset Manager
I want to be able to manage assets
So that I can keep track of all IT assets in my organization
Scenario: Add two assets to the Asset Manager
Given the Asset Manager is running
And the InMemoryAssetRepository is initialized
And the AssetLocationMediator is mocked
When I create an asset with a cost of $2000.00
And I create another asset with a cost of $2000.00
Then the total cost of all assets should be $4000.00
我们应该在项目的根目录中创建一个名为features
的目录。我们应该将这个文件放在那个目录中。此外,我们应该添加一个名为steps
的子目录。我们将很快得到steps
。
接下来,我们将创建一个名为 steps.py
的新 Python 文件,并将其放置在 steps
目录中。这个 Python 文件定义了用于场景的步骤实现。AssetManager
、InMemoryAssetRepository
、AssetLocationMediator
和 Asset
类被导入,并定义了六个函数来对应场景中的步骤。这些函数使用 context
对象在步骤之间存储和检索数据。使用 unittest.mock
库的 Mock
类来为 AssetLocationMediator
类创建一个模拟对象。Asset
类使用命名参数来实例化,以澄清哪个参数属于哪个属性。如果我们向 Copilot Chat 请求 steps.py
文件的内容,我们将看到类似以下列表的输出。
列出 6.9 我们在 steps.py
中定义的行为测试的实现细节
from behave import given, when, then
from itam.service.asset_manager import AssetManager
from itam.infrastructure.repository.in_memory_asset_repository import InMemoryAssetRepository
from itam.domain.asset import Asset
from itam.infrastructure.mediators.asset_location_mediator import AssetLocationMediator
from unittest.mock import Mock
@given('the Asset Manager is running')
def step_impl(context):
context.asset_repository = InMemoryAssetRepository()
context.asset_location_mediator = Mock(spec=AssetLocationMediator)
context.asset_manager = AssetManager(context.asset_repository, context.asset_location_mediator)
@given('the InMemoryAssetRepository is initialized')
def step_impl(context):
pass
@given('the AssetLocationMediator is mocked')
def step_impl(context):
pass
@when('I create an asset with a cost of ${cost}')
def step_impl(context, cost):
asset = Asset(id=1, name='Test Asset 1', status='Available', category='Test Category', cost=float(cost), useful_life=5, salvage_value=0, purchase_date='2022-01-01', locations=['Test Location'], funding_details={'Test Funding': 1000})
context.asset_manager.create(asset)
@when('I create another asset with a cost of ${cost}')
def step_impl(context, cost):
asset = Asset(id=2, name='Test Asset 2', status='Available', category='Test Category', cost=float(cost), useful_life=5, salvage_value=0, purchase_date='2022-01-01', locations=['Test Location'], funding_details={'Test Funding': 1000})
context.asset_manager.create(asset)
@then('the total cost of all assets should be ${total_cost}')
def step_impl(context, total_cost):
assets = context.asset_manager.get_assets()
assert sum(asset.cost for asset in assets) == float(total_cost)
如果您尚未安装它,请使用 pip 安装 behave
库:pip install behave.
此外,您应将其添加到 requirements.txt
文件中,以确保在稍后构建可部署版本的此应用程序时会被引入。我们将通过从项目的根目录发出以下命令来运行行为测试。
列出 6.10 运行行为测试并生成输出
% behave features
Feature: Asset Manager # features/asset_manager.feature:1
As an IT Asset Manager
I want to be able to manage assets
So that I can keep track of all IT assets in my organization
Scenario: Add two assets to the Asset Manager # features/asset_manager.feature:6
Given the Asset Manager is running # features/steps/steps.py:8 0.000s
And the InMemoryAssetRepository is initialized # features/steps/steps.py:14 0.000s
And the AssetLocationMediator is mocked # features/steps/steps.py:18 0.000s
When I create an asset with a cost of $2000.00 # features/steps/steps.py:22 0.000s
And I create another asset with a cost of $2000.00 # features/steps/steps.py:27 0.000s
Then the total cost of all assets should be $4000.00 # features/steps/steps.py:32 0.000s
1 feature passed, 0 failed, 0 skipped
1 scenario passed, 0 failed, 0 skipped
6 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.001s
在本节中,我们通过使用三种类型的测试:单元测试、集成测试和行为测试,为良好的软件开发奠定了基础。现在,有人可能会争辩说它在项目的开发生命周期中出现得很晚。这个争论也不无道理。在现实世界中,我们会在开发代码时开发我们的测试。有些人可能会认为我们应该在编写代码之前构建测试。您可能持有这种信念,也可能不持有,但无论如何,您都应该尽早测试,并经常测试。
在本书的下一部分中,我们将深入研究一些可用于确定我们软件总体质量的指标,并请求 Copilot 帮助我们评估到目前为止我们代码的质量。
6.2 评估质量
理解软件应用程序的性能、可靠性、可维护性和总体质量是软件工程的重要方面。本章将深入探讨软件质量指标领域的迷人和复杂内容 – 这些量化标准和基准指导我们理解软件系统质量的。
软件质量指标是必不可少的工具,它允许利益相关者 – 开发人员、测试人员、经理和用户 – 评估软件产品的状态,识别其优点和改进空间。它们为产品开发、测试、调试、维护和改进倡议等各种流程提供了经验基础。通过量化软件的特定特性,这些指标提供了一种具体的方法来理解软件质量这一抽象概念。
在本节中,我们将探讨软件质量度量的几个重要类别,包括产品度量、过程度量和项目度量。我们将分析它们的重要性、计算方法以及如何有效利用它们来评估和提高软件质量。这个探讨将包括静态度量,即应用于静态软件系统的度量,以及动态度量,它们评估系统在执行过程中的行为。
软件质量度量不仅有助于软件系统的技术完整性,还有助于确保客户满意度、盈利能力和长期的商业成功。因此,了解这些度量是对软件开发领域的任何从业人员都是非常宝贵的,从工程师和项目经理到高管和软件用户。
在本节中,我们将研究类或代码复杂性和可维护性的一些常见指标。复杂的软件很难理解,这使得开发人员,尤其是新手开发人员,很难把握软件不同部分是如何相互交互的。这可能会减慢员工的适应速度和开发时间。
复杂的代码往往会导致更高的维护工作量。当代码复杂时,修改或修复 bug 可能需要更长的时间,因为很难预测改动系统中某一部分的影响。这可能会导致软件整个生命周期的更高成本。
复杂的软件往往更容易出错。因为它更难理解,开发人员在进行改动时更有可能引入 bug。此外,复杂的代码可能存在许多相互依赖的关系,一处的改动可能在其他地方产生意想不到的影响。
软件越复杂,就需要更多的测试用例来进行彻底测试。由于涉及逻辑的复杂性,编写这些测试用例可能也更加困难。
编写简单和易维护的代码应该是我们的首要任务之一。观察与我们的代码相伴的度量变化应该有助于我们在这方面的努力。在这个目标的推动下,我们可以(也应该)首先使用的度量是圈复杂度。
圈复杂度
圈复杂度是量化软件模块中独立路径的数量的度量。它衡量了代码中的决策复杂性,包括循环、条件和分支。较高的圈复杂度值表示增加的复杂性,并暗示着可能存在更多的 bug 和理解、维护代码的挑战。
在文件 department_visitor.py 中,输入片段 6.3 的提示任何位置。Copilot 将立即输出答案。
# Question: What is the cyclomatic complexity of the class DepartmentStatisticsVisitor?
# Answer: 1
Copilot 会告诉您这个类的复杂性为 1。您可能不清楚这个值的含义。如果是后者,您可以要求 Copilot 加以解释。
# Question: Is 1 an excellent cyclomatic complexity?
# Answer: Yes
# Question: Why is one a good value for cyclomatic complexity?
# Answer: Because it is low
Copilot 告诉我们,如果圈复杂度低,则好。 这在直觉上是有道理的。 代码复杂度低意味着更容易理解,因此更容易理解和推理。 也更有可能更容易维护。 我们将要探讨的下一个指标是Halstead 复杂度度量。
Halstead 复杂度度量
Halstead 复杂度度量评估软件程序的复杂性,基于代码中使用的唯一运算符和操作数的数量。 这些度量包括程序长度(N1)、程序词汇量(n1)、体积(V)、难度(D)、工作量(E)等。 这些度量提供了有关代码的大小和认知复杂性的见解。
与上次类似,我们将从一个提示开始,要求 Copilot 确定我们的访问者类的 Halstead 复杂度度量。
# Question: What is the Halstead Complexity Measure of the class DepartmentStatisticsVisitor?
# Answer: 2
# Question: What is the Halstead Difficulty Measure of the class DepartmentStatisticsVisitor?
# Answer: 1
# Question: Is 2 a good Halstead Complexity Measure?
# Answer: Yes
# Question: Is 1 a good Halstead Difficulty Measure?
# Answer: Yes
# Question: What is a bad Halstead Difficulty Measure?
# Answer: 10
# Question: What is a bad Halstead Complexity Measure?
# Answer: 10
# Question: What does a high Halstead Difficulty Measures mean?
# Answer: It means that the code is hard to understand
您可能想要继续进行一段时间的问答会话,以查看 Copilot 可以从中获取的信息。 一旦您准备好继续,还有一个指标要探讨:可维护性指数。
可维护性指数
可维护性指数是一个综合指标,结合了多个因素,包括圈复杂度、代码行数和 Halstead 复杂度度量,以提供软件可维护性的整体度量。 更高的可维护性指数表示更容易维护和潜在较低的复杂性。
您应该在访问者文件中开始一个类似的讨论,以了解可维护性指数。
# Question: What is the maintainability index of the class DepartmentStatisticsVisitor?
# Answer: 100
# Question: Do we want a high Maintainability Index or low Maintainability Index?
# Answer: high
# Question: Why do we want a high Maintainability Index?
# Answer: Because it is easier to maintain
如果我们得到一个较低的可维护性指数,我们可以重构以减少这个数字。 指标在于它给了我们一个钉子来挂我们的帽子;也就是说,我们可以采取这个措施来改善它。 指标使我们超越了个体的纯美感或主观性。 指标是真实的、可操作的数据。 但 Copilot 还有(至少)一项更多的技巧。 Copilot 不仅能够编写和评估我们的代码,还可以解决代码的缺陷。 让我们来捕虫吧。
6.3 搜索错误
在这一部分,我们将使用一个基本的(尽管相当牵强)示例来演示我们如何使用 Copilot 来查找和修复我们代码中的问题。 这段代码应该循环遍历整数列表并计算总和。 但是,存在一个“眨眼就会错过”的错误。 总和被赋予了 i 的值,而不是将 i 的值添加到累加总和中。
列表 6.11 简单循环遍历整数列表并计算总和
l = [1, 2, 3, 4, 5]
if __name__ == '__main__':
sum = 0
for i in l:
sum = i
print("sum is", sum)
要调试此问题,我们将引入一个新工具:Copilot 实验室。在 Copilot 聊天之前,Copilot 实验室是我们的 IDE 中某些功能可用的唯一方式,具体来说是 VS Code。例如,我们需要使用 Copilot 实验室来查找和修复错误。Copilot 实验室今天仍然具有的主要优势是,它可以访问编辑器窗格中突出显示的内容。此功能使 Copilot 实验室能够直接在您的 IDE 中的可编辑代码上操作。安装扩展到您的 IDE 后,您应该在 IDE 的左侧看到一个 Copilot 实验室工具包。如果您需要提醒如何将扩展安装到您的 IDE 中,请参考附录 A 到 C,其中包含有关安装扩展的说明。
图 6.1 Copilot 实验室工具包菜单,其中包括查找和修复错误的选项。该工具包还提供增强您的代码以及对其进行文档化的功能。
我们将暂时更改 main.py 文件的内容为列表 6.9 中列出的代码。完成此更改后,请突出显示代码,并在 Copilot 实验室工具包中按下“修复 Bug”按钮。您应该会看到类似于图 6.2 中的输出。Copilot 实验室能够确定此代码中的问题,并提供解决此问题的建议。
图 6.2 Copilot 实验室,使用 GPT 模型,已经识别出了错误以及如何解决此错误
或者,我们可以将这段代码复制到 ChatGPT 中,并要求它找到错误。然而,可以争论的是,这可能不太方便,因为在请求 ChatGPT 修复之前,您必须知道代码中存在错误。
6.4 覆盖代码
代码覆盖率是衡量您的代码被测试覆盖程度的一种指标。通常以百分比表示,代表您的代码被测试执行的比例。
代码覆盖率可以用作评估测试效果的指标。如果您的代码覆盖率较低,可能表示您的代码的某些部分未经过测试,这可能导致未捕获的错误和其他问题。另外,如果代码覆盖率高,则您可以放心您的代码经过了充分测试。这并不保证您的代码是无错的,但应该表明对于应该在测试中捕获的错误,您具有很高的信心。
为了确定我们的 Python 项目中的代码覆盖率,我们将使用 coverage
库中提供的代码覆盖率工具 coverage。coverage
库通过对我们的代码进行工具化来收集运行时的覆盖率数据。它可以收集任何 Python 代码的覆盖率数据,包括测试、脚本和模块。通过使用像 coverage 这样的代码覆盖率工具,我们可以更好地了解我们的代码有多少被我们的测试所覆盖,并识别可能需要更多测试的代码区域。
首先,让我们使用 pip 安装 coverage:pip install coverage.
接下来,让我们使用 coverage 运行我们的测试:coverage run -m pytest.
这将运行您的测试并收集覆盖率数据。
接下来,我们将生成一个覆盖率报告。覆盖率报告将显示项目中每个文件的代码覆盖率。我们使用以下命令创建基于文本的覆盖率报告:coverage report
或使用以下命令生成报告的 HTML 版本:coverage html
。报告的 HTML 版本将位于 htmlcov 目录中。图 6.3 显示了覆盖率报告。
图 6.3 代码覆盖率报告显示了我们信息技术资产管理系统项目中每个文件的覆盖情况。
代码覆盖率达到 70% 是一个不错的起点。在现实世界中,我们将继续与我们的团队和生成式人工智能小伙伴合作,将这个指标提高到高 90%。
我们将转向一个新的主题:使用生成式人工智能为我们描述代码列表。
6.5 将代码转换成描述 - 从代码到描述
通常,人们会交给你现有的源代码。确定这段代码的确切入口点、代码的目的以及棕地项目的整体结构可能是具有挑战性的。幸运的是,这正是生成式人工智能真正擅长的领域之一:将代码翻译成文本描述。
首先,我们将把(有 bug 的)代码从上一节复制到 Copilot 聊天对话框中,并加上提示语:“这段代码是做什么的?”
图 6.4 上一节的有 bug 代码,并附有提示,要求 Copilot 聊天向我们解释这段代码。
这个解释引人注目的地方在于,Copilot 聊天检测到代码中存在一个 bug,并提出了如何修复这个 bug 的建议。
图 6.5 Copilot 聊天解释了代码的目的,识别了 bug,并提出了修复建议。
接下来,我们将提出一个更具挑战性的问题。我们将要求它解释我们在本章前面设置的行为测试。将代码复制到 Copilot 聊天窗口中,并询问这个类做什么。您可能会收到类似以下清单的回复。
列表 6.12 Copilot Chat 解释了我们之前编写的行为测试的行为。
The code you provided is not a class but a series of step definitions for a Behavior-Driven Development (BDD) test using the Behave framework in Python. These steps correspond to a specific scenario written in Gherkin language (Given, When, Then format).
These step definitions describe how to interact with an AssetManager class and an InMemoryAssetRepository class. They're meant to test the creation of assets and the calculation of the total cost of all assets.
Let's go through each step:
@given('the Asset Manager is running'): This sets up the initial state for the test. It initializes the AssetManager with an InMemoryAssetRepository (which presumably stores assets in memory during the execution of the test) and a mocked AssetLocationMediator.
@given('the InMemoryAssetRepository is initialized') and @given('the AssetLocationMediator is mocked'): These steps appear to be placeholders, as they don't execute any actions.
@when('I create an asset with a cost of ${cost}') and @when('I create another asset with a cost of ${cost}'): These steps create Asset instances with the given cost and add them to the AssetManager.
@then('the total cost of all assets should be ${total_cost}'): This step retrieves all the assets from the AssetManager, calculates the total cost, and checks that it matches the expected total cost.
This sequence of steps allows the user to test the process of creating assets and validating the total cost of all assets in the AssetManager.
# Question: What does this method do?
# Answer: It gets an asset by its id
async def get_asset(self, asset_id: int):
asset = self._asset_rest_port.read(asset_id)
if asset is None:
raise HTTPException(status_code=404, detail="Asset not found")
return from_asset(asset)
6.6 从一种语言翻译成另一种语言
看起来很遥远,您可能会发现自己正在查看一个语言只是稍有了解的编程语言编写的代码库。或者,您可能想在当前语言中使用来自另一种语言的特定函数。例如,以 JavaScript 重写 Asset
和 FundingDetails
类会是什么样子呢?好吧,让我们问问 Copilot Chat。
列表 6.13 Copilot Chat 将我们的 Asset
和 FundingDetails
类翻译成了 JavaScript。
class Asset {
constructor(id, name, status, category, cost, useful_life, salvage_value, purchase_date, locations, funding_details) {
this.id = id;
this.name = name;
this.status = status;
this.category = category;
this.cost = cost;
this.useful_life = useful_life;
this.salvage_value = salvage_value;
this.purchase_date = purchase_date;
this.locations = locations;
this.funding_details = funding_details;
}
}
class FundingDetails {
constructor(asset, funding_source, funding_amount) {
this.asset = asset;
this.funding_source = funding_source;
this.funding_amount = funding_amount;
}
}
我们可能会认为翻译一个或两个类太容易了。好吧,我们将尝试更复杂的事情。如果用 JavaScript 写 AssetManager
行为测试,它会是什么样子?
列表 6.14 Copilot Chat 将我们的 AssetManager
行为测试翻译成了 JavaScript。
const AssetManager = require('./asset_manager');
const Asset = require('./asset');
describe('AssetManager', () => {
let assetManager;
beforeEach(() => {
assetManager = new AssetManager();
});
describe('addAsset', () => {
it('should add an asset to the asset list', () => {
const asset = new Asset(1, 'Computer', 'Active', 'Hardware', 1000.0, 5, 100.0, new Date(), [], null);
assetManager.addAsset(asset);
expect(assetManager.assets.length).toBe(1);
expect(assetManager.assets[0]).toBe(asset);
});
});
describe('removeAsset', () => {
it('should remove an asset from the asset list', () => {
const asset = new Asset(1, 'Computer', 'Active', 'Hardware', 1000.0, 5, 100.0, new Date(), [], null);
assetManager.addAsset(asset);
assetManager.removeAsset(asset);
expect(assetManager.assets.length).toBe(0);
});
});
describe('getAssetById', () => {
it('should return the asset with the given ID', () => {
const asset1 = new Asset(1, 'Computer', 'Active', 'Hardware', 1000.0, 5, 100.0, new Date(), [], null);
const asset2 = new Asset(2, 'Printer', 'Active', 'Hardware', 500.0, 3, 50.0, new Date(), [], null);
assetManager.addAsset(asset1);
assetManager.addAsset(asset2);
const result = assetManager.getAssetById(2);
expect(result).toBe(asset2);
});
it('should return null if no asset with the given ID is found', () => {
const result = assetManager.getAssetById(1);
expect(result).toBeNull();
});
});
});
另外,您可能希望让 GPT 以惯用的 JavaScript 重写代码。您只需要说:“用惯用的 JavaScript 编写 AssetManager
。”Copilot Chat 将告诉您它使用 ES6 中引入的 class
语法来定义 AssetManager
类。
列表 6.15 AssetManager
类被以惯用的 JavaScript 重写。
class AssetManager {
constructor() {
this.assets = [];
}
addAsset(asset) {
this.assets.push(asset);
}
removeAsset(asset) {
const index = this.assets.indexOf(asset);
if (index !== -1) {
this.assets.splice(index, 1);
}
}
getAssetById(id) {
return this.assets.find(asset => asset.id === id) || null;
}
}
这是 AssetManager
行为测试在 JavaScript 中使用 Jest 测试框架的示例(Copilot Chat 在没有我们的建议的情况下包含了这个):
列表 6.16 Copilot Chat 用惯用的 JavaScript 重写了我们的 AssetManager
测试。
const AssetManager = require('./asset_manager');
const Asset = require('./asset');
describe('AssetManager', () => {
let assetManager;
beforeEach(() => {
assetManager = new AssetManager();
});
describe('addAsset', () => {
it('should add an asset to the asset list', () => {
const asset = new Asset(1, 'Computer', 'Active', 'Hardware', 1000.0, 5, 100.0, new Date(), [], null);
assetManager.addAsset(asset);
expect(assetManager.assets.length).toBe(1);
expect(assetManager.assets[0]).toBe(asset);
});
});
describe('removeAsset', () => {
it('should remove an asset from the asset list', () => {
const asset = new Asset(1, 'Computer', 'Active', 'Hardware', 1000.0, 5, 100.0, new Date(), [], null);
assetManager.addAsset(asset);
assetManager.removeAsset(asset);
expect(assetManager.assets.length).toBe(0);
});
});
describe('getAssetById', () => {
it('should return the asset with the given ID', () => {
const asset1 = new Asset(1, 'Computer', 'Active', 'Hardware', 1000.0, 5, 100.0, new Date(), [], null);
const asset2 = new Asset(2, 'Printer', 'Active', 'Hardware', 500.0, 3, 50.0, new Date(), [], null);
assetManager.addAsset(asset1);
assetManager.addAsset(asset2);
const result = assetManager.getAssetById(2);
expect(result).toBe(asset2);
});
it('should return null if no asset with the given ID is found', () => {
const result = assetManager.getAssetById(1);
expect(result).toBeNull();
});
});
});
在此示例中,我们使用 Jest 测试框架为 AssetManager
类定义了一组行为测试。我们使用 describe
库确定测试套件和单个测试。
在每个测试中,我们创建 AssetManager
类的一个实例并对其执行某些操作,例如添加或删除资产。然后,我们使用 expect 函数对 AssetManager
类的行为进行断言,例如检查资产是否已添加到资产列表或资产是否已从资产列表中删除。
请注意,在 JavaScript 中,定义类和构造函数的语法与 Python 略有不同,我们使用 required 函数从各自的模块中导入 AssetManager 和 Asset 类。
最后,为了大结局:我们将开始一个新的 ChatGPT 4.0 会话,并将第三章中的 Mermaid 图表粘贴到其中。请以提示“这是我应用程序的 Mermaid 图表,请用 Golang 实现。”为主导。它将返回用 Golang 渲染的代码。
列表 6.17 Copilot Chat 用惯用的 Golang 重写了我们的 Asset
类。
type Asset struct {
id int
name string
status string
category string
fundingDetails *FundingDetails
}
func NewAsset(name, status, category string, fundingDetails *FundingDetails) *Asset {
return &Asset{
name: name,
status: status,
category: category,
fundingDetails: fundingDetails,
}
}
接下来,告诉它切换到 Java。
列表 6.18 Copilot Chat 用惯用的 Java 重写了我们的 Asset
类。
class Asset {
private int id;
private String name;
private String status;
private String category;
private FundingDetails fundingDetails;
public Asset(String name, String status, String category, FundingDetails fundingDetails) {
this.name = name;
this.status = status;
this.category = category;
this.fundingDetails = fundingDetails;
}
6.7 摘要
-
单元测试:重点测试代码的单个组件或单元,以识别特定单元内的错误和问题。单元测试将是您代码库中数量最多的部分。
-
集成测试:测试软件的不同组件或模块之间的交互,以确保无缝集成并检测通信问题。
-
行为测试:从最终用户的角度测试系统的功能,确保其符合要求和规格。
-
模拟对象:以受控的方式模拟自然对象的行为,对于测试和模拟错误条件非常有用。Mock 对象特别擅长模仿测试运行所需但不在测试范围内的系统的某些部分。例如,如果您的类有一个构造函数参数为数据库,但您不想直接测试数据库,因为数据可能会更改,导致您的测试无法得出结论、不可重复或不确定。
-
圈复杂度:衡量软件模块独立路径的数量,表示复杂性和潜在漏洞。
-
Halstead 复杂度度量:根据独特的运算符和操作数评估软件复杂度,提供关于代码大小和认知复杂度的见解。
-
可维护性指数:组合了圈复杂度、代码行数和 Halstead 度量等因素,评估软件的可维护性。
-
代码覆盖率:用于评估测试效果的衡量标准,表示代码被测试的程度以及出现未捕获错误的潜力。通常情况下,覆盖率越高越好。
-
语言熟悉度:需要在一个陌生的编程语言中导航代码或希望在当前语言中使用另一种语言的功能。
第七章:编码基础设施和管理部署
本章内容包括
-
使用 Copilot 的帮助创建 Dockerfile
-
使用大型语言模型起草您的基础设施代码
-
使用容器注册表管理 Docker 镜像
-
发挥 Kubernetes 的强大能力
-
无缝地使用 GitHub Actions 发布您的代码
没有比让应用程序闲置更令人沮丧的事情了。因此,将经过充分测试的应用程序快速推向生产是每个称职开发人员的明确目标。由于我们上一章节花费了时间测试我们的产品,现在它已经准备好发布了。
本章将重点介绍从开发到产品发布的关键时刻。在这个关键阶段,理解部署策略和最佳实践对确保成功的产品发布至关重要。
随着我们的应用程序成功地得到了保障和测试,现在是时候将注意力转向推出产品了。为此,我们将利用大型语言模型(LLMs)的强大功能,探索针对云基础设施量身定制的各种部署选项。
通过利用 LLMs 的力量并拥抱它们的部署选项和方法,我们可以自信地在推出产品的复杂领域中航行,为我们的客户提供强大且可扩展的解决方案,同时利用云计算的好处。
首先,我们将为 Docker 开发部署文件。我们将探讨如何创建 Docker 镜像和定义部署文件。此外,我们还将讨论容器化我们的应用程序的最佳实践,以及实现无缝部署的方法。
接下来,我们将利用 Terraform 来定义我们的基础设施代码,并自动部署 AWS 上的 Elastic Compute Cloud(EC2)实例。我们将演示如何编写 Terraform 脚本来在 EC2 实例上提供和部署我们的应用程序,以确保一致且可重现的基础设施设置。
接下来,我们将利用 LLMs 将我们的应用程序部署到 Kubernetes(AWS EKS/ECS)。我们将让 GitHub Copilot 创建适当的 Kubernetes 部署文件,以简化我们的部署流程并高效地管理我们应用程序的生命周期。鉴于我们应用程序的相对简单性,我们不需要像 Helm 这样的 Kubernetes 包管理器。然而,随着服务复杂性和依赖关系的增加,您可能希望将其作为一个选项进行探索。幸运的是,Copilot 也可以为您编写 Helm 图表!
最后,我们将简要展示如何使用 GitHub Actions 从本地迁移到自动化部署。我们可以通过将 LLMs 与这一广泛使用的持续集成和交付(CI/CD)工具集成,自动化我们的构建和部署过程,确保更快和更有效的部署。
注意
虽然本章将使用 AWS 作为我们的云服务提供商,但本章涵盖的原则和实践可以适用于其他云平台甚至没有虚拟化的本地基础设施(裸机),这使我们能够根据业务需求调整和扩展我们的产品部署策略。你会发现,通过使用 LLM 和使用基础设施即代码,你可以(部分地)减轻云平台常见的供应商锁定问题。
你应该注意,如果你选择将这个(或任何应用程序)部署到 AWS,那么你的活动将会产生费用。AWS 和大多数云服务提供商为你提供了免费试用期来了解他们的平台(例如 Google Cloud Platform 和 Azure),但一旦这些信用额用完,你可能会被意外的大额账单所冲击。如果你决定在本章跟随进行,你应该为你能轻松承受的金额设置阈值警报。Andreas Wittig 和 Michael Wittig 的 Amazon Web Services in Action, Third Edition, 2023 年 3 月, Manning Publishing, 第 1.9 节是设置此类计费通知警报的绝佳资源。
7.1 构建一个 Docker 镜像并在本地“部署”它
正如你可能还记得第六章所讲的,Docker 是一个容器化平台,允许我们以传统意义上的很少或几乎没有安装(Docker 外部)的方式运行应用程序。与模拟整个操作系统的虚拟机不同,容器共享主机系统的内核(操作系统的核心部分),并利用主机系统的操作系统功能,同时将应用程序进程和文件系统与主机隔离开。这允许你在单个主机系统上运行多个隔离的应用程序,每个应用程序都有自己的环境和资源限制。下面的图表应该让你对 Docker 运行时和主机之间的关系有所了解。
图 7.1 Docker 利用主机操作系统,同时隔离每个容器。这使得 Docker 容器相比虚拟机更加轻量,因为它们不需要完整的操作系统来运行。
从生产准备性的角度来看,其中一个更令人兴奋的功能是 Docker 更容易运行一些在某种意义上可以自我修复的应用程序。如果它们在运行时失败或崩溃,你可以配置它们在不需要干预的情况下重新启动。在本节中,我们将使用 Copilot 创建文件(称为 Dockerfile),从中构建我们的 Docker 镜像。
Docker 镜像
Docker 镜像就像 Docker 容器的蓝图。它们是可移植的,包括应用程序运行所需的所有依赖项(库、环境变量、代码等)。
运行中的 Docker 实例称为 Docker 容器。由于其轻量级特性,您可以在单个主机上运行多个容器而不会出现问题。我们之所以能够这样做,是因为容器化技术共享操作系统内核,以独立的用户空间运行。
注意
最初,您的作者希望将 AWS CodeWhisperer 用作本章的 LLM。考虑到预期的云平台,这似乎是合乎逻辑的。但是,在撰写本文时,AWS CodeWhisperer 仅支持使用编程语言进行编程。它没有基础设施即代码的功能。
我们将从以下提示开始,让 Copilot 为我们起草 Dockerfile。
# Create a Dockerfile for this Python app. The main class is main.py. Use Python 3.10 and install
# the dependencies using the requirements.txt file in this directory. The app should run on port 8080.
您可能会留下一个空文件(除了此注释以外)。基础设施即代码的支持正在不断发展(与 LLM 生态系统一样)。根据 Copilot 聊天的说法,GitHub Copilot 能够为您创建 Dockerfile;但是,您需要按照以下步骤引导它:
-
在
Dockerfile
中,键入FROM python:
,然后等待 Copilot 建议使用的 Python 版本。选择您想要使用的版本。 -
输入
WORKDIR /app
以设置容器的工作目录。 -
输入
COPY . /app
以将项目内容复制到容器中。 -
输入
RUN pip install --trusted-host pypi.python.org -r requirements.txt
以安装项目的依赖项。 -
输入
EXPOSE 8080
以将端口 8080 暴露给容器。 -
输入
CMD ["python", "main.py"]
以指定容器启动时要运行的命令。
或者,您可能希望将先前编写的相同提示复制并粘贴到 Dockerfile 中的 Copilot 聊天提示窗口中。 Copilot 聊天将为您提供所需的 Dockerfille 内容。
列表 7.1 构建 Docker 镜像并准备其运行时生命周期的 Dockerfile 内容
FROM python:3.10-slim-buster
WORKDIR /app
COPY . /app
RUN pip install --trusted-host pypi.python.org -r requirements.txt
EXPOSE 8080
CMD ["python", "main.py"]
使用 Dockerfile,我们将构建一个用于部署和运行我们的应用程序的镜像。我们可以输入以下命令来构建我们的应用程序(从 Dockerfile 所在的目录运行,并且不要忘记末尾的点)。您将需要互联网访问以下载依赖项并创建镜像。
docker build -t itam:latest .
构建 Docker 镜像可能需要几秒钟到几分钟,具体取决于您的系统上安装了哪些镜像和软件包以及您的互联网连接速度。您的耐心很快就会得到回报,因为您很快就会有一个几乎可以安装在任何地方的应用程序,从最普通的商品硬件到您最喜欢的云提供商提供的最大型硬件。然而,在任何地方运行之前,您应该尝试在本地运行它。如果您忘记了命令,Copilot 聊天将乐意并乐于协助。
docker run -p 8000:8000 -d --name itam itam:latest
您可以通过在命令行中输入此命令来确认您的 Docker 容器正在运行:docker ps | grep itam
。您应该看到正在运行的实例。
7.2 通过 Copiloting Terraform 架设基础设施
当您创建和测试应用时,在计算机上使用 Docker 镜像是有用的。但是当到达启动应用程序的时候,我们将需要一台比我们的本地计算机更强大的机器。在本节中,我们将使用 GitHub Copilot 来帮助我们通过让 Copilot 编写用于基础设施即代码工具 Terraform 的必需部署描述符来设置和控制我们的 AWS 基础架构。Terraform 由 HashiCorp 制作,使用领域特定语言(DSL)编写我们希望我们的基础架构看起来像什么的内容。此 DSL 使我们不必理解每个云服务提供商用于配置硬件的所有复杂性和细微差别。此外,它允许我们使用基础设施即代码存储和版本化我们的基础架构。
要开始,我们需要创建一个名为ec2.tf
的文件,并添加提示以告知 Copilot 我们打算将其作为 Terraform 文件,并说明我们希望如何建立我们的基础架构。您可能会注意到 Copilot 需要我们在继续之前输入给定行的第一个单词。
列表 7.2 Terraform 文件的示例,包括实例大小和如何构建和配置的说明
# Create a Terraform file that provisions an AWS EC2 instance of type t2.micro, installs the Docker daemon, and returns the instance's hostname.
provider "aws" {
region = "us-west-2"
}
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
}
您可能会发现 Copilot 跳过了一个微小但关键的细节:Copilot 没有提供安装和配置 Docker 的代码。鉴于 Docker 是运行我们的应用所必需的,我们需要纠正这个疏忽。事实上,您可能需要手动更新文件以包含安装 Docker 的命令,如下所示:
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
user_data = <<-EOF
#!/bin/bash
sudo yum update -y
sudo yum install -y docker
sudo service docker start
sudo usermod -a -G docker ec2-user
sudo docker run -d -p 80:80 nginx
EOF
}
Copilot 应该生成一个完整的 Terraform 文件,类似于列表 7.3 的内容。很可能,代码与此列表不完全匹配,但只要包含关键特性即可:即提供程序、实例、添加 Docker 守护程序的脚本、密钥对和安全组。
列表 7.3 完整列出了用于创建最小 EC2 实例的 Terraform 文件
# Create a Terraform file that provisions an AWS EC2 instance of type t2.micro, installs the Docker daemon, and returns the hostname of the instance.
provider "aws" {
region = "us-west-2"
}
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
user_data = <<-EOF
#!/bin/bash
sudo yum update -y
sudo yum install -y docker
sudo service docker start
sudo usermod -a -G docker ec2-user
sudo docker run -d -p 80:80 nginx
EOF
connection {
type = "ssh"
user = "ec2-user"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}
lifecycle {
create_before_destroy = true
}
depends_on = [aws_security_group.allow_http]
}
resource "aws_security_group" "allow_http" {
name = "allow_http"
description = "Allow HTTP inbound traffic"
vpc_id = "vpc-12345678"
ingress {
description = "HTTP from VPC"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
output "public_dns" {
value = aws_instance.example.public_dns
}
如果您使用的是默认的虚拟专用云(VPC),那么第 35 行上的vpc_id
条目并不是严格必要的。尽管您会发现 AWS 团队选择的许多默认配置和约定都是有道理的,但如果您有更严格的安全要求,或者如果您想要了解有关您的基础架构的一切而不假设任何内容,那么您可能会考虑使用 Terraform 从头开始设置新的 VPC。您将需要将第 21 行的密钥对条目更改为您可以访问的密钥对。
完成了这个文件后,您应该运行terraform init
命令。terraform init
命令初始化一个新的或现有的 Terraform 工作目录。此命令下载并安装配置文件中指定的所需提供程序插件和模块。此命令准备就绪。
接下来,您将让 Terraform 解释它打算进行的更改。您可以使用 terraform plan
命令来完成此操作。terraform plan
命令为您的基础架构更改创建执行计划。此命令向您展示了当您应用配置文件时 Terraform 将对基础架构进行的更改。该计划将向您展示将创建、修改或销毁哪些资源,以及将对基础架构进行的任何其他更改。
注意
当您第一次运行 terraform plan 时,可能会出现错误 “Error: configuring Terraform AWS Provider: no valid credential sources for Terraform AWS Provider found。” 您会遇到此错误,是因为 Terraform 尝试连接到 AWS,但无法向 AWS 提供适当的凭据。要解决此问题,您需要创建(或编辑)名为 ~/.aws/credentials
的文件,并添加您的 ITAM AWS 访问密钥 ID 和 AWS 密钥访问密钥凭据。有关如何正确执行此操作的完整详细信息,请参阅 Amazon Web Services in Action, Third Edition 的 4.2.2 配置 CLI 部分。
最后,要应用 Terraform 更改,您将使用 terraform apply
命令。然后,Terraform 将读取当前目录中的配置文件,并将任何更改应用于您的基础架构。如果您自上次运行 terraform apply
以来对配置文件进行了任何更改,例如,如果我们需要启动一个新的数据库实例或更改 EC2 的大小,则 Terraform 将向您显示将要进行的更改的预览,并在应用更改之前提示您确认。
如果您要应用这些更改,那么在几分钟内,您将在您的 VPC 中运行一个全新的 EC2 实例。然而,这只是问题的一半。拥有您手边的计算能力非常棒;然而,您需要某些东西来应用此能力。在这种情况下,我们可以使用这个 EC2 来运行我们的信息系统资产管理系统。以下部分将简要介绍将本地构建的镜像传输到另一台机器的过程。
7.3 移动 Docker 镜像(困难的方法)
首先,我们将从本地计算机导出一个 Docker 镜像,并将其加载到远程计算机上。我们将使用命令 docker save
和 load
来完成此操作。您可以在本地计算机上使用 docker save
命令将镜像保存到 tar 存档中。以下命令将镜像保存到名为 <image-name>.tar
的 tar 存档中:docker save -o <image-name>.tar <image-name>:<tag>
。
接下来,您可以使用诸如 SCP 或 SFTP 之类的文件传输协议将 tar 存档传输到远程计算机。您可以在远程计算机上使用 docker load 命令从 tar 存档加载镜像:docker load -i <image-name>.tar.
这将在远程计算机上将镜像加载到本地 Docker 镜像缓存中。一旦镜像加载完成,您可以使用 docker run
命令启动镜像并运行 Docker 容器,就像您构建它后所做的那样。此外,您随后可以将此镜像添加到您的 Docker compose 文件中,其中包含了您的 Postgres 数据库和 Kafka 实例。
注意
Terraform 的前期介绍被大大简化了。当你准备认真学习 Terraform 时,你的首选资源应该是 Scott Winkler 的 Terraform 实战(2021 年 5 月,Manning 出版社)。
本节介绍了如何打包我们的镜像并将其加载到远程主机上。尽管这很容易脚本化,但随着容器注册表的出现,现在更容易地管理部署而无需将它们全部传送到互联网上。在下一节中,我们将探讨其中一个工具 Amazon 的弹性容器注册表(ECR)。
7.4 将 Docker 镜像轻松传送到其他地方
Docker 镜像,我们容器的蓝图,是容器化应用的基本构建块。正确管理它们确保我们保持清洁、高效和有组织的开发和部署工作流程。Amazon ECR 是一个完全托管的 Docker 容器注册表,使开发人员能够轻松地存储、管理和部署 Docker 容器镜像。
首先,让我们深入了解如何将 Docker 镜像推送到 ECR。这个过程对于使您的镜像可以使用和部署是至关重要的。我们将逐步介绍设置您的本地环境、与 ECR 进行身份验证和推送您的镜像。在我们将镜像移到 ECR 之前,我们必须创建一个仓库来存放该镜像。这可以从 AWS 管理控制台中进行,或者我们将要做的就是使用 AWS 命令行界面(CLI)。为我们的镜像创建一个新仓库的命令是:aws ecr create-repository --repository-name itam
接下来,我们需要为我们的 Docker 镜像打上 ECR 仓库的 URL 和镜像名称的标签。我们可能想要将其称为 latest 或使用语义化版本。打标签将使我们能够轻松地回滚或前进系统的版本。我们将使用以下命令为我们的应用镜像打上最新标签:docker tag itam:latest 123456789012.dkr.ecr.us-west-2.amazonaws.com/itam:latest.
然后,我们需要使用 aws ecr get-login-password
命令对 Docker 进行身份验证以访问 ECR 注册表。这将生成一个 Docker 登录命令,您可以使用该命令对 Docker 进行身份验证以访问注册表。登录的命令是 aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-west-2.amazonaws.com
最后,我们将使用 docker push
命令将 Docker 镜像推送到 ECR 注册表。我们会这样做:docker push 123456789012.dkr.ecr.us-west-2.amazonaws.com/itam:latest
一旦我们的镜像在我们的注册表中,我们的部署选项就大大增加了。例如,我们可以编写一个 bash 脚本,登录到我们的 EC2 实例并执行 docker pull 命令来下载并在该 EC2 实例上运行镜像。或者,我们可能希望采用更可靠的部署模式。在接下来的部分中,我们将逐步介绍在一个名为弹性 Kubernetes 服务(EKS)的强大云服务上设置和启动我们的应用程序的过程。EKS 是由 AWS(亚马逊网络服务)提供的托管 Kubernetes 服务。让我们开始吧!
7.5 将我们的应用程序部署到 AWS 弹性 Kubernetes 服务(EKS)
Kubernetes 相对于简单地在 EC2 实例上运行 Docker 镜像具有许多优势。首先,使用 Kubernetes 管理和扩展我们的应用程序变得更加简单。而且,使用 Kubernetes,我们不必花费大量额外的时间考虑我们的基础架构应该是什么样子。此外,由于其对其镜像生命周期的自动管理(称为 pod),我们的应用程序基本上是自愈的。这意味着如果出现问题,Kubernetes 可以自动修复它,始终保持我们的应用程序平稳运行。
首先,我们需要一个用 YAML(另一种标记语言或 YAML 不是标记语言,这取决于你问的是谁)编写的部署描述符,它将描述我们的信息技术资产管理系统在任何时间点应处于的状态。该文件(通常称为 deployment.yaml)将提供模板,Kubernetes 将根据该模板与当前运行的系统进行比较,并在需要时进行更正。
清单 7.4 用于信息技术资产管理系统的 Kubernetes 部署文件
# Create a Kubernetes deployment file for the itam application. The image name is itam:latest
# The deployment will run on port 8000
apiVersion: apps/v1
kind: Deployment
metadata:
name: itam-deployment
labels:
app: itam
spec:
replicas: 1
selector:
matchLabels:
app: itam
template:
metadata:
labels:
app: itam
spec:
containers:
- name: itam
image: itam:latest
imagePullPolicy: Always
ports:
- containerPort: 8000
然而,这样是行不通的。Kubernetes 将无法找到我们在部署描述符文件中引用的镜像。为了纠正这一点,我们需要告诉 Kubernetes 使用我们新建的 ECR。幸运的是,这并不像听起来那么困难。我们只需更新文件中的图像条目,指向 ECR 镜像,以及授予 EKS 访问 ECR 的权限(也许有点棘手,但还是可以管理的)。
首先,更新部署 yaml 文件以使用 ECR 镜像。它将类似于这样:image: 123456789012.dkr.ecr.us-west-2.amazonaws.com/itam:latest
。然后,我们需要定义一个供 EKS 使用的策略。然后,我们将使用 AWS CLI 或 IAM 管理控制台应用该策略。虽然应用策略略微超出了本书的范围,但您可以使用 Copilot 定义此策略。生成的策略将类似于以下清单。
清单 7.5 允许 EKS 从 ECR 拉取镜像的 IAM 策略
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPull",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<aws_account_id>:role/<role>"
},
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
],
"Resource": "arn:aws:ecr:<region>:<aws_account_id>:repository/<repository_name>"
}
]
}
一旦 EKS 能从 ECR 拉取镜像,你会看到一个 Pod 开始运行。然而,你没有办法从外部访问这个 Pod。我们需要创建一个 Service。在 Kubernetes 中,Service 是一种抽象,定义了一组逻辑上的 Pods(在 Kubernetes 对象模型中,你创建或部署的最小、最简单的单位)以及访问它们的策略。
Services 使应用程序的不同部分之间,以及不同应用程序之间能够进行通信。它们通过将 Pods 暴露给网络以及 Kubernetes 内部的其他 Pods 来帮助分配网络流量和进行负载平衡。
7.6 列表:一个 Kubernetes 服务文件,以便为我们的应用程序启用外部访问。
# Please create a service for the application that uses a load balancer type egress
apiVersion: v1
kind: Service
metadata:
name: itam-service
spec:
type: LoadBalancer
selector:
app: itam
ports:
- name: http
port: 80
targetPort: 8000
Kubernetes 负责将来自此入口的所有请求通过服务路由到正在运行的 Pods,无论它们运行在哪个主机上。这实现了无缝的故障切换。Kubernetes 预期某些事情会失败。它对此有所准备。因此,分布式系统中的许多最佳实践都被融入了 Kubernetes。进入 Kubernetes 是实现一个可靠、高可用系统的重要第一步。在接下来的部分中,我们将探讨如何减轻将应用程序重复且持续地部署到 Kubernetes 上的负担。我们将探讨使用 GitHub Actions 构建一个小型部署管道。
7.6 章节:在 GitHub Actions 中设置一个持续集成/持续部署管道。
如果发布过程很困难,那么它就不会经常进行。这限制了我们为应用程序和利益相关者增加价值的能力。然而,自动化部署过程显著减少了发布所需的时间。这使得更频繁的发布成为可能,加快了开发的步伐,并加速了向用户交付新功能。持续集成/持续部署(CI/CD)管道可以降低与部署相关的风险。通过进行更小、更频繁的更新,任何出现的问题都可以迅速被隔离和修复,从而将对最终用户的潜在影响降到最低。这些管道促进代码更顺畅地集成并加速部署,简化了软件发布过程。
GitHub Actions 允许我们直接在 GitHub 仓库中构建自定义的 CI/CD 管道。这使得开发工作流程更高效,并使得自动化各种步骤成为可能,让我们能够专注于编写代码,而不是处理集成和部署的后勤工作。
本节提供了使用 GitHub Actions 和 GitHub Copilot 设置 CI/CD 管道的简要介绍。请注意,这不是一份详尽的指南,而是一份概述,介绍了潜在的好处和一般工作流程。这应该作为一个入门教程,让你了解如何使用这些工具来优化你的软件开发过程。
首先,我们将在项目中创建一个文件,路径为:.github/workflows.
。注意前导点。我们可以将此文件命名为itam.yaml
或任何您想要的名称。在此文件的第一行上,添加以下提示:# 创建一个 GitHub Actions 工作流,每次合并到主分支时都会构建 ITAM 应用程序并将其部署到 EKS。
注意
:像
我们在本章中交给 Copilot 的许多基础设施相关任务一样,Copilot 需要大量帮助来为我们创建此文件。我们需要了解此文件的结构以及如何开始每一行。在这种情况下,像向 ChatGPT 或 Copilot Chat 请求构建文件一样是有道理的。
列表 7.7 我们用于构建和部署应用程序的 GitHub Actions 文件的开头
# Create a GitHub Actions workflow that builds the ITAM application on every merge to the main branch and deploys it to EKS.
name: Build and Deploy to EKS
on:
push:
branches:
- main
jobs:
构建作业首先将代码从我们的 GitHub 存储库中检出。它使用在模块 actions/checkout 版本 2 中编写的代码。类似地,接下来,它将获取 EKS 命令行界面并配置凭据以连接到 EKS。您将注意到 AWS 访问密钥和密码是自动传递到应用程序中的值。GitHub Actions 使用内置的秘密管理系统来存储诸如 API 密钥、密码或证书等敏感数据。此系统已集成到 GitHub 平台中,并允许您在存储库和组织级别添加、删除或更新秘密(和其他敏感数据)。秘密在存储之前已加密,不会显示在日志中,也无法下载。它们仅作为环境变量暴露给 GitHub Actions 运行器,这是处理敏感数据的安全方式。
同样,您可以创建环境参数并在您的操作中使用它们。例如,看看变量ECR_REGISTRY
。该变量是使用login-ecr
函数的输出创建的。在这种情况下,您仍然需要在您的操作文件中硬编码弹性容器注册表(ECR)。但是,您希望这样做是因为一致性和仅在文件中的一个位置管理它的需要。大多数这些步骤应该对您来说很熟悉,因为我们在整章中都使用过它们。这就是自动化的魔力:它为您做这些事情。
列表 7.8 我们的 GitHub Actions 文件的构建和部署步骤
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up EKS CLI
uses: aws-actions/amazon-eks-cli@v0.1.0
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2
- name: Build and push Docker image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: itam
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
- name: Deploy to EKS
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: itam
IMAGE_TAG: ${{ github.sha }}
run: |
envsubst < k8s/deployment.yaml | kubectl apply -f -
envsubst < k8s/service.yaml | kubectl apply -f -
文件的最后部分将登录到 AWS ECR。操作文件中的步骤调用此部分。完成后,它将输出返回到调用函数。
列表 7.9 我们用于构建和部署应用程序到 EKS 的 GitHub Actions 文件的开头
login-ecr:
runs-on: ubuntu-latest
steps:
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
with:
registry: <your-ecr-registry>
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
总之,探索 Code-as-Infrastructure 使我们能够了解其在任何项目中的重要作用以及如何通过代码更好地管理它。像 Terraform 这样的工具提供了管理基础设施的简化解决方案,而 GitHub 的以代码为中心的功能有助于维护整体工作流程。
引入持续集成和持续部署(CI/CD)流水线,主要通过 GitHub Actions 平台,突出了自动化软件交付流程的重要性。自动化这些流程可以提高软件开发生命周期的速度和可靠性,并减少人为错误的机会。
管理基础设施即代码的旅程不断发展,新工具和实践不断涌现。它需要持续的学习和适应心态。本章为您展示了其好处和可能性。
7.7 摘要
-
从应用程序开发到产品发布的过渡:讨论了从应用程序准备投入生产到发布上线的过程。
-
云基础设施的部署策略和最佳实践:详细说明了如何设置您的云基础设施以及实现最佳性能的推荐准则。
-
使用 Docker 进行应用程序容器化:它解释了 Docker 如何将应用程序及其依赖项捆绑到单个对象中,从而便于部署和管理。
-
用于基础设施即代码的 Terraform 入门:介绍了 Terraform 如何使您能够使用代码来管理基础设施,从而提高效率并减少错误。
-
通过 Kubernetes 进行应用部署管理:讨论了 Kubernetes 如何简化容器化应用程序的部署、扩展和管理。
-
将方法适应不同的云平台或本地基础设施:解释了书中讨论的方法如何适应不同的云平台或本地部署。
-
GitHub Copilot 在创建 Dockerfile 和 Terraform 文件中的作用:讨论了 GitHub Copilot,一个基于人工智能的代码助手,如何帮助您更高效、更准确地创建 Docker 和 Terraform 文件。
-
将 Docker 镜像从本地导出到远程机器:解释了将 Docker 镜像从本地计算机移动到远程服务器的步骤。
-
在 AWS 的弹性 Kubernetes 服务(EKS)上部署:讨论了如何在 AWS EKS 上部署容器化应用程序,这是一项托管的 Kubernetes 服务。
-
创建 Kubernetes YAML 部署描述符:详细说明了如何编写 YAML 格式的 Kubernetes 部署描述符,描述了部署的期望状态。
-
用于网络流量分发和通信的 Kubernetes 服务形成:解释了如何创建 Kubernetes 服务,这些服务抽象了您与 Pod 的通信和流量路由方式。
-
将 Docker 镜像推送到亚马逊的弹性容器注册表(ECR):描述了如何将 Docker 镜像上传到 ECR,这是 AWS 提供的完全托管的 Docker 容器注册表。
-
从本地到使用 GitHub Actions 自动化部署的迁移:讨论了如何使用 GitHub Actions,一个 CI/CD 平台,自动化部署流程。