Category Archive : K8S相关

在K8S里使用filebeat作为sidecar收集nginx日志

简介

通过sidecar方法进行接入,与提供日志的容器部署在同一个pod里,主要是配置statefulset里的containers和configmap里的filebeat.yaml
1.把nginx的日志文件挂载在access_log这个volume里,同时在filebeat这个pod里也挂载access_log这个volume
2.filebeat通过subpath的方法挂载单独一个filebeat.yml到/usr/share/filebeat/filebeat.yml。注意,如果不用subpath挂载单个文件的话,是会覆盖掉/usr/share/filebeat/目录的

3.configmap里设置elasticsearch的地址和index,指定日志文件

 

statefulset.yaml

containers:
  - image: nginx:latest
    name: nginx
    ports:
        - containerPort: 80
    volumeMounts:
        - name: access-log #日志同时挂载在nginx和filebeat中
          mountPath: /var/log/nginx/
  - image: docker.elastic.co/beats/filebeat:6.8.12
    imagePullPolicy: Always
    name: filebeat
    volumeMounts:
        - name: access-log #日志同时挂载在nginx和filebeat中
          mountPath: /log
        - name: filebeat-config
          mountPath: /usr/share/filebeat/filebeat.yml
          subPath: filebeat.yml
  volumes:
    - name: filebeat-config
      configMap:
        name: filebeat-config
        items:
        - key: filebeat.yml
          path: filebeat.yml

configmap.yaml

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: filebeat-config
data:
  filebeat.yml: |
    filebeat.inputs:
    - type: log
      paths:
        - "/log/access.log"
    setup.template.name: "filebeat"
    setup.template.pattern: "filebeat-*"
    output.elasticsearch:
      hosts: ["{{ .Values.elastricsearch.addr }}"]
      index: "frontend-filebeat"

 


架构图

[docker]通过rsyslog记录日志并转发nginx日志到python程序

记录我是如何把rsyslog做成docker镜像,获取nginx的accesslog并且转发到python的

关键点1 nginx日志配置

nginx日志要设置成json格式输出,nginx.conf如下图所示,这个可以在docker镜像中通过volume把nginx.conf挂载进去,然后把/var/log/nginx/access.log挂载到本地

user  root;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';
    log_format main '{"time_local": "$time_local", '
   '"path": "$request_uri", '
   '"ip": "$remote_addr", '
   '"time": "$time_iso8601", '
   '"user_agent": "$http_user_agent", '
   '"user_id_got": "$uid_got", '
   '"user_id_set": "$uid_set", '
   '"remote_user": "$remote_user", '
   '"request": "$request", '
   '"status": "$status", '
   '"body_bytes_sent": "$body_bytes_sent", '
   '"request_time": "$request_time", '
   '"http_referrer": "$http_referer" }';
    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    #tcp_nopush     on;
    keepalive_timeout  65;
    #gzip  on;
    include /etc/nginx/conf.d/*.conf;
}

关键点2 rsyslog配置与Dockerfile

编写一个51-nginx-forward.conf文件放置在/etc/rsyslog.d/下即可

module(load="imfile")
input(type="imfile"
      File="/var/log/nginx/access.log"
      Tag="mywebsite:")
# omfwd module for forwarding the logs to another tcp server
if( $syslogtag == 'mywebsite:')  then {
  action(type="omfwd" target="python服务器IP地址" port="6000" protocol="tcp"
            action.resumeRetryCount="100"
            queue.type="linkedList" queue.size="10000")
}

我们可以用一个Dockerfile来运行rsyslog,docker run的时候注意日志的挂载

FROM ubuntu:16.04
RUN apt-get update && apt-get install -y rsyslog; \
    rm -rf /var/lib/apt/lists/*
ADD 51-nginx-forward.conf /etc/rsyslog.d/.
# RUN cat /dev/null> /var/log/mail.log
CMD service rsyslog start && tail -f /var/log/syslog

关键点3 python程序通过tcp的方式读取rsyslog

python程序与rsyslog建立tcp连接,可以实时的进行数据库的插入语句

import asyncio
import json
import time
import database_init
class LogAnalyser:
    def __init__(self):
        pass
    def process(self, str_input):
        # print(str_input)
        str_input = str_input.decode("utf-8", errors="ignore")
        # Add your processing steps here
        # ...
        try:
            # Extract created_at from the log string
            str_splits = str_input.split("{", 1)
            json_text = "{" + str_splits[1]
            data = json.loads(json_text)
            created_at = data["time"]
            request_all = data["request"].split(" /", 1)
            http_type = request_all[0]
            path = data["path"]
            request_time = data["request_time"]
            if PREFIX in data["path"]:
                path = data["path"]
                return http_type, path, created_at,request_time  # The order is relevant for INSERT query params
        except Exception as e:
            print("error in read_rsylog.py,Class LogAnalyser,function process")
            print(e)
        return None
@asyncio.coroutine
def handle_echo(reader, writer):
    log_filter = LogAnalyser()
    while True:
        line = yield from reader.readline()
        if not line:
            break
        params = log_filter.process(line)
        if params:
            # 进行一堆操作,例如进行数据库的插入
            # execute_sql(params=params)
if __name__ == '__main__':
    CURSOR = database_init.DBConnect().CURSOR
    CONN = database_init.DBConnect().CONN
    PREFIX = database_init.DBConnect().CONFIG["TEST_SWAGGER"]["PREFIX"]
    database_init.DBConnect().create_table()
    loop = asyncio.get_event_loop()
    coro = asyncio.start_server(handle_echo, None, 6000, loop=loop)
    server = loop.run_until_complete(coro)
    # Serve requests until Ctrl+C is pressed
    print('Serving on {}'.format(server.sockets[0].getsockname()))
    try:
        loop.run_forever()
    except KeyboardInterrupt:
        pass
    # Close the server
    print("Closing the server.")
    server.close()
    loop.run_until_complete(server.wait_closed())
    loop.close()
    CURSOR.close()
    CONN.close()

helm更新latest镜像

有不少朋友跟我说,helm更新statefullset或者deployment时,使用latest镜像,无法更新,其实这个问题很好解决的,可以使用git-hash来解决,参考文章https://www.yinyubo.cn/?p=535
也可以使用我们本篇文章里的办法,添加环境变量来解决
helm更新的原理是,yaml文件没有变更,则不会更新,我们要想使用latest镜像先terminating老的pod,再running一个新的pod,只要使我们的statefullset或者deployment的yaml文件发生变更即可。下面贴出解决代码

containers:
        - image: '镜像名:latest'
          imagePullPolicy: Always
          env:
            - name: upgrade_time
              value: {{ date "2006-01-02-150405" .Release.Time }}

镜像使用latest,拉取策略使用alway pull的策略。在环境变量里添加一个upgrade_time升级时间,该时间使用helm的date功能生成,这样我们的yaml就能做到每次helm upgrade都发生变更,每次都能去拉取最新的镜像并且升级。并且我们可以在部署之后,通过kubectl exec -it “pod名字” sh 进入容器,检查env里的upgrade_time看看是否更新。
很简单吧,快试试吧

在K8S里使用NFS做storageclass

参考github:https://github.com/kubernetes-incubator/external-storage/tree/master/nfs-client


1.使用helm安装nfs-client,注意填写nfs服务器的地址和暴露路径

$ helm install stable/nfs-client-provisioner --set nfs.server=x.x.x.x --set nfs.path=/exported/path

2.编写storageclass.yaml并使其生效

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: managed-nfs-storage
provisioner: fuseim.pri/ifs

3.设置这个storageclass为默认storageclass

kubectl patch storageclass nfs-client -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

4.注意这边下载quay.azk8s.cn/external_storage/nfs-client-provisioner:v3.1.0-k8s1.11这个镜像可能很慢,可以调整成国内源下载,再重命名。例如下面的命令

docker pull quay.azk8s.cn/external_storage/nfs-client-provisioner:v3.1.0-k8s1.11
docker tag quay.azk8s.cn/external_storage/nfs-client-provisioner:v3.1.0-k8s1.11 quay.io/external_storage/nfs-client-provisioner:v3.1.0-k8s1.11

K8S重新加入master节点,避免etcd错误

我们有时候会有删除,再重新加入master节点的需求,比如master机器改名。这里注意重新加入时,经常会出现etcd报错,如下

[check-etcd] Checking that the etcd cluster is healthy
error execution phase check-etcd: etcd cluster is not healthy: failed to dial endpoint https://192.168.0.92:2379 with maintenance client: context deadline exceeded

这个时候,就需要去还没有停止的master节点里的etcd的pod里去,删除该老master节点对应的etcd信息。


kubernetes节点信息

master03是我们将要删除重新加入的节点

# root@master01:~# kubectl get nodes
NAME       STATUS   ROLES    AGE   VERSION
master01   Ready    master   64d   v1.17.1
master02   Ready    master   95s   v1.17.1
master03   Ready    master   18h   v1.17.1
slaver01   Ready    <none>   64d   v1.17.1
slaver04   Ready    <none>   13d   v1.17.1
slaver05   Ready    <none>   13d   v1.17.1

删除master

在master01机器上执行

kubectl drain master03
kubectl delete node master03

在master03机器上执行

kubeadm reset
rm -rf /etc/kubernetes/manifests/

删除etcd信息

在master01节点上执行命令,进入etcd的容器里

kubectl exec -it etcd-master01 sh

输入命令

etcdctl --endpoints 127.0.0.1:2379 --cacert /etc/kubernetes/pki/etcd/ca.crt --cert /etc/kubernetes/pki/etcd/server.crt --key /etc/kubernetes/pki/etcd/server.key member list

检查返回值

7d39fc3ab8790afc, started, master03, https://192.168.0.93:2380, https://192.168.0.93:2379, false
b54177b91845ab93, started, master01, https://192.168.0.91:2380, https://192.168.0.91:2379, false
bc771924f2f5445f, started, master02, https://192.168.0.92:2380, https://192.168.0.92:2379, false

因为我们的master03机器对应的hash是7d39fc3ab8790afc。我们下一步就是根据hash删除etcd信息,执行如下命令

etcdctl --endpoints 127.0.0.1:2379 --cacert /etc/kubernetes/pki/etcd/ca.crt --cert /etc/kubernetes/pki/etcd/server.crt --key /etc/kubernetes/pki/etcd/server.key member remove 12637f5ec2bd02b8

获取添加master的命令


在master01上输入命令

kubeadm init phase upload-certs --upload-certs

返回34f76df3029230ca3136f5ff689ed54b1af6501a59fb0ea728ff8fed31ad52b4
再在master01上输入命令

kubeadm token create --print-join-command

返回 kubeadm join cluster.kube.com:16443 –token f4amr0.c2nc87swc7jbybut –discovery-token-ca-cert-hash sha256:2c45bcc43dad9cf43c3b7e610c0cdb7d588213d4258fc060e7384276e664922e
通过组合上面的“蓝色字体部分“+“–control-plane –certificate-key“ +“红色字体部分”,获得加入master的完整命令
kubeadm join cluster.kube.com:16443 –token uerys4.h8z3lfo2j3zf8g2u –discovery-token-ca-cert-hash sha256:2c45bcc43dad9cf43c3b7e610c0cdb7d588213d4258fc060e7384276e664922e –control-plane –certificate-key 34f76df3029230ca3136f5ff689ed54b1af6501a59fb0ea728ff8fed31ad52b4


添加Master节点

执行命令

kubeadm join cluster.kube.com:16443 --token uerys4.h8z3lfo2j3zf8g2u --discovery-token-ca-cert-hash sha256:2c45bcc43dad9cf43c3b7e610c0cdb7d588213d4258fc060e7384276e664922e --control-plane --certificate-key 34f76df3029230ca3136f5ff689ed54b1af6501a59fb0ea728ff8fed31ad52b4

ceph集群无法初始化osd问题

安装ceph的osd时.运行清空磁盘命令

ceph-deploy disk zap node3-ceph /dev/sdb

有时候会遇到这样的报错。

[node3-ceph][WARNIN]  stderr: wipefs: error: /dev/sdb: probing initialization failed: Device or resource busy
[node3-ceph][WARNIN] --> failed to wipefs device, will try again to workaround probable race condition

遇到这种报错时,只能上这台机器,手动进行dd命令清空磁盘并重启

dd if=/dev/zero of=/dev/sdb bs=512K count=1
reboot

重启完成后,再进入ceph-admin的主机进行ceph-deploy disk zap node3-ceph /dev/sdb 就能够清理磁盘成功了

使用helm动态更新k8s里的docker镜像关键点

需求分析

1.我们使用helm来实现应用程序的更新。
2.应用程序更新的关键就是镜像。每次我们的代码合入develop分支之后,都会产生一个新的docker镜像。
3.我们需要让helm知道我们使用了最新的docker镜像。这样部署的应用才是最新的。
4.helm包是通过nexus上传的,从设计上来说,不适合每一次cd流程就产生一个helm包并上传,helm本身也没有提供上传接口。helm包的设计是希望一个helm包能一直通用于某一类程序。
5.综上所述,我们的cd流程的关键是,在helm包不更新的情况下,让helm包能每次cd流程后,通过developmengt.yaml使用最新的docker镜像在K8S上进行部署


使用误区

误以为developmengt.yaml里配置container.image的tag为latest,imagePullPolicy为always,就能每次部署的时候拉取最新的镜像。
接下来,我们看看默认配置


我们经常会错误的配置Chart.Appversion为latest,如下图

helm upgrade [RELEASE] [CHART] [flags]
例如: helm upgrade lizhenwei nginx-chart

原因是:对于helm来说,参数没有任何变化,chart版本也没有变化,所以是不会去更新的


正确用法

我们的chart不会去不停的更新,所以我们要做的是参数的更新。
所以我们要让docker的镜像tag动起来
1.每次的CICD流程除了上传latest镜像以外,还要再上传一个带tag号的镜像,tag号可以用git的commitHash值来区分,一般来说,有个8位的hash值就够了
2.helm的development.yaml里的镜像tag要进行对应的修改,如下

image: "{{ .Values.image.repository }}:{{ .Values.commitHash }}"

Values.commitHash是来自values.yaml文件中的commitHash参数

3.helm的upgrade命令增加comitHash参数。升级命令里增加set参数

helm upgrade -i dev-ui sfere/cloudview-ui --set commitHash=$commitHash

因为commitHash参数的改变,所以对于helm来说,产生了参数变化,helm是会去进行更新操作,这个时候就会去把参数填入development.yaml的image选项里,从而触发拉取新代码,更新应用

在Kubernetes里使用gradle缓存加速编译和docker in docker例子

需求

1.我们的代码编译需要用到gradle6.2版本,jdk13版本,docker in docker策略
2.因为是在CI环境中使用,所以gradle容器会因为流水线的触发,不停的启动和删除。下载jar包会非常消耗时间,我们需要持久化这些gradle缓存。
3.挂载这些gradle缓存文件到机器上,可以用ceph集群和NFS,这里我偷懒,先用NFS做,后期资源充足再换成ceph。
4.因为我们有并行流水线的可能,所以gradle容器可能一次不止一个,而gradle的caches一次只能被一个进程占用,为了避免多容器占用同一个gradle的caches,我们需要有策略。


已经踏过的坑

1.不能使用apline来制作gradle容器,因为我们的代码里有用到protoc,他会在gradle里安装protoc-3.10.1-linux-x86_64.exe,但是这个程序并不兼容alpine系统,最终会导致报错。报错信息如下图

2.不要使用adoptopenjdk/openjdk13:latest镜像来制作Dockerfile,这个镜像用的jdk版本是jdk13 ea版本。这个ea版本和正常的jdk13有一些细微的差点,这会导致gradle最后编译失败,报错信息如下图

3.不要直接把gradle缓存目录直接挂载进去,这会导致多任务时阻塞,同时编译失败,如下图


解决过程

1.创建NFS

NFS的搭建是比较简单的。在网上可以很容易搜索到教程,这里需要注意节点机器上也需要安装NFS工具,apt-get install nfs-common,不然会报错,导致K8S在这个节点上创建不了容器,因为挂载volume就失败了
我们在NFS服务器上创建一个目录,用于存放gradle的缓存

vi /etc/exports
添加
/nfsdata/gradle-cache/ *(rw,sync,no_root_squash,no_subtree_check)

2.创建gradle镜像,用于k8s中编译使用

使用Dockerfile,内容如下

FROM gradle:6.2.0-jdk13
# 因为需要docker in docker 所以这里用阿里云一键安装docker
RUN  curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
# 随意创建一个文件
RUN echo "test" > test.log
# 因为使用了私有仓库,所以之间拷贝docker配置文件进去
COPY daemon.json /etc/docker/daemon.json
# 原来的gradle命令是"jshell"这里我们替换掉,避免容器启动后,程序结束就消失
ENTRYPOINT ["tail","-f","test.log"]

docker build -t gradle_lzw .

3.使用这个gradle镜像运行起来,进行一次编译,把gradle caches拷贝出来,放到NFS中

#运行容器
docker run -d -v /var/run/docker.sock:/var/run/docker.sock gradle_lzw
# 进入容器编译代码, docker exec  -it gradle_lzw gradle XXXXX(gradle编译命令)
# 复制出缓存文件 docker cp gradle:/home/gradle/.gradle /home/temp/local/.gradle

关于gradle的缓存可以看这个https://github.com/keeganwitt/docker-gradle

4.把上一步获得的gradle缓存文件上传到NFS服务器上

scp -r root@{有gradle缓存的机器IP}:/home/temp/local/.gradle /nfsdata/gradle-cache/.

5.为了避免多容器并发占用gradle缓存目录,我们只能绕开直接挂载。我们先把缓存文件挂载到一个无用的目录中,然后再从这个目录复制到gradle指定的缓存目录中l

项目流水线配置

Jenkinsfile内容参考:

pipeline {
    agent {
        kubernetes {
            //label使用项目名称,因为不同的项目,build方式是不同的,如果错误的使用了相同的label。Jenkins就不会去读取BuildPod.yaml
            label 'jnlp-项目名称'
            //
            yamlFile 'BuildPod.yaml'
        }
    }
    stages {
        stage('build') {
         //使用gradle容器
		 container('gradle'){
		 sh '''
         //复制缓存文件
         cp -rf /opt/.gradle/caches /home/gradle/.gradle/caches
         gradle {build 命令}
          '''
          }
		}
	}
}

BuildPod.yaml配置,gradle-cache是缓存目录,dind是docker in docker的必要容器

kind: Pod
metadata:
  labels:
    some-label: some-label-value
spec:
  containers:
  - name: jnlp
    image: jenkins/jnlp-slave
    tty: true
    volumeMounts:
    - name: workspace-volume
      mountPath: /home/jenkins
  - name: gradle
    image: gradle_lzw:latest
    tty: true
    volumeMounts:
    - name: workspace-volume
      mountPath: /home/jenkins
    - name: gradle-cache
      mountPath: /opt/.gradle
    env:
    - name: DOCKER_HOST
      value: tcp://localhost:2375
  - name: dind
    image: docker:18.05-dind
    securityContext:
      privileged: true
    volumeMounts:
      - name: dind-storage
        mountPath: /var/lib/docker
  imagePullSecrets:
    - name: repok8s
  volumes:
  - name: gradle-cache
    nfs:
      server: {NFS服务器IP}
      path: "/nfsdata/gradle-cache/.gradle"
  - name: workspace-volume # pod中有一个volume让其中的多个容器共享
    emptyDir: {}
  - name: dind-storage
    emptyDir: {}

打造K8S与Jenkins的持续集成系统-CI部分

说明

持续集成系统包括CI和CD。在这篇文章里,我们只讨论CI的实现。关于K8S,Jenkins,私有仓库的安装这里不进行说明,只讲解如何将这些工具组合在一起。

1.拥有一个K8S集群

我的环境是6台虚拟机,3台Master,3台Slaver。如下图所示

运行结果如下图:

2.Jenkins中安装好了kubernetes插件


安装好之后,在系统设置中,能够添加kubernetes信息

3.根据Jenkins的配置要求,去自己的K8S平台上找到对应的信息。填入Jenkins中


我们根据图中的要求,可以在K8S里进行一个个的查找我们想要的信息

字段
查找方法
Kubernetes 地址
IP地址是在keepalived里配置的虚拟IP

在搭建k8S平台时,用到了haproxy,当时指向了**443端口
kubernetes名称 随意填写
kubernetes命名空间 需要在K8S里创建一个命名空间,然后在这个地方使用
凭据
1.进入K8S机器上,输入命令kubectl config view –minify –flatten查看凭据

2.把凭据信息复制成一个文件

3.在Jenkins里上传这个文件

4.这个凭据信息即可在Jenkins配置里使用
JENKINS地址 配置本次使用的Jenkins的url即可
Pod Retention 选择Never,Jenkins任务完成后就删除pod

4.添加私有密钥用于拉取我们的docker私有镜像

在K8S里输入如下命令
kubectl create secret docker-registry repo-k8s \
-n k8s-jx \
--docker-server='repo.local:5000' \
--docker-username='
zhenwei.li
' \
--docker-password='*******' \
--docker-email='357244849@qq
.com
'

5.在Jenkins里编写一个最简单的流水线任务。来测试Jenkins是否可以拉起K8S的节点,产生pod

5.1.从github上抄一段代码示例下来https://github.com/jenkinsci/kubernetes-plugin/blob/master/src/test/resources/org/csanchez/jenkins/plugins/kubernetes/pipeline/jnlpWorkingDir.groovy

podTemplate(label:'jnlp-slave',cloud: 'kubernetes',containers: [
    containerTemplate(name: 'busybox', image: 'nginx:latest', ttyEnabled: true)
],imagePullSecrets:['secret/repok8s']) {
    node ('jnlp-slave') {
        stage('Run') {
            sh 'env | sort'
            container('busybox') {
                sh 'echo "hello"'
                sh 'pwd'
                sh 'ls'
            }
        }
    }
}

5.2创建一个流水线任务

5.3运行这个流水线任务,查看结果

5.4在K8S上输入查找pod的命令,应该会发现有一个pod被运行起来了,名字中含有jnlp-slave的字样,2/2运行两个容器,一个是jnlp容器,一个是busybox容器

kubectl get pods --all-namespaces -o wide

6.在代码仓库中添加CI所需要的Jenkinsfile和BuildPod.yaml等流水线文件

文件名
用处
sonar-project.properties 定义代码需要执行什么语言的扫描,哪些目录需要剔除
JenkinsfileTag 定义打tag操作对应的流水线行为
Jenkinsfile 定义代码提交与合并的流水线行为
BuildPod.yaml K8S产生的pod中,包含哪些容器,容器的环境变量等参数设置

7.BuildPod.yaml中需要包含哪些内容(以nodejs代码举例)

8.Jenkinsfile中需要包含哪些内容

最后在Jenkins里查看运行效果


 

使用dd命令在linux系统里制作U盘装系统

需求

我们需要在一台全新的机器上安装一台新的debian机器。

准备工作:

1.一台已经有linux系统的机器(一般来说,linux机器都会有dd命令)
2.一个U盘
3.一个待安装的全新机器

实施步骤

1.在这台已经有linux系统的机器上下载debian的ISO镜像。放在指定目录下,如:

/home/sfere/debian-10.1.0-amd64-DVD-1.iso

2.把U盘插入这台linux电脑里,输入命令

fdisk -l

检查硬盘挂载情况

3.使用dd命令,把U盘做成启动盘。此步需要注意,上面看到我们的28.9G的U盘是/dev/sdb4。但是我们下面的命令里要用/dev/sdb而不要加最后的4。如果加了4会导致下一步安装失败

dd if=/home/sfere/debian-10.1.0-amd64-DVD-1.iso of=/dev/sdb bs=8M;sync

完成之后,效果如下:

root@debian:~# dd if=/home/sfere/debian-10.1.0-amd64-DVD-1.iso of=/dev/sdb bs=4M;sync
930+1 records in
930+1 records out
3901456384 bytes (3.9 GB, 3.6 GiB) copied, 563.589 s, 6.9 MB/s

4.把U盘拔下来,插入另外一台全新的电脑中,bios启动顺序中,选择USB为第一位启动。即可开始安装


苏ICP备18047533号-2