开发人员正在把应用程序迁移到云上,于是他们在设计及部署云原生应用程序方面也积累了越来越多的经验。从这种经验中出现了一组最佳实践(俗称十二要素)。考虑这些要素来设计应用程序,可以让我们在把应用程序部署到云上时,能有更高的可移植性和弹性,相比之下,部署到内部环境中的应用程序则需要花更长的时间来提供新资源。
本文描述了流行的十二要素APP方法论,以及开发运行于谷歌云平台(Google Cloud Platform,简称GCP)上的应用程序时如何应用它。我们使用这种方法可以开发出可扩展和具有弹性的应用程序,这些应用程序能够以最大灵活性持续地部署。本文旨在供那些熟悉GCP、版本控制、持续集成和容器技术的开发人员参考。
十二要素设计还有助于解耦应用程序的组件,从而可以轻松地替换每个组件,或无缝地上下扩展。因为这些要素是独立于任何编程语言或软件堆栈的,所以十二要素设计可以应用于各种各样的应用程序。
应该在版本控制系统中(如Git或Mercurial)跟踪应用程序的代码。处理应用时,可以在本地开发环境中确认代码。在版本控制系统中存储代码可以提供对代码更改的审计跟踪、解决合并冲突的系统方法及将代码回滚到以前版本的能力,从而让团队合作开发。它还提供了一个进行持续集成(continuous integration,简称CI)和持续部署(continuous deployment,简称CD)的地方。
尽管开发人员可能在他们的开发环境中处理不同版本的代码,但在任何给定的时间,事实来源都是版本控制系统中的代码。存储库中的这些代码是构建、测试和部署的内容,并且,存储库的数量与环境的数量无关。存储库中的代码用于产生单个构建,和特定于环境的配置结合在一起用于产生一个不可变的版本(无法修改,包括对配置的修改),然后,它可以被部署到一个环境中(这个版本所需的任何更改都将导致一个新版本的产生。)
云源代码存储库让我们能够在一个功能齐全、可扩展的私有Git存储库中协作和管理我们的代码。它具有跨所有存储库的代码搜索功能。我们还可以连接到其他GCP产品,例如云构建(Cloud Build)、应用引擎(App Engine)、Starkdriver和云发布/订阅(Cloud Pub/Sub)。
在谈到十二要素APP的依赖项时,有两个注意事项:依赖项声明和依赖项隔离。
十二要素APP应该永远没有隐式依赖性。我们应该显式声明任何依赖项并让它们进入版本控制中。这使我们能够用可重复的方式快速地开始使用代码,并更容易跟踪依赖项的更改。很多编程语言提供一种显式声明依赖项的方式,比如Python的pip和Ruby的Bundler。
我们还应该把应用程序及其依赖项封装到容器中,从而把它们隔离开来。容器使我们能够把应用程序及其依赖项与其环境隔离开来,这样无论开发和运行环境有任何不同,都能确保应用程序一致地工作。
容器注册表(Container Registry )是供团队管理映像及进行漏洞分析的唯一去处。它还提供了对容器映像的细粒度访问,让我们决定谁可以访问什么。由于容器注册表使用云存储桶作为服务容器映像的后端,因此,我们可以通过调整该云存储桶的权限,控制谁可以访问容器注册表映像。
现有的CI/CD集成还让我们设置全自动管道,以获得快速反馈。我们可以推送映像到它们的注册表,然后使用HTTP端点从任何机器上来拉取映像,无论这些机器是计算引擎实例( Compute Engine instance)还是我们自己的硬件。接着,容器分析可以为容器注册表中的映像提供漏洞信息。
每个现代应用程序都需要某种形式的配置。通常,我们对每个环境(如开发环境、测试环境和生产环境)都有不同的配置。这些配置经常包括服务账户凭据和数据库等支持服务的资源句柄。
每个环境的配置都应该在代码的外部,并且不应该进入版本控制。每个人只处理代码的一个版本,但我们有多个配置。部署环境决定使用哪个配置。这可以让二进制代码的一个版本部署到每个环境中,其中唯一的不同是运行时配置。一个检查配置是否已经正确地外部化的简单方法是,检查是否可以在不泄露任何凭据的情况下公开代码。
配置外部化的方法之一是创建配置文件。然而,配置文件通常特定于编程语言或开发框架。
一个更好的方法是在环境变量中存储配置。这些环境变量容易在运行时针对每个环境进行更改,它们不太可能进入版本控制,并且,它们与编程语言和开发框架无关。在谷歌Kubernetes引擎(Google Kubernetes Engine,简称GKE)中,我们可以使用ConfigMaps。这让我们可以在运行时将环境变量、端口号、配置文件、命令行参数以及其他配置工件绑定到pod容器和系统组件 。
应用程序正常操作时使用的每个服务(如文件系统、数据库、缓冲系统和消息队列)都应该作为服务被访问并在配置中外部化。我们应该考虑把这些支持服务作为底层资源的抽象。比如,当应用程序把数据写入存储时,把存储作为支持服务对待,可以让我们无缝地更改底层存储类型,这是因为它已与应用程序分离。这样一来,我们就可以执行一个更改,如从本地PostgreSQL数据库切换到Cloud SQL的PostgreSQL,而无需更改应用程序的代码。
重要的是要把软件部署过程分成三个截然不同的阶段:构建、发布及运行。每个阶段都应该形成一个唯一可识别的工件。每个部署都应该链接到一个特定版本,它是环境配置和内部版本结合形成的结果。这使回滚变得更加容易,每个产品部署历史都有一个可见的审计踪迹。
我们可以手动触发构建阶段,但在我们提交通过了所有要求的测试的代码时,通常会自动触发该阶段。构建阶段获取代码、获取所需的库和资源,并把这些打包到一个自包含的二进制文件或容器中。构建阶段的结果就是构建工件。
构建阶段完成时,发布阶段把构建工件和特定环境的配置结合在一起。这会生成一个版本。该版本可以通过持续部署应用程序自动地部署到环境中。或者可以通过同一个持续部署应用程序触发该版本。
最后,运行阶段推出该版本并启动之。比如,如果我们要部署到GKE,那么云构建(Cloud Build)可以调用gke-deploy构建步骤以部署到我们的GKE集群中。云构建可以使用YAML或JSON格式的构建配置文件,跨多种编程语言和环境,管理并自动化构建、发布和运行阶段。
可以把十二要素APP作为一个或更多进程运行在环境中。这些进程应该是无状态的,相互之间不应该共享数据。这使得这些应用可以通过复制其进程来扩展。创建无状态应用还使进程可跨计算基础设施移植。
如果我们已习惯“粘性”会话的概念,那么就需要我们改变对处理和持久化数据的看法。这是因为进程可以随时消失,而我们无法依赖本地存储的可用内容,否则任何后续请求将由同一进程处理。因而,我们必须明确保留所有需要在外部支持服务(如数据库)中重用的数据。
如果需要持久化数据,那么可以使用Cloud Memorystore,把它作为支持服务以缓存我们应用程序的状态,并在进程之间共享公共数据,以鼓励松散耦合。
在非云环境中,web应用程序常常被编写成在应用程序容器中运行,这些容器包括GlassFish、Apache Tomcat和Apache HTTP Server。与之相反,十二要素APP不依赖外部应用程序容器,而是绑定webserver库,使之成为该应用程序本身的一部分。
服务公开由PORT环境变量指定的端口号是一种体系结构的最佳实践。
使用平台即服务模型时,导出端口绑定的应用程序能够在外部使用端口绑定信息(作为环境变量)。在GCP中,可以在平台服务上部署应用程序,这些平台服务包括计算引擎(Compute Engine)、GKE、应用引擎(App Engine)或云运行(Cloud Run)。
在这些服务中,路由层把请求从面向公共的主机名路由到端口绑定的web进程。比如,在把应用程序部署到应用引擎时,需要声明给应用程序添加webserver库的依赖项,如Express(用于Node.js)、Flask和Gunicorn(用于Python)或Jetty(用于Java)。我们不应该在代码中硬编码端口号。相反,我们应该在环境中提供端口号,比如在一个环境变量中提供。这使得我们的应用程序在GCP上运行时具有可移植性。
由于Kubernetes具有内置的服务发现(service discovery)功能,在Kubernetes中,我们可以把服务端口映射到容器来抽象端口绑定。服务发现使用内部DNS名来完成。
与硬编码webserver侦听的端口相反,配置使用了环境变量。以下所示的代码截自一个应用引擎应用程序,显示了如何接收在环境变量中传递的端口值。
const express = require('express')
const request = require('got')
const app = express()
app.enable('trust proxy')
const PORT = process.env.PORT || 8080
app.listen(PORT, () => {
console.log('App listening on port ${PORT}')
console.log('Press Ctrl+C to quit.')
})
我们应该基于进程类型(如后台、web、工作进程),把应用程序分解成独立的进程。这使我们的应用程序根据单个工作负载要求进行上下扩展。大多数云原生的应用程序允许我们按需扩展。我们应该把应用程序设计为多个分布式进程,这些进程能够独立地执行工作块,并通过添加更多的进程进行扩展。
以下内容描述了一些结构,以便应用程序扩展变得可行。用可处置性和无状态性的原则为核心构建的应用程序可以很好地从这些水平扩展的结构中受益。
使用应用引擎
我们可以使用应用引擎把我们的应用程序托管到GCP的托管基础设施上。实例是计算单元,这些计算单元是应用引擎用来自动扩展应用程序的。在任何给定的时间,我们的应用程序可以运行在一个实例或多个实例上,而请求则被分散到所有这些实例上。
应用引擎调度程序决定如何处理每个新的请求。该调度程序可能使用一个现有的实例(或者是空闲的,或者是接受并发请求的),把该请求放到一个待处理的请求队列中,或为该请求启动一个新的实例。该决定要考虑可用实例的数量、我们的应用程序处理请求的速度(延迟),以及启动一个新的实例所需的时间。
如果我们使用自动扩展,那么,我们可以通过设置目标CPU利用率、目标吞吐量和最大并发请求,在性能和成本之间进行平衡。
我们可以在app.yaml文件中指定扩展的类型,app.yaml文件是我们为获取服务版本上传的文件。根据这个配置输入,该应用引擎基础设施将使用动态或常驻实例。关于扩展类型的更多信息,请参考应用引擎文档。
使用计算引擎
或者,我们可以在计算引擎上部署和管理应用程序。在这种情况下,我们可以根据CPU利用率、正在处理的请求或其他来自应用程序的遥测信号,使用托管实例组(managed instance groups,简称MIG)扩展应用程序来响应可变负载
下图说明了托管实例组提供的关键功能。
使用托管实例组可以让我们的应用程序扩展到传入的需求并具有高可用性。这个概念对于无状态应用程序(如web前端)和基于批处理、高性能的工作负载非常适用。
使用云函数(Cloud Function )
云函数是无状态、单一目的的函数,它们运行在GCP上,其运行的底层架构是由谷歌为我们管理的。云函数响应事件触发器(如,一次到云存储桶的上传或云发布/订阅消息)。每个函数调用对单个事件或请求做出响应。
云函数通过把传入的请求分配给函数的实例进行处理。当入站请求量超过现有实例的数量时,云函数可能启动新的实例来处理请求。这种自动、完全托管的扩展行为允许云函数并行处理很多请求,每个请求都使用函数的不同实例。
使用GKE自动扩展
有些关键Kubernetes结构适用于扩展进程:
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: my-sample-web-app-hpa
namespace: dev
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-sample-web-app
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
对于运行在云基础设施上的应用程序,我们应该把它们和底层基础设施作为一次性资源来对待。我们的应用程序应该能够处理底层基础设施的暂时丢失,并应该能够正常地关闭和重启。
要考虑的主要原则包括:
以下的代码段来自一个应用引擎应用程序,展示了我们如何拦截SIGTERM信号以关闭打开的数据库连接。
const express = require('express')
const dbConnection = require('./db')
// Other business logic related code
app.listen(PORT, () => {
console.log('App listening on port ${PORT}')
console.log('Press Ctrl+C to quit.')
})
process.on('SIGTERM', () => {
console.log('App Shutting down')
dbConnection.close() // Other closing of database connection
})
企业应用程序在其开发生命周期中会跨不同的环境迁移。通常,这些环境是开发、测试和预发布(staging)以及生产环境。最好让这些环境尽可能地相似。
环境的等价性是一个大多数开发人员认为已给定的特性。尽管如此,随着企业的成长及其IT生态系统的发展,环境的等价性变得越来越难以维持。
由于这几年来,开发人员采用了源码控制、配置管理和模板化配置文件,因此,维持环境的等价性变得轻松了一些。这样一来,把应用程序一致地部署到多个环境中就变得更轻松了。例如,使用Docker和Cocker Compose,我们可以确保应用程序堆栈跨环境保持其形状和工具组合。
下表列出的GCP服务和工具,在我们设计运行于GCP的应用程序时可以使用它们。这些组件有不同的用途,它们共同帮助我们构建让我们的环境更加一致的工作流。
GCP 组件 | 用途 |
---|---|
云资源存储库 | 供团队存储、管理和跟踪代码的单一去处。 |
云存储、 云资源存储库 | 存储构建工件 |
云KMS | 在一个中心云服务中存储我们的加密密钥,供其它云资源和应用程序直接使用。 |
云存储 | 存储自定义映像,这些映像是我们从资源磁盘、映像、截图或存储在云存储的映像中创建的。我们可以使用这些映像来创建为我们的应用程序量身制定的虚拟机实例 |
容器注册表 | 存储、管理和保护Docker容器映像。 |
部署管理器 | 编写灵活的模板和配置文件,并使用它们来创建使用GCP产品变体的部署 |
日志让我们能够了解应用程序的健康状况。对日志的收集、处理和分析与应用程序核心逻辑的解耦是非常重要的。在我们的应用程序需要动态扩展并运行于公共云上时,解耦日志消除了管理日志存储位置和分布式(通常是临时性的)虚拟机进行聚合的开销,因此特别有用。
GCP提供了一套工具,以帮助处理日志的收集、处理和结构化分析。在我们的计算引擎虚拟机中安装Starkdriver Logging Agent是个很好的方法。(这个代理默认预安装在应用程序引擎和GKE VM映像中。)该代理监控一组预先配置的日志记录位置。运行于虚拟机的应用程序生成的日志将被收集并以流的形式被传输到Stackdriver Logging中。
当为GKE集群启用日志记录时,日志代理被部署到该集群的每个节点上。该代理收集日志,用相关的元数据丰富日志,并在数据存储中将它们持久保存。可以使用Stackdriver Logging来查看这些日志。我们可以使用Fluentd 守护程序集对记录的内容进行更多的控制。请参阅使用Fluentd为谷歌Kubernetes引擎自定义Stackdriver日志以获得更多的信息。
管理流程通常包括一次性任务或定时的、可重复的任务,如生成报告、执行批处理脚本、启动数据库备份和迁移模式。十二要素宣言中的管理流程要素是考虑一次性任务而写的。对于云原生应用程序,在创建可重复的任务时,该要素变得越来越重要,本部分的指南针对的是类似任务。
定时触发器常常是作为定时任务(cron job)构建的,并由应用程序本身来处理。该模型有用,但是,它引入了与应用程序紧密耦合的逻辑,需要维护和协调,尤其是当应用程序跨时区分布时。
因此,在为管理流程进行设计时,应该把这些任务的管理与应用程序本身解耦。根据应用程序运行所需的工具和基础设施,可以考虑以下建议:
本文描述的十二要素为我们应该如何构建云原生应用程序提供了指导。这些应用程序是企业的基础构建块。
一个典型的企业有很多这样的应用程序,它们通常是由几个团队合作开发的,以交付业务功能。重要的是,在应用程序开发生命周期中建立一些其他原则(而不是事后才考虑),以解决应用程序相互之间如何通信和如何保护它们,以及如何进行访问控制。
以下部分概述了在应用程序设计和开发过程中应考虑的一些其他问题。
应用程序使用API进行通信。当我们在构建应用程序时,要考虑应用程序的生态系统会怎样使用该应用程序,从设计一个API策略开始。一个良好的API设计可以让应用程序开发人员和外部利益相关者轻松地使用。在实现任何代码前,使用OpenAPI规范记录API是一个良好的习惯。
API抽象了底层的应用程序功能。一个设计良好的API端点应该把提供服务的应用程序基础设施与在使用的应用程序隔离并解耦。这种解耦让我们能够独立地改变底层服务及其基础设施,而不会影响到应用程序的使用者。
对我们开发的API进行分类、记录和发布是非常重要的,这样API的使用者才能够发现并使用这些API。理想情况下,我们希望API使用者自我服务。我们可以通过设置开发人员门户网站来实现。开发人员门户网站为所有API使用者作为入口点提供服务,这些API使用者包括企业内部的,或像来自合作伙伴生态系统的开发人员这样的外部使用者。
谷歌的API管理产品套件Apigee有助于管理API的整个生命周期,从设计,构建一直到发布。
安全性的范畴很广,包括操作系统、网络和防火墙、数据及数据库安全性、应用程序安全性、身份认证、访问管理。在企业生态系统中,解决安全问题的所有方面是至关重要的。
从应用程序的角度来看,API提供对企业生态系统中应用程序的访问。因此,我们应该确保在应用程序设计和构建过程中,这些构建块可以解决安全问题。帮助保护对应用程序的访问要考虑的问题如下所示:
安全环境在企业内持续地发展,使我们更难在应用程序中编写安全性结构代码。API管理产品(如Apigee)有助于确保在本节中提到的所有层上的API安全。
领取专属 10元无门槛券
私享最新 技术干货