• kubernetes/k8s pod下多容器的设计模式(ambassador 大使代理模式,adapter 适配模式,sidecar 边车模式, init containers初始化容器)


    英文好的可以直接阅读原文:引用原文(英文):https://learnk8s.io/sidecar-containers-patterns

    TL;TR:k8s patterns包含了云原生架构中各种的最佳实践,这里面绕不开用的最多的就是pod下多容器的pattern,也是k8s与swarm区别最大的地方。利用好这些pattern可以在不修改任何代码的情况下实现不同的行为比如TLS加固。

    k8s把最小单位从容器上升到了pod是它设计的核心思想,这种设计带来了与原生docker容器无法比拟的优势,我们知道容器利用了linux下的各种命名空间用来隔离各种资源,但是pod作为多个容器的上一层,它可以利用命名空间是的这些容器共享某些资源从而达到亲缘性,比如共用网络、共用存储空间实现unionfile等。

    示例: 一个安全的http服务

    如何利用pod下多容器模式如何实现一个ElasticSearch服务的强化:

    apiVersion: apps/v1
     kind: Deployment
     metadata:
       name: elasticsearch
     spec:
       selector:
         matchLabels:
             es.test: elasticsearch
       template:
         metadata:
           labels:
             es.test: elasticsearch
         spec:
           containers:
           - name: elasticsearch
             image: elasticsearch:7.9.3
             env:
               - name: discovery.type
                 value: single-node
             ports:
             - name:  http
               containerPort:  9200
     apiVersion: v1
     kind: Service
     metadata:
       name: elasticsearch
     spec:
       selector:
         es.test: elasticsearch
       ports:
     port: 9200
     targetPort: 9200 
    kubectl run -it --rm --image=curlimages/curl curl 
       -- curl http://elasticsearch:9200
     {
       "name" : "elasticsearch-77d857c8cf-mk2dv",
       "cluster_name" : "docker-cluster",
       "cluster_uuid" : "z98oL-w-SLKJBhh5KVG4kg",
       "version" : {
         "number" : "7.9.3",
         "build_flavor" : "default",
         "build_type" : "docker",
         "build_hash" : "c4138e51121ef06a6404866cddc601906fe5c868",
         "build_date" : "2020-10-16T10:36:16.141335Z",
         "build_snapshot" : false,
         "lucene_version" : "8.6.2",
         "minimum_wire_compatibility_version" : "6.8.0",
         "minimum_index_compatibility_version" : "6.0.0-beta1"
       },
       "tagline" : "You Know, for Search"
     }

    现在的访问是明文的,那么如何方便的使用多容器pod来实现TLS加固传输呢,如果你想到用ingress(通常用来路由外部流量到pod),这里从ingress到pod之间还是未加密的如下图:

    The external traffic is routed to the Ingress and then to Pods.

    那么满足zero-trust的办法就是给这个pod加入一个nginx代理tls加密流量如下图:

    If you include a proxy container in the pod, you can terminate TLS in the Nginx pod.

    增加一个nginx容器代理tls流量

    apiVersion: apps/v1
     kind: Deployment
     metadata:
       name: elasticsearch
     spec:
       selector:
         matchLabels:
           app.kubernetes.io/name: elasticsearch
       template:
         metadata:
           labels:
             app.kubernetes.io/name: elasticsearch
         spec:
           containers:
             - name: elasticsearch
               image: elasticsearch:7.9.3
               env:
                 - name: discovery.type
                   value: single-node
                 - name: network.host
                   value: 127.0.0.1
                 - name: http.port
                   value: '9201'
             - name: nginx-proxy
               image: nginx:1.19.5
               volumeMounts:
                 - name: nginx-config
                   mountPath: /etc/nginx/conf.d
                   readOnly: true
                 - name: certs
                   mountPath: /certs
                   readOnly: true
               ports:
                 - name: https
                   containerPort: 9200
           volumes:
             - name: nginx-config
               configMap:
                 name: elasticsearch-nginx
             - name: certs
               secret:
                 secretName: elasticsearch-tls
     apiVersion: v1
     kind: ConfigMap
     metadata:
       name: elasticsearch-nginx
     data:
       elasticsearch.conf: |
         server {
             listen 9200 ssl;
             server_name elasticsearch;
             ssl_certificate /certs/tls.crt;
             ssl_certificate_key /certs/tls.key;
         location / {         proxy_pass http://localhost:9201;     } }

    前面的配置中我们利用用service可以让curl明文的访问es的接口,而这个配置中改为用nginx代理了9200,es只对localhost暴露9201,也就是从pod以外是访问不到es了。nginx在9200端口监听了https请求并转发给http的9200本地的端口给ES。

    The request is proxies by Nginx on port 9220 and forwarded to port 9201 on Elastisearch

    代理容器是最常用的一种Pattern

    这种添加一个代理容器到一个pod的解决方式称之为:Ambassador Pattern

    本文中所有模式都可以在google的研究文稿中找到详细的论述:https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/45406.pdf

    添加基本的TLS还只是开始,除此之外还可以用这个模式做到以下几点:

    • 如果你希望集群上的流量都用tls证书加密,你可以在所有pod上加上一个nginx代理。或者你也可以更进一步使用mutual TLS来保证所有认证的请求已经被很好的加密,正如lstio 和linkerd在service meshes中做的。
    • 在OAuth认证中也可以使用nginx代理来保证所有请求是被jwt验证的。
    • 用在连接外部的数据库,比如一些不支持TLS或者旧版本的数据库时这也是很方便的方式。

    暴露一个标准的接口用来度量

    假设你已经熟悉如何使用 Prometheus来监控所有集群中的服务,但是你也正在使用一些原生并不支持 Prometheus度量的服务,比如Elasticsearch。

    如何在不更改代码的情况下添加 Prometheus度量?

    Adapter Pattern 适配模式

    以ES为例,我们可以添加一个 exporter容器以Prometheus的格式暴露ES的度量。这非常简单,用一个开源的exporter for es 即可:

    apiVersion: apps/v1
     kind: Deployment
     metadata:
       name: elasticsearch
     spec:
       selector:
         matchLabels:
           app.kubernetes.io/name: elasticsearch
       template:
         metadata:
           labels:
             app.kubernetes.io/name: elasticsearch
         spec:
           containers:
             - name: elasticsearch
               image: elasticsearch:7.9.3
               env:
                 - name: discovery.type
                   value: single-node
               ports:
                 - name: http
                   containerPort: 9200
             - name: prometheus-exporter
               image: justwatch/elasticsearch_exporter:1.1.0
               args:
                 - '--es.uri=http://localhost:9200'
               ports:
                 - name: http-prometheus
                   containerPort: 9114
     apiVersion: v1
     kind: Service
     metadata:
       name: elasticsearch
     spec:
       selector:
         app.kubernetes.io/name: elasticsearch
       ports:
         - name: http
           port: 9200
           targetPort: http
         - name: http-prometheus
           port: 9114
           targetPort: http-prometheus

    通过这种方式可以更广泛的使用prometheus度量从而达到更好的应用与基础架构的分离。

    日志跟踪 / Sidecar Pattern

    边车模式,我一直把他想想成老式三轮摩托车的副座,它始终与摩托车主题保持一致并提供各种辅助功能,实现方式也是添加容器来曾强pod中应用。边车最经典的应用就是日志跟踪。

    在容器化的环境中最标准的做法是标准输出日志到一个中心化的收集器中用于分析和管理。但是很多老的应用是将日志写入文件,而更改日志输出有时候是一件困难的事。

    那么添加一个日志跟踪的边车就意味着你可能不必去更改日志代码。回到ElasticSearch这个例子,虽然它默认是标准输出把它写入文件有点做作,这里作为示例我们可以这样部署:

    apiVersion: apps/v1
     kind: Deployment
     metadata:
       name: elasticsearch
       labels:
         app.kubernetes.io/name: elasticsearch
     spec:
       selector:
         matchLabels:
           app.kubernetes.io/name: elasticsearch
       template:
         metadata:
           labels:
             app.kubernetes.io/name: elasticsearch
         spec:
           containers:
             - name: elasticsearch
               image: elasticsearch:7.9.3
               env:
                 - name: discovery.type
                   value: single-node
                 - name: path.logs
                   value: /var/log/elasticsearch
               volumeMounts:
                 - name: logs
                   mountPath: /var/log/elasticsearch
                 - name: logging-config
                   mountPath: /usr/share/elasticsearch/config/log4j2.properties
                   subPath: log4j2.properties
                   readOnly: true
               ports:
                 - name: http
                   containerPort: 9200
             - name: logs
               image: alpine:3.12
               command:
                 - tail
                 - -f
                 - /logs/docker-cluster_server.json
               volumeMounts:
                 - name: logs
                   mountPath: /logs
                   readOnly: true
           volumes:
             - name: logging-config
               configMap:
                 name: elasticsearch-logging
             - name: logs
               emptyDir: {}

    这里的logs容器就是sidecar的一个具体实现,现实中可以使用具体的日志收集器代替比如filebeat。当app持续写入数据时,边车中的日志收集程序会不断的以只读的形式收集日志,这里的logs边车就把写入文件的logs变为标准输出而不需要修改任何代码。

    其他边车模式常用的场景

    • 实时的重启ConfigMaps而不需要重启pod
    • 从Hashcorp Vault注入秘钥
    • 添加一个本地的redis作为一个低延迟的内存缓存服务

    在pod前的准备工作中使用Init Containers

    k8s除了提供多容器外还提供了一种叫做初始化容器的功能,顾名思义它就是在pods 容器启动前工作的容器,我一般把它当做job这样的概念,一般场景中init containers这些容器在执行完后就不再运行了处于pause状态,这里特别要注意的是它的执行会严格按照编排的从上至下的顺序逐一初始化,这种顺序也是实现初始化工作不可缺少的。下面还是以ES为例子:

    ES 文档中建议生产中设置vm.max_map_count这个sysctl属性。

    这就带来了一个问题,这个属性只能在节点级别才可以被修改,容器级别是没有做到隔离。

    所以在不修改k8s代码的情况下你不得不使用特权级别来运行es已达到修改的目的,而这也不是你所希望的,

    因为他会带来很严重的安全问题

    那么使用Init Containers就可以很好地解决这个问题,做法就是只在初始化容器中提权修改设置,那么后面的es只是普通容器就可以运行。如下:

    apiVersion: apps/v1
     kind: Deployment
     metadata:
       name: elasticsearch
     spec:
       selector:
         matchLabels:
           app.kubernetes.io/name: elasticsearch
       template:
         metadata:
           labels:
             app.kubernetes.io/name: elasticsearch
         spec:
           initContainers:
             - name: update-sysctl
               image: alpine:3.12
               command: ['/bin/sh']
               args:
                 - -c
                 - |
                   sysctl -w vm.max_map_count=262144
               securityContext:
                 privileged: true
           containers:
             - name: elasticsearch
               image: elasticsearch:7.9.3
               env:
                 - name: discovery.type
                   value: single-node
               ports:
                 - name: http
                   containerPort: 9200

    除了上面这种常见的做法外初始化容器还可以这么用,当你HashicCorp Vault 来管理secrets而不是k8s secrets时,你可以在初始化容器中读取并放入一个emptyDir中。比如这样:

    apiVersion: apps/v1
     kind: Deployment
     metadata:
       name: myapp
       labels:
         app.kubernetes.io/name: myapp
     spec:
       selector:
         matchLabels:
           app.kubernetes.io/name: myapp
       template:
         metadata:
           labels:
             app.kubernetes.io/name: myapp
         spec:
           initContainers:
             - name: get-secret
               image: vault
               volumeMounts:
                 - name: secrets
                   mountPath: /secrets
               command: ['/bin/sh']
               args:
                 - -c
                 - |
                   vault read secret/my-secret > /secrets/my-secret
           containers:
             - name: myapp
               image: myapp
               volumeMounts:
                 - name: secrets
                   mountPath: /secrets
           volumes:
             - name: secrets
               emptyDir: {}

    更多的初始化容器应用场景

    • 你希望在运行app前跑数据库的迁移脚本
    • 从外部读取/拉取一个超大文件时可以避免容器臃肿

    总结

    这些pattern非常巧妙地用很小的代价非侵入的解决现实中常见的问题,这里要特别说明的是除了初始化容器会在运行后暂停不占用资源外,pods中增加的容器都是吃资源的,实际使用中我们不希望因为解决一个小问题反倒拖累整个pod,所以在边车这类容器组件的选择上要慎重,要足够的高效轻量,常见的像nginx、go写的大部分组件就是一个很好的选择,java写的就呵呵了。

    如果希望挖掘更多多容器的设计细节可以查看官方文档:https://kubernetes.io/docs/concepts/workloads/pods/,还有google的容器设计论文:https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/45406.pdf

    作者:KKcat
        
    个人博客:http://jinzhao.me/
        
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    Django框架 之 querySet详解
    Django框架 之 admin管理工具(组件使用)
    Django框架 之 跨域请求伪造
    Django框架 之 form组件的钩子
    Django框架 之 Form表单和Ajax上传文件
    Django框架 之 modelform组件
    [BZOJ4477] [JSOI2015]字符串树(可持久化Trie+LCA)
    [BZOJ 4523] [CQOI2016]路由表(Trie+单调栈)
    [Luogu2870] [USACO07DEC]最佳牛线Best Cow Line(贪心+后缀数组)
    [BZOJ3998][TJOI2015]弦论(后缀数组)
  • 原文地址:https://www.cnblogs.com/jinzhao/p/14980483.html
Copyright © 2020-2023  润新知