vlambda博客
学习文章列表

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署

Chapter 6. Cloud-Native Application Deployment

云原生应用程序最独特的事情之一就是它们的部署。在传统的应用程序部署中,团队通过登录到服务器并安装应用程序来部署他们的应用程序。但是在云中通常有很多服务器,登录到每台服务器并手动安装应用程序是不可行的,而且很容易出错。为了解决这些问题,我们使用云配置工具来自动部署云原生应用程序。

在本章中,我们将深入探讨微服务的部署模型——包括如何将应用程序打包为 Docker 容器、如何设置 CI/CD 管道以及如何 保护您的服务免受安全攻击,例如 分布式拒绝服务 (DDoS)。我们将介绍以下内容:

  • Deployment models, packaging, and containerization (using Docker)
  • Deployment patterns (blue-green, canary release, and dark release)
  • DDoS
  • CI/CD

Deployment models


我们将介绍用于在云环境中部署我们的应用程序的 deployment 模型。

Virtualization

云的基本构建块是虚拟机(以下简称VM),相当于用户可以登录并安装或维护应用程序的物理服务器(或主机)。不同之处在于可以在单个主机上托管多个 VM,从而提高资源利用率。这是通过使用虚拟化实现的,其中在主机上安装了管理程序,然后可以分配物理服务器上的可用资源,例如与托管在其上的不同 VM 的计算、内存、存储和网络连接。可以使用以下策略将云原生应用程序部署在此类 VM 上:

  • Several applications per VM
  • One application per VM

当每个 VM 运行多个应用程序时,一个应用程序可能会占用 VM 上的所有可用资源并饿死其他应用程序。另一方面,每个 VM 运行单个应用程序可确保应用程序是隔离的,因此它们不会相互影响,但这种部署的缺点是浪费资源,因为每个应用程序可能并不总是消耗所有的它可用的资源。

PaaS

PaaS 或平台即服务是部署云原生应用程序的另一个 流行选项。 PaaS 提供额外的服务来补充云原生应用程序的开发、扩展和维护。通过 buildpack 进行自动化构建和部署等服务极大地减少了设置额外基础架构以支持这些活动所花费的时间。 PaaS 还提供了一些基本的基础设施服务,例如开箱即用的监控、日志聚合、秘密管理和负载平衡。 Cloud Foundry、Google App Engine、Heroku 和 OpenShift 是一些例子 即服务。

Containers

做出的努力提供了独立操作所需的隔离级别,同时还保护 资源利用,导致了容器技术的发展。通过利用 Linux 内核的特性,容器在进程级别提供 CPU、内存、存储和网络隔离。下图展示了虚拟化之间的区别:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署

容器消除了对来宾操作系统的需求,从而大大增加了可以运行的容器数量,而不是同一主机上的虚拟机数量。容器的占用空间也更小,大约为 MB,而 VM 可以轻松超过几 GB。

容器在所需的 CPU 和内存量方面也非常节省资源,因为它们不必支持在运行成熟的操作系统时必须支持的许多外围系统:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署

上图显示了云原生应用程序部署策略的演变,旨在提高应用程序的资源利用率和隔离性。堆栈的顶部是在主机上运行的 VM 中运行的容器。这允许应用程序扩展两度:

  • Increasing the number of containers within a VM
  • Increasing the number of VMs running containers

Docker

Docker 是一个容器runtime,它已广受欢迎,并已证明自己是部署云原生应用程序的强大平台。 Docker 在所有主要平台上都可用,例如 Windows、Mac 和 Linux。由于容器需要 Linux 内核,因此在 Linux 环境中运行 Docker 引擎更容易。但是,有多种资源可用于在 Windows 和 Mac 环境中舒适地运行 Docker 容器。我们将演示如何将我们一直在开发的服务部署为 Docker 容器,包括连接到在其自己的容器中运行的外部数据库。

在我们的示例中,我们将使用 Docker Toolbox 并使用 Docker Machine 创建一个 VM,Docker 引擎将在其中运行。我们将使用 Docker 命令行客户端连接到该引擎并使用提供的各种命令。

Building Docker images

我们将开始containerize我们当前的项目作为一组Docker 容器。我们将完成每个项目的步骤。

Eureka server

  1. Add a .dockerignore file with the following contents in $WORKSPACE/eureka-server/.dockerignore:
.* 
target/* 
!target/eureka-server-*.jar 
  1. Add a Dockerfile with the following contents in $WORKSPACE/eureka-server/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" ] 
  1. Build the runnable JAR, which will be available in the target folder:
mvn package 
  1. Build the Docker container:
docker build -t cloudnativejava/eureka-server .

上述命令的输出如以下屏幕截图所示:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署
  1. Before running the container, we need to create a network on which the different containers can communicate freely with each other. This can be created by running the following command:
docker network create app_nw 

上述命令的输出如以下屏幕截图所示:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署
  1. Run the container with the name eureka and attach it to the network created earlier:
docker run -d --network app_nw --name eureka cloudnativejava/eureka-server 

上述命令的输出如以下屏幕截图所示:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署

Product API

接下来我们在产品 API 项目上工作:

  1. Add a new Spring profile, docker in the application.yml by appending the following contents to the existing file:
--- 
spring: 
  profiles: docker 
eureka: 
  instance: 
    preferIpAddress: true 
  client: 
    serviceUrl: 
      defaultZone: http://eureka:8761/eureka/ 
  1. Build the Spring Boot JAR to reflect changes to application.yml:
mvn clean package
  1. Add a .dockerignore file with the following contents:
.* 
target/* 
!target/product-*.jar 
  1. Add a Dockerfile with the following contents:
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" ] 
  1. Build the Docker container:
docker build -t cloudnativejava/product-api . 

上述命令的输出如以下屏幕截图所示:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署
  1. Start several Docker containers:
docker run -d -p 8011:8080 \ 
    --network app_nw \ 
    cloudnativejava/product-api 
 
docker run -d -p 8012:8080 \ 
    --network app_nw \ 
    cloudnativejava/product-api

上述命令的输出如以下屏幕截图所示:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署

产品 API 将在以下 URL 提供:

  • http://<docker-host>:8011/product/1
  • http://<docker-host>:8012/product/1

Connecting to an external Postgres container

要将 product API 连接到外部数据库而不是内存数据库,首先创建一个容器镜像,其中已经填充了数据:

  1. Create a file, import-postgres.sql, with the following contents:
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; 
  1. Create a Dockerfile.postgres with the following contents:
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 
  1. Now build the Postgres container image which will have the database initialized with the contents of import-postgres.sql:
docker build -t cloudnativejava/datastore -f Dockerfile.postgres .

上述命令的输出如以下屏幕截图所示:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署
  1. Add a new Spring profile, postgres to the application.yml by appending the following contents to the existing file:
--- 
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> 替换为适合您环境的值。

  1. Build the Spring Boot JAR to reflect changes to application.yml:
mvn clean package
  1. Build the Docker container:
docker build -t cloudnativejava/product-api . 

上述命令的输出如以下屏幕截图所示:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署
  1. If you already have containers running off the old image you can stop and remove them:
old_ids=$(docker ps -f ancestor=cloudnativejava/product-api -q) 
docker stop $old_ids 
docker rm $old_ids 
  1. Start the database container:
docker run -d -p 5432:5432  
    --network app_nw  
    --name datastore  
    cloudnativejava/datastore

上述命令的输出如以下屏幕截图所示:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署
  1. Start several Docker containers for the product API:
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

上述命令的输出显示在 以下 屏幕截图中:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署

产品 API 将在以下 URL 提供:

  • http://<docker-host>:8011/product/1
  • http://<docker-host>:8012/product/1

Deployment patterns


在介绍了云原生应用程序的打包和部署模型之后,我们现在将介绍用于部署云原生应用程序的模式.传统上,应用程序部署在多个环境中,例如开发、测试、暂存、预生产等,并且这些环境中的每一个都可能是最终生产环境的缩小版本。应用程序通过一系列预生产环境并最终部署到生产环境。但是,一个显着的区别是,虽然在所有其他环境中都可以容忍停机,但生产部署中的停机可能会导致严重的业务后果。

使用云原生应用程序,可以发布零停机时间的软件。这是实现的,通过将自动化严格应用到应用程序的开发、测试和部署的各个方面来实现。我们将介绍 持续集成 (CI) / 持续部署 (CD) 在后面的部分,但我们将在这里介绍一些能够快速部署应用程序的模式。所有这些模式都依赖于路由器组件的存在,与负载均衡器不同,它可以将请求路由到某个集合应用程序实例。在某些情况下,应用程序本身是使用隐藏在功能标志后面的功能构建的,可以通过更改应用程序配置来启用这些功能。

Blue-green deployment

蓝绿部署是一个发生在三个阶段的模式。 initial 部署状态在以下 图。应用程序的所有流量都被路由到现有实例,这些实例被视为蓝色实例。蓝绿部署的表示如下:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署

在蓝绿部署的第一阶段,一组带有新版本应用程序的新实例被配置并可用。在此阶段,最终用户无法使用新的绿色应用程序实例,并在内部验证部署。这显示在这里:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署

在部署的下一阶段,在路由器上抛出一个比喻开关,它现在开始将所有请求路由到绿色实例而不是旧的蓝色实例。旧的蓝色实例会保留一段时间,如果检测到任何严重问题,我们可以根据需要快速将部署回滚到应用程序的旧实例:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署

在部署的最后阶段,应用程序的旧蓝色实例被停用,绿色实例成为下一个稳定的生产版本:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署

蓝绿部署在应用程序的两个稳定版本 之间切换时非常有效,并且通过备用方案的可用性确保快速恢复环境。

Canary deployment

Canary deployment 也是蓝绿部署的一种变体。 Canary 部署解决了同时运行两个生产实例时配置的资源浪费问题,尽管时间很短。在金丝雀部署中,绿色环境是蓝色环境的缩小版本,它依赖于路由器的能力,将一小部分请求始终路由到新的绿色环境,而将大部分请求路由到蓝色的环境。 以下 图描述了这一点:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署

这在发布需要由少数 beta 测试用户测试的应用程序的新功能,然后根据该用户组的反馈向所有用户推出时特别有用。一旦确定绿色环境已准备好全面推出,绿色环境中的实例将逐渐增加,同时蓝色环境中的实例将逐渐减少。下面的图表序列最好地说明了这一点:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署

这样就避免了必须运行两个生产级环境的问题,并且可以从一个版本平滑过渡到另一个版本,同时还可以轻松回退到旧版本。

Dark release

另一种用于部署云原生应用程序的流行部署 pattern 是暗发布模式。在这里,新功能隐藏在功能标志后面并为选定的用户组启用,或者在某些情况下,用户完全不知道该功能,而应用程序会模仿用户的行为并练习应用程序的隐藏功能。一旦该功能被认为已准备好并稳定地向所有用户推出,然后通过切换功能标志来启用它。

Applying CI/CD to automate

云原生应用程序部署的核心方面之一依赖于能力 来有效地自动化和构建软件交付管道。这主要是通过使用 CI/CD 工具来完成的,这些工具可以从源存储库中获取源代码、运行测试、构建可部署的工件并将它们部署到目标环境。大多数现代 CI/CD 工具(例如 Jenkins)都支持配置构建管道,这些构建管道可用于基于脚本形式的配置文件构建多个工件。

我们将以 Jenkins 流水线脚本为例来演示如何配置简单的构建流水线。在我们的示例中,我们将简单地构建两个工件,即 eureka-serverproduct-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' 
      } 
    } 
  } 
} 

管道脚本执行以下操作:

  1. Checks out the source code from GitHub
  2. Configures the Maven tool
  3. Builds two artifacts by running the Maven build within two directories of the checked-out source repository
  4. Stores the test results and the resultant JARs from the build

在 Jenkins 中创建一个新的流水线作业:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署

在管道配置中,指定 GitHub 存储库以及该 Git 存储库中 Jenkinsfile 的路径:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署

在运行构建时,应该会构建两个工件:

读书笔记《cloud-native-applications-in-java》云-本机应用程序部署

可以扩展管道脚本来构建我们在本章前面使用 Jenkins 的 Docker 插件手动构建的 Docker 容器。

Summary


在本章中,我们了解了可用于部署云原生应用程序的各种部署模式,以及如何持续集成诸如 Jenkins 之类的工具可用于自动化构建和部署。我们还学习了如何使用 Docker 容器构建和运行示例云原生应用程序。