vlambda博客
学习文章列表

读书笔记《hands-on-docker-for-microservices-with-python》库伯内斯的地方发展

Local Development with Kubernetes

在本章中,您将学习如何定义集群、部署所有交互的微服务,以及如何在本地工作以进行开发。我们将在上一章介绍的概念的基础上进行,我们将描述如何在 Kubernetes 中配置整个系统在实践中,部署多个微服务,以及如何使其作为一个整个在您自己的本地计算机上。

在这里,我们将介绍另外两个微服务:前端和用户后端。它们在第 1 章中进行了讨论,采取行动 - 设计、计划,和执行,在打破整体的战略计划部分。我们将在本章中看到如何配置它们以在 Kubernetes 中工作。这是对 第 2 章中介绍的 Thoughts Backend 的补充,创建使用 Python 的 REST 服务; 第 3 章构建,使用 Docker 运行和测试您的服务,以及第 4 章 , 创建管道和工作流。我们将讨论如何正确配置这三个,并添加一些其他选项以确保它们在生产环境中部署后的顺利运行。

本章将涵盖以下主题:

  • Implementing multiple services
  • Configuring the services
  • Deploying the full system locally

在本章结束时,您将拥有一个工作的本地 Kubernetes 系统,其中部署了三个微服务并作为一个整体工作。您将了解不同元素的工作原理以及如何配置和调整它们。

Technical requirements

Implementing multiple services

在 GitHub 存储库中,您可以找到我们将在本章中使用的三个微服务。它们基于 第 1 章中介绍的单体架构,采取行动– 设计、计划和执行,并分为三个要素:

  • Thoughts Backend: As described in the previous chapter, this handles the storage of thoughts and the search for them.
  • Users Backend: This stores the users and allows them to log in. Based on the description of the authentication method, this creates a token that can be used to authenticate against other systems.
  • Frontend: This comes from the monolith, but instead of accessing a database directly, it makes requests to the User and Thoughts Backends to replicate the functionality.
请注意,静态文件仍由前端提供服务,即使我们描述了集群的最后阶段独立地为它们提供服务。这样做是为了简单并避免额外的服务。

上述服务以与 Chapter 3 中的 Thoughts Backend 类似的方式进行 Docker 化,使用 Docker 构建、运行和测试您的服务。让我们看看其他微服务的一些细节。

Describing the Users Backend microservice

用户后端的代码可以在 https://github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter06/users_backend。该结构与 Thoughts Backend 非常相似,后者是一个与 PostgreSQL 数据库通信的 Flask-RESTPlus 应用程序。

它有两个端点,如其 Swagger 界面所示:

读书笔记《hands-on-docker-for-microservices-with-python》库伯内斯的地方发展

端点如下:

Endpoint Input Returns
POST /api/login

{用户名:<用户名>,密码:<密码>}

{Authorized: <token header>}
POST /admin/users {username: <username>, password: <password>} <new_user>

admin 端点允许您创建新用户,登录 API 返回可用于 Thoughts Backend 的有效标头。

用户使用以下模式存储在数据库中:

Field Format Comments
id Integer Primary key
username String (50) Username
password String(50) Password stored in plain text, which is a bad idea, but simplifies the example
creation Datetime The time of the creation of the user

在 SQLAlchemy 模型定义中,此模式使用以下代码进行描述:

class UserModel(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50))
# DO NOT EVER STORE PLAIN PASSWORDS IN DATABASES
# THIS IS AN EXAMPLE!!!!!
password = db.Column(db.String(50))
creation = db.Column(db.DateTime, server_default=func.now())
Note that the creation date gets stored automatically. Also, note that we store the password in plain text. This is a terrible, terrible idea in a production service. You can check out an article called How to store a password in the database? ( https://www.geeksforgeeks.org/store-password-database/) to get general ideas for encrypting passwords with a salt seed. You can use a package such as pyscrypt ( https://github.com/ricmoo/pyscrypt) to implement this kind of structure in Python.

用户 brucestephen 被添加到 db 示例中,作为获取示例数据的一种方式。

Describing the Frontend microservice

前端代码可在 GitHub 存储库中找到。它基于 Django 单体(https://github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter01/Monolith)在第 1 章采取行动——设计、计划和执行

与单体的主要区别在于不访问数据库。因此,Django ORM 没有用处。它们被对其他后端的 HTTP 请求所取代。为了发出请求,我们使用了奇妙的 requests 库。

例如,search.py​​ 文件被转换为以下代码,它将搜索委托给 Thoughts Backend 微服务。请注意客户的请求如何转换为对 GET /api/thoughts 端点的内部 API 调用。结果以 JSON 格式解码并呈现在模板中:

import requests

def search(request):
username = get_username_from_session(request)
search_param = request.GET.get('search')

url = settings.THOUGHTS_BACKEND + '/api/thoughts/'
params = {
'search': search_param,
}
result = requests.get(url, params=params)
results = result.json()

context = {
'thoughts': results,
'username': username,
}
return render(request, 'search.html', context)

可以在 repo 的 Chapter01 子目录中比较整体等效代码 (https://github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master /Chapter01/Monolith/mythoughts/thoughts/search.py​​)。

Note how we make a get request through the requests library to the defined search endpoint, which results in the json format being returned and rendered.

THOUGTHS_BACKEND 根 URL 来自设置,采用通常的 Django 方式。

此示例很简单,因为不涉及身份验证。参数从用户界面捕获,然后路由到后端。请求在后端和获得结果后都被正确格式化,然后呈现。这是两个微服务协同工作的核心。

一个更有趣的案例是 list_thoughthttps://github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter06/frontend/ mythoughts/thoughts/thoughts.py#L18) 视图。以下代码列出了登录用户的想法:

def list_thoughts(request):
username = get_username_from_session(request)
if not username:
return redirect('login')

url = settings.THOUGHTS_BACKEND + '/api/me/thoughts/'
headers = {
'Authorization': request.COOKIES.get('session'),
}
result = requests.get(url, headers=headers)
if result.status_code != http.client.OK:
return redirect('login')

context = {
'thoughts': result.json(),
'username': username,
}
return render(request, 'list_thoughts.html', context)

在这里,在做任何事情之前,我们需要检查用户是否登录。这是在 get_username_from_session 调用中完成的,它返回 usernameNone ,如果他们没有登录。如果他们没有登录,返回被重定向到登录屏幕。

由于此端点需要身份验证,我们需要在 Authorization 标头中将来自用户的会话添加到我们的请求中。用户的会话可以从request.COOKIES 字典中获取。

作为保障,我们需要检查后端返回的状态码是否正确。对于此调用,任何不是 200(HTTP 调用正确)的结果状态代码都会产生到登录页面的重定向。

For simplicity and clarity, our example services are not handling different error cases. In a production system, there should be a differentiation between errors where the issue is that either the user is not logged in or there's another kind of user error (a 400 error), or the backend service is not available (a 500 status code).

Error handling, when done properly, is difficult, but worth doing well, especially if the error helps users to understand what happened.

get_username_from_session函数封装了对validate_token_header的调用,和上一章介绍的一样:

def get_username_from_session(request):
cookie_session = request.COOKIES.get('session')
username = validate_token_header(cookie_session,
settings.TOKENS_PUBLIC_KEY)
if not username:
return None

return username

settings 文件包含解码令牌所需的公钥。

在本章中,为简单起见,我们将密钥直接复制到 设置文件。这不是生产环境的方式。任何秘密都应该通过 Kubernetes 环境配置获得。我们将在接下来的章节中看到如何做到这一点。

环境文件需要指定用户后端和想法后端的基本 URL 的位置,以便能够连接到它们。

Connecting the services

仅使用 docker-compose 可以测试协同工作的服务。检查用户后端和思想后端中的 docker-compose.yaml 文件是否对外公开不同的端口。

Thoughts 后端公开端口 8000,用户后端公开端口 8001。这允许前端连接到它们(并公开端口 8002)。此图显示了该系统的工作原理:

读书笔记《hands-on-docker-for-microservices-with-python》库伯内斯的地方发展

您可以看到这三个服务是如何隔离的,因为 docker-compose 将创建自己的网络供它们连接。两个后端都有自己的容器,充当数据库。

前端服务需要连接到其他服务。服务的 URL 应添加到 environment.env 文件中,并应使用计算机的 IP 指示服务。

内部 IP,例如 localhost 或 127.0.0.1 不起作用,因为它被解释 在容器内。 可以通过运行获取本地IP ifconfig

例如,如果您的本地 IP 是 10.0.10.3,则 environment.env 文件应包含以下内容:

THOUGHTS_BACKEND_URL=http://10.0.10.3:8000
USER_BACKEND_URL=http://10.0.10.3:8001

如果您在浏览器中访问前端服务,它应该连接到其他服务。

A possibility could be to generate a bigger docker-compose file that includes everything. This could make sense if all the microservices are in the same Git repo, a technique known as monorepo (https://gomonorepo.org/). Possible problems include keeping both the internal docker-compose to work with a single system and the general one in sync so that the automated tests should detect any problems.

这个结构有点麻烦,我们可以把它改造成一个合适的Kubernetes集群,针对本地开发。

Configuring the services

要在 Kubernetes 中配置应用程序,我们需要为每个应用程序定义以下 Kubernetes 对象:

  • Deployment: The deployment will control the creation of pods, so they will always be available. It will also create them based on the image and will add configuration, where needed. The pod runs the app.
  • Service: The service will make the RESTful requests available inside the cluster, with a short name. This routes the requests to any available pod.
  • Ingress: This makes the service available outside of the cluster, so we can access the app from outside the cluster.

在本节中,我们将作为示例详细查看 Thoughts Backend 配置。稍后,我们将看到不同部分是如何连接的。我们创建了一个 Kubernetes 子目录(https://github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter06/thoughts_backend/kubernetes) 来存储.yaml< /kbd> 包含每个定义的文件。

我们将使用 example 命名空间,因此请确保它已创建:

$ kubectl create namespace example

让我们从第一个 Kubernetes 对象开始。

Configuring the deployment

对于 Thoughts Backend 部署,我们将部署一个带有两个容器的 pod,一个带有数据库,另一个带有应用程序。这种配置使本地工作变得容易,但请记住,重新创建 pod 将重新启动两个容器。

配置文件在此处完全可用(https://github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter06/thoughts_backend/kubernetes/deployment.yaml) ,所以让我们来看看它的不同部分。第一个元素描述了它是什么,它的名字,以及它所在的命名空间:

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: thoughts-backend
labels:
app: thoughts-backend
namespace: example

然后,我们生成 spec。它包含我们应该保留多少个 pod 以及每个 pod 的模板。 selector 定义了监控的标签,它应该与模板中的 labels 匹配:

spec:
replicas: 1
selector:
matchLabels:
app: thoughts-backend

template 部分在其自己的 spec 部分中定义容器:


template:
metadata:
labels:
app: thoughts-backend
spec:
containers:
- name: thoughts-backend-service
...
- name: thoughts-backend-db
...

thoughts-backend-db 更简单。唯一需要的元素是定义容器和图像的名称。我们需要将拉取策略定义为 Never 以表明该镜像在本地 Docker 存储库中可用,并且不需要从远程注册表中拉取它:

- name: thoughts-backend-db
image: thoughts_backend_db:latest
imagePullPolicy: Never

thoughts-backend-service 需要定义服务的暴露端口以及环境变量。变量值是我们之前创建数据库时使用的变量值,除了 POSTGRES_HOST,我们的优势是同一个 pod 中的所有容器共享同一个 IP:

 - name: thoughts-backend-service
image: thoughts_server:latest
imagePullPolicy: Never
ports:
- containerPort: 8000
env:
- name: DATABASE_ENGINE
value: POSTGRESQL
- name: POSTGRES_DB
value: thoughts
- name: POSTGRES_USER
value: postgres
- name: POSTGRES_PASSWORD
value: somepassword
- name: POSTGRES_PORT
value: "5432"
- name: POSTGRES_HOST
value: "127.0.0.1"

要在 Kubernetes 中进行部署,您需要应用该文件,如下所示:

$ kubectl apply -f thoughts_backend/kubernetes/deployment.yaml
deployment "thoughts-backend" created

现在在集群中创建了部署:

$ kubectl get deployments -n example
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
thoughts-backend 1 1 1 1 20s

这会自动创建 pod。如果 pod 被删除或崩溃,部署将使用不同的名称重新启动它:

$ kubectl get pods -n example
NAME READY STATUS RESTARTS AGE
thoughts-backend-6dd57f5486-l9tgg 2/2 Running 0 1m

部署正在跟踪最新的映像,但除非将其删除,否则它不会创建新的 pod。要进行更改,请务必手动删除 pod,然后重新创建:

$ kubectl delete pod thoughts-backend-6dd57f5486-l9tgg -n example
pod "thoughts-backend-6dd57f5486-l9tgg" deleted
$ kubectl get pods -n example
NAME READY STATUS RESTARTS AGE
thoughts-backend-6dd57f5486-nf2ds 2/2 Running 0 28s

该应用程序在集群内仍然无法被发现,除了通过其特定的 pod 名称来引用它,它可以更改,因此我们需要为此创建一个服务。

Configuring the service

我们创建一个 Kubernetes 服务来为创建的部署公开的应用程序创建一个名称。可以在 service.yaml 文件中检查该服务。让我们来看看:

---
apiVersion: v1
kind: Service
metadata:
namespace: example
labels:
app: thoughts-service
name: thoughts-service
spec:
ports:
- name: thoughts-backend
port: 80
targetPort: 8000
selector:
app: thoughts-backend
type: NodePort

初始数据类似于部署。 spec 部分定义了开放端口,将对端口 80 上服务的访问路由到 thoughts-backend< 容器中的端口 8000 /kbd>,部署的名称。 selector 部分将所有请求路由到任何匹配的 pod。

类型为 NodePort 以允许从集群外部访问。一旦我们找到外部暴露的 IP,这允许我们检查它是否正常工作:

$ kubectl apply -f kubernetes/service.yaml
service "thoughts-service" configured
$ kubectl get service -n example
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
thoughts-service 10.100.252.250 <nodes> 80:31600/TCP 1m

我们可以通过使用描述的 pod 访问 localhost 来访问 Thoughts Backend。在这种情况下,http://127.0.0.1:31600

读书笔记《hands-on-docker-for-microservices-with-python》库伯内斯的地方发展

该服务为我们提供了一个内部名称,但如果我们想控制它如何对外公开,​​我们需要配置一个 Ingress。

Configuring the Ingress

最后,我们在 ingress.yaml (https://github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter06/thoughts_backend/kubernetes /ingress.yaml)。文件复制到这里。请注意我们如何将元数据设置为存在于正确的命名空间中:

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: thoughts-backend-ingress
namespace: example
spec:
rules:
- host: thoughts.example.local
http:
paths:
- backend:
serviceName: thoughts-service
servicePort: 80
path: /

这个 Ingress 将使服务暴露给端口 80 上的节点。由于可以在同一个节点上公开多个服务,因此它们通过它们的主机名来区分,在本例中为 thoughts.example.local

The Ingress controller we are using only allows exposing ports 80 (HTTP) and 443 (HTTPS) in servicePort.

应用服务后,我们可以尝试访问该页面,但是,除非我们将调用指向正确的主机,否则我们将收到 404 错误:

$ kubectl apply -f kubernetes/ingress.yaml
ingress "thoughts-backend-ingress" created
$ kubectl get ingress -n example
NAME HOSTS ADDRESS PORTS AGE
thoughts-backend-ingress thoughts.example.local localhost 80 1m
$ curl http://localhost
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.15.8</center>
</body>
</html>

我们需要能够将任何对 thoughts.example.local 的请求指向我们的本地主机。在 Linux 和 macOS 中,最简单的方法是更改​​ /etc/hosts 文件以包含以下行:

127.0.0.1 thoughts.example.local

然后,我们可以使用浏览器检查我们的应用程序,这次是在 http://thoughts.example.local(和端口 80):

读书笔记《hands-on-docker-for-microservices-with-python》库伯内斯的地方发展

定义不同的主机条目允许我们从外部访问所有服务,以便能够调整它们并调试问题。我们将以相同的方式定义其余的 Ingress。

如果你得到一个 Connection refused 错误和字 localhost 运行时不出现 kubectl get ingress -n example,您的 Kubernetes 安装没有安装 Ingress 控制器。仔细检查安装文档 https://github.com/kubernetes/ingress-nginx /blob/master/docs/deploy/index.md.

所以现在我们在本地 Kubernetes 中部署了一个工作应用程序!

Deploying the full system locally

我们的每个微服务都独立工作,但要让整个系统正常工作,我们需要部署它们中的三个(Thoughts Backend、Users Backend 和 Frontend)并将它们相互连接。特别是前端,需要其他两个微服务启动并运行。使用 Kubernetes,我们可以在本地部署它。

要部署整个系统,我们需要先部署用户后端,然后再部署前端。我们将描述这些系统中的每一个,将它们与已经部署的 Thoughts Backend 相关联,我们之前看到了如何部署它。

Deploying the Users Backend

用户后端文件与想法后端非常相似。您可以在 GitHub 存储库中查看它们(https://github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/tree/master/Chapter06/users_backend/kubernetes)。确保 deployment.yaml 值中的环境设置正确:

$ kubectl apply -f users_backend/kubernetes/deployment.yaml
deployment "users-backend" created
$ kubectl apply -f users_backend/kubernetes/service.yaml
service "users-service" created
$ kubectl apply -f users_backend/kubernetes/ingress.yaml
ingress "users-backend-ingress" created

请记住确保在 /etc/hosts 中包含新主机名:

127.0.0.1 users.example.local

您可以在 http://users.example.local 中访问用户后端。

Adding the Frontend

Frontend 服务和 Ingress 与之前的非常相似。部署略有不同。让我们看一下配置,分三组:

  1. First, we add the metadata about the namespace, name, and the kind (deployment) as shown in the following code:
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
labels:
app: frontend
namespace: example
  1. Then, we define the spec with the template and the number of replicas. Only one replica is fine for a local system:
spec:
replicas: 1
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
  1. Finally, we spec out the template with the container definition:
        spec:
containers:
- name: frontend-service
image: thoughts_frontend:latest
imagePullPolicy: Never
ports:
- containerPort: 8000
env:
- name: THOUGHTS_BACKEND_URL
value: http://thoughts-service
- name: USER_BACKEND_URL
value: http://users-service

与之前定义的 Thoughts Backend 部署的主要区别在于,它只有一个容器,并且其上的环境更简单。

我们将后端 URL 环境定义为服务端点。这些端点在集群内可用,因此它们将被定向到正确的容器。

Remember that the *.example.local addresses are only available in your computer, as they only live in /etc/hosts. Inside the container, they won't be available.

This is suitable for local development, but an alternative is to have a DNS domain that can be redirected to 127.0.0.1 or similar.

我们应该在 /etc/hosts 文件中添加一个新域名:

127.0.0.1 frontend.example.local

Django 要求您设置 ALLOWED_HOSTS 设置的值,以允许它接受主机名,因为默认情况下,它只允许来自本地主机的连接。请参阅 Django 文档(https://docs.djangoproject.com/en /2.2/ref/settings/#allowed-hosts) 了解更多信息。为简化起见,我们可以允许任何主机使用 '*'。查看 GitHub 上的代码 (https://github.com/PacktPublishing/Hands-On-Docker-for-Microservices-with-Python/blob/master/Chapter06/frontend/mythoughts/mythoughts/settings.py# L28)。

In production, it's good practice to limit the hosts to the Fully Qualified Domain Name ( FQDN), the full DNS name of a host, but the Kubernetes Ingress will check the host header and reject it if it's not correct.

前端应用程序像我们之前所做的那样部署:

$ kubectl apply -f frontend/kubernetes/deployment.yaml
deployment "frontend" created
$ kubectl apply -f frontend/kubernetes/service.yaml
service "frontend-service" created
$ kubectl apply -f frontend/kubernetes/ingress.yaml
ingress "frontend-ingress" created

然后我们就可以访问整个系统,登录,搜索等等。

Remember that there are two users, bruce and stephen. Their passwords are the same as their usernames. You don't need to be logged in to search.

在您的浏览器中,转到 http://frontend.example.local/

读书笔记《hands-on-docker-for-microservices-with-python》库伯内斯的地方发展

恭喜!你有一个工作的 Kubernetes 系统,包括不同的部署微服务。您可以独立访问每个微服务以对其进行调试或执行创建新用户等操作。

如果您需要部署新版本,请使用 docker-compose 构建适当的容器并删除 pod 以强制重新创建它。

Summary

在本章中,我们看到了如何在 Kubernetes 本地集群中部署我们的微服务以允许本地开发和测试。将整个系统部署在本地计算机上大大简化了开发新功能或调试系统行为的过程。生产环境会很相似,所以这也为它打下了基础。

我们首先描述了缺失的两个微服务。用户后端处理用户的身份验证,前端是 第 1 章中介绍的单体架构的修改版本, Making the Move – Design, Plan, and Execute,连接到两个后端。我们展示了如何以 docker-compose 方式构建和运行它们。

之后,我们描述了如何设置 .yaml 文件的组合以在 Kubernetes 中正确配置应用程序。每个微服务都有自己的部署来定义可用的 Pod,一个服务来定义一个稳定的访问点,一个入口来允许外部访问。我们详细描述了它们,然后将它们应用于所有微服务。

在下一章中,我们将看到如何从本地部署转移到部署一个准备好用于生产的 Kubernetes 集群。

Questions

  1. What are the three microservices that we are deploying?
  2. Which microservice requires the other two to be available?
  3. Why do we need to use external IPs to connect the microservices while running in docker-compose?
  4. What are the main Kubernetes objects required for each application?
  5. Are any of the objects not required?
  6. Can you see any issues if we scale any of the microservices to more than one pod?
  7. Why are we using the /etc/hosts file?

Further reading