入门实践丨如何在K3s上部署Web应用程序
阅读原文时间:2021年06月23日阅读:1

在本文中,我们将使用Flask和JavaScript编写的、带有MongoDB数据库的TODO应用程序,并学习如何将其部署到Kubernetes上。这篇文章是针对初学者的,如果你之前没有深度接触过Kubernetes集群,也不要担心!

我们将使用K3s,这是一个轻量级的Kubernetes发行版,非常适合快速入门。但首先让我们谈谈我们想要实现的目标。

首先,我将介绍示例应用程序。这其实已经简化了许多细节,但它说明了常见的用例。然后我们将熟悉了解容器化应用程序的过程。在我们继续之前,我会讨论我们如何使用容器来让我们的开发更加轻松,特别是如果我们在一个团队中工作,或者是当我们在一个新的环境中工作时,希望减轻开发人员的负担。

一旦我们将应用程序容器化,下一步就是将它们部署到Kubernetes上。虽然我们可以手动创建服务、Ingress和网关,但我们可以使用Knative以在任何时候都支持我们的应用程序。

我们将使用一个简单的TODO应用程序来演示前端、REST API后端和MongoDB协同工作。这要归功于Prashant Shahi提出的这个例子。我做了一些小改动,纯粹是为了教学的目的:

https://github.com/prashant-shahi

首先,git clone代码库:

git clone https://github.com/benjamintanweihao/Flask-MongoDB-K3s-KNative-TodoApp

接下来,我们将检查目录,了解情况:

 cd Flask-MongoDB-K3s-KNative-TodoApp
 tree

该文件夹结构是一个典型的Flask应用程序。Entry point是app.py,它还包含REST APIs。Templates文件夹包含了将被渲染成HTML的文件:

打开 app.py,我们可以看到所有的主要部分:

├── app.py
├── requirements.txt
├── static
│   ├── assets
│   │   ├── style.css
│   │   ├── twemoji.js
│   │   └── twemoji.min.js
└── templates
    ├── index.html
    └── update.html

从上面的代码段,您可以看到应用程序需要MongoDB作为数据库。使用lists()方法,您可以看到如何定义路由(即@ app.route(“/ list”))、如何从MongoDB获取数据,以及模板是如何呈现的示例。

mongodb_host = os.environ.get('MONGO_HOST', 'localhost')
mongodb_port = int(os.environ.get('MONGO_PORT', '27017'))
client = MongoClient(mongodb_host, mongodb_port)
db = client.camp2016
todos = db.todo 

app = Flask(__name__)
title = "TODO with Flask"

@app.route("/list")
def lists ():
    #Display the all Tasks
    todos_l = todos.find()
    a1="active"
    return render_template('index.html',a1=a1,todos=todos_l,t=title,h=heading)

if __name__ == "__main__":
    env = os.environ.get('APP_ENV', 'development')
    port = int(os.environ.get('PORT', 5000))
    debug = False if env == 'production' else True
    app.run(host='0.0.0.0', port=port, debug=debug)

这里需要注意的另一件事是使用了MONGO_HOST和MONGO_PORT的环境变量和Flask相关的环境变量。其中,最重要的是debug。当变量设置为True时,Flask服务器会在检测到和发生更改时自动重新加载。这在开发过程中特别方便,也是我们要充分利用的特性。

在处理应用程序时,我曾经花费大量时间设置环境并安装所有依赖项。在那之后,我可以通过添加新功能来启动和运行。然而,这仅仅描述了一个理想的场景,对吗?

你有多少次回到你已经开发的应用程序(比如六个月前),却发现自己正在慢慢陷入依赖项地狱?依赖项通常是一个灵活的目标,除非您采取措施锁定对象,否则您的应用程序可能无法正常工作。解决这个问题的方法之一是将所有依赖项打包到Docker容器中。

Docker带来的另一件特性是自动化。这意味着不再需要复制和粘贴命令,也不再需要设置数据库之类的东西。

Docker化 Flask程序

以下是Dockerfile:

FROM alpine:3.7
COPY . /app
WORKDIR /app

RUN apk add --no-cache bash git nginx uwsgi uwsgi-python py2-pip \
    && pip2 install --upgrade pip \
    && pip2 install -r requirements.txt \
    && rm -rf /var/cache/apk/*

EXPOSE 5000
ENTRYPOINT ["python"]

我们从一个最小的(在大小和功能方面)基础镜像开始。然后,应用程序的内容进入容器中的/app目录。接下来,我们执行一系列命令来安装Python、Nginx web server和Flask应用程序的所有需求。这些正是在新系统上设置应用程序所需的步骤。

您可以这样构建Docker容器:

% docker build -t <yourusername>/todo-app .

你将看到这样如下输出:

# ...
Successfully built c650af8b7942
Successfully tagged benjamintanweihao/todo-app:latest

那 MongoDB 呢?

您是否应该经历为MongoDB创建Dockerfile的相同过程?在此之前,已经有人做过这样的尝试,具体演示请查看案例链接:https://hub.docker.com/_/mongo.不过现在您有两个容器,其中Flask容器依赖于MongoDB容器。

一种方法是先启动MongoDB容器,然后启动Flask容器。但是,假设您想添加缓存并决定引入Redis容器。那么启动每个容器的过程会很快变枯燥繁琐。解决方案是Docker Compose,这是一个允许您定义和运行多个Docker容器的工具,正符合我们当前面临的情况。

Docker Compose

以下是Docker compose文件,docker-compose.yaml:

services:
  flaskapp:
    build: .
    image: benjamintanweihao/todo-app:latest
    ports:
      - 5000:5000
    container_name: flask-app
    environment:
      - MONGO_HOST=mongo
      - MONGO_PORT=27017
    networks:
      - todo-net
    depends_on:
      - mongo
    volumes:
      - .:/app # <---
  mongo:
    image: mvertes/alpine-mongo
    ports:
      - 27017:27017
    networks:
      - todo-net

networks:
  todo-net:
    driver: bridge

即使您不熟悉Docker Compose,这里的YAML文件也并不复杂。让我们看一下重要的部分。

在最开头,这个文件定义了由flaskapp和mongo组成的服务,以及指定桥接连接的网络。这将创建一个网络连接,以便服务中定义的容器可以相互通信。

每个服务都定义镜像、端口映射和前面定义的网络。在flaskapp中也定义了环境变量(请查看app.py,看看它们是否确实是相同的)。

我想提醒您注意flask应用程序中指定的volume。我们在这里所做的是将主机的当前目录(应该是包含app.py的项目目录)映射到容器的/app目录 我们为什么要这样做?回想一下,在Dockerfile中,我们将app复制到/app目录中,如下所示:

COPY . /app

假设你想对应用程序做一个更改。你不可能轻易改变容器中的app.py。通过对本地目录的映射,你基本上是在用你目录中的本地副本覆盖容器中的app.py。因此,假设Flask应用程序处于调试模式(如果你在这一点上没有改变任何东西的话,它就是调试模式),当你启动容器并做出改变时,渲染的输出会反映出这个改变。

但是,重要的是要意识到容器中的app.py仍然是旧版本,您仍然需要记住构建新镜像(希望您已将CI/CD设置为自动执行此操作)

让我们看看这是怎么回事。运行以下命令:

docker-compose up

接下来你将看到:

Creating network "flask-mongodb-k3s-knative-todoapp_my-net" with driver "bridge"
Creating flask-mongodb-k3s-knative-todoapp_mongo_1 ... done
Creating flask-app                                 ... done
Attaching to flask-mongodb-k3s-knative-todoapp_mongo_1, flask-app
# ... more output truncated
flask-app   |  * Serving Flask app "app" (lazy loading)
flask-app   |  * Environment: production
flask-app   |    WARNING: Do not use the development server in a production environment.
flask-app   |    Use a production WSGI server instead.
flask-app   |  * Debug mode: on
flask-app   |  * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
flask-app   |  * Restarting with stat
mongo_1     | 2021-05-15T15:41:37.993+0000 I NETWORK  [listener] connection accepted from 172.23.0.1:48844 #2 (2 connections now open)
mongo_1     | 2021-05-15T15:41:37.993+0000 I NETWORK  [conn2] received client metadata from 172.23.0.1:48844 conn2: { driver: { name: "PyMongo", version: "3.11.4" }, os: { type: "Linux", name: "", architecture: "x86_64", version: "5.8.0-53-generic" }, platform: "CPython 2.7.15.final.0" }
flask-app   |  * Debugger is active!
flask-app   |  * Debugger PIN: 183-021-098

现在开始在浏览器中访问:http://localhost:5000

如果你看到这个,恭喜你!Flask和Mongo在一起正常工作了。您可以随意使用应用程序来感受它。

现在让我们对应用程序标题中的app.py做一个小小的改动:

index d322672..1c447ba 100644
--- a/app.py
+++ b/app.py
-heading = "tOdO Reminder"
+heading = "TODO Reminder!!!!!"

保存文件并重新加载应用程序:

完成后,您可以输入以下命令:

docker-compose down

截至目前,我们已将我们的应用程序及其支持服务(现在只是MongoDB)容器化。我们如何开始将我们的应用程序部署到Kubernetes?

在此之前,让我们安装Kubernetes。为此,我选择了K3s,因为它是安装Kubernetes和启动和运行的最简单方法。

% curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server --no-deploy=traefik"  sh -s -

过一会儿,你就可以安装 Kubernetes了:

[INFO]  Finding release for channel stable
[INFO]  Using v1.20.6+k3s1 as release
[INFO]  Downloading hash https://github.com/k3s-io/k3s/releases/download/v1.20.6+k3s1/sha256sum-amd64.txt
# truncated ...
[INFO]  systemd: Starting k3s

验证是否已正确设置K3s:

% kubectl get no
NAME      STATUS   ROLES                  AGE     VERSION
artemis   Ready    control-plane,master   2m53s   v1.20.6+k3s1

MongoDB

有多种方法可以完成这一操作。您可以使用我们创建的镜像,MongoDB operator或Helm:

helm install mongodb-release bitnami/mongodb --set architecture=standalone --set auth.enabled=false


 Please be patient while the chart is being deployed 

MongoDB(R) can be accessed on the following DNS name(s) and ports from within your cluster:

    mongodb-release.default.svc.cluster.local

To connect to your database, create a MongoDB(R) client container:

    kubectl run --namespace default mongodb-release-client --rm --tty -i --restart='Never' --env="MONGODB_ROOT_PASSWORD=$MONGODB_ROOT_PASSWORD" --image docker.io/bitnami/mongodb:4.4.6-debian-10-r0 --command -- bash

Then, run the following command:
    mongo admin --host "mongodb-release"

To connect to your database from outside the cluster execute the following commands:

    kubectl port-forward --namespace default svc/mongodb-release 27017:27017 &
    mongo --host 127.0.0.1

安装Knative和Istio

在本文中,我们将使用Knative。Knative构建在Kubernetes之上,使得开发人员可以很容易地部署和运行应用程序,而不必知道Kubernetes的很多细节。

Knative由两部分组成:Serving和Eventing。在本节中,我们将讨论Serving部分。使用Knative Serving,您可以在几秒钟内创建可弹性伸缩的、安全的和无状态的服务,这就是我们需要对TODO应用程序做的!在此之前,我们先安装Knative:

以下说明基于:

https://knative.dev/docs/install/install-serving-with-yaml/

kubectl apply -f https://github.com/knative/serving/releases/download/v0.22.0/serving-crds.yaml
kubectl apply -f https://github.com/knative/serving/releases/download/v0.22.0/serving-core.yaml
kubectl apply -f https://github.com/knative/net-istio/releases/download/v0.22.0/istio.yaml
kubectl apply -f https://github.com/knative/net-istio/releases/download/v0.22.0/net-istio.yaml

这设置了Knative和istio。你可能想知道为什么我们需要Istio。原因是Knative需要一个Ingress Controller,使其可以执行流量分发(例如, Todo应用程序的版本1和版本2需要同时运行)和自动HTTP请求重试。

Istio有替代方案吗?或许可以考虑Gloo(https://docs.solo.io/gloo-edge/master/installation/knative/)。但当前不支持Traefik,这就是为什么我们在安装K3s时必须禁用它。由于Istio是默认且最受支持的,我们将使用它。

现在等待所有的knative-serving 的pod运行:

kubectl get pods --namespace knative-serving -w
NAME                                READY   STATUS    RESTARTS   AGE
controller-57956677cf-2rqqd         1/1     Running   0          3m39s
webhook-ff79fddb7-mkcrv             1/1     Running   0          3m39s
autoscaler-75895c6c95-2vv5b         1/1     Running   0          3m39s
activator-799bbf59dc-t6v8k          1/1     Running   0          3m39s
istio-webhook-5f876d5c85-2hnvc      1/1     Running   0          44s
networking-istio-6bbc6b9664-shtd2   1/1     Running   0          44s

设置自定义域

默认情况下,Knative Serving使用example.com作为默认域。如果您按照说明设置了K3s,则应该安装负载均衡器。这意味着通过一些设置,您可以使用sslip.io之类的DNS服务创建自定义域。

sslip.io是一种服务,当使用带有嵌入式IP地址的主机名进行查询时,它会返回该IP地址。例如,192.168.0.1.sslip.io等URL将指向192.168.0.1。这是极好的服务,你不必去买你自己的域名。

继续并应用以下manifest:

kubectl apply -f https://storage.googleapis.com/knative-nightly/serving/latest/serving-default-domain.yaml

如果您打开 serving-default-domain. yaml,您需要在 spec 中注意到以下内容:

# other parts truncated
spec:
    serviceAccountName: controller
    containers:
        - name: default-doma
          image: ko://knative.dev/serving/cmd/default-domain
          args: ["-magic-dns=sslip.io"]

这将启用您将在下一步中需要使用的DNS。

测试是否一切正常

下载kn二进制文件。您可以查阅链接:https://knative.dev/development/client/install-kn/。一定要重命名二进制文件 kn然后把它放在$PATH的某个地方。一旦解决了这个问题,就继续创建示例Hello World服务。我已经将benjamintanweihao/helloworld python镜像推送到Docker Hub:

% kn service create helloworld-python --image=docker.io/benjamintanweihao/helloworld-python --env TARGET="Python Sample v1"

这将产生以下输出:

Creating service 'helloworld-python' in namespace 'default':

  0.037s The Route is still working to reflect the latest desired specification.
  0.099s Configuration "helloworld-python" is waiting for a Revision to become ready.
 29.277s ...
 29.314s Ingress has not yet been reconciled.
 29.446s Waiting for load balancer to be ready
 29.605s Ready to serve.

Service 'helloworld-python' created to latest revision 'helloworld-python-00001' is available at URL:
http://helloworld-python.default.192.168.86.26.sslip.io

输入以下代码即可列出所有命名空间中所有已部署的Knative服务:

% kn service  list -A

如果有kubectl,这就变成:

% kubectl get ksvc -A

要删除服务,只需执行以下操作:

kn service delete helloworld-python # or kubectl delete ksvc helloworld-python

如果您还没有这样做,请确保TODO应用程序镜像已推送到DockerHub。记住用DockerHub ID替换{username}:

% docker push {username}/todo-app:latest

推送镜像后,可以使用kn命令创建TODO服务。记住用DockerHub ID替换{username}:

kn service create todo-app --image=docker.io/{username}/todo-app --env MONGO_HOST="mongodb-release.default.svc.cluster.local"

如果一切运行顺利,你将看到:

Creating service 'todo-app' in namespace 'default':

  0.022s The Route is still working to reflect the latest desired specification.
  0.085s Configuration "todo-app" is waiting for a Revision to become ready.
  4.586s ...
  4.608s Ingress has not yet been reconciled.
  4.675s Waiting for load balancer to be ready
  4.974s Ready to serve.

Service 'todo-app' created to latest revision 'todo-app-00001' is available at URL:
http://todo-app.default.192.168.86.26.sslip.io

现在访问http://todo-app.default.192.168.86.26.sslip.io (或者在上一个输出的最后一行的内容)您应该可以看到应用程序!现在我们来看看Knative为你做了什么。KNative仅需一行命令就可以为您启动一个服务,并且为您提供了一个可以从集群访问的URL。

我对Knative的了解仅仅停留于表面,但我希望这个教程可以激励你更多地了解它!当我开始看Knative的时候,我不太明白它做了什么。希望这个例子能让我们看到Knative的惊人之处和它的便利性。

在本文中,我们简要介绍了使用Python构建的web应用程序并需要MongoDB,并学习了如何:

  • 使用Docker容器化TODO应用程序
  • 使用Docker减轻依赖项
  • 使用Docker进行开发
  • 使用Docker Compose打包多个容器
  • 安装K3s
  • 安装Knative(Serving)和Istio
  • 使用Helm署MongoDB
  • 使用Knative部署TODO应用程序

虽然将应用程序迁移到 Kubernetes 当然不是一项简单的任务,但是将应用程序容器化通常会让你成功一半。当然,本文还有很多东西没有涉及,比如安全性和可扩展性。

K3s 是轻量级的Kubernetes发行版,可以极其轻松地使用笔记本/台式机测试和运行 Kubernetes 工作负载,对个人开发者来说十分友好。同时,也十分适合企业在边缘端大规模部署集群。

当我开始研究 Knative 的时候,我并不十分理解它的作用。希望这个例子能够帮助我们理解 Knative 的魅力及其带来的便利。实际上,Knative 的亮点之一就是“在几秒钟之内就能启动一个可扩展的、安全的、无状态的服务”。

我将在以后的文章中更多地介绍 Knative,并更深入地探讨其核心特征。我希望你能通过这篇教程,将它们应用到你的应用程序中!