云原生应用最独特的一点是它们的部署。在传统的应用部署中,团队通过登录到服务器并安装应用来部署应用。但在云中,通常有许多服务器,登录到每个服务器并手动安装应用是不可行的,并且可能非常容易出错。为了解决这些问题,我们使用云资源调配工具来自动部署云原生应用。
在本章中,我们将深入探讨微服务的部署模型,包括如何将您的应用打包为 Docker 容器,如何设置 CI/CD 管道,以及如何保护您的服务免受安全攻击,例如分布式拒绝服务(DDoS)。我们将介绍以下内容:
- 部署模型、打包和集装箱化(使用 Docker)
- 部署模式(蓝绿色、金丝雀色和深色)
- DDoS
- CI/CD
我们将介绍用于在云环境中部署应用的部署模型。
云的基本构建块是虚拟机(从现在起称为 VM),它相当于用户可以登录、安装或维护应用的物理服务器(或主机)。不同之处在于,可以在单个主机上托管多个 VM,从而提高资源利用率。这是通过使用虚拟化实现的,虚拟机监控程序安装在主机上,然后可以将物理服务器上的可用资源(如计算、内存、存储和网络)分配给托管在其上的不同虚拟机。可以使用以下策略在此类虚拟机上部署云原生应用:
- 每个虚拟机有几个应用
- 每个虚拟机一个应用
当每个 VM 运行多个应用时,一个应用可能占用 VM 上的所有可用资源,并耗尽其他应用。另一方面,每个 VM 运行一个应用可以确保应用是隔离的,这样它们就不会相互影响,但这种部署的缺点是浪费资源,因为每个应用可能并不总是消耗所有可用的资源。
PaaS 或平台即服务是部署云原生应用的另一个流行选项。PaaS 提供了补充云原生应用的开发、扩展和维护的附加服务。通过构建包实现的自动化构建和部署等服务极大地减少了设置支持这些活动的附加基础架构所花费的时间。PaaS 还提供一些基本的基础设施服务,如监控、日志聚合、机密管理和开箱即用的负载平衡。CloudFoundry、GoogleAppEngine、Heroku 和 OpenShift 都是 PaaS 的一些例子。
为了提供独立运行所需的隔离级别,同时节约资源利用,容器技术得到了发展。通过利用 Linux 内核的特性,容器在进程级别提供 CPU、内存、存储和网络隔离。下图显示了虚拟化之间的区别:
容器消除了对来宾操作系统的需求,从而大大增加了可以运行的容器数量,而不是同一主机上的虚拟机数量。容器的占用空间也较小,以 MB 为单位,而虚拟机很容易超过几 GB。
容器在所需的 CPU 和内存量方面也非常节省资源,因为它们不必支持运行成熟操作系统时必须支持的许多外围系统:
上图显示了云原生应用部署策略的演变,旨在提高应用的资源利用率和隔离度。堆栈顶部是主机上运行的虚拟机中运行的容器。这允许应用按两个角度扩展:
- 增加 VM 中容器的数量
- 增加运行容器的虚拟机的数量
Docker 是一个已经非常流行的容器运行时,并且已经证明自己是部署云原生应用的健壮平台。Docker 可用于所有主要平台,如 Windows、Mac 和 Linux。因为容器需要 Linux 内核,所以在 Linux 环境中运行 Docker 引擎更容易。但是,有几种资源可用于在 Windows 和 Mac 环境中舒适地运行 Docker 容器。我们将演示如何部署到 Docker 容器为止我们一直在开发的服务,包括连接到在其自身容器中运行的外部数据库。
在我们的示例中,我们将使用 Docker 工具箱和 Docker Machine 创建一个 VM,Docker 引擎将在其中运行。我们将使用 Docker 命令行客户端连接到此引擎,并使用提供的各种命令。
我们将开始将当前项目作为一组 Docker 容器进行容器化。我们将完成每个项目的步骤。
- 在
$WORKSPACE/eureka-server/.dockerignore
中增加一个包含以下内容的.dockerignore
文件:
.*
target/*
!target/eureka-server-*.jar
- 在
$WORKSPACE/eureka-server/Dockerfile
中添加具有以下内容的 Dockerfile:
FROM openjdk:8-jdk-alpine
RUN mkdir -p /app
ADD target/eureka-server-0.0.1-SNAPSHOT.jar /app/app.jar
EXPOSE 8761
ENTRYPOINT [ "/usr/bin/java", "-jar", "/app/app.jar" ]
- 构建 runnable JAR,该 JAR 将在目标文件夹中可用:
mvn package
- 构建 Docker 容器:
docker build -t cloudnativejava/eureka-server .
上述命令的输出显示在以下屏幕截图中:
- 在运行容器之前,我们需要创建一个网络,在这个网络上不同的容器可以自由地相互通信。可以通过运行以下命令来创建此命令:
docker network create app_nw
上述命令的输出显示在以下屏幕截图中:
- 运行名为
eureka
的容器并将其连接到先前创建的网络:
docker run -d --network app_nw --name eureka cloudnativejava/eureka-server
上述命令的输出显示在以下屏幕截图中:
接下来,我们将研究产品 API 项目:
- 通过将以下内容附加到现有文件中,在
application.yml
中添加一个新的弹簧外形docker
:
---
spring:
profiles: docker
eureka:
instance:
preferIpAddress: true
client:
serviceUrl:
defaultZone: http://eureka:8761/eureka/
- 构建 Spring 引导 JAR 以反映对
application.yml
的更改:
mvn clean package
- 增加一个
.dockerignore
文件,内容如下:
.*
target/*
!target/product-*.jar
- 添加包含以下内容的 Dockerfile:
FROM openjdk:8-jdk-alpine
RUN mkdir -p /app
ADD target/product-0.0.1-SNAPSHOT.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT [ "/usr/bin/java", "-jar", "/app/app.jar", "--spring.profiles.active=docker" ]
- 构建 Docker 容器:
docker build -t cloudnativejava/product-api .
上述命令的输出显示在以下屏幕截图中:
- 启动几个 Docker 容器:
docker run -d -p 8011:8080 \
--network app_nw \
cloudnativejava/product-api
docker run -d -p 8012:8080 \
--network app_nw \
cloudnativejava/product-api
以下屏幕截图显示了上述命令的输出:
产品 API 将在以下 URL 上提供:
http://<docker-host>:8011/product/1
http://<docker-host>:8012/product/1
要将product
API 连接到外部数据库而不是内存数据库,请首先创建一个容器映像,其中包含已填充的数据:
- 创建一个文件
import-postgres.sql
,包含以下内容:
create table product(id serial primary key, name varchar(20), cat_id int not null);
begin;
insert into product(name, cat_id) values ('Apples', 1);
insert into product(name, cat_id) values ('Oranges', 1);
insert into product(name, cat_id) values ('Bananas', 1);
insert into product(name, cat_id) values ('Carrots', 2);
insert into product(name, cat_id) values ('Beans', 2);
insert into product(name, cat_id) values ('Peas', 2);
commit;
- 创建一个包含以下内容的
Dockerfile.postgres
:
FROM postgres:alpine
ENV POSTGRES_USER=dbuser
POSTGRES_PASSWORD=dbpass
POSTGRES_DB=product
EXPOSE 5432
RUN mkdir -p /docker-entrypoint-initdb.d
ADD import-postgres.sql /docker-entrypoint-initdb.d/import.sql
- 现在构建 Postgres 容器映像,该映像将使用
import-postgres.sql
的内容初始化数据库:
docker build -t cloudnativejava/datastore -f Dockerfile.postgres .
上述命令的输出显示在以下屏幕截图中:
- 通过将以下内容附加到现有文件,将新的弹簧外形
postgres
添加到application.yml
:
---
spring:
profiles: postgres
datasource:
url: jdbc:postgresql://<docker-host>:5432/product
username: dbuser
password: dbpass
driver-class-name: org.postgresql.Driver
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: none
确保用适合您环境的值替换<docker-host>
。
- 构建 Spring 引导 JAR 以反映对
application.yml
的更改:
mvn clean package
- 构建 Docker 容器:
docker build -t cloudnativejava/product-api .
上述命令的输出显示在以下屏幕截图中:
- 如果您已经有容器从旧映像运行,则可以停止并删除它们:
old_ids=$(docker ps -f ancestor=cloudnativejava/product-api -q)
docker stop $old_ids
docker rm $old_ids
- 启动数据库容器:
docker run -d -p 5432:5432
--network app_nw
--name datastore
cloudnativejava/datastore
上述命令的输出显示在以下屏幕截图中:
- 为产品 API 启动几个 Docker 容器:
docker run -d -p 8011:8080
--network app_nw
cloudnativejava/product-api
--spring.profiles.active=postgres
docker run -d -p 8012:8080
--network app_nw
cloudnativejava/product-api
--spring.profiles.active=postgres
上述命令的输出显示在以下屏幕截图中:
产品 API 将在以下 URL 上提供:
http://<docker-host>:8011/product/1
http://<docker-host>:8012/product/1
在介绍了云原生应用的打包和部署模型之后,我们现在将介绍用于部署云原生应用的模式。传统上,应用部署在多个环境中,如开发、测试、登台、预生产等,而这些环境中的每一个都可能是最终生产环境的缩小版本。应用经过一系列预生产环境,最终部署到生产环境。然而,一个显著的区别是,尽管在所有其他环境中都可以容忍停机,但生产部署中的停机可能会导致严重的业务后果。
使用云原生应用,可以零停机时间发布软件。这是通过在应用的开发、测试和部署的各个方面严格应用自动化来实现的。我们将在后面的章节中介绍持续集成(CI)/持续部署(CD),但这里我们将介绍一些能够快速部署应用的模式。所有这些模式都依赖于路由器组件的存在,与负载平衡器不同,路由器组件可以将请求路由到特定的应用实例集。在某些情况下,应用本身是使用隐藏在功能标志后面的功能构建的,这些功能标志可以通过更改应用配置来启用。
蓝绿色部署是一种分三个阶段进行的模式。部署的初始状态如下图所示。应用的所有流量都路由到现有实例,这些实例被视为蓝色实例。蓝绿部署的表示如下:
在 blue-green 部署的第一阶段,将提供一组具有新版本应用的新实例并使其可用。在此阶段,新的绿色应用实例对最终用户不可用,部署将在内部进行验证。如下所示:
在部署的下一个阶段,路由器上会抛出一个象征性的交换机,它现在开始将所有请求路由到绿色实例,而不是旧的蓝色实例。旧的蓝色实例会保留一段时间进行观察,如果检测到任何关键问题,我们可以根据需要快速回滚部署到应用的旧实例:
在部署的最后阶段,应用的较旧的蓝色实例将被停用,绿色实例将成为下一个稳定的生产版本:
在应用的两个稳定版本之间切换时,蓝绿色部署是有效的,而快速恢复是由回退环境的可用性保证的。
金丝雀部署也是蓝绿色部署的一种变体。Canary 部署解决了在同时运行两个生产实例(尽管持续时间很短)时调配的浪费资源。在金丝雀部署中,绿色环境是蓝色环境的缩小版本,依赖路由器的能力将一小部分请求一致地路由到新的绿色环境,而大部分请求路由到蓝色环境。下图描述了这一点:
这在发布应用的新功能时特别有用,这些功能需要与几个 beta 测试用户一起测试,然后根据该用户组向所有用户发布的反馈进行测试。一旦确定绿色环境已准备好完全展开,绿色环境中的实例将逐渐增加,同时蓝色环境中的实例将逐渐减少。下面的一系列图表最好地说明了这一点:
这样就避免了必须运行两个生产级环境的问题,并且可以从一个版本平稳地过渡到另一个版本,同时还可以轻松地回退到旧版本。
另一种用于部署云原生应用的流行部署模式是黑暗发布模式。在这里,新功能隐藏在功能标志后面,并为选定的用户组启用,或者在某些情况下,当应用模仿用户的行为并练习应用的隐藏功能时,用户完全不知道该功能。一旦认为该功能已准备就绪并稳定,可向所有用户推出,则可通过切换功能标志来启用该功能。
云原生应用部署的一个核心方面依赖于有效地自动化和构建软件交付管道的能力。这主要是通过使用 CI/CD 工具来实现的,这些工具可以从源存储库获取源代码,运行测试,构建可部署的工件,并将它们部署到目标环境。大多数现代 CI/CD 工具(如 Jenkins)都支持配置构建管道,这些管道可用于基于脚本形式的配置文件构建多个构件。
我们将以 Jenkins 管道脚本为例,演示如何配置简单的构建管道。在我们的示例中,我们将简单地构建两个工件,即eureka-server
和product-api
可运行 JAR。新增一个名为Jenkinsfile
的文件,内容如下:
node {
def mvnHome
stage('Preparation') { // for display purposes
// Get some code from a GitHub repository
git 'https://github.com/...'
// Get the Maven tool.
// ** NOTE: This 'M3' Maven tool must be configured
// ** in the global configuration.
mvnHome = tool 'M3'
}
stage('Eureka Server') {
dir('eureka-server') {
stage('Build - Eureka Server') {
// Run the maven build
if (isUnix()) {
sh "'${mvnHome}/bin/mvn' -Dmaven.test.failure.ignore clean package"
} else {
bat(/"${mvnHome}binmvn" -Dmaven.test.failure.ignore clean package/)
}
}
stage('Results - Eureka Server') {
archiveArtifacts 'target/*.jar'
}
}
}
stage('Product API') {
dir('product') {
stage('Build - Product API') {
// Run the maven build
if (isUnix()) {
sh "'${mvnHome}/bin/mvn' -Dmaven.test.failure.ignore clean package"
} else {
bat(/"${mvnHome}binmvn" -Dmaven.test.failure.ignore clean package/)
}
}
stage('Results - Product API') {
junit '**/target/surefire-reports/TEST-*.xml'
archiveArtifacts 'target/*.jar'
}
}
}
}
管道脚本执行以下操作:
- 从 GitHub 签出源代码
- 配置 Maven 工具
- 通过在签出的源存储库的两个目录中运行 Maven 构建来构建两个工件
- 存储测试结果和生成的 JAR
在 Jenkins 中创建新的管道作业:
在管道配置中,指定 GitHub 存储库和该 Git 存储库中的Jenkinsfile
路径:
运行构建时,应生成两个构件:
管道脚本可以扩展以构建 Docker 容器,我们在本章前面使用 Jenkins 的 Docker 插件手工构建了 Docker 容器。
在本章中,我们了解了可用于部署云原生应用的各种部署模式,以及如何使用 Jenkins 等持续集成工具来自动化构建和部署。我们还学习了如何使用 Docker 容器构建和运行示例云原生应用。