Python-DevOps-指南-六-

168 阅读38分钟

Python DevOps 指南(六)

原文:annas-archive.org/md5/68b28228356df0415ddc83eb0aaea548

译者:飞龙

协议:CC BY-NC-SA 4.0

第十二章:容器编排:Kubernetes

如果您正在尝试使用 Docker,或者在单台机器上运行一组 Docker 容器就是您的全部需求,那么 Docker 和 Docker Compose 就足够满足您的需求。然而,一旦您从数字1(单台机器)转移到数字2(多台机器),您就需要开始考虑如何在网络中编排这些容器。对于生产场景来说,这是必须的。您至少需要两台机器来实现容错和高可用性。

在我们这个云计算时代,扩展基础设施的推荐方法是“外部”(也称为“水平扩展”),通过向整个系统添加更多实例来实现,而不是通过“上升”(或“垂直扩展”)的旧方法,即向单个实例添加更多 CPU 和内存。一个 Docker 编排平台使用这些许多实例或节点作为原始资源(CPU、内存、网络),然后将这些资源分配给平台内运行的各个容器。这与我们在第十一章中提到的使用容器而不是经典虚拟机(VMs)的优势相关联:您可以更精细地为容器分配这些资源,因此您的基础设施投入将得到更好的利用率。

在为特定目的配置服务器并在每个实例上运行特定软件包(如 Web 服务器软件、缓存软件、数据库软件)的模式已经发生了转变,现在将它们配置为资源分配的通用单元,并在其上运行 Docker 容器,由 Docker 编排平台协调管理。您可能熟悉将服务器视为“宠物”与将它们视为“牲畜”的区别。在基础架构设计的早期阶段,每个服务器都有明确的功能(如邮件服务器),许多时候每个特定功能仅有一个服务器。这些服务器有命名方案(格里格还记得在点博时代使用行星系统的命名方案),花费大量时间进行管理和维护,因此被称为“宠物”。当 Puppet、Chef 和 Ansible 等配置管理工具出现时,通过在每台服务器上使用相同的安装程序,同时轻松配置多个相同类型的服务器(例如 Web 服务器农场)变得更加容易。这与云计算的兴起同时发生,前面提到的水平扩展的概念以及对容错和高可用性的更多关注,这些都是良好设计的系统基础设施的关键属性。这些服务器或云实例被视为牲畜,是可以丢弃的单位,它们在集合中有价值。

容器和无服务器计算时代也带来了另一个称谓,“昆虫”。确实,人们可以将容器的出现和消失视为一种短暂存在,就像是一个短暂的昆虫一样。函数即服务比 Docker 容器更短暂,其存在期间短暂而强烈,与其调用的持续时间相一致。

在容器的情况下,它们的短暂性使得在大规模上实现它们的编排和互操作性变得困难。这正是容器编排平台填补的需求。以前有多个 Docker 编排平台可供选择,如 Mesosphere 和 Docker Swarm,但如今我们可以安全地说 Kubernetes 已经赢得了这场比赛。本章剩余部分将简要概述 Kubernetes,并示例演示如何在 Kubernetes 中运行相同的应用程序,从docker-compose迁移到 Kubernetes。我们还将展示如何使用 Helm,一个 Kubernetes 包管理器,来安装名为 charts 的包,用于监控和仪表盘工具 Prometheus 和 Grafana,并如何自定义这些 charts。

Kubernetes 概念简介

理解构成 Kubernetes 集群的许多部分的最佳起点是官方 Kubernetes 文档

在高层次上,Kubernetes 集群由节点组成,可以等同于运行在云中的裸金属或虚拟机的服务器。节点运行 pod,即 Docker 容器的集合。Pod 是 Kubernetes 中的部署单位。一个 pod 中的所有容器共享同一网络,并且可以像在同一主机上运行一样互相引用。有许多情况下,运行多个容器在一个 pod 中是有利的。通常情况下,您的应用程序容器作为 pod 中的主容器运行,如果需要,您可以运行一个或多个所谓的“sidecar”容器,用于功能,例如日志记录或监视。一个特殊的 sidecar 容器案例是“init 容器”,它保证首先运行,并可用于诸如运行数据库迁移等的日常管理任务。我们将在本章后面进一步探讨这个问题。

应用程序通常会为了容错性和性能而使用多个 pod。负责启动和维护所需 pod 数量的 Kubernetes 对象称为部署(deployment)。为了让 pod 能够与其他 pod 通信,Kubernetes 提供了另一种对象,称为服务(service)。服务通过选择器(selectors)与部署绑定。服务也可以向外部客户端暴露,可以通过在每个 Kubernetes 节点上暴露一个 NodePort 作为静态端口,或者创建对应实际负载均衡器的 LoadBalancer 对象来实现,如果云提供商支持的话。

对于管理诸如密码、API 密钥和其他凭据等敏感信息,Kubernetes 提供了 Secret 对象。我们将看到一个示例,使用 Secret 存储数据库密码。

使用 Kompose 从 docker-compose.yaml 创建 Kubernetes Manifests

让我们再次查看讨论 第十一章 中的 Flask 示例应用程序的 docker_compose.yaml 文件:

$ cat docker-compose.yaml
version: "3"
services:
  app:
    image: "griggheo/flask-by-example:v1"
    command: "manage.py runserver --host=0.0.0.0"
    ports:
      - "5000:5000"
    environment:
      APP_SETTINGS: config.ProductionConfig
      DATABASE_URL: postgresql://wordcount_dbadmin:$DBPASS@db/wordcount
      REDISTOGO_URL: redis://redis:6379
    depends_on:
      - db
      - redis
  worker:
    image: "griggheo/flask-by-example:v1"
    command: "worker.py"
    environment:
      APP_SETTINGS: config.ProductionConfig
      DATABASE_URL: postgresql://wordcount_dbadmin:$DBPASS@db/wordcount
      REDISTOGO_URL: redis://redis:6379
    depends_on:
      - db
      - redis
  migrations:
    image: "griggheo/flask-by-example:v1"
    command: "manage.py db upgrade"
    environment:
      APP_SETTINGS: config.ProductionConfig
      DATABASE_URL: postgresql://wordcount_dbadmin:$DBPASS@db/wordcount
    depends_on:
      - db
  db:
    image: "postgres:11"
    container_name: "postgres"
    ports:
      - "5432:5432"
    volumes:
      - dbdata:/var/lib/postgresql/data
  redis:
    image: "redis:alpine"
    ports:
      - "6379:6379"
volumes:
  dbdata:

我们将使用一个名为 Kompose 的工具来将此 YAML 文件翻译成一组 Kubernetes Manifests。

要在 macOS 机器上获取新版本的 Kompose,首先从 Git 仓库 下载它,然后将其移动到 /usr/local/bin/kompose,并使其可执行。请注意,如果您依赖操作系统的软件包管理系统(例如 Ubuntu 系统上的 apt 或 Red Hat 系统上的 yum)来安装 Kompose,可能会获得一个较旧且不兼容这些说明的版本。

运行 kompose convert 命令从现有的 docker-compose.yaml 文件创建 Kubernetes manifest 文件:

$ kompose convert
INFO Kubernetes file "app-service.yaml" created
INFO Kubernetes file "db-service.yaml" created
INFO Kubernetes file "redis-service.yaml" created
INFO Kubernetes file "app-deployment.yaml" created
INFO Kubernetes file "db-deployment.yaml" created
INFO Kubernetes file "dbdata-persistentvolumeclaim.yaml" created
INFO Kubernetes file "migrations-deployment.yaml" created
INFO Kubernetes file "redis-deployment.yaml" created
INFO Kubernetes file "worker-deployment.yaml" created

此时,请删除 docker-compose.yaml 文件:

$ rm docker-compose.yaml

将 Kubernetes Manifests 部署到基于 minikube 的本地 Kubernetes 集群

我们的下一步是将 Kubernetes Manifests 部署到基于 minikube 的本地 Kubernetes 集群。

在 macOS 上运行 minikube 的先决条件是安装 VirtualBox。从其 下载页面 下载 macOS 版本的 VirtualBox 包,并安装它,然后将其移动到 /usr/local/bin/minikube 以使其可执行。请注意,此时写作本文时,minikube 安装了 Kubernetes 版本为 1.15. 如果要按照这些示例进行操作,请指定要使用 minikube 安装的 Kubernetes 版本:

$ minikube start --kubernetes-version v1.15.0
 minikube v1.2.0 on darwin (amd64)
 Creating virtualbox VM (CPUs=2, Memory=2048MB, Disk=20000MB) ...
 Configuring environment for Kubernetes v1.15.0 on Docker 18.09.6
 Downloading kubeadm v1.15.0
 Downloading kubelet v1.15.0
 Pulling images ...
 Launching Kubernetes ...
 Verifying: apiserver proxy etcd scheduler controller dns
 Done! kubectl is now configured to use "minikube"

与 Kubernetes 集群交互的主要命令是 kubectl

通过从 发布页面 下载并移动到 /usr/local/bin/kubectl 并使其可执行,来在 macOS 机器上安装 kubectl

在运行 kubectl 命令时,您将使用的一个主要概念是 context,它表示您希望与之交互的 Kubernetes 集群。minikube 的安装过程已经为我们创建了一个称为 minikube 的上下文。指定 kubectl 指向特定上下文的一种方法是使用以下命令:

$ kubectl config use-context minikube
Switched to context "minikube".

另一种更方便的方法是从 Git 仓库 安装 kubectx 实用程序,然后运行:

$ kubectx minikube
Switched to context "minikube".
提示

另一个在 Kubernetes 工作中很实用的客户端实用程序是 kube-ps1。对于基于 Zsh 的 macOS 设置,请将以下片段添加到文件 ~/.zshrc 中:

source "/usr/local/opt/kube-ps1/share/kube-ps1.sh"
PS1='$(kube_ps1)'$PS1

这些行将 shell 提示符更改为显示当前 Kubernetes 上下文和命名空间。当您开始与多个 Kubernetes 集群进行交互时,这将帮助您区分生产环境和暂存环境。

现在在本地 minikube 集群上运行 kubectl 命令。例如,kubectl get nodes 命令显示集群中的节点。在本例中,只有一个带有 master 角色的节点:

$ kubectl get nodes
NAME       STATUS   ROLES    AGE     VERSION
minikube   Ready    master   2m14s   v1.15.0

首先,从 dbdata-persistentvolumeclaim.yaml 文件创建持久卷声明(PVC)对象,该文件由 Kompose 创建,对应于在使用 docker-compose 运行时为 PostgreSQL 数据库容器分配的本地卷:

$ cat dbdata-persistentvolumeclaim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  creationTimestamp: null
  labels:
    io.kompose.service: dbdata
  name: dbdata
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi
status: {}

要在 Kubernetes 中创建此对象,请使用 kubectl create 命令,并使用 -f 标志指定清单文件名:

$ kubectl create -f dbdata-persistentvolumeclaim.yaml
persistentvolumeclaim/dbdata created

使用 kubectl get pvc 命令列出所有 PVC,验证我们的 PVC 是否存在:

$ kubectl get pvc
NAME     STATUS   VOLUME                                     CAPACITY
ACCESS MODES   STORAGECLASS   AGE
dbdata   Bound    pvc-39914723-4455-439b-a0f5-82a5f7421475   100Mi
RWO            standard       1m

下一步是为 PostgreSQL 创建 Deployment 对象。使用之前由 Kompose 工具创建的清单文件 db-deployment.yaml

$ cat db-deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  annotations:
    kompose.cmd: kompose convert
    kompose.version: 1.16.0 (0c01309)
  creationTimestamp: null
  labels:
    io.kompose.service: db
  name: db
spec:
  replicas: 1
  strategy:
    type: Recreate
  template:
    metadata:
      creationTimestamp: null
      labels:
        io.kompose.service: db
    spec:
      containers:
      - image: postgres:11
        name: postgres
        ports:
        - containerPort: 5432
        resources: {}
        volumeMounts:
        - mountPath: /var/lib/postgresql/data
          name: dbdata
      restartPolicy: Always
      volumes:
      - name: dbdata
        persistentVolumeClaim:
          claimName: dbdata
status: {}

要创建部署,请使用 kubectl create -f 命令,并指向清单文件:

$ kubectl create -f db-deployment.yaml
deployment.extensions/db created

要验证是否已创建部署,请列出集群中的所有部署,并列出作为部署一部分创建的 pod:

$ kubectl get deployments
NAME     READY   UP-TO-DATE   AVAILABLE   AGE
db       1/1     1            1           1m

$ kubectl get pods
NAME                  READY   STATUS    RESTARTS   AGE
db-67659d85bf-vrnw7   1/1     Running   0          1m

接下来,为示例 Flask 应用程序创建数据库。使用类似于 docker exec 的命令在运行中的 Docker 容器内运行 psql 命令。在 Kubernetes 集群中,命令的形式是 kubectl exec

$ kubectl exec -it db-67659d85bf-vrnw7 -- psql -U postgres
psql (11.4 (Debian 11.4-1.pgdg90+1))
Type "help" for help.

postgres=# create database wordcount;
CREATE DATABASE
postgres=# \q

$ kubectl exec -it db-67659d85bf-vrnw7 -- psql -U postgres wordcount
psql (11.4 (Debian 11.4-1.pgdg90+1))
Type "help" for help.

wordcount=# CREATE ROLE wordcount_dbadmin;
CREATE ROLE
wordcount=# ALTER ROLE wordcount_dbadmin LOGIN;
ALTER ROLE
wordcount=# ALTER USER wordcount_dbadmin PASSWORD 'MYPASS';
ALTER ROLE
wordcount=# \q

下一步是创建与 db 部署相对应的 Service 对象,将部署暴露给运行在集群内的其他服务,例如 Redis worker 服务和主应用程序服务。这是 db 服务的清单文件:

$ cat db-service.yaml
apiVersion: v1
kind: Service
metadata:
  annotations:
    kompose.cmd: kompose convert
    kompose.version: 1.16.0 (0c01309)
  creationTimestamp: null
  labels:
    io.kompose.service: db
  name: db
spec:
  ports:
  - name: "5432"
    port: 5432
    targetPort: 5432
  selector:
    io.kompose.service: db
status:
  loadBalancer: {}

需要注意的一点是以下部分:

  labels:
    io.kompose.service: db

此部分同时出现在部署清单和服务清单中,并确实是将两者联系在一起的方法。服务将与具有相同标签的任何部署关联。

使用 kubectl create -f 命令创建 Service 对象:

$ kubectl create -f db-service.yaml
service/db created

列出所有服务,并注意已创建的 db 服务:

$ kubectl get services
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
db           ClusterIP   10.110.108.96   <none>        5432/TCP   6s
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP    4h45m

下一个要部署的服务是 Redis。基于由 Kompose 生成的清单文件创建 Deployment 和 Service 对象:

$ cat redis-deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  annotations:
    kompose.cmd: kompose convert
    kompose.version: 1.16.0 (0c01309)
  creationTimestamp: null
  labels:
    io.kompose.service: redis
  name: redis
spec:
  replicas: 1
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        io.kompose.service: redis
    spec:
      containers:
      - image: redis:alpine
        name: redis
        ports:
        - containerPort: 6379
        resources: {}
      restartPolicy: Always
status: {}

$ kubectl create -f redis-deployment.yaml
deployment.extensions/redis created

$ kubectl get pods
NAME                    READY   STATUS    RESTARTS   AGE
db-67659d85bf-vrnw7     1/1     Running   0          37m
redis-c6476fbff-8kpqz   1/1     Running   0          11s

$ kubectl create -f redis-service.yaml
service/redis created

$ cat redis-service.yaml
apiVersion: v1
kind: Service
metadata:
  annotations:
    kompose.cmd: kompose convert
    kompose.version: 1.16.0 (0c01309)
  creationTimestamp: null
  labels:
    io.kompose.service: redis
  name: redis
spec:
  ports:
  - name: "6379"
    port: 6379
    targetPort: 6379
  selector:
    io.kompose.service: redis
status:
  loadBalancer: {}

$ kubectl get services
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
db           ClusterIP   10.110.108.96   <none>        5432/TCP   84s
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP    4h46m
redis        ClusterIP   10.106.44.183   <none>        6379/TCP   10s

到目前为止,已部署的两个服务,dbredis,互不影响。应用程序的下一部分是工作进程,需要与 PostgreSQL 和 Redis 进行通信。这就是使用 Kubernetes 服务的优势所在。工作部署可以通过服务名称引用 PostgreSQL 和 Redis 的端点。Kubernetes 知道如何将来自客户端(作为工作部署中的 pod 的一部分运行的容器)的请求路由到服务器(作为 dbredis 部署中的 pod 的一部分运行的 PostgreSQL 和 Redis 容器)。

工作节点部署中使用的环境变量之一是DATABASE_URL。 它包含应用程序使用的数据库密码。 不应在部署清单文件中明文显示密码,因为该文件需要检入版本控制。 取而代之,创建一个 Kubernetes Secret 对象。

首先,将密码字符串编码为base64

$ echo MYPASS | base64
MYPASSBASE64

然后,创建一个描述要创建的 Kubernetes Secret 对象的清单文件。 由于密码的base64编码不安全,请使用sops来编辑和保存加密的清单文件secrets.yaml.enc

$ sops --pgp E14104A0890994B9AC9C9F6782C1FF5E679EFF32 secrets.yaml.enc

在编辑器中添加这些行:

apiVersion: v1
kind: Secret
metadata:
  name: fbe-secret
type: Opaque
data:
  dbpass: MYPASSBASE64

secrets.yaml.enc 文件现在可以检入,因为它包含密码的base64值的加密版本。

要解密加密文件,请使用sops -d命令:

$ sops -d secrets.yaml.enc
apiVersion: v1
kind: Secret
metadata:
  name: fbe-secret
type: Opaque
data:
  dbpass: MYPASSBASE64

sops -d的输出导向kubectl create -f以创建 Kubernetes Secret 对象:

$ sops -d secrets.yaml.enc | kubectl create -f -
secret/fbe-secret created

检查 Kubernetes Secrets 并描述已创建的 Secret:

$ kubectl get secrets
NAME                  TYPE                                  DATA   AGE
default-token-k7652   kubernetes.io/service-account-token   3      3h19m
fbe-secret            Opaque                                1      45s

$ kubectl describe secret fbe-secret
Name:         fbe-secret
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
dbpass:  12 bytes

要获取base64编码的 Secret,请使用:

$ kubectl get secrets fbe-secret -ojson | jq -r ".data.dbpass"
MYPASSBASE64

要在 macOS 机器上获取纯文本密码,请使用以下命令:

$ kubectl get secrets fbe-secret -ojson | jq -r ".data.dbpass" | base64 -D
MYPASS

在 Linux 机器上,base64解码的正确标志是-d,因此正确的命令将是:

$ kubectl get secrets fbe-secret -ojson | jq -r ".data.dbpass" | base64 -d
MYPASS

现在可以在工作节点的部署清单中使用该秘密。 修改由Kompose实用程序生成的worker-deployment.yaml文件,并添加两个环境变量:

  • DBPASS是从fbe-secret Secret 对象中检索的数据库密码。

  • DATABASE_URL是 PostgreSQL 的完整数据库连接字符串,包括数据库密码,并将其引用为${DBPASS}

这是修改后的worker-deployment.yaml的版本:

$ cat worker-deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  annotations:
    kompose.cmd: kompose convert
    kompose.version: 1.16.0 (0c01309)
  creationTimestamp: null
  labels:
    io.kompose.service: worker
  name: worker
spec:
  replicas: 1
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        io.kompose.service: worker
    spec:
      containers:
      - args:
        - worker.py
        env:
        - name: APP_SETTINGS
          value: config.ProductionConfig
        - name: DBPASS
          valueFrom:
            secretKeyRef:
              name: fbe-secret
              key: dbpass
        - name: DATABASE_URL
          value: postgresql://wordcount_dbadmin:${DBPASS}@db/wordcount
        - name: REDISTOGO_URL
          value: redis://redis:6379
        image: griggheo/flask-by-example:v1
        name: worker
        resources: {}
      restartPolicy: Always
status: {}

通过调用kubectl create -f创建工作节点部署对象的方式与其他部署相同:

$ kubectl create -f worker-deployment.yaml
deployment.extensions/worker created

列出 pods:

$ kubectl get pods
NAME                      READY   STATUS              RESTARTS   AGE
db-67659d85bf-vrnw7       1/1     Running             1          21h
redis-c6476fbff-8kpqz     1/1     Running             1          21h
worker-7dbf5ff56c-vgs42   0/1     Init:ErrImagePull   0          7s

注意,工作节点显示为状态Init:ErrImagePull。 要查看有关此状态的详细信息,请运行kubectl describe

$ kubectl describe pod worker-7dbf5ff56c-vgs42 | tail -10
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type     Reason     Age                  From               Message
  ----     ------     ----                 ----               -------
  Normal   Scheduled  2m51s                default-scheduler
  Successfully assigned default/worker-7dbf5ff56c-vgs42 to minikube

  Normal   Pulling    76s (x4 over 2m50s)  kubelet, minikube
  Pulling image "griggheo/flask-by-example:v1"

  Warning  Failed     75s (x4 over 2m49s)  kubelet, minikube
  Failed to pull image "griggheo/flask-by-example:v1": rpc error:
  code = Unknown desc = Error response from daemon: pull access denied for
  griggheo/flask-by-example, repository does not exist or may require
  'docker login'

  Warning  Failed     75s (x4 over 2m49s)  kubelet, minikube
  Error: ErrImagePull

  Warning  Failed     62s (x6 over 2m48s)  kubelet, minikube
  Error: ImagePullBackOff

  Normal   BackOff    51s (x7 over 2m48s)  kubelet, minikube
  Back-off pulling image "griggheo/flask-by-example:v1"

部署尝试从 Docker Hub 拉取griggheo/flask-by-example:v1私有 Docker 镜像,并且缺少访问私有 Docker 注册表所需的适当凭据。 Kubernetes 包括一种特殊类型的对象,用于处理这种情况,称为imagePullSecret

使用sops创建包含 Docker Hub 凭据和kubectl create secret调用的加密文件:

$ sops --pgp E14104A0890994B9AC9C9F6782C1FF5E679EFF32 \
create_docker_credentials_secret.sh.enc

文件的内容是:

DOCKER_REGISTRY_SERVER=docker.io
DOCKER_USER=Type your dockerhub username, same as when you `docker login`
DOCKER_EMAIL=Type your dockerhub email, same as when you `docker login`
DOCKER_PASSWORD=Type your dockerhub pw, same as when you `docker login`

kubectl create secret docker-registry myregistrykey \
--docker-server=$DOCKER_REGISTRY_SERVER \
--docker-username=$DOCKER_USER \
--docker-password=$DOCKER_PASSWORD \
--docker-email=$DOCKER_EMAIL

使用sops解密加密文件并通过bash运行它:

$ sops -d create_docker_credentials_secret.sh.enc | bash -
secret/myregistrykey created

检查秘密:

$ kubectl get secrets myregistrykey -oyaml
apiVersion: v1
data:
  .dockerconfigjson: eyJhdXRocyI6eyJkb2NrZXIuaW8iO
kind: Secret
metadata:
  creationTimestamp: "2019-07-17T22:11:56Z"
  name: myregistrykey
  namespace: default
  resourceVersion: "16062"
  selfLink: /api/v1/namespaces/default/secrets/myregistrykey
  uid: 47d29ffc-69e4-41df-a237-1138cd9e8971
type: kubernetes.io/dockerconfigjson

对工作节点部署清单的唯一更改是添加这些行:

      imagePullSecrets:
      - name: myregistrykey

在此行后包含它:

     restartPolicy: Always

删除工作节点部署并重新创建:

$ kubectl delete -f worker-deployment.yaml
deployment.extensions "worker" deleted

$ kubectl create -f worker-deployment.yaml
deployment.extensions/worker created

现在工作节点处于运行状态,并且没有错误:

$ kubectl get pods
NAME                      READY   STATUS    RESTARTS   AGE
db-67659d85bf-vrnw7       1/1     Running   1          22h
redis-c6476fbff-8kpqz     1/1     Running   1          21h
worker-7dbf5ff56c-hga37   1/1     Running   0          4m53s

使用kubectl logs命令检查工作节点的日志:

$ kubectl logs worker-7dbf5ff56c-hga37
20:43:13 RQ worker 'rq:worker:040640781edd4055a990b798ac2eb52d'
started, version 1.0
20:43:13 *** Listening on default...
20:43:13 Cleaning registries for queue: default

接下来是解决应用程序部署的步骤。当应用程序在第十一章中以docker-compose设置部署时,使用单独的 Docker 容器来运行更新 Flask 数据库所需的迁移。这种任务很适合作为同一 Pod 中主应用程序容器的侧车容器运行。侧车容器将在应用程序部署清单中定义为 Kubernetes 的initContainer。此类容器保证在属于其所属的 Pod 内的其他容器启动之前运行。

将此部分添加到由Kompose实用程序生成的app-deployment.yaml清单文件中,并删除migrations-deployment.yaml文件:

      initContainers:
      - args:
        - manage.py
        - db
        - upgrade
        env:
        - name: APP_SETTINGS
          value: config.ProductionConfig
        - name: DATABASE_URL
          value: postgresql://wordcount_dbadmin:@db/wordcount
        image: griggheo/flask-by-example:v1
        name: migrations
        resources: {}

$ rm migrations-deployment.yaml

在应用程序部署清单中复用为工作程序部署创建的fbe-secret Secret 对象:

$ cat app-deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  annotations:
    kompose.cmd: kompose convert
    kompose.version: 1.16.0 (0c01309)
  creationTimestamp: null
  labels:
    io.kompose.service: app
  name: app
spec:
  replicas: 1
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        io.kompose.service: app
    spec:
      initContainers:
      - args:
        - manage.py
        - db
        - upgrade
        env:
        - name: APP_SETTINGS
          value: config.ProductionConfig
        - name: DBPASS
          valueFrom:
            secretKeyRef:
              name: fbe-secret
              key: dbpass
        - name: DATABASE_URL
          value: postgresql://wordcount_dbadmin:${DBPASS}@db/wordcount
        image: griggheo/flask-by-example:v1
        name: migrations
        resources: {}
      containers:
      - args:
        - manage.py
        - runserver
        - --host=0.0.0.0
        env:
        - name: APP_SETTINGS
          value: config.ProductionConfig
        - name: DBPASS
          valueFrom:
            secretKeyRef:
              name: fbe-secret
              key: dbpass
        - name: DATABASE_URL
          value: postgresql://wordcount_dbadmin:${DBPASS}@db/wordcount
        - name: REDISTOGO_URL
          value: redis://redis:6379
        image: griggheo/flask-by-example:v1
        name: app
        ports:
        - containerPort: 5000
        resources: {}
      restartPolicy: Always
status: {}

使用kubectl create -f创建应用程序部署,然后列出 Pod 并描述应用程序 Pod:

$ kubectl create -f app-deployment.yaml
deployment.extensions/app created

$ kubectl get pods
NAME                      READY   STATUS    RESTARTS   AGE
app-c845d8969-l8nhg       1/1     Running   0          7s
db-67659d85bf-vrnw7       1/1     Running   1          22h
redis-c6476fbff-8kpqz     1/1     Running   1          21h
worker-7dbf5ff56c-vgs42   1/1     Running   0          4m53s

将应用程序部署到minikube的最后一步是确保为应用程序创建 Kubernetes 服务,并将其声明为类型LoadBalancer,以便从集群外访问:

$ cat app-service.yaml
apiVersion: v1
kind: Service
metadata:
  annotations:
    kompose.cmd: kompose convert
    kompose.version: 1.16.0 (0c01309)
  creationTimestamp: null
  labels:
    io.kompose.service: app
  name: app
spec:
  ports:
  - name: "5000"
    port: 5000
    targetPort: 5000
  type: LoadBalancer
  selector:
    io.kompose.service: app
status:
  loadBalancer: {}
注意

db服务类似,app服务通过在应用程序部署和服务清单中存在的标签声明与app部署关联:

  labels:
    io.kompose.service: app

使用kubectl create创建服务:

$ kubectl create -f app-service.yaml
service/app created

$ kubectl get services
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
app          LoadBalancer   10.99.55.191    <pending>     5000:30097/TCP   2s
db           ClusterIP      10.110.108.96   <none>        5432/TCP         21h
kubernetes   ClusterIP      10.96.0.1       <none>        443/TCP          26h
redis        ClusterIP      10.106.44.183   <none>        6379/TCP         21h

接下来运行:

$ minikube service app

此命令将使用 URLhttp://192.168.99.100:30097/打开默认浏览器,并显示 Flask 站点的主页。

在下一节中,我们将使用相同的 Kubernetes 清单文件部署我们的应用程序到一个将由 Pulumi 在 Google Cloud Platform(GCP)中配置的 Kubernetes 集群中。

使用 Pulumi 在 Google Cloud Platform(GCP)中启动 GKE Kubernetes 集群

在本节中,我们将使用Pulumi GKE 示例以及GCP 设置文档,因此在继续之前,请使用这些链接获取所需的文档。

首先创建一个新目录:

$ mkdir pulumi_gke
$ cd pulumi_gke

使用macOS 说明设置 Google Cloud SDK。

使用gcloud init命令初始化 GCP 环境。创建一个新的配置和一个名为pythonfordevops-gke-pulumi的新项目:

$ gcloud init
Welcome! This command will take you through the configuration of gcloud.

Settings from your current configuration [default] are:
core:
  account: grig.gheorghiu@gmail.com
  disable_usage_reporting: 'True'
  project: pulumi-gke-testing

Pick configuration to use:
 [1] Re-initialize this configuration [default] with new settings
 [2] Create a new configuration
Please enter your numeric choice:  2

Enter configuration name. Names start with a lower case letter and
contain only lower case letters a-z, digits 0-9, and hyphens '-':
pythonfordevops-gke-pulumi
Your current configuration has been set to: [pythonfordevops-gke-pulumi]

Pick cloud project to use:
 [1] pulumi-gke-testing
 [2] Create a new project
Please enter numeric choice or text value (must exactly match list
item):  2

Enter a Project ID. pythonfordevops-gke-pulumi
Your current project has been set to: [pythonfordevops-gke-pulumi].

登录到 GCP 帐户:

$ gcloud auth login

登录到默认应用程序pythonfordevops-gke-pulumi

$ gcloud auth application-default login

运行pulumi new命令创建一个新的 Pulumi 项目,指定gcp-python作为模板,pythonfordevops-gke-pulumi作为项目名称:

$ pulumi new
Please choose a template: gcp-python
A minimal Google Cloud Python Pulumi program
This command will walk you through creating a new Pulumi project.

Enter a value or leave blank to accept the (default), and press <ENTER>.
Press ^C at any time to quit.

project name: (pulumi_gke_py) pythonfordevops-gke-pulumi
project description: (A minimal Google Cloud Python Pulumi program)
Created project 'pythonfordevops-gke-pulumi'

stack name: (dev)
Created stack 'dev'

gcp:project: The Google Cloud project to deploy into: pythonfordevops-gke-pulumi
Saved config

Your new project is ready to go! 

To perform an initial deployment, run the following commands:

   1\. virtualenv -p python3 venv
   2\. source venv/bin/activate
   3\. pip3 install -r requirements.txt

Then, run 'pulumi up'.

以下文件由pulumi new命令创建:

$ ls -la
ls -la
total 40
drwxr-xr-x  7 ggheo  staff  224 Jul 16 15:08 .
drwxr-xr-x  6 ggheo  staff  192 Jul 16 15:06 ..
-rw-------  1 ggheo  staff   12 Jul 16 15:07 .gitignore
-rw-r--r--  1 ggheo  staff   50 Jul 16 15:08 Pulumi.dev.yaml
-rw-------  1 ggheo  staff  107 Jul 16 15:07 Pulumi.yaml
-rw-------  1 ggheo  staff  203 Jul 16 15:07 __main__.py
-rw-------  1 ggheo  staff   34 Jul 16 15:07 requirements.txt

我们将使用Pulumi 示例GitHub 存储库中的gcp-py-gke示例。

examples/gcp-py-gke复制**.pyrequirements.txt*到当前目录:

$ cp ~/pulumi-examples/gcp-py-gke/*.py .
$ cp ~/pulumi-examples/gcp-py-gke/requirements.txt .

配置与 Pulumi 在 GCP 中操作所需的与 GCP 相关的变量:

$ pulumi config set gcp:project pythonfordevops-gke-pulumi
$ pulumi config set gcp:zone us-west1-a
$ pulumi config set password --secret PASS_FOR_KUBE_CLUSTER

创建并使用 Python virtualenv,安装requirements.txt中声明的依赖项,然后通过运行pulumi up命令启动在mainpy中定义的 GKE 集群:

$ virtualenv -p python3 venv
$ source venv/bin/activate
$ pip3 install -r requirements.txt
$ pulumi up
提示

确保通过在 GCP Web 控制台中将其与 Google 计费账户关联来启用 Kubernetes Engine API。

可以在GCP 控制台中看到 GKE 集群。

生成适当的kubectl配置以及使用它与新配置的 GKE 集群进行交互。通过 Pulumi 程序将kubectl配置方便地导出为output

$ pulumi stack output kubeconfig > kubeconfig.yaml
$ export KUBECONFIG=./kubeconfig.yaml

列出组成 GKE 集群的节点:

$ kubectl get nodes
NAME                                                 STATUS   ROLES    AGE
   VERSION
gke-gke-cluster-ea17e87-default-pool-fd130152-30p3   Ready    <none>   4m29s
   v1.13.7-gke.8
gke-gke-cluster-ea17e87-default-pool-fd130152-kf9k   Ready    <none>   4m29s
   v1.13.7-gke.8
gke-gke-cluster-ea17e87-default-pool-fd130152-x9dx   Ready    <none>   4m27s
   v1.13.7-gke.8

将 Flask 示例应用程序部署到 GKE

使用相同的 Kubernetes 清单文件在 GKE 集群中部署minikube示例,通过kubectl命令。首先创建redis部署和服务:

$ kubectl create -f redis-deployment.yaml
deployment.extensions/redis created

$ kubectl get pods
NAME                             READY   STATUS    RESTARTS   AGE
canary-aqw8jtfo-f54b9749-q5wqj   1/1     Running   0          5m57s
redis-9946db5cc-8g6zz            1/1     Running   0          20s

$ kubectl create -f redis-service.yaml
service/redis created

$ kubectl get service redis
NAME    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
redis   ClusterIP   10.59.245.221   <none>        6379/TCP   18s

创建一个 PersistentVolumeClaim,用作 PostgreSQL 数据库的数据卷:

$ kubectl create -f dbdata-persistentvolumeclaim.yaml
persistentvolumeclaim/dbdata created

$ kubectl get pvc
NAME     STATUS   VOLUME                                     CAPACITY
dbdata   Bound    pvc-00c8156c-b618-11e9-9e84-42010a8a006f   1Gi
   ACCESS MODES   STORAGECLASS   AGE
   RWO            standard       12s

创建db部署:

$ kubectl create -f db-deployment.yaml
deployment.extensions/db created

$ kubectl get pods
NAME                             READY   STATUS             RESTARTS  AGE
canary-aqw8jtfo-f54b9749-q5wqj   1/1     Running            0         8m52s
db-6b4fbb57d9-cjjxx              0/1     CrashLoopBackOff   1         38s
redis-9946db5cc-8g6zz            1/1     Running            0         3m15s

$ kubectl logs db-6b4fbb57d9-cjjxx

initdb: directory "/var/lib/postgresql/data" exists but is not empty
It contains a lost+found directory, perhaps due to it being a mount point.
Using a mount point directly as the data directory is not recommended.
Create a subdirectory under the mount point.

当尝试创建db部署时遇到了问题。GKE 提供了一个已挂载为*/var/lib/postgresql/data*的持久卷,并且根据上述错误消息,该目录并非空。

删除失败的db部署:

$ kubectl delete -f db-deployment.yaml
deployment.extensions "db" deleted

创建一个新的临时 Pod,用于挂载与 Pod 内部的*/data*中相同的dbdata PersistentVolumeClaim,以便检查其文件系统。为了故障排除目的,启动这种类型的临时 Pod 是一个有用的技术手段:

$ cat pvc-inspect.yaml
kind: Pod
apiVersion: v1
metadata:
  name: pvc-inspect
spec:
  volumes:
    - name: dbdata
      persistentVolumeClaim:
        claimName: dbdata
  containers:
    - name: debugger
      image: busybox
      command: ['sleep', '3600']
      volumeMounts:
        - mountPath: "/data"
          name: dbdata

$ kubectl create -f pvc-inspect.yaml
pod/pvc-inspect created

$ kubectl get pods
NAME                             READY   STATUS    RESTARTS   AGE
canary-aqw8jtfo-f54b9749-q5wqj   1/1     Running   0          20m
pvc-inspect                      1/1     Running   0          35s
redis-9946db5cc-8g6zz            1/1     Running   0          14m

使用kubectl exec打开 Pod 内部的 shell,以便检查*/data*:

$ kubectl exec -it pvc-inspect -- sh
/ # cd /data
/data # ls -la
total 24
drwx------    3 999      root          4096 Aug  3 17:57 .
drwxr-xr-x    1 root     root          4096 Aug  3 18:08 ..
drwx------    2 999      root         16384 Aug  3 17:57 lost+found
/data # rm -rf lost\+found/
/data # exit

注意*/data中包含一个需要移除的名为lost+found*的目录。

删除临时 Pod:

$ kubectl delete pod pvc-inspect
pod "pvc-inspect" deleted

再次创建db部署,在这次操作中成功完成:

$ kubectl create -f db-deployment.yaml
deployment.extensions/db created

$ kubectl get pods
NAME                             READY   STATUS    RESTARTS   AGE
canary-aqw8jtfo-f54b9749-q5wqj   1/1     Running   0          23m
db-6b4fbb57d9-8h978              1/1     Running   0          19s
redis-9946db5cc-8g6zz            1/1     Running   0          17m

$ kubectl logs db-6b4fbb57d9-8h978
PostgreSQL init process complete; ready for start up.

2019-08-03 18:12:01.108 UTC [1]
LOG:  listening on IPv4 address "0.0.0.0", port 5432
2019-08-03 18:12:01.108 UTC [1]
LOG:  listening on IPv6 address "::", port 5432
2019-08-03 18:12:01.114 UTC [1]
LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2019-08-03 18:12:01.135 UTC [50]
LOG:  database system was shut down at 2019-08-03 18:12:01 UTC
2019-08-03 18:12:01.141 UTC [1]
LOG:  database system is ready to accept connections

创建wordcount数据库和角色:

$ kubectl exec -it db-6b4fbb57d9-8h978 -- psql -U postgres
psql (11.4 (Debian 11.4-1.pgdg90+1))
Type "help" for help.

postgres=# create database wordcount;
CREATE DATABASE
postgres=# \q

$ kubectl exec -it db-6b4fbb57d9-8h978 -- psql -U postgres wordcount
psql (11.4 (Debian 11.4-1.pgdg90+1))
Type "help" for help.

wordcount=# CREATE ROLE wordcount_dbadmin;
CREATE ROLE
wordcount=# ALTER ROLE wordcount_dbadmin LOGIN;
ALTER ROLE
wordcount=# ALTER USER wordcount_dbadmin PASSWORD 'MYNEWPASS';
ALTER ROLE
wordcount=# \q

创建db服务:

$ kubectl create -f db-service.yaml
service/db created
$ kubectl describe service db
Name:              db
Namespace:         default
Labels:            io.kompose.service=db
Annotations:       kompose.cmd: kompose convert
                   kompose.version: 1.16.0 (0c01309)
Selector:          io.kompose.service=db
Type:              ClusterIP
IP:                10.59.241.181
Port:              5432  5432/TCP
TargetPort:        5432/TCP
Endpoints:         10.56.2.5:5432
Session Affinity:  None
Events:            <none>

根据数据库密码的base64值创建 Secret 对象。密码的明文值存储在使用sops加密的文件中:

$ echo MYNEWPASS | base64
MYNEWPASSBASE64

$ sops secrets.yaml.enc

apiVersion: v1
kind: Secret
metadata:
  name: fbe-secret
type: Opaque
data:
  dbpass: MYNEWPASSBASE64

$ sops -d secrets.yaml.enc | kubectl create -f -
secret/fbe-secret created

kubectl describe secret fbe-secret
Name:         fbe-secret
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
===
dbpass:  21 bytes

创建另一个表示 Docker Hub 凭据的 Secret 对象:

$ sops -d create_docker_credentials_secret.sh.enc | bash -
secret/myregistrykey created

考虑到正在考虑的情景是将应用程序部署到 GKE 的生产类型部署,将worker-deployment.yaml中的replicas设置为3以确保始终运行三个 worker Pod:

$ kubectl create -f worker-deployment.yaml
deployment.extensions/worker created

确保有三个 worker Pod 正在运行:

$ kubectl get pods
NAME                             READY   STATUS    RESTARTS   AGE
canary-aqw8jtfo-f54b9749-q5wqj   1/1     Running   0          39m
db-6b4fbb57d9-8h978              1/1     Running   0          16m
redis-9946db5cc-8g6zz            1/1     Running   0          34m
worker-8cf5dc699-98z99           1/1     Running   0          35s
worker-8cf5dc699-9s26v           1/1     Running   0          35s
worker-8cf5dc699-v6ckr           1/1     Running   0          35s

$ kubectl logs worker-8cf5dc699-98z99
18:28:08 RQ worker 'rq:worker:1355d2cad49646e4953c6b4d978571f1' started,
 version 1.0
18:28:08 *** Listening on default...

类似地,在app-deployment.yaml中将replicas设置为两个:

$ kubectl create -f app-deployment.yaml
deployment.extensions/app created

确保有两个应用程序 Pod 正在运行:

$ kubectl get pods
NAME                             READY   STATUS    RESTARTS   AGE
app-7964cff98f-5bx4s             1/1     Running   0          54s
app-7964cff98f-8n8hk             1/1     Running   0          54s
canary-aqw8jtfo-f54b9749-q5wqj   1/1     Running   0          41m
db-6b4fbb57d9-8h978              1/1     Running   0          19m
redis-9946db5cc-8g6zz            1/1     Running   0          36m
worker-8cf5dc699-98z99           1/1     Running   0          2m44s
worker-8cf5dc699-9s26v           1/1     Running   0          2m44s
worker-8cf5dc699-v6ckr           1/1     Running   0          2m44s

创建app服务:

$ kubectl create -f app-service.yaml
service/app created

注意到创建了一个类型为 LoadBalancer 的服务:

$ kubectl describe service app
Name:                     app
Namespace:                default
Labels:                   io.kompose.service=app
Annotations:              kompose.cmd: kompose convert
                          kompose.version: 1.16.0 (0c01309)
Selector:                 io.kompose.service=app
Type:                     LoadBalancer
IP:                       10.59.255.31
LoadBalancer Ingress:     34.83.242.171
Port:                     5000  5000/TCP
TargetPort:               5000/TCP
NodePort:                 5000  31305/TCP
Endpoints:                10.56.1.6:5000,10.56.2.12:5000
Session Affinity:         None
External Traffic Policy:  Cluster
Events:
Type    Reason                Age   From                Message
----    ------                ----  ----                -------
Normal  EnsuringLoadBalancer  72s   service-controller  Ensuring load balancer
Normal  EnsuredLoadBalancer   33s   service-controller  Ensured load balancer

测试应用程序,访问基于LoadBalancer Ingress对应的 IP 地址的端点 URL:http://34.83.242.171:5000

我们演示了如何从原始 Kubernetes 清单文件创建 Kubernetes 对象(如 Deployments、Services 和 Secrets)。随着应用程序变得更加复杂,此方法的局限性开始显现,因为定制这些文件以适应不同环境(例如,分阶段、集成和生产环境)将变得更加困难。每个环境都将有其自己的环境值和秘密,您需要跟踪这些内容。一般而言,跟踪在特定时间安装了哪些清单将变得越来越复杂。Kubernetes 生态系统中存在许多解决此问题的方案,其中最常见的之一是使用Helm包管理器。把 Helm 视为 yumapt 包管理器的 Kubernetes 等价物。

下一节将展示如何使用 Helm 在 GKE 集群内安装和自定义 Prometheus 和 Grafana。

安装 Prometheus 和 Grafana Helm Charts

在当前版本(截至本文撰写时为 v2),Helm 具有一个名为 Tiller 的服务器端组件,需要在 Kubernetes 集群内具有特定权限。

为 Tiller 创建一个新的 Kubernetes Service Account,并赋予适当的权限:

$ kubectl -n kube-system create sa tiller

$ kubectl create clusterrolebinding tiller \
  --clusterrole cluster-admin \
  --serviceaccount=kube-system:tiller

$ kubectl patch deploy --namespace kube-system \
tiller-deploy -p  '{"spec":{"template":{"spec":{"serviceAccount":"tiller"}}}}'

从官方Helm 发布页面下载并安装适用于您操作系统的 Helm 二进制文件,然后使用helm init命令安装 Tiller:

$ helm init

创建一个名为 monitoring 的命名空间:

$ kubectl create namespace monitoring
namespace/monitoring created

monitoring 命名空间中安装 Prometheus Helm chart

$ helm install --name prometheus --namespace monitoring stable/prometheus
NAME:   prometheus
LAST DEPLOYED: Tue Aug 27 12:59:40 2019
NAMESPACE: monitoring
STATUS: DEPLOYED

列出 monitoring 命名空间中的 pods、services 和 configmaps:

$ kubectl get pods -nmonitoring
NAME                                             READY   STATUS    RESTARTS AGE
prometheus-alertmanager-df57f6df6-4b8lv          2/2     Running   0        3m
prometheus-kube-state-metrics-564564f799-t6qdm   1/1     Running   0        3m
prometheus-node-exporter-b4sb9                   1/1     Running   0        3m
prometheus-node-exporter-n4z2g                   1/1     Running   0        3m
prometheus-node-exporter-w7hn7                   1/1     Running   0        3m
prometheus-pushgateway-56b65bcf5f-whx5t          1/1     Running   0        3m
prometheus-server-7555945646-d86gn               2/2     Running   0        3m

$ kubectl get services -nmonitoring
NAME                            TYPE        CLUSTER-IP    EXTERNAL-IP  PORT(S)
   AGE
prometheus-alertmanager         ClusterIP   10.0.6.98     <none>       80/TCP
   3m51s
prometheus-kube-state-metrics   ClusterIP   None          <none>       80/TCP
   3m51s
prometheus-node-exporter        ClusterIP   None          <none>       9100/TCP
   3m51s
prometheus-pushgateway          ClusterIP   10.0.13.216   <none>       9091/TCP
   3m51s
prometheus-server               ClusterIP   10.0.4.74     <none>       80/TCP
   3m51s

$ kubectl get configmaps -nmonitoring
NAME                      DATA   AGE
prometheus-alertmanager   1      3m58s
prometheus-server         3      3m58s

通过kubectl port-forward命令连接到 Prometheus UI:

$ export PROMETHEUS_POD_NAME=$(kubectl get pods --namespace monitoring \
-l "app=prometheus,component=server" -o jsonpath="{.items[0].metadata.name}")

$ echo $PROMETHEUS_POD_NAME
prometheus-server-7555945646-d86gn

$ kubectl --namespace monitoring port-forward $PROMETHEUS_POD_NAME 9090
Forwarding from 127.0.0.1:9090 -> 9090
Forwarding from [::1]:9090 -> 9090
Handling connection for 9090

在浏览器中转到 localhost:9090 并查看 Prometheus UI。

monitoring 命名空间中安装 Grafana Helm chart

$ helm install --name grafana --namespace monitoring stable/grafana
NAME:   grafana
LAST DEPLOYED: Tue Aug 27 13:10:02 2019
NAMESPACE: monitoring
STATUS: DEPLOYED

列出 monitoring 命名空间中与 Grafana 相关的 pods、services、configmaps 和 secrets:

$ kubectl get pods -nmonitoring | grep grafana
grafana-84b887cf4d-wplcr                         1/1     Running   0

$ kubectl get services -nmonitoring | grep grafana
grafana                         ClusterIP   10.0.5.154    <none>        80/TCP

$ kubectl get configmaps -nmonitoring | grep grafana
grafana                   1      99s
grafana-test              1      99s

$ kubectl get secrets -nmonitoring | grep grafana
grafana                                     Opaque
grafana-test-token-85x4x                    kubernetes.io/service-account-token
grafana-token-jw2qg                         kubernetes.io/service-account-token

检索 Grafana Web UI 中 admin 用户的密码:

$ kubectl get secret --namespace monitoring grafana \
-o jsonpath="{.data.admin-password}" | base64 --decode ; echo

SOMESECRETTEXT

通过kubectl port-forward命令连接到 Grafana UI:

$ export GRAFANA_POD_NAME=$(kubectl get pods --namespace monitoring \
-l "app=grafana,release=grafana" -o jsonpath="{.items[0].metadata.name}")

$ kubectl --namespace monitoring port-forward $GRAFANA_POD_NAME 3000
Forwarding from 127.0.0.1:3000 -> 3000
Forwarding from [::1]:3000 -> 3000

在浏览器中转到 localhost:3000 并查看 Grafana UI。使用上述检索到的密码以 admin 用户身份登录。

使用 helm list 列出当前已安装的 charts。安装 chart 后,当前的安装称为 “Helm release”:

$ helm list
NAME        REVISION  UPDATED                   STATUS    CHART
    APP VERSION NAMESPACE
grafana     1         Tue Aug 27 13:10:02 2019  DEPLOYED  grafana-3.8.3
    6.2.5       monitoring
prometheus. 1         Tue Aug 27 12:59:40 2019  DEPLOYED  prometheus-9.1.0
    2.11.1      monitoring

大多数情况下,您需要自定义一个 Helm chart。如果您从本地文件系统下载 chart 并使用 helm 安装,则会更容易进行此操作。

使用 helm fetch 命令获取最新稳定版本的 Prometheus 和 Grafana Helm charts,该命令将下载这些 chart 的 tgz 归档文件:

$ mkdir charts
$ cd charts
$ helm fetch stable/prometheus
$ helm fetch stable/grafana
$ ls -la
total 80
drwxr-xr-x   4 ggheo  staff    128 Aug 27 13:59 .
drwxr-xr-x  15 ggheo  staff    480 Aug 27 13:55 ..
-rw-r--r--   1 ggheo  staff  16195 Aug 27 13:55 grafana-3.8.3.tgz
-rw-r--r--   1 ggheo  staff  23481 Aug 27 13:54 prometheus-9.1.0.tgz

解压 tgz 文件,然后删除它们:

$ tar xfz prometheus-9.1.0.tgz; rm prometheus-9.1.0.tgz
$ tar xfz grafana-3.8.3.tgz; rm grafana-3.8.3.tgz

默认情况下,模板化的 Kubernetes 清单存储在 chart 目录下名为 templates 的目录中,因此在此例中,这些位置将分别是 prometheus/templatesgrafana/templates。给定 chart 的配置值在 chart 目录下的 values.yaml 文件中声明。

作为 Helm 图表定制的示例,让我们向 Grafana 添加一个持久卷,这样当重启 Grafana pods 时就不会丢失数据。

修改文件 grafana/values.yaml,并在该部分将 persistence 父键下的 enabled 子键的值设置为 true(默认为 false)。

## Enable persistence using Persistent Volume Claims
## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/
##
persistence:
  enabled: true
  # storageClassName: default
  accessModes:
    - ReadWriteOnce
  size: 10Gi
  # annotations: {}
  finalizers:
    - kubernetes.io/pvc-protection
  # subPath: ""
  # existingClaim:

使用 helm upgrade 命令来升级现有的 grafana Helm 发布。命令的最后一个参数是包含图表的本地目录的名称。在 grafana 图表目录的父目录中运行此命令:

$ helm upgrade grafana grafana/
Release "grafana" has been upgraded. Happy Helming!

验证在 monitoring 命名空间中为 Grafana 创建了 PVC:

kubectl describe pvc grafana -nmonitoring
Name:        grafana
Namespace:   monitoring
StorageClass:standard
Status:      Bound
Volume:      pvc-31d47393-c910-11e9-87c5-42010a8a0021
Labels:      app=grafana
             chart=grafana-3.8.3
             heritage=Tiller
             release=grafana
Annotations: pv.kubernetes.io/bind-completed: yes
             pv.kubernetes.io/bound-by-controller: yes
             volume.beta.kubernetes.io/storage-provisioner:kubernetes.io/gce-pd
Finalizers:  [kubernetes.io/pvc-protection]
Capacity:    10Gi
Access Modes:RWO
Mounted By:  grafana-84f79d5c45-zlqz8
Events:
Type    Reason                 Age   From                         Message
----    ------                 ----  ----                         -------
Normal  ProvisioningSucceeded  88s   persistentvolume-controller  Successfully
provisioned volume pvc-31d47393-c910-11e9-87c5-42010a8a0021
using kubernetes.io/gce-pd

另一个 Helm 图表定制的示例是修改 Prometheus 中存储数据的默认保留期,从 15 天改为其他。

prometheus/values.yaml 文件中将 retention 值更改为 30 天:

  ## Prometheus data retention period (default if not specified is 15 days)
  ##
  retention: "30d"

通过运行 helm upgrade 命令来升级现有的 Prometheus Helm 发布。在 prometheus 图表目录的父目录中运行此命令:

$ helm upgrade prometheus prometheus
Release "prometheus" has been upgraded. Happy Helming!

验证保留期已更改为 30 天。运行 kubectl describe 命令针对 monitoring 命名空间中运行的 Prometheus pod,并查看输出的 Args 部分:

$ kubectl get pods -nmonitoring
NAME                                            READY   STATUS   RESTARTS   AGE
grafana-84f79d5c45-zlqz8                        1/1     Running  0          9m
prometheus-alertmanager-df57f6df6-4b8lv         2/2     Running  0          87m
prometheus-kube-state-metrics-564564f799-t6qdm  1/1     Running  0          87m
prometheus-node-exporter-b4sb9                  1/1     Running  0          87m
prometheus-node-exporter-n4z2g                  1/1     Running  0          87m
prometheus-node-exporter-w7hn7                  1/1     Running  0          87m
prometheus-pushgateway-56b65bcf5f-whx5t         1/1     Running  0          87m
prometheus-server-779ffd445f-4llqr              2/2     Running  0          3m

$ kubectl describe pod prometheus-server-779ffd445f-4llqr -nmonitoring
OUTPUT OMITTED
      Args:
      --storage.tsdb.retention.time=30d
      --config.file=/etc/config/prometheus.yml
      --storage.tsdb.path=/data
      --web.console.libraries=/etc/prometheus/console_libraries
      --web.console.templates=/etc/prometheus/consoles
      --web.enable-lifecycle

销毁 GKE 集群

如果不再需要,记得删除用于测试目的的任何云资源,因为这真的很“贵”。否则,月底收到云服务提供商的账单时,可能会有不愉快的惊喜。

通过 pulumi destroy 销毁 GKE 集群:

$ pulumi destroy

Previewing destroy (dev):

     Type                            Name                            Plan
 -   pulumi:pulumi:Stack             pythonfordevops-gke-pulumi-dev  delete
 -   ├─ kubernetes:core:Service      ingress                         delete
 -   ├─ kubernetes:apps:Deployment   canary                          delete
 -   ├─ pulumi:providers:kubernetes  gke_k8s                         delete
 -   ├─ gcp:container:Cluster        gke-cluster                     delete
 -   └─ random:index:RandomString    password                        delete

Resources:
    - 6 to delete

Do you want to perform this destroy? yes
Destroying (dev):

     Type                            Name                            Status
 -   pulumi:pulumi:Stack             pythonfordevops-gke-pulumi-dev  deleted
 -   ├─ kubernetes:core:Service      ingress                         deleted
 -   ├─ kubernetes:apps:Deployment   canary                          deleted
 -   ├─ pulumi:providers:kubernetes  gke_k8s                         deleted
 -   ├─ gcp:container:Cluster        gke-cluster                     deleted
 -   └─ random:index:RandomString    password                        deleted

Resources:
    - 6 deleted

Duration: 3m18s

练习

  • 在 GKE 中不再运行 PostgreSQL 的 Docker 容器,而是使用 Google Cloud SQL for PostgreSQL。

  • 使用 AWS 云开发工具包 来启动 Amazon EKS 集群,并将示例应用部署到该集群中。

  • 在 EKS 中不再运行 PostgreSQL 的 Docker 容器,而是使用 Amazon RDS PostgreSQL。

  • 尝试使用 Kustomize 作为管理 Kubernetes 清单 YAML 文件的 Helm 替代方案。

第十三章:无服务器技术

无服务器是当今 IT 行业中引起很多关注的一个词汇。像这样的词汇经常会导致人们对它们的实际含义有不同的看法。表面上看,无服务器意味着一个你不再需要担心管理服务器的世界。在某种程度上,这是正确的,但只适用于使用无服务器技术提供的功能的开发人员。本章展示了在幕后需要发生很多工作才能实现这个无服务器的神奇世界。

许多人将术语无服务器等同于函数即服务(FaaS)。这在某种程度上是正确的,这主要是因为 AWS 在 2015 年推出了 Lambda 服务。AWS Lambdas 是可以在云中运行的函数,而无需部署传统服务器来托管这些函数。因此,无服务器这个词就诞生了。

然而,FaaS 并不是唯一可以称为无服务器的服务。如今,三大公共云提供商(亚马逊、微软和谷歌)都提供容器即服务(CaaS),允许您在它们的云中部署完整的 Docker 容器,而无需提供托管这些容器的服务器。这些服务也可以称为无服务器。这些服务的示例包括 AWS Fargate、Microsoft Azure 容器实例和 Google Cloud Run。

无服务器技术的一些用例是什么?对于像 AWS Lambda 这样的 FaaS 技术,特别是由于 Lambda 函数可以被其他云服务触发的事件驱动方式,用例包括:

  • 提取-转换-加载(ETL)数据处理,其中,例如,将文件上传到 S3,触发 Lambda 函数对数据进行 ETL 处理,并将其发送到队列或后端数据库

  • 对其他服务发送到 CloudWatch 的日志进行 ETL 处理

  • 基于 CloudWatch Events 触发 Lambda 函数以类似 cron 的方式调度任务

  • 基于 Amazon SNS 触发 Lambda 函数的实时通知

  • 使用 Lambda 和 Amazon SES 处理电子邮件

  • 无服务器网站托管,静态 Web 资源(如 Javascript、CSS 和 HTML)存储在 S3 中,并由 CloudFront CDN 服务前端处理,以及由 API Gateway 处理的 REST API 将 API 请求路由到 Lambda 函数,后者与后端(如 Amazon RDS 或 Amazon DynamoDB)通信

每个云服务提供商的在线文档中都可以找到许多无服务器使用案例。例如,在 Google Cloud 无服务器生态系统中,Web 应用程序最适合由 Google AppEngine 处理,API 最适合由 Google Functions 处理,而 CloudRun 则适合在 Docker 容器中运行进程。举个具体的例子,考虑一个需要执行 TensorFlow 框架的机器学习任务(如对象检测)的服务。由于 FaaS 的计算、内存和磁盘资源限制,再加上 FaaS 设置中库的有限可用性,可能更适合使用 Google Cloud Run 这样的 CaaS 服务来运行这样的服务,而不是使用 Google Cloud Functions 这样的 FaaS 服务。

三大云服务提供商还围绕其 FaaS 平台提供丰富的 DevOps 工具链。例如,当你使用 AWS Lambda 时,你可以轻松地添加 AWS 的这些服务:

  • AWS X-Ray 用于追踪/可观测性。

  • Amazon CloudWatch 用于日志记录、警报和事件调度。

  • AWS Step Functions 用于无服务器工作流协调。

  • AWS Cloud9 用于基于浏览器的开发环境。

如何选择 FaaS 和 CaaS?在一个维度上,这取决于部署的单位。如果你只关心短暂的函数,少量依赖和少量数据处理,那么 FaaS 可以适合你。另一方面,如果你有长时间运行的进程,有大量依赖和重的计算需求,那么使用 CaaS 可能更好。大多数 FaaS 服务对运行时间(Lambda 的最长时间为 15 分钟)、计算能力、内存大小、磁盘空间、HTTP 请求和响应限制都有严格的限制。FaaS 短执行时间的优势在于你只需支付函数运行的时长。

如果你还记得第 第十二章 开头关于宠物与牲畜与昆虫的讨论,函数可以真正被视为短暂存在、执行一些处理然后消失的短命昆虫。由于它们的短暂特性,FaaS 中的函数也是无状态的,这是在设计应用程序时需要牢记的重要事实。

选择 FaaS 和 CaaS 的另一个维度是你的服务与其他服务之间的交互次数和类型。例如,AWS Lambda 函数可以由其他至少八个 AWS 服务异步触发,包括 S3、简单通知服务(SNS)、简单电子邮件服务(SES)和 CloudWatch。这种丰富的交互使得编写响应事件的函数更加容易,因此在这种情况下 FaaS 胜出。

正如您在本章中将看到的,许多 FaaS 服务实际上基于 Kubernetes,这些天是事实上的容器编排标准。尽管您的部署单元是一个函数,但在幕后,FaaS 工具会创建和推送 Docker 容器到一个您可能管理或可能不管理的 Kubernetes 集群中。OpenFaas 和 OpenWhisk 是这种基于 Kubernetes 的 FaaS 技术的示例。当您自托管这些 FaaS 平台时,您很快就会意识到服务器构成了大部分无服务器的单词。突然间,您不得不非常关心如何照料和喂养您的 Kubernetes 集群。

当我们将 DevOps 一词分为其部分 Dev 和 Ops 时,无服务器技术更多地针对 Dev 方面。它们帮助开发人员在部署其代码时感觉少一些摩擦。特别是在自托管场景中,负担在 Ops 身上,以提供将支持 FaaS 或 CaaS 平台的基础设施(有时非常复杂)。然而,即使 Dev 方面在使用无服务器时可能觉得 Ops 需求较少(这确实会发生,尽管根据定义这种分割使其成为非 DevOps 情况),在使用无服务器平台时仍然有很多与 Ops 相关的问题需要担心:安全性、可伸缩性、资源限制和容量规划、监视、日志记录和可观察性。传统上,这些被视为 Ops 的领域,但在我们讨论的新兴 DevOps 世界中,它们需要由 Dev 和 Ops 共同解决并合作。一个 Dev 团队在完成编写代码后不应该觉得任务已经完成。相反,它应该承担责任,并且是的,以在生产环境中全程完成服务,并内置良好的监控、日志记录和跟踪。

我们从这一章开始,展示如何使用它们的 FaaS 提供的相同 Python 函数(表示简单 HTTP 端点),将其部署到“三巨头”云提供商。

以下示例中使用的某些命令会产生大量输出。除了在理解命令时必要的情况下,我们将省略大部分输出行,以节省树木并使读者能够更好地专注于文本。

将相同的 Python 函数部署到“三巨头”云提供商

对于 AWS 和 Google,我们使用无服务器平台,通过抽象出参与 FaaS 运行时环境的云资源的创建来简化这些部署。无服务器平台目前尚不支持 Microsoft Azure 的 Python 函数,因此在这种情况下,我们展示如何使用 Azure 特定的 CLI 工具。

安装无服务器框架

无服务器平台基于 nodejs。要安装它,请使用npm

$ npm install -g serverless

将 Python 函数部署到 AWS Lambda

首先克隆无服务器平台示例 GitHub 存储库:

$ git clone https://github.com/serverless/examples.git
$ cd aws-python-simple-http-endpoint
$ export AWS_PROFILE=gheorghiu-net

Python HTTP 端点定义在文件handler.py中:

$ cat handler.py
import json
import datetime

def endpoint(event, context):
    current_time = datetime.datetime.now().time()
    body = {
        "message": "Hello, the current time is " + str(current_time)
    }

    response = {
        "statusCode": 200,
        "body": json.dumps(body)
    }

    return response

无服务器平台使用声明性方法指定需要创建的资源,该方法使用一个名为serverless.yaml的 YAML 文件。以下是声明一个名为currentTime的函数的文件,对应于之前定义的handler模块中的 Python 函数endpoint

$ cat serverless.yml
service: aws-python-simple-http-endpoint

frameworkVersion: ">=1.2.0 <2.0.0"

provider:
  name: aws
  runtime: python2.7 # or python3.7, supported as of November 2018

functions:
  currentTime:
    handler: handler.endpoint
    events:
      - http:
          path: ping
          method: get

serverless.yaml中修改 Python 版本为 3.7:

provider:
  name: aws
  runtime: python3.7

通过运行serverless deploy命令将函数部署到 AWS Lambda:

$ serverless deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless:
Uploading service aws-python-simple-http-endpoint.zip file to S3 (1.95 KB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..............
Serverless: Stack update finished...
Service Information
service: aws-python-simple-http-endpoint
stage: dev
region: us-east-1
stack: aws-python-simple-http-endpoint-dev
resources: 10
api keys:
  None
endpoints:
  GET - https://3a88jzlxm0.execute-api.us-east-1.amazonaws.com/dev/ping
functions:
  currentTime: aws-python-simple-http-endpoint-dev-currentTime
layers:
  None
Serverless:
Run the "serverless" command to setup monitoring, troubleshooting and testing.

通过使用curl命中其端点来测试部署的 AWS Lambda 函数:

$ curl https://3a88jzlxm0.execute-api.us-east-1.amazonaws.com/dev/ping
{"message": "Hello, the current time is 23:16:30.479690"}%

使用serverless invoke命令直接调用 Lambda 函数:

$ serverless invoke --function currentTime
{
    "statusCode": 200,
    "body": "{\"message\": \"Hello, the current time is 23:18:38.101006\"}"
}

直接调用 Lambda 函数并同时检查日志(发送到 AWS CloudWatch Logs):

$ serverless invoke --function currentTime --log
{
    "statusCode": 200,
    "body": "{\"message\": \"Hello, the current time is 23:17:11.182463\"}"
}
--------------------------------------------------------------------
START RequestId: 5ac3c9c8-f8ca-4029-84fa-fcf5157b1404 Version: $LATEST
END RequestId: 5ac3c9c8-f8ca-4029-84fa-fcf5157b1404
REPORT RequestId: 5ac3c9c8-f8ca-4029-84fa-fcf5157b1404
Duration: 1.68 ms Billed Duration: 100 ms   Memory Size: 1024 MB
Max Memory Used: 56 MB

请注意,前述输出中的Billed Duration为 100 毫秒。这显示了使用 FaaS 的一个优势——以非常短的时间段计费。

还有一件事我们想要引起您的注意,即无服务器平台在创建作为 Lambda 设置的一部分的 AWS 资源时在幕后进行了大量工作。无服务器平台创建了一个称为 CloudFormation 堆栈的栈,此案例中称为aws-python-simple-http-endpoint-dev。您可以使用aws CLI 工具检查它:

$ aws cloudformation describe-stack-resources \
  --stack-name aws-python-simple-http-endpoint-dev
  --region us-east-1 | jq '.StackResources[].ResourceType'
"AWS::ApiGateway::Deployment"
"AWS::ApiGateway::Method"
"AWS::ApiGateway::Resource"
"AWS::ApiGateway::RestApi"
"AWS::Lambda::Function"
"AWS::Lambda::Permission"
"AWS::Lambda::Version"
"AWS::Logs::LogGroup"
"AWS::IAM::Role"
"AWS::S3::Bucket"

请注意,这个 CloudFormation 堆栈包含不少于 10 种 AWS 资源类型,否则您将不得不手动创建或手动关联这些资源。

将 Python 函数部署到 Google Cloud Functions

在本节中,我们将以服务器平台示例 GitHub 存储库中google-python-simple-http-endpoint目录中的代码为例:

$ gcloud projects list
PROJECT_ID                  NAME                        PROJECT_NUMBER
pulumi-gke-testing          Pulumi GKE Testing          705973980178
pythonfordevops-gke-pulumi  pythonfordevops-gke-pulumi  787934032650

创建一个新的 GCP 项目:

$ gcloud projects create pythonfordevops-cloudfunction

初始化本地的gcloud环境:

$ gcloud init
Welcome! This command will take you through the configuration of gcloud.

Settings from your current configuration [pythonfordevops-gke-pulumi] are:
compute:
  region: us-west1
  zone: us-west1-c
core:
  account: grig.gheorghiu@gmail.com
  disable_usage_reporting: 'True'
  project: pythonfordevops-gke-pulumi

Pick configuration to use:
[1] Re-initialize this configuration with new settings
[2] Create a new configuration
[3] Switch to and re-initialize existing configuration: [default]
Please enter your numeric choice:  2

Enter configuration name. Names start with a lower case letter and
contain only lower case letters a-z, digits 0-9, and hyphens '-':
pythonfordevops-cloudfunction
Your current configuration has been set to: [pythonfordevops-cloudfunction]

Choose the account you would like to use to perform operations for
this configuration:
 [1] grig.gheorghiu@gmail.com
 [2] Log in with a new account
Please enter your numeric choice:  1

You are logged in as: [grig.gheorghiu@gmail.com].

Pick cloud project to use:
 [1] pulumi-gke-testing
 [2] pythonfordevops-cloudfunction
 [3] pythonfordevops-gke-pulumi
 [4] Create a new project
Please enter numeric choice or text value (must exactly match list
item):  2

Your current project has been set to: [pythonfordevops-cloudfunction].

授权本地 Shell 与 GCP:

$ gcloud auth login

使用无服务器框架部署与 AWS Lambda 示例相同的 Python HTTP 端点,但这次作为 Google Cloud Function:

$ serverless deploy

  Serverless Error ---------------------------------------

  Serverless plugin "serverless-google-cloudfunctions"
  initialization errored: Cannot find module 'serverless-google-cloudfunctions'
Require stack:
- /usr/local/lib/node_modules/serverless/lib/classes/PluginManager.js
- /usr/local/lib/node_modules/serverless/lib/Serverless.js
- /usr/local/lib/node_modules/serverless/lib/utils/autocomplete.js
- /usr/local/lib/node_modules/serverless/bin/serverless.js

  Get Support --------------------------------------------
     Docs:          docs.serverless.com
     Bugs:          github.com/serverless/serverless/issues
     Issues:        forum.serverless.com

  Your Environment Information ---------------------------
     Operating System:          darwin
     Node Version:              12.9.0
     Framework Version:         1.50.0
     Plugin Version:            1.3.8
     SDK Version:               2.1.0

我们刚遇到的错误是由于尚未安装package.json中指定的依赖项:

$ cat package.json
{
  "name": "google-python-simple-http-endpoint",
  "version": "0.0.1",
  "description":
  "Example demonstrates how to setup a simple HTTP GET endpoint with python",
  "author": "Sebastian Borza <sebito91@gmail.com>",
  "license": "MIT",
  "main": "handler.py",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "serverless-google-cloudfunctions": "².1.0"
  }
}

无服务器平台是用 node.js 编写的,因此它的包需要使用npm install安装:

$ npm install

再次尝试部署:

$ serverless deploy

  Error --------------------------------------------------

  Error: ENOENT: no such file or directory,
  open '/Users/ggheo/.gcloud/keyfile.json'

要生成凭据密钥,请在 GCP IAM 服务帐户页面上创建一个名为sa的新服务帐户。在此案例中,新服务帐户的电子邮件设置为sa-255@pythonfordevops-cloudfunction.iam.gserviceaccount.com

创建一个凭据密钥,并将其下载为~/.gcloud/pythonfordevops-cloudfunction.json

指定项目和serverless.yml中密钥的路径:

$ cat serverless.yml

service: python-simple-http-endpoint

frameworkVersion: ">=1.2.0 <2.0.0"

package:
  exclude:
    - node_modules/**
    - .gitignore
    - .git/**

plugins:
  - serverless-google-cloudfunctions

provider:
  name: google
  runtime: python37
  project: pythonfordevops-cloudfunction
  credentials: ~/.gcloud/pythonfordevops-cloudfunction.json

functions:
  currentTime:
    handler: endpoint
    events:
      - http: path

转到 GCP 部署管理器页面并启用 Cloud Deployment Manager API;然后还为 Google Cloud Storage 启用计费。

再次尝试部署:

$ serverless deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Compiling function "currentTime"...
Serverless: Uploading artifacts...

  Error --------------------------------------------------

  Error: Not Found
  at createError
  (/Users/ggheo/code/mycode/examples/google-python-simple-http-endpoint/
  node_modules/axios/lib/core/createError.js:16:15)
  at settle (/Users/ggheo/code/mycode/examples/
  google-python-simple-http-endpoint/node_modules/axios/lib/
  core/settle.js:18:12)
  at IncomingMessage.handleStreamEnd
  (/Users/ggheo/code/mycode/examples/google-python-simple-http-endpoint/
  node_modules/axios/lib/adapters/http.js:202:11)
  at IncomingMessage.emit (events.js:214:15)
  at IncomingMessage.EventEmitter.emit (domain.js:476:20)
  at endReadableNT (_stream_readable.js:1178:12)
  at processTicksAndRejections (internal/process/task_queues.js:77:11)

  For debugging logs, run again after setting the "SLS_DEBUG=*"
  environment variable.

阅读关于 GCP 凭据和角色的无服务器平台文档

需要分配给部署所使用的服务帐户的以下角色:

  • 部署管理器编辑器

  • 存储管理员

  • 日志管理员

  • 云函数开发者角色

还要阅读有关需启用的 GCP API 的 Serverless 平台文档文档

在 GCP 控制台中需要启用以下 API:

  • Google Cloud Functions

  • Google Cloud Deployment Manager

  • Google Cloud Storage

  • Stackdriver Logging

转到 GCP 控制台中的部署管理器,并检查错误消息:

sls-python-simple-http-endpoint-dev failed to deploy

sls-python-simple-http-endpoint-dev has resource warnings
sls-python-simple-http-endpoint-dev-1566510445295:
{"ResourceType":"storage.v1.bucket",
"ResourceErrorCode":"403",
"ResourceErrorMessage":{"code":403,
"errors":[{"domain":"global","location":"Authorization",
"locationType":"header",
"message":"The project to be billed is associated
with an absent billing account.",
"reason":"accountDisabled"}],
"message":"The project to be billed is associated
 with an absent billing account.",
 "statusMessage":"Forbidden",
 "requestPath":"https://www.googleapis.com/storage/v1/b",
 "httpMethod":"POST"}}

在 GCP 控制台中删除sls-python-simple-http-endpoint-dev部署,并再次运行serverless deploy

$ serverless deploy

Deployed functions
first
  https://us-central1-pythonfordevops-cloudfunction.cloudfunctions.net/http

最初,由于我们没有为 Google Cloud Storage 启用计费,serverless deploy命令一直失败。在serverless.yml中指定的服务的部署标记为失败,并且即使启用了 Cloud Storage 计费后,后续的serverless deploy命令仍然失败。一旦在 GCP 控制台中删除了失败的部署,serverless deploy命令就开始工作了。

直接调用部署的 Google Cloud Function:

$ serverless invoke --function currentTime
Serverless: v1os7ptg9o48 {
    "statusCode": 200,
    "body": {
        "message": "Received a POST request at 03:46:39.027230"
    }
}

使用serverless logs命令检查日志:

$ serverless logs --function currentTime
Serverless: Displaying the 4 most recent log(s):

2019-08-23T03:35:12.419846316Z: Function execution took 20 ms,
finished with status code: 200
2019-08-23T03:35:12.400499207Z: Function execution started
2019-08-23T03:34:27.133107221Z: Function execution took 11 ms,
finished with status code: 200
2019-08-23T03:34:27.122244864Z: Function execution started

使用curl测试函数端点:

$ curl \
https://undefined-pythonfordevops-cloudfunction.cloudfunctions.net/endpoint
<!DOCTYPE html>
<html lang=en>
  <p><b>404.</b> <ins>That’s an error.</ins>
  <p>The requested URL was not found on this server.
  <ins>That’s all we know.</ins>

由于我们没有在serverless.yml中定义区域,端点 URL 以undefined开头并返回错误。

serverless.yml中将区域设置为us-central1

provider:
  name: google
  runtime: python37
  region: us-central1
  project: pythonfordevops-cloudfunction
  credentials: /Users/ggheo/.gcloud/pythonfordevops-cloudfunction.json

使用serverless deploy部署新版本,并使用curl测试函数端点:

$ curl \
https://us-central1-pythonfordevops-cloudfunction.cloudfunctions.net/endpoint
{
    "statusCode": 200,
    "body": {
        "message": "Received a GET request at 03:51:02.560756"
    }
}%

将 Python 函数部署到 Azure

Serverless 平台尚不支持基于 Python 的Azure Functions。我们将演示如何使用 Azure 本地工具部署 Azure Python 函数。

根据您特定操作系统的官方 Microsoft 文档注册 Microsoft Azure 账户并安装 Azure Functions 运行时。如果使用 macOS,使用brew

$ brew tap azure/functions
$ brew install azure-functions-core-tools

为 Python 函数代码创建新目录:

$ mkdir azure-functions-python
$ cd azure-functions-python

安装 Python 3.6,因为 Azure Functions 不支持 3.7。创建并激活virtualenv

$ brew unlink python
$ brew install \
https://raw.githubusercontent.com/Homebrew/homebrew-core/
f2a764ef944b1080be64bd88dca9a1d80130c558/Formula/python.rb \
--ignore-dependencies

$ python3 -V
Python 3.6.5

$ python3 -m venv .venv
$ source .venv/bin/activate

使用 Azure func实用程序创建名为python-simple-http-endpoint的本地函数项目:

$ func init python-simple-http-endpoint
Select a worker runtime:
1\. dotnet
2\. node
3\. python
4\. powershell (preview)
Choose option: 3

切换到新创建的python-simple-http-endpoint目录,并使用func new命令创建 Azure HTTP 触发器函数:

$ cd python-simple-http-endpoint
$ func new
Select a template:
1\. Azure Blob Storage trigger
2\. Azure Cosmos DB trigger
3\. Azure Event Grid trigger
4\. Azure Event Hub trigger
5\. HTTP trigger
6\. Azure Queue Storage trigger
7\. Azure Service Bus Queue trigger
8\. Azure Service Bus Topic trigger
9\. Timer trigger
Choose option: 5
HTTP trigger
Function name: [HttpTrigger] currentTime
Writing python-simple-http-endpoint/currentTime/__init__.py
Writing python-simple-http-endpoint/currentTime/function.json
The function "currentTime" was created successfully
from the "HTTP trigger" template.

检查创建的 Python 代码:

$ cat currentTime/__init__.py
import logging

import azure.functions as func

def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    name = req.params.get('name')
    if not name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            name = req_body.get('name')

    if name:
        return func.HttpResponse(f"Hello {name}!")
    else:
        return func.HttpResponse(
             "Please pass a name on the query string or in the request body",
             status_code=400
        )

在本地运行函数:

$ func host start

[8/24/19 12:21:35 AM] Host initialized (299ms)
[8/24/19 12:21:35 AM] Host started (329ms)
[8/24/19 12:21:35 AM] Job host started
[8/24/19 12:21:35 AM]  INFO: Starting Azure Functions Python Worker.
[8/24/19 12:21:35 AM]  INFO: Worker ID: e49c429d-9486-4167-9165-9ecd1757a2b5,
Request ID: 2842271e-a8fe-4643-ab1a-f52381098ae6, Host Address: 127.0.0.1:53952
Hosting environment: Production
Content root path: python-simple-http-endpoint
Now listening on: http://0.0.0.0:7071
Application started. Press Ctrl+C to shut down.
[8/24/19 12:21:35 AM] INFO: Successfully opened gRPC channel to 127.0.0.1:53952

Http Functions:

  currentTime: [GET,POST] http://localhost:7071/api/currentTime

在另一个终端中测试:

$ curl http://127.0.0.1:7071/api/currentTime\?name\=joe
Hello joe!%

currentTime/init.py中更改 HTTP 处理程序,以在其响应中包含当前时间:

import datetime

def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    name = req.params.get('name')
    if not name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            name = req_body.get('name')

    current_time = datetime.datetime.now().time()
    if name:
        return func.HttpResponse(f"Hello {name},
        the current time is {current_time}!")
    else:
        return func.HttpResponse(
             "Please pass a name on the query string or in the request body",
             status_code=400
        )

使用curl测试新函数:

$ curl http://127.0.0.1:7071/api/currentTime\?name\=joe
Hello joe, the current time is 17:26:54.256060!%

使用pip安装 Azure CLI:

$ pip install azure.cli

使用azCLI 实用程序在交互模式下创建 Azure 资源组、存储帐户和函数应用。此模式提供自动完成、命令描述和示例的交互式 shell。请注意,如果要跟随操作,需要指定一个不同且唯一的functionapp名称。您可能还需要指定支持免费试用帐户的其他 Azure 区域,如eastus

$ az interactive
az>> login
az>> az group create --name myResourceGroup --location westus2
az>> az storage account create --name griggheorghiustorage --location westus2 \
--resource-group myResourceGroup --sku Standard_LRS
az>> az functionapp create --resource-group myResourceGroup --os-type Linux \
--consumption-plan-location westus2 --runtime python \
--name pyazure-devops4all \
--storage-account griggheorghiustorage
az>> exit

使用func实用程序将functionapp项目部署到 Azure:

$ func azure functionapp publish pyazure-devops4all --build remote
Getting site publishing info...
Creating archive for current directory...
Perform remote build for functions project (--build remote).
Uploading 2.12 KB

OUTPUT OMITTED

Running post deployment command(s)...
Deployment successful.
App container will begin restart within 10 seconds.
Remote build succeeded!
Syncing triggers...
Functions in pyazure-devops4all:
    currentTime - [httpTrigger]
      Invoke url:
      https://pyazure-devops4all.azurewebsites.net/api/
      currenttime?code=b0rN93O04cGPcGFKyX7n9HgITTPnHZiGCmjJN/SRsPX7taM7axJbbw==

使用curl命中其端点测试在 Azure 上部署的函数:

$ curl "https://pyazure-devops4all.azurewebsites.net/api/currenttime\
?code\=b0rN93O04cGPcGFKyX7n9HgITTPnHZiGCmjJN/SRsPX7taM7axJbbw\=\=\&name\=joe"
Hello joe, the current time is 01:20:32.036097!%

删除不再需要的任何云资源始终是个好主意。在这种情况下,您可以运行:

$ az group delete --name myResourceGroup

将 Python 函数部署到自托管 FaaS 平台

正如本章前面提到的,许多 FaaS 平台正在运行在 Kubernetes 集群之上。这种方法的一个优点是您部署的函数作为常规 Docker 容器在 Kubernetes 内部运行,因此您可以使用现有的 Kubernetes 工具,特别是在可观察性方面(监视、日志记录和跟踪)。另一个优点是潜在的成本节约。通过将您的无服务器函数作为容器运行在现有的 Kubernetes 集群中,您可以使用集群的现有容量,并且不像将您的函数部署到第三方 FaaS 平台时那样按函数调用付费。

在本节中,我们考虑其中一个平台:OpenFaaS。一些运行在 Kubernetes 上的类似 FaaS 平台的其他示例包括以下内容:

部署 Python 函数到 OpenFaaS

对于本示例,我们使用 Rancher 的“Kubernetes-lite”分发称为 k3s。我们使用 k3s 而不是 minikube 来展示 Kubernetes 生态系统中可用的各种工具。

通过运行k3sup 实用程序,在 Ubuntu EC2 实例上设置 k3s Kubernetes 集群。

下载并安装 k3sup

$ curl -sLS https://get.k3sup.dev | sh
$ sudo cp k3sup-darwin /usr/local/bin/k3sup

验证远程 EC2 实例的 SSH 连通性:

$ ssh ubuntu@35.167.68.86 date
Sat Aug 24 21:38:57 UTC 2019

通过 k3sup install 安装 k3s

$ k3sup install --ip 35.167.68.86 --user ubuntu
OUTPUT OMITTED
Saving file to: kubeconfig

检查 kubeconfig 文件:

$ cat kubeconfig
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: BASE64_FIELD
    server: https://35.167.68.86:6443
  name: default
contexts:
- context:
    cluster: default
    user: default
  name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
  user:
    password: OBFUSCATED
    username: admin

KUBECONFIG 环境变量指向本地的 kubeconfig 文件,并针对远程 k3s 集群测试 kubectl 命令:

$ export KUBECONFIG=./kubeconfig

$ kubectl cluster-info
Kubernetes master is running at https://35.167.68.86:6443
CoreDNS is running at
https://35.167.68.86:6443/api/v1/namespaces/kube-system/
services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use
'kubectl cluster-info dump'.

$ kubectl get nodes
NAME            STATUS   ROLES    AGE   VERSION
ip-10-0-0-185   Ready    master   10m   v1.14.6-k3s.1

下一步是在 k3s Kubernetes 集群上安装 OpenFaas 无服务器平台。

在本地的 macOS 上安装 faas-cli

$ brew install faas-cli

为 Tiller 创建 RBAC 权限,Tiller 是 Helm 的服务器组件:

$ kubectl -n kube-system create sa tiller \
  && kubectl create clusterrolebinding tiller \
  --clusterrole cluster-admin \
  --serviceaccount=kube-system:tiller
serviceaccount/tiller created
clusterrolebinding.rbac.authorization.k8s.io/tiller created

通过 helm init 安装 Tiller:

$ helm init --skip-refresh --upgrade --service-account tiller

下载、配置并安装 OpenFaaS 的 Helm chart:

$ wget \
https://raw.githubusercontent.com/openfaas/faas-netes/master/namespaces.yml

$ cat namespaces.yml
apiVersion: v1
kind: Namespace
metadata:
  name: openfaas
  labels:
    role: openfaas-system
    access: openfaas-system
    istio-injection: enabled
---
apiVersion: v1
kind: Namespace
metadata:
  name: openfaas-fn
  labels:
    istio-injection: enabled
    role: openfaas-fn

$ kubectl apply -f namespaces.yml
namespace/openfaas created
namespace/openfaas-fn created

$ helm repo add openfaas https://openfaas.github.io/faas-netes/
"openfaas" has been added to your repositories

为连接到 OpenFaaS 网关的基本身份验证生成随机密码:

$ PASSWORD=$(head -c 12 /dev/urandom | shasum| cut -d' ' -f1)

$ kubectl -n openfaas create secret generic basic-auth \
--from-literal=basic-auth-user=admin \
--from-literal=basic-auth-password="$PASSWORD"
secret/basic-auth created

通过安装 Helm chart 部署 OpenFaaS:

$ helm repo update \
 && helm upgrade openfaas --install openfaas/openfaas \
    --namespace openfaas  \
    --set basic_auth=true \
    --set serviceType=LoadBalancer \
    --set functionNamespace=openfaas-fn

OUTPUT OMITTED

NOTES:
To verify that openfaas has started, run:
kubectl --namespace=openfaas get deployments -l "release=openfaas,app=openfaas"
注意

此处使用的没有 TLS 的 basic_auth 设置仅用于实验/学习。任何重要环境都应配置为确保凭据通过安全的 TLS 连接传递。

验证在 openfaas 命名空间中运行的服务:

$ kubectl get service -nopenfaas
NAME                TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)
alertmanager        ClusterIP      10.43.193.61    <none>        9093/TCP
basic-auth-plugin   ClusterIP      10.43.83.12     <none>        8080/TCP
gateway             ClusterIP      10.43.7.46      <none>        8080/TCP
gateway-external    LoadBalancer   10.43.91.91     10.0.0.185    8080:31408/TCP
nats                ClusterIP      10.43.33.153    <none>        4222/TCP
prometheus          ClusterIP      10.43.122.184   <none>        9090/TCP

将远程实例的端口 8080 转发到本地端口 8080

$ kubectl port-forward -n openfaas svc/gateway 8080:8080 &
[1] 29183
Forwarding from 127.0.0.1:8080 -> 8080

转到 OpenFaaS Web UI,网址为http://localhost:8080,使用用户名 admin 和密码 $PASSWORD 登录。

继续通过创建一个 OpenFaaS Python 函数。使用 faas-cli 工具创建一个名为 hello-python 的新 OpenFaaS 函数:

$ faas-cli new --lang python hello-python
Folder: hello-python created.
Function created in folder: hello-python
Stack file written: hello-python.yml

检查 hello-python 函数的配置文件:

$ cat hello-python.yml
version: 1.0
provider:
  name: openfaas
  gateway: http://127.0.0.1:8080
functions:
  hello-python:
    lang: python
    handler: ./hello-python
    image: hello-python:latest

检查自动创建的目录 hello-python

$ ls -la hello-python
total 8
drwx------  4 ggheo  staff  128 Aug 24 15:16 .
drwxr-xr-x  8 ggheo  staff  256 Aug 24 15:16 ..
-rw-r--r--  1 ggheo  staff  123 Aug 24 15:16 handler.py
-rw-r--r--  1 ggheo  staff    0 Aug 24 15:16 requirements.txt

$ cat hello-python/handler.py
def handle(req):
    """handle a request to the function
    Args:
        req (str): request body
    """

    return req

编辑 handler.py 并将从 Serverless 平台的 simple-http-example 打印当前时间的代码复制过来:

$ cat hello-python/handler.py
import json
import datetime

def handle(req):
    """handle a request to the function
 Args:
 req (str): request body
 """

    current_time = datetime.datetime.now().time()
    body = {
        "message": "Received a {} at {}".format(req, str(current_time))
    }

    response = {
        "statusCode": 200,
        "body": body
    }
    return json.dumps(response, indent=4)

下一步是构建 OpenFaaS Python 函数。 使用 faas-cli build 命令将根据自动生成的 Dockerfile 构建 Docker 镜像:

$ faas-cli build -f ./hello-python.yml
[0] > Building hello-python.
Clearing temporary build folder: ./build/hello-python/
Preparing ./hello-python/ ./build/hello-python//function
Building: hello-python:latest with python template. Please wait..
Sending build context to Docker daemon  8.192kB
Step 1/29 : FROM openfaas/classic-watchdog:0.15.4 as watchdog

DOCKER BUILD OUTPUT OMITTED

Successfully tagged hello-python:latest
Image: hello-python:latest built.
[0] < Building hello-python done.
[0] worker done.

检查本地是否存在 Docker 镜像:

$ docker images | grep hello-python
hello-python                          latest
05b2c37407e1        29 seconds ago      75.5MB

将 Docker 镜像打标签并推送到 Docker Hub 注册表,以便在远程 Kubernetes 群集上使用:

$ docker tag hello-python:latest griggheo/hello-python:latest

编辑 hello-python.yml 并更改:

image: griggheo/hello-python:latest

使用 faas-cli push 命令将镜像推送到 Docker Hub:

$ faas-cli push -f ./hello-python.yml
[0] > Pushing hello-python [griggheo/hello-python:latest].
The push refers to repository [docker.io/griggheo/hello-python]
latest: digest:
sha256:27e1fbb7f68bb920a6ff8d3baf1fa3599ae92e0b3c607daac3f8e276aa7f3ae3
size: 4074
[0] < Pushing hello-python [griggheo/hello-python:latest] done.
[0] worker done.

现在,将 OpenFaaS Python 函数部署到远程 k3s 群集。 使用 faas-cli deploy 命令部署函数:

$ faas-cli deploy -f ./hello-python.yml
Deploying: hello-python.
WARNING! Communication is not secure, please consider using HTTPS.
Letsencrypt.org offers free SSL/TLS certificates.
Handling connection for 8080

unauthorized access, run "faas-cli login"
to setup authentication for this server

Function 'hello-python' failed to deploy with status code: 401

使用 faas-cli login 命令获取认证凭据:

$ echo -n $PASSWORD | faas-cli login -g http://localhost:8080 \
-u admin --password-stdin
Calling the OpenFaaS server to validate the credentials...
Handling connection for 8080
WARNING! Communication is not secure, please consider using HTTPS.
Letsencrypt.org offers free SSL/TLS certificates.
credentials saved for admin http://localhost:8080

编辑 hello-python.yml 并更改:

gateway: http://localhost:8080

因为我们从处理程序返回 JSON,请将这些行添加到 hello-python.yml

    environment:
      content_type: application/json

hello-python.yml 的内容:

$ cat hello-python.yml
version: 1.0
provider:
  name: openfaas
  gateway: http://localhost:8080
functions:
  hello-python:
    lang: python
    handler: ./hello-python
    image: griggheo/hello-python:latest
    environment:
      content_type: application/json

再次运行 faas-cli deploy 命令:

$ faas-cli deploy -f ./hello-python.yml
Deploying: hello-python.
WARNING! Communication is not secure, please consider using HTTPS.
Letsencrypt.org offers free SSL/TLS certificates.
Handling connection for 8080
Handling connection for 8080

Deployed. 202 Accepted.
URL: http://localhost:8080/function/hello-python

如果需要进行代码更改,请使用以下命令重新构建和重新部署函数。 请注意, faas-cli remove 命令将删除函数的当前版本:

$ faas-cli build -f ./hello-python.yml
$ faas-cli push -f ./hello-python.yml
$ faas-cli remove -f ./hello-python.yml
$ faas-cli deploy -f ./hello-python.yml

现在使用 curl 测试已部署的函数:

$ curl localhost:8080/function/hello-python --data-binary 'hello'
Handling connection for 8080
{
    "body": {
        "message": "Received a hello at 22:55:05.225295"
    },
    "statusCode": 200
}

使用 faas-cli 直接调用函数进行测试:

$ echo -n "hello" | faas-cli invoke hello-python
Handling connection for 8080
{
    "body": {
        "message": "Received a hello at 22:56:23.549509"
    },
    "statusCode": 200
}

下一个示例将更加全面。 我们将演示如何使用 AWS CDK 为存储在 DynamoDB 表中的todo项目提供创建/读取/更新/删除(CRUD)REST 访问的 API 网关后面的几个 Lambda 函数。 我们还将展示如何使用在 AWS Fargate 中部署的容器运行 Locust 负载测试工具来负载测试我们的 REST API。 Fargate 容器也将由 AWS CDK 提供。

使用 AWS CDK 来配置 DynamoDB 表,Lambda 函数和 API Gateway 方法:

我们在 第十章 简要提到了 AWS CDK。 AWS CDK 是一款允许您使用真实代码(当前支持的语言为 TypeScript 和 Python)定义基础架构期望状态的产品,与使用 YAML 定义文件(如 Serverless 平台所做的方式)不同。

在全局级别使用 npm 安装 CDK CLI(根据您的操作系统,您可能需要使用 sudo 来运行以下命令):

$ npm install cdk -g

为 CDK 应用程序创建一个目录:

$ mkdir cdk-lambda-dynamodb-fargate
$ cd cdk-lambda-dynamodb-fargate

使用 cdk init 创建一个示例 Python 应用程序:

$ cdk init app --language=python
Applying project template app for python
Executing Creating virtualenv...

# Welcome to your CDK Python project!

This is a blank project for Python development with CDK.
The `cdk.json` file tells the CDK Toolkit how to execute your app.

列出创建的文件:

$ ls -la
total 40
drwxr-xr-x   9 ggheo  staff   288 Sep  2 10:10 .
drwxr-xr-x  12 ggheo  staff   384 Sep  2 10:10 ..
drwxr-xr-x   6 ggheo  staff   192 Sep  2 10:10 .env
-rw-r--r--   1 ggheo  staff  1651 Sep  2 10:10 README.md
-rw-r--r--   1 ggheo  staff   252 Sep  2 10:10 app.py
-rw-r--r--   1 ggheo  staff    32 Sep  2 10:10 cdk.json
drwxr-xr-x   4 ggheo  staff   128 Sep  2 10:10 cdk_lambda_dynamodb_fargate
-rw-r--r--   1 ggheo  staff     5 Sep  2 10:10 requirements.txt
-rw-r--r--   1 ggheo  staff  1080 Sep  2 10:10 setup.py

检查主文件 app.py

$ cat app.py
#!/usr/bin/env python3

from aws_cdk import core

from cdk_lambda_dynamodb_fargate.cdk_lambda_dynamodb_fargate_stack \
import CdkLambdaDynamodbFargateStack

app = core.App()
CdkLambdaDynamodbFargateStack(app, "cdk-lambda-dynamodb-fargate")

app.synth()

CDK 程序由一个包含一个或多个堆栈的 app 组成。堆栈对应于 CloudFormation 堆栈对象。

检查定义 CDK 堆栈的模块:

$ cat cdk_lambda_dynamodb_fargate/cdk_lambda_dynamodb_fargate_stack.py
from aws_cdk import core

class CdkLambdaDynamodbFargateStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # The code that defines your stack goes here

因为我们将有两个堆栈,一个用于 DynamoDB/Lambda/API Gateway 资源,另一个用于 Fargate 资源,请重命名:

cdk_lambda_dynamodb_fargate/cdk_lambda_dynamodb_fargate_stack.py

cdk_lambda_dynamodb_fargate/cdk_lambda_dynamodb_stack.py

将类 CdkLambdaDynamodbFargateStack 改为 CdkLambdaDynamodbStack

同时更改app.py以引用更改后的模块和类名:

from cdk_lambda_dynamodb_fargate.cdk_lambda_dynamodb_stack \
import CdkLambdaDynamodbStack

CdkLambdaDynamodbStack(app, "cdk-lambda-dynamodb")

激活virtualenv

$ source .env/bin/activate

我们将采用URL 缩短器 CDK 示例,并使用无服务器平台 AWS Python REST API 示例中的代码修改它,以构建用于创建、列出、获取、更新和删除todo项目的 REST API。使用 Amazon DynamoDB 来存储数据。

检查examples/aws-python-rest-api-with-dynamodb中的serverless.yml文件,并使用serverless命令部署它,以查看创建的 AWS 资源:

$ pwd
~/code/examples/aws-python-rest-api-with-dynamodb

$ serverless deploy
Serverless: Stack update finished...
Service Information
service: serverless-rest-api-with-dynamodb
stage: dev
region: us-east-1
stack: serverless-rest-api-with-dynamodb-dev
resources: 34
api keys:
  None
endpoints:
POST - https://tbst34m2b7.execute-api.us-east-1.amazonaws.com/dev/todos
GET - https://tbst34m2b7.execute-api.us-east-1.amazonaws.com/dev/todos
GET - https://tbst34m2b7.execute-api.us-east-1.amazonaws.com/dev/todos/{id}
PUT - https://tbst34m2b7.execute-api.us-east-1.amazonaws.com/dev/todos/{id}
DELETE - https://tbst34m2b7.execute-api.us-east-1.amazonaws.com/dev/todos/{id}
functions:
  create: serverless-rest-api-with-dynamodb-dev-create
  list: serverless-rest-api-with-dynamodb-dev-list
  get: serverless-rest-api-with-dynamodb-dev-get
  update: serverless-rest-api-with-dynamodb-dev-update
  delete: serverless-rest-api-with-dynamodb-dev-delete
layers:
  None
Serverless: Run the "serverless" command to setup monitoring, troubleshooting and
            testing.

上一条命令创建了五个 Lambda 函数、一个 API Gateway 和一个 DynamoDB 表。

在我们正在构建的堆栈中的 CDK 目录中,添加一个 DynamoDB 表:

$ pwd
~/code/devops/serverless/cdk-lambda-dynamodb-fargate

$ cat cdk_lambda_dynamodb_fargate/cdk_lambda_dynamodb_stack.py
from aws_cdk import core
from aws_cdk import aws_dynamodb

class CdkLambdaDynamodbStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # define the table stores Todo items
        table = aws_dynamodb.Table(self, "Table",
                                    partition_key=aws_dynamodb.Attribute(
                                      name="id",
                                      type=aws_dynamodb.AttributeType.STRING),
                                    read_capacity=10,
                                    write_capacity=5)

安装所需的 Python 模块:

$ cat requirements.txt
-e .
aws-cdk.core
aws-cdk.aws-dynamodb

$ pip install -r requirements.txt

通过运行cdk synth来检查将要创建的 CloudFormation 堆栈:

$ export AWS_PROFILE=gheorghiu-net
$ cdk synth

app.py中将包含区域值的名为variable的变量传递给构造函数CdkLambdaDynamodbStack

app_env = {"region": "us-east-2"}
CdkLambdaDynamodbStack(app, "cdk-lambda-dynamodb", env=app_env)

再次运行cdk synth

$ cdk synth
Resources:
  TableCD117FA1:
    Type: AWS::DynamoDB::Table
    Properties:
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      ProvisionedThroughput:
        ReadCapacityUnits: 10
        WriteCapacityUnits: 5
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
    Metadata:
      aws:cdk:path: cdk-lambda-dynamodb-fargate/Table/Resource
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Modules: aws-cdk=1.6.1,
      @aws-cdk/aws-applicationautoscaling=1.6.1,
      @aws-cdk/aws-autoscaling-common=1.6.1,
      @aws-cdk/aws-cloudwatch=1.6.1,
      @aws-cdk/aws-dynamodb=1.6.1,
      @aws-cdk/aws-iam=1.6.1,
      @aws-cdk/core=1.6.1,
      @aws-cdk/cx-api=1.6.1,@aws-cdk/region-info=1.6.1,
      jsii-runtime=Python/3.7.4

通过运行cdk deploy部署 CDK 堆栈:

$ cdk deploy
cdk-lambda-dynamodb-fargate: deploying...
cdk-lambda-dynamodb-fargate: creating CloudFormation changeset...
 0/3 | 11:12:25 AM | CREATE_IN_PROGRESS   | AWS::DynamoDB::Table |
 Table (TableCD117FA1)
 0/3 | 11:12:25 AM | CREATE_IN_PROGRESS   | AWS::CDK::Metadata   |
 CDKMetadata
 0/3 | 11:12:25 AM | CREATE_IN_PROGRESS   | AWS::DynamoDB::Table |
 Table (TableCD117FA1) Resource creation Initiated
 0/3 | 11:12:27 AM | CREATE_IN_PROGRESS   | AWS::CDK::Metadata   |
 CDKMetadata Resource creation Initiated
 1/3 | 11:12:27 AM | CREATE_COMPLETE      | AWS::CDK::Metadata   |
 CDKMetadata
 2/3 | 11:12:56 AM | CREATE_COMPLETE      | AWS::DynamoDB::Table |
 Table (TableCD117FA1)
 3/3 | 11:12:57 AM | CREATE_COMPLETE      | AWS::CloudFormation::Stack |
 cdk-lambda-dynamodb-fargate

Stack ARN:
arn:aws:cloudformation:us-east-2:200562098309:stack/
cdk-lambda-dynamodb/3236a8b0-cdad-11e9-934b-0a7dfa8cb208

下一步是向堆栈添加 Lambda 函数和 API Gateway 资源。

在 CDK 代码目录中,创建一个lambda目录,并从无服务器平台 AWS Python REST API 示例复制 Python 模块:

$ pwd
~/code/devops/serverless/cdk-lambda-dynamodb-fargate

$ mkdir lambda
$ cp ~/code/examples/aws-python-rest-api-with-dynamodb/todos/* lambda
$ ls -la lambda
total 48
drwxr-xr-x   9 ggheo  staff   288 Sep  2 10:41 .
drwxr-xr-x  10 ggheo  staff   320 Sep  2 10:19 ..
-rw-r--r--   1 ggheo  staff     0 Sep  2 10:41 __init__.py
-rw-r--r--   1 ggheo  staff   822 Sep  2 10:41 create.py
-rw-r--r--   1 ggheo  staff   288 Sep  2 10:41 decimalencoder.py
-rw-r--r--   1 ggheo  staff   386 Sep  2 10:41 delete.py
-rw-r--r--   1 ggheo  staff   535 Sep  2 10:41 get.py
-rw-r--r--   1 ggheo  staff   434 Sep  2 10:41 list.py
-rw-r--r--   1 ggheo  staff  1240 Sep  2 10:41 update.py

将所需模块添加到requirements.txt中,并使用pip安装它们:

$ cat requirements.txt
-e .
aws-cdk.core
aws-cdk.aws-dynamodb
aws-cdk.aws-lambda
aws-cdk.aws-apigateway

$ pip install -r requirements.txt

在堆栈模块中创建 Lambda 和 API Gateway 构造:

$ cat cdk_lambda_dynamodb_fargate/cdk_lambda_dynamodb_stack.py
from aws_cdk import core
from aws_cdk.core import App, Construct, Duration
from aws_cdk import aws_dynamodb, aws_lambda, aws_apigateway

class CdkLambdaDynamodbStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # define the table stores Todo todos
        table = aws_dynamodb.Table(self, "Table",
            partition_key=aws_dynamodb.Attribute(
                name="id",
                type=aws_dynamodb.AttributeType.STRING),
            read_capacity=10,
            write_capacity=5)

        # define the Lambda functions
        list_handler = aws_lambda.Function(self, "TodoListFunction",
            code=aws_lambda.Code.asset("./lambda"),
            handler="list.list",
            timeout=Duration.minutes(5),
            runtime=aws_lambda.Runtime.PYTHON_3_7)

        create_handler = aws_lambda.Function(self, "TodoCreateFunction",
            code=aws_lambda.Code.asset("./lambda"),
            handler="create.create",
            timeout=Duration.minutes(5),
            runtime=aws_lambda.Runtime.PYTHON_3_7)

        get_handler = aws_lambda.Function(self, "TodoGetFunction",
            code=aws_lambda.Code.asset("./lambda"),
            handler="get.get",
            timeout=Duration.minutes(5),
            runtime=aws_lambda.Runtime.PYTHON_3_7)

        update_handler = aws_lambda.Function(self, "TodoUpdateFunction",
            code=aws_lambda.Code.asset("./lambda"),
            handler="update.update",
            timeout=Duration.minutes(5),
            runtime=aws_lambda.Runtime.PYTHON_3_7)

        delete_handler = aws_lambda.Function(self, "TodoDeleteFunction",
            code=aws_lambda.Code.asset("./lambda"),
            handler="delete.delete",
            timeout=Duration.minutes(5),
            runtime=aws_lambda.Runtime.PYTHON_3_7)

        # pass the table name to each handler through an environment variable
        # and grant the handler read/write permissions on the table.
        handler_list = [
            list_handler,
            create_handler,
            get_handler,
            update_handler,
            delete_handler
        ]
        for handler in handler_list:
            handler.add_environment('DYNAMODB_TABLE', table.table_name)
            table.grant_read_write_data(handler)

        # define the API endpoint
        api = aws_apigateway.LambdaRestApi(self, "TodoApi",
            handler=list_handler,
            proxy=False)

        # define LambdaIntegrations
        list_lambda_integration = \
            aws_apigateway.LambdaIntegration(list_handler)
        create_lambda_integration = \
            aws_apigateway.LambdaIntegration(create_handler)
        get_lambda_integration = \
            aws_apigateway.LambdaIntegration(get_handler)
        update_lambda_integration = \
            aws_apigateway.LambdaIntegration(update_handler)
        delete_lambda_integration = \
            aws_apigateway.LambdaIntegration(delete_handler)

        # define REST API model and associate methods with LambdaIntegrations
        api.root.add_method('ANY')

        todos = api.root.add_resource('todos')
        todos.add_method('GET', list_lambda_integration)
        todos.add_method('POST', create_lambda_integration)

        todo = todos.add_resource('{id}')
        todo.add_method('GET', get_lambda_integration)
        todo.add_method('PUT', update_lambda_integration)
        todo.add_method('DELETE', delete_lambda_integration)

值得注意的是我们刚刚审查的代码的几个特点:

  • 我们能够在每个handler对象上使用add_environment方法,将在 Python 代码中用于 Lambda 函数的环境变量DYNAMODB_TABLE传递,并将其设置为table.table_name。在构建时不知道 DynamoDB 表的名称,因此 CDK 将其替换为令牌,并在部署堆栈时将令牌设置为表的正确名称(有关更多详细信息,请参阅Tokens文档)。

  • 当我们遍历所有 Lambda 处理程序列表时,我们充分利用了一个简单的编程语言结构,即for循环。尽管这看起来很自然,但仍值得指出,因为循环和变量传递是基于 YAML 的基础设施即代码工具(如 Terraform)中尴尬实现的功能,如果有的话。

  • 我们定义了与 API Gateway 的各个端点相关联的 HTTP 方法(GET、POST、PUT、DELETE),并将正确的 Lambda 函数与每个端点关联。

使用cdk deploy部署堆栈:

$ cdk deploy
cdk-lambda-dynamodb-fargate failed: Error:
This stack uses assets, so the toolkit stack must be deployed
to the environment
(Run "cdk bootstrap aws://unknown-account/us-east-2")

通过运行cdk bootstrap来修复:

$ cdk bootstrap
Bootstrapping environment aws://ACCOUNTID/us-east-2...
CDKToolkit: creating CloudFormation changeset...
Environment aws://ACCOUNTID/us-east-2 bootstrapped.

再次部署 CDK 堆栈:

$ cdk deploy
OUTPUT OMITTED

Outputs:
cdk-lambda-dynamodb.TodoApiEndpointC1E16B6C =
https://k6ygy4xw24.execute-api.us-east-2.amazonaws.com/prod/

Stack ARN:
arn:aws:cloudformation:us-east-2:ACCOUNTID:stack/cdk-lambda-dynamodb/
15a66bb0-cdba-11e9-aef9-0ab95d3a5528

下一步是使用curl测试 REST API。

首先创建一个新的todo项目:

$ curl -X \
POST https://k6ygy4xw24.execute-api.us-east-2.amazonaws.com/prod/todos \
--data '{ "text": "Learn CDK" }'
{"id": "19d55d5a-cdb4-11e9-9a8f-9ed29c44196e", "text": "Learn CDK",
"checked": false,
"createdAt": "1567450902.262834",
"updatedAt": "1567450902.262834"}%

创建第二个todo项目:

$ curl -X \
POST https://k6ygy4xw24.execute-api.us-east-2.amazonaws.com/prod/todos \
--data '{ "text": "Learn CDK with Python" }'
{"id": "58a992c6-cdb4-11e9-9a8f-9ed29c44196e", "text": "Learn CDK with Python",
"checked": false,
"createdAt": "1567451007.680936",
"updatedAt": "1567451007.680936"}%

通过指定其 ID 来尝试获取刚创建项目的详细信息:

$ curl \
https://k6ygy4xw24.execute-api.us-east-2.amazonaws.com/
prod/todos/58a992c6-cdb4-11e9-9a8f-9ed29c44196e
{"message": "Internal server error"}%

通过检查 Lambda 函数TodoGetFunction的 CloudWatch 日志来进行调查:

[ERROR] Runtime.ImportModuleError:
Unable to import module 'get': No module named 'todos'

要修复,请更改lambda/get.py中的行:

from todos import decimalencoder

到:

import decimalencoder

使用cdk deploy重新部署堆栈。

试着再次使用curl获取todo项目的详细信息:

$ curl \
https://k6ygy4xw24.execute-api.us-east-2.amazonaws.com/
prod/todos/58a992c6-cdb4-11e9-9a8f-9ed29c44196e
{"checked": false, "createdAt": "1567451007.680936",
"text": "Learn CDK with Python",
"id": "58a992c6-cdb4-11e9-9a8f-9ed29c44196e",
"updatedAt": "1567451007.680936"}

对需要 decimalencoder 模块的 lambda 目录中的所有模块进行 import decimalencoder 更改,并使用 cdk deploy 重新部署。

列出所有 todos 并使用 jq 工具格式化输出:

$ curl \
https://k6ygy4xw24.execute-api.us-east-2.amazonaws.com/prod/todos | jq
[
  {
    "checked": false,
    "createdAt": "1567450902.262834",
    "text": "Learn CDK",
    "id": "19d55d5a-cdb4-11e9-9a8f-9ed29c44196e",
    "updatedAt": "1567450902.262834"
  },
  {
    "checked": false,
    "createdAt": "1567451007.680936",
    "text": "Learn CDK with Python",
    "id": "58a992c6-cdb4-11e9-9a8f-9ed29c44196e",
    "updatedAt": "1567451007.680936"
  }
]

删除一个 todo 并验证列表不再包含它:

$ curl -X DELETE \
https://k6ygy4xw24.execute-api.us-east-2.amazonaws.com/prod/todos/
19d55d5a-cdb4-11e9-9a8f-9ed29c44196e

$ curl https://k6ygy4xw24.execute-api.us-east-2.amazonaws.com/prod/todos | jq
[
  {
    "checked": false,
    "createdAt": "1567451007.680936",
    "text": "Learn CDK with Python",
    "id": "58a992c6-cdb4-11e9-9a8f-9ed29c44196e",
    "updatedAt": "1567451007.680936"
  }
]

现在测试使用 curl 更新现有的 todo 项目:

$ curl -X \
PUT https://k6ygy4xw24.execute-api.us-east-2.amazonaws.com/prod/todos/
58a992c6-cdb4-11e9-9a8f-9ed29c44196e \
--data '{ "text": "Learn CDK with Python by reading the PyForDevOps book" }'
{"message": "Internal server error"}%

检查与此端点相关联的 Lambda 函数的 CloudWatch 日志显示:

[ERROR] Exception: Couldn't update the todo item.
Traceback (most recent call last):
  File "/var/task/update.py", line 15, in update
    raise Exception("Couldn't update the todo item.")

更改 lambda/update.py 中的验证测试为:

    data = json.loads(event['body'])
    if 'text' not in data:
        logging.error("Validation Failed")
        raise Exception("Couldn't update the todo item.")

同样将 checked 的值更改为 True,因为我们已经看到了一个我们正在尝试更新的帖子:

ExpressionAttributeValues={
         ':text': data['text'],
         ':checked': True,
         ':updatedAt': timestamp,
       },

使用 cdk deploy 重新部署堆栈。

使用 curl 测试更新 todo 项目:

$ curl -X \
PUT https://k6ygy4xw24.execute-api.us-east-2.amazonaws.com/prod/todos/
58a992c6-cdb4-11e9-9a8f-9ed29c44196e \
--data '{ "text": "Learn CDK with Python by reading the PyForDevOps book"}'
{"checked": true, "createdAt": "1567451007.680936",
"text": "Learn CDK with Python by reading the PyForDevOps book",
"id": "58a992c6-cdb4-11e9-9a8f-9ed29c44196e", "updatedAt": 1567453288764}%

列出 todo 项目以验证更新:

$ curl https://k6ygy4xw24.execute-api.us-east-2.amazonaws.com/prod/todos | jq
[
  {
    "checked": true,
    "createdAt": "1567451007.680936",
    "text": "Learn CDK with Python by reading the PyForDevOps book",
    "id": "58a992c6-cdb4-11e9-9a8f-9ed29c44196e",
    "updatedAt": 1567453288764
  }
]

下一步是为我们刚刚部署的 REST API 运行负载测试的 AWS Fargate 容器。每个容器将运行一个 Docker 镜像,该镜像使用 Taurus 测试自动化框架 运行 Molotov 负载测试工具。我们在 第五章 中介绍了 Molotov 作为一个简单而非常有用的基于 Python 的负载测试工具。

首先在名为 loadtest 的目录中创建一个运行 Taurus 和 Molotov 的 Dockerfile:

$ mkdir loadtest; cd loadtest
$ cat Dockerfile
FROM blazemeter/taurus

COPY scripts /scripts
COPY taurus.yaml /bzt-configs/

WORKDIR /bzt-configs
ENTRYPOINT ["sh", "-c", "bzt -l /tmp/artifacts/bzt.log /bzt-configs/taurus.yaml"]

Dockerfile 使用 taurus.yaml 配置文件运行 Taurus 的 bzt 命令行:

$ cat taurus.yaml
execution:
- executor: molotov
  concurrency: 10  # number of Molotov workers
  iterations: 5  # iteration limit for the test
  ramp-up: 30s
  hold-for: 5m
  scenario:
    script: /scripts/loadtest.py  # has to be valid Molotov script

在这个配置文件中,concurrency 的值设置为 10,这意味着我们正在模拟 10 个并发用户或虚拟用户(VUs)。executor 被定义为一个基于名为 loadtest.py 的脚本的 molotov 测试。以下是这个作为 Python 模块的脚本:

$ cat scripts/loadtest.py
import os
import json
import random
import molotov
from molotov import global_setup, scenario

@global_setup()
def init_test(args):
    BASE_URL=os.getenv('BASE_URL', '')
    molotov.set_var('base_url', BASE_URL)

@scenario(weight=50)
async def _test_list_todos(session):
    base_url= molotov.get_var('base_url')
    async with session.get(base_url + '/todos') as resp:
        assert resp.status == 200, resp.status

@scenario(weight=30)
async def _test_create_todo(session):
    base_url= molotov.get_var('base_url')
    todo_data = json.dumps({'text':
      'Created new todo during Taurus/molotov load test'})
    async with session.post(base_url + '/todos',
      data=todo_data) as resp:
        assert resp.status == 200

@scenario(weight=10)
async def _test_update_todo(session):
    base_url= molotov.get_var('base_url')
    # list all todos
    async with session.get(base_url + '/todos') as resp:
        res = await resp.json()
        assert resp.status == 200, resp.status
        # choose random todo and update it with PUT request
        todo_id = random.choice(res)['id']
        todo_data = json.dumps({'text':
          'Updated existing todo during Taurus/molotov load test'})
        async with session.put(base_url + '/todos/' + todo_id,
          data=todo_data) as resp:
            assert resp.status == 200

@scenario(weight=10)
async def _test_delete_todo(session):
    base_url= molotov.get_var('base_url')
    # list all todos
    async with session.get(base_url + '/todos') as resp:
        res = await resp.json()
        assert resp.status == 200, resp.status
        # choose random todo and delete it with DELETE request
        todo_id = random.choice(res)['id']
        async with session.delete(base_url + '/todos/' + todo_id) as resp:
            assert resp.status == 200

脚本有四个装饰为 scenarios 的函数,由 Molotov 运行。它们会测试 CRUD REST API 的各种端点。权重指示每个场景在整体测试持续时间中被调用的大致百分比。例如,在这个例子中,函数 _test_list_todos 将大约 50% 的时间被调用,_test_create_todo 将大约 30% 的时间被调用,而 _test_update_todo_test_delete_todo 每次将各自运行约 10% 的时间。

构建本地 Docker 镜像:

$ docker build -t cdk-loadtest .

创建本地的 artifacts 目录:

$ mkdir artifacts

运行本地 Docker 镜像,并将本地 artifacts 目录挂载为 Docker 容器内的 /tmp/artifacts

$ docker run --rm -d \
--env BASE_URL=https://k6ygy4xw24.execute-api.us-east-2.amazonaws.com/prod \
-v `pwd`/artifacts:/tmp/artifacts cdk-loadtest

通过检查 artifacts/molotov.out 文件来调试 Molotov 脚本。

Taurus 的结果可以通过 docker logs CONTAINER_ID 或检查文件 artifacts/bzt.log 进行检查。

通过检查 Docker 日志获得的结果:

$ docker logs -f a228f8f9a2bc
19:26:26 INFO: Taurus CLI Tool v1.13.8
19:26:26 INFO: Starting with configs: ['/bzt-configs/taurus.yaml']
19:26:26 INFO: Configuring...
19:26:26 INFO: Artifacts dir: /tmp/artifacts
19:26:26 INFO: Preparing...
19:26:27 INFO: Starting...
19:26:27 INFO: Waiting for results...
19:26:32 INFO: Changed data analysis delay to 3s
19:26:32 INFO: Current: 0 vu  1 succ  0 fail  0.546 avg rt  /
Cumulative: 0.546 avg rt, 0% failures
19:26:39 INFO: Current: 1 vu  1 succ  0 fail  1.357 avg rt  /
Cumulative: 0.904 avg rt, 0% failures
ETC
19:41:00 WARNING: Please wait for graceful shutdown...
19:41:00 INFO: Shutting down...
19:41:00 INFO: Post-processing...
19:41:03 INFO: Test duration: 0:14:33
19:41:03 INFO: Samples count: 1857, 0.00% failures
19:41:03 INFO: Average times: total 6.465, latency 0.000, connect 0.000
19:41:03 INFO: Percentiles:
+---------------+---------------+
| Percentile, % | Resp. Time, s |
+---------------+---------------+
|           0.0 |          0.13 |
|          50.0 |          1.66 |
|          90.0 |        14.384 |
|          95.0 |         26.88 |
|          99.0 |        27.168 |
|          99.9 |        27.584 |
|         100.0 |        27.792 |
+---------------+---------------+

为 Lambda 持续时间创建 CloudWatch 仪表板(Figure 13-1)以及 DynamoDB 配置和消耗的读写能力单位(Figure 13-2):

pydo 1301

Figure 13-1. Lambda 持续时间

pydo 1302

Figure 13-2. DynamoDB 配置和消耗的读写能力单位

DynamoDB 的指标显示我们的 DynamoDB 读取容量单位配置不足。这导致了延迟,特别是在 List 函数中(Lambda 执行时长图中显示为红线,达到 14.7 秒),该函数从 DynamoDB 表中检索所有 todo 项目,因此读取操作较重。我们在创建 DynamoDB 表时将配置的读取容量单位设置为 10,CloudWatch 图表显示其上升至 25。

让我们将 DynamoDB 表类型从 PROVISIONED 更改为 PAY_PER_REQUEST。在 cdk_lambda_dynamodb_fargate/cdk_lambda_dynamodb_stack.py 中进行更改:

        table = aws_dynamodb.Table(self, "Table",
            partition_key=aws_dynamodb.Attribute(
                name="id",
                type=aws_dynamodb.AttributeType.STRING),
            billing_mode = aws_dynamodb.BillingMode.PAY_PER_REQUEST)

运行 cdk deploy,然后运行本地 Docker 负载测试容器。

这次结果好多了:

+---------------+---------------+
| Percentile, % | Resp. Time, s |
+---------------+---------------+
|           0.0 |         0.136 |
|          50.0 |         0.505 |
|          90.0 |         1.296 |
|          95.0 |         1.444 |
|          99.0 |         1.806 |
|          99.9 |         2.226 |
|         100.0 |          2.86 |
+---------------+---------------+

Lambda 执行时长的图表(图 13-3)和 DynamoDB 消耗的读取和写入容量单位的图表(图 13-4)看起来也好多了。

pydo 1303

图 13-3. Lambda 执行时长

pydo 1304

图 13-4. DynamoDB 消耗的读取和写入容量单位

注意,DynamoDB 消耗的读取容量单位是由 DynamoDB 自动按需分配的,并且正在扩展以满足 Lambda 函数的增加读取请求。对读取请求贡献最大的函数是 List 函数,在 Molotov loadtest.py 脚本中通过 session.get(base_url + /todos) 在列出、更新和删除场景中调用。

接下来,我们将创建一个 Fargate CDK 栈,该栈将根据先前创建的 Docker 镜像运行容器:

$ cat cdk_lambda_dynamodb_fargate/cdk_fargate_stack.py
from aws_cdk import core
from aws_cdk import aws_ecs, aws_ec2

class FargateStack(core.Stack):
    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        vpc = aws_ec2.Vpc(
            self, "MyVpc",
            cidr= "10.0.0.0/16",
            max_azs=3
        )
        # define an ECS cluster hosted within the requested VPC
        cluster = aws_ecs.Cluster(self, 'cluster', vpc=vpc)

        # define our task definition with a single container
        # the image is built & published from a local asset directory
        task_definition = aws_ecs.FargateTaskDefinition(self, 'LoadTestTask')
        task_definition.add_container('TaurusLoadTest',
            image=aws_ecs.ContainerImage.from_asset("loadtest"),
            environment={'BASE_URL':
            "https://k6ygy4xw24.execute-api.us-east-2.amazonaws.com/prod/"})

        # define our fargate service. TPS determines how many instances we
        # want from our task (each task produces a single TPS)
        aws_ecs.FargateService(self, 'service',
            cluster=cluster,
            task_definition=task_definition,
            desired_count=1)

FargateStack 类的代码中需要注意的几点:

  • 使用 aws_ec2.Vpc CDK 构造函数创建了一个新的 VPC。

  • 在新 VPC 中创建了一个 ECS 集群。

  • 基于 loadtest 目录中的 Dockerfile 创建了一个 Fargate 任务定义;CDK 聪明地根据此 Dockerfile 构建 Docker 镜像,然后推送到 ECR Docker 注册表。

  • 创建了一个 ECS 服务来运行基于推送到 ECR 的镜像的 Fargate 容器;desired_count 参数指定我们要运行多少个容器。

app.py 中调用 FargateStack 构造函数:

$ cat app.py
#!/usr/bin/env python3

from aws_cdk import core

from cdk_lambda_dynamodb_fargate.cdk_lambda_dynamodb_stack \
import CdkLambdaDynamodbStack
from cdk_lambda_dynamodb_fargate.cdk_fargate_stack import FargateStack

app = core.App()
app_env = {
    "region": "us-east-2",
}

CdkLambdaDynamodbStack(app, "cdk-lambda-dynamodb", env=app_env)
FargateStack(app, "cdk-fargate", env=app_env)

app.synth()

部署 cdk-fargate 栈:

$ cdk deploy cdk-fargate

转到 AWS 控制台,检查带有运行 Fargate 容器的 ECS 集群(图 13-5)。

pydo 1305

图 13-5. 带有运行中 Fargate 容器的 ECS 集群

检查 CloudWatch 仪表板,查看 Lambda 执行时长(图 13-6)和 DynamoDB 消耗的读取和写入容量单位(图 13-7),注意延迟看起来不错。

pydo 1306

图 13-6. Lambda 执行时长

pydo 1307

图 13-7. DynamoDB 消耗的读取和写入容量单位

cdk_lambda_dynamodb_fargate/cdk_fargate_stack.py 中的 Fargate 容器数量增加到 5:

       aws_ecs.FargateService(self, 'service',
           cluster=cluster,
           task_definition=task_definition,
           desired_count=5)

重新部署 cdk-fargate 栈:

$ cdk deploy cdk-fargate

检查 CloudWatch 仪表板以查看 Lambda 持续时间(图 13-8)和 DynamoDB 消耗的读写容量单位(图 13-9)。

pydo 1308

图 13-8. Lambda 持续时间

pydo 1309

图 13-9. DynamoDB 消耗的读写容量单位

由于我们现在模拟了 5 × 10 = 50 个并发用户,DynamoDB 读取容量单位和 Lambda 持续时间指标均按预期增加。

为了模拟更多用户,我们可以同时增加 taurus.yaml 配置文件中的concurrency值,并增加 Fargate 容器的desired_count。在这两个值之间,我们可以轻松增加对 REST API 端点的负载。

删除 CDK 堆栈:

$ cdk destroy cdk-fargate
$ cdk destroy cdk-lambda-dynamodb

值得注意的是,我们部署的无服务器架构(API Gateway + 五个 Lambda 函数 + DynamoDB 表)非常适合我们的 CRUD REST API 应用程序。我们还遵循了最佳实践,并通过 AWS CDK 使用 Python 代码定义了所有基础设施。

练习

  • 在 Google 的 CaaS 平台上运行一个简单的 HTTP 端点:Cloud Run

  • 在我们提到的基于 Kubernetes 的其他 FaaS 平台上运行一个简单的 HTTP 端点:Kubeless, Fn ProjectFission

  • 在生产级 Kubernetes 集群(如 Amazon EKS、Google GKE 或 Azure AKS)中安装和配置Apache OpenWhisk

  • 将 AWS 的 REST API 示例移植到 GCP 和 Azure。GCP 提供 Cloud Endpoints 来管理多个 API。类似地,Azure 提供 API Management