用Docker和Kubernetes部署Rust教程

1,196 阅读8分钟

用Docker和Kubernetes部署Rust

大家好!我是Chris Allen。我叫Chris Allen,我将用一个小小的Rust应用来演示用Docker和Kubernetes来部署Rust。Rust和Haskell应用的部署方式类似。这主要是因为这两种语言的编译器都会生成本地二进制可执行文件。

以下是我们将使用的技术和原因:

  • 编程语言将是Rust。部分原因是为了改变节奏,部分原因是它类似于但不完全是部署Haskell,FP Complete在这方面有很多经验。Rust是一种高性能和安全导向的系统编程语言。

  • 对于持续集成,我们将使用GitLab CIGitLab CI是GitLab的持续集成和持续部署平台。你不必使用GitLab的源码库或其他功能来利用GitLab CI的优势!我们发现GitLab CI是一个很好的平台。我们发现,GitLab CI是我们大多数客户的一个甜蜜点。除了原生支持使用Docker镜像作为构建环境外,GitLab还能让你轻松使用自己的专用服务器作为构建运行器。与TravisCI或CircleCI等托管产品相比,这可以极大地改善构建时间。此外,它还采用了我们喜欢的方法,将CI构建过程保留在代码库中

  • 我们将使用Docker作为我们的运行时部署环境。将应用程序部署为Docker镜像将使应用程序更具有可复制性,并能重复使用现有的面向Docker的基础设施。

  • 容器和服务的协调将由Kubernetes完成。Kubernetes可以自动部署、扩展和管理容器化的应用程序。除其他外,它可以为你的应用实现薄片化资源,而不需要借助共享应用环境。Kubernetes还提供自我修复、自动推出和回滚、水平扩展、服务发现和负载平衡。最重要的是,你的Kubernetes服务规范是普通的文本文件,可以和你的应用程序一起进行版本控制。自愈意味着,如果一台服务器或EC2实例消失了,Kubernetes集群可以在不同的机器上重新启动运行在该服务器上的服务,而无需人工干预。

Totto the bot-o

首先,这里是我们的Rust应用程序。

extern crate futures;
extern crate telegram_bot;
extern crate tokio_core;

use std::env;

use futures::Stream;
use tokio_core::reactor::Core;
use telegram_bot::*;

fn main() {
    let mut core = Core::new().unwrap();

    let token = env::var("TELEGRAM_BOT_TOKEN").unwrap();
    let api = Api::configure(token).build(core.handle()).unwrap();

    // Fetch new updates via long poll method
    let future = api.stream().for_each(|update| {

        // If the received update contains a new message...
        if let UpdateKind::Message(message) = update.kind {

            if let MessageKind::Text {ref data, ..} = message.kind {
                // Print received text message to stdout.
                println!("<{}>: {}", &message.from.first_name, data);

                // Answer message with "Hi".
                api.spawn(message.text_reply(
                    format!("Hi, {}! You just wrote '{}'", &message.from.first_name, data)
                ));
            }
        }

        Ok(())
    });

    core.run(future).unwrap();
}

这与telegram-bot库提供的可爱例子相同。它所做的就是运行一个Telegram机器人,重复你所说的内容。

要在本地构建这个程序用于开发,你可以在终端运行:

cargo build

在你的终端中。你还需要Cargo.toml ,该文件指定了项目的依赖性。上面的源代码(主模块)需要放在一个位于src/main.rs 的文件中。你可以看到这些是如何设置的:https://gitlab.com/bitemyapp/totto/

要运行这个应用程序,看看它是否在本地工作,你首先需要有一个Telegram账户。一旦你在Telegram上,你要和机器人之父对话,为你的Totto实例获得一个API令牌。从那里,你可以通过以下方式在MacOS或Linux上运行该应用程序。

export TELEGRAM_BOT_TOKEN=my_token_I_got_from_botfather
cargo run totto

cargo run 后的应用程序名称应与name 中指定的Cargo.toml 相匹配 :gitlab.com/bitemyapp/t…

如果你直接给机器人发消息说 "ping",它就会回复说。

嗨,#{你的名字}!你刚刚写了'ping'

作为Telegram对你信息的回复,其中your_name 是你在Telegram上的任何名字。

Dockerization

我想演示的一件事是为Rust应用程序构建一个传统的运行时容器环境,以及一个可以尽可能小的最小环境。

对于一个更传统的Docker环境,你可以看一下这个Docker文件。

FROM rust@sha256:1cdce1c7208150f065dac04b580ab8363a03cff7ddb745ddc2659d58dbc12ea8 as build

COPY ./ ./

RUN cargo build --release

RUN mkdir -p /build-out

RUN cp target/release/totto /build-out/

# Ubuntu 18.04
FROM ubuntu@sha256:5f4bdc3467537cbbe563e80db2c3ec95d548a9145d64453b06939c4592d67b6d

ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get -y install ca-certificates libssl-dev && rm -rf /var/lib/apt/lists/*

COPY --from=build /build-out/totto /

CMD /totto

我们把rustubuntu 镜像的SHA256哈希值固定下来,以提高Docker构建的可重复性。你可以在这里找到Rust镜像。我们也在利用中间容器的优势,将构建环境的需求与运行环境的需求分开。我们不想在我们的部署镜像中带着编译器和构建工件。要了解更多关于这种方法的信息,请看Deni Bertovic关于用Docker构建Haskell应用程序的文章

为了让我们的Rust应用程序发挥作用,我们在运行环境中需要两个主要东西:

  • 用于在运行时链接的安全套接字层库(动态链接)。
  • CA证书链,用于验证Telegram API服务器的真实性。这一点将在本篇文章的后半部分出现并解决。

为了满足这些要求,我们有:

RUN apt-get update && apt-get -y install ca-certificates libssl-dev && rm -rf /var/lib/apt/lists/*

我们安装libssl-dev ,因为这个应用程序的默认Rust构建将动态链接OpenSSL。HTTP客户端的TLS支持需要OpenSSL。因为这个依赖是动态链接的,我们必须确保libssl-dev 在运行环境中安装。许多开发者会放弃使用像ubuntu:18.04 这样的Docker镜像,而选择使用alpinescratch 。我建议从ubuntu 开始,除非你对精简的Docker镜像有明显的需求。Ubuntu的Docker镜像提供了一个相当传统的Linux环境,而且可以更快地将环境配置正确,供生产使用。然而,由于我知道人们会想要精简版,我也有...

用scratch、musl和静态链接的Rust应用程序实现Docker化

为了好玩,我们将使用scratch ,而不是Alpine,尽管Alpine在最小的应用程序中更常见。

Alpine 是一个非常简约的Linux发行版,为最大限度的小型Docker镜像而设计。如果你想看看Alpine中包括什么,这里有一个Docker文件的例子。如果你好奇镜像里有什么,你可以在你的电脑上解压 。rootfs.tar.xz

scratch 是一个基线Docker镜像,不包含任何东西。它是Alpine等Docker发行版的基础。如果你愿意自己整理所有的依赖关系,你可以使用 进行部署。scratch

这里是我们的Dockerfile ,用于静态二进制。

FROM yasuyuky/rust-ssl-static@sha256:3df2c8949e910452ee09a5bcb121fada9790251f4208c6fd97bb09d20542f188 as build

COPY ./ ./

ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get -y install ca-certificates libssl-dev && rm -rf /var/lib/apt/lists/*

ENV PKG_CONFIG_ALLOW_CROSS=1

RUN cargo build --target x86_64-unknown-linux-musl --release

RUN mkdir -p /build-out

RUN cp target/x86_64-unknown-linux-musl/release/totto /build-out/

RUN ls /build-out/

FROM scratch

COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt

COPY --from=build /build-out/totto /

ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
ENV SSL_CERT_DIR=/etc/ssl/certs

CMD ["/totto"]

这在结构上类似于我们动态链接的Docker构建和运行环境。一些不同之处在于,我们使用yasuyuky的rust-ssl-static 镜像作为构建镜像,并使用scratch 作为最简单的运行环境。TLS/SSL支持在编译时被链接到二进制文件中,所以我们不再需要它在运行时环境中作为一个单独的库存在。

ENV PKG_CONFIG_ALLOW_CROSS=1 为后面的 cargo build命令设置环境变量。

试图运行静态图像时可能出现的错误

如果你得到一个这样的错误:

$ docker run -e TELEGRAM_BOT_TOKEN registry.gitlab.com/bitemyapp/totto:latest
docker: Error response from daemon: OCI runtime create failed: container_linux.go:348: starting container process caused "exec: \"/bin/sh\": stat /bin/sh: no such file or directory": unknown.
ERRO[0000] error waiting for container: context canceled

你可能是错误地调用了命令或入口。scratch,与alpine 不同,它里面没有任何东西。包括/bin/sh!因此,你的命令必须是基于exec的。

CMD ["/totto"]

而不是基于shell的:

CMD /totto

因为在scratch 中没有shell,除非你复制一个到环境中。为了处理环境变量的设置,你可以像上面那样使用DockerfileENV 命令。

用Kubernetes部署我们的Docker镜像

在这一节中,我假设你使用的是kubectl,并且你已经将你的KUBECONFIG 环境变量设置为指向一个你可以访问的集群。我们这个应用的Kubernetes部署规范是这样的:

---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: totto
spec:
  replicas: 1
  minReadySeconds: 5
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: totto
    spec:
      containers:
        - name: totto
          image: registry.gitlab.com/bitemyapp/totto:latest
          imagePullPolicy: Always
          env:
            - name: TELEGRAM_BOT_TOKEN
              valueFrom:
                secretKeyRef:
                  name: totto-telegram-token
                  key: totto-token
          resources:
            requests:
              cpu: 10m
              memory: 10M
            limits:
              cpu: 20m
              memory: 20M

确保image: 指向你的Kubernetes集群可以访问的注册表。我为我的公共仓库使用了GitLab的注册表,因为它不需要认证。如果你想设置从私有注册表拉取镜像,请看Kubernetes的相关文档: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/

始终为部署在Kubernetes上的应用程序设置你的resources:!没有这个,Kubernetes就不能很好地管理资源分配。如果你部署的应用程序有资源限制,你的节点往往会被过度订阅。

资源限制设置得相当低,因为我们的Rust应用程序不会需要太多。这并不罕见,因为在生产环境中,许多零散的任务工作者应用程序会有类似的资源利用率。当你使用Kubernetes时,即使你已经在AWS这样的云环境中运行,能够为你的应用程序进行薄片化和合理的资源分配也是节省运营费用的重要原因。AWS不提供拥有千分之二十的CPU和二十兆字节内存的EC2实例。

将部署规范应用于Kubernetes集群并触发镜像的部署。

$ kubectl apply -f etc/kubernetes/totto.yaml
deployment "totto" configured
$ kubectl rollout status -f etc/kubernetes/totto.yaml
Waiting for rollout to finish: 0 of 1 updated replicas are available...
deployment "totto" successfully rolled out

好吧,如果你看到这个,应用程序现在可能已经部署并运行了,但我现在要谈谈配置和我遇到的一些错误。

配置应用程序

幸运的是,配置这个Telegram机器人应用程序是非常简单的我们的应用程序依赖于能够从这行代码的环境变量中获得Telegram bot的API令牌。

let token = env::var("TELEGRAM_BOT_TOKEN").unwrap();

相应地,我们需要确保在生产中为我们的Docker容器设置TELEGRAM_BOT_TOKEN 环境变量。你可能还记得pod规范中的这一部分。

env:
  - name: TELEGRAM_BOT_TOKEN
    valueFrom:
      secretKeyRef:
        name: totto-telegram-token
        key: totto-token

为了使其发挥作用,我使用了以下kubectl命令来创建秘密。

kubectl create secret generic totto-telegram-token --from-file=/home/callen/Secrets/totto-token

这里name: 是秘密名称,key:from-file= 中提供的文件名。totto-token 中的内容只是Telegram的API令牌,没有其他内容。有了这个,当Docker容器在Kubernetes集群中运行时,环境应该被正确设置。

从一个图像标签重新部署Kubernetes吊舱

Kubernetes的开发者目前不支持自动重新拉取相同的镜像和标签,而且将来也不太可能增加这样的支持

你会想为一个真正的项目做一个更动态的图像标签设置。Kubernetes不会假设一个由图像名称和标签识别的特定图像自上次拉取后发生了 "变异"。解决这个问题的一个方法是让你的Makefile在图像名称上附加一个构建标识符。

export CI_REGISTRY_IMAGE ?= registry.gitlab.fpcomplete.com/chrisallen/totto
export CI_PIPELINE_ID ?= $(shell date +"%Y-%m-%d-%s")
export DOCKER_IMAGE_CURRENT ?= ${CI_REGISTRY_IMAGE}:${CI_BUILD_LIB_TYPE}_${CI_BUILD_REF_SLUG}_${CI_PIPELINE_ID}

为了方便和保持这个演示的简单,我使用了一个单一的图像标签。

export FPCO_CI_REGISTRY_IMAGE ?= registry.gitlab.fpcomplete.com/chrisallen/totto
export CI_REGISTRY_IMAGE ?= registry.gitlab.com/bitemyapp/totto
export FPCO_DOCKER_IMAGE ?= ${CI_REGISTRY_IMAGE}:latest
export DOCKER_IMAGE ?= ${CI_REGISTRY_IMAGE}:latest

这些图像名称是稳定的,不会从构建到构建发生变化。为了使部署回滚成为可能,你真的希望有不同的图像名称或标签,这样你就可以把:latest ,回到最后一个已知良好的部署。如果你不保留你所部署的镜像,你就会错过Docker的大部分好处。

为了解决这个问题,对于我试图部署的非常简单和临时的应用程序,我会在每次kubectl apply ,在我的pod规格中摆弄一个变量。处理这个问题的一个选择是使用Helm来为你的pod specs设置模板。当你只是测试一个部署时,另一个黑客的解决方案是kubectl delete ,然后重新应用你的部署。

SSL证书问题

我在研究如何在Dockerscratch 环境下将Telegram机器人作为静态二进制文件部署时遇到了一些问题。为了在我应用pod规格后询问pod,我使用kubectl rollout status -f etc/kubernetes/totto.yaml ,在一个终端上监控部署情况。当我注意到它在大约15秒后没有结束时,我在另一个终端上做了如下操作。

$ kubectl get pods
[... listing of the pods and their status, I noticed there was a crash loop for Totto ...]

$ kubectl describe pod totto-2950502675-3x2nk
[... some more detailed information ...]

$ kubectl logs totto-2950502675-3x2nk
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error(Hyper(Io(Custom { kind: Other, error: Ssl(ErrorStack([Error { code: 336134278, library: "SSL routines", function: "ssl3_get_server_certificate", reason: "certificate verify failed", file: "s3_clnt.c", line: 1264 }])) })), State { next_error: None, backtrace: None })', libcore/result.rs:945:5
note: Run with `RUST_BACKTRACE=1` for a backtrace.

好吧,问题是scratch 镜像真的是开箱即空。这意味着它没有在环境中预装任何可信的CA证书。

参照:github.com/japaric/cro…

为了使我们的机器人能够工作,Rust telegram机器人库底层的HTTP客户端需要能够信任https://api.telegram.org 。为了解决这个问题,我利用了我们有一个基于Ubuntu Xenial的较大的中间镜像,并将CA证书复制到我们基于scratch 的运行时镜像中。

COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt

注意,我使用了ENV 指令和CMD 的执行形式。

ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
ENV SSL_CERT_DIR=/etc/ssl/certs

CMD ["/totto"]

exec 形式只期望一个二进制文件的路径。

# Ok
CMD ["/totto"]

# Not ok
CMD ["SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt SSL_CERT_DIR=/etc/ssl/certs /totto"]

在这一切完成后

根据我的Linux桌面上的docker images ,带有CA证书的静态链接二进制文件的最终镜像大小为10.3MB。这并没有比二进制文件大多少。如果你觉得自己很厚脸皮,你可以在部署二进制文件之前在其上运行strip ,以使其更小,但这可能会妨碍以后的调试工作。

最终的源代码可以在以下网址找到:https://gitlab.com/bitemyapp/totto

$ kubectl get pods | grep totto
totto-2950502675-zsw3n      1/1     Running     0   42m

Screenshot from 2018-07-17 17-31-16