一、DeOps简介

1.什么是DeOps?

 1.1 敏捷开发

  提高开发效率,及时跟进用户需求,缩短开发周期。

  敏捷开发包括编写代码和构建代码两个阶段,可以使用 git 或者 svn 来管理代码,用 maven 对代码 进行构建

1.2 持续集成

持续集成强调开发人员提交了新代码之后,立刻自动的进行构建、(单元)测试。根据测试结果,我 们可以确定新代码和原有代码能否正确地集成在一起。持续集成过程中很重视自动化测试验证结果,对 可能出现的一些问题进行预警,以保障最终合并的代码没有问题。

 二、常见的持续集成工具

2.1 持续集成 (CI)

1.Jenkins
Jenkins 是用 Java 语言编写的,是目前使用最多和最受欢迎的持续集成工具,使用 Jenkins,可以自动监测到 git 或者 svn 存储库代码的更新,基于最新的代码进行构建,把构建好的源码或者镜像发布到生产环境。Jenkins
还有个非常好的功能:它可以在多台机器上进行分布式地构建和负载测试
2. TeamCity
3. Travis CI
4. Go CD
5. Bamboo
6. GitLab CI
7. Codeship

它的好处主要有以下几点:

1)较早的发现错误:每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,哪个环节出现问题都可以较早的发现

2)快速的发现错误:每完成一部分代码的更新,就会把代码集成到主干中,这样就可以快速的发现 错误,比较容易的定位错误

3)提升团队绩效:持续集成中代码更新速度快,能及时发现小问题并进行修改,使团队能创造出更 好的产品

4)防止分支过多的偏离主干:经常持续集成,会使分支代码经常向主干更新,当单元测试失败或者出现 bug,如果开发者需要在没有调试的情况下恢复仓库的代码到没有 bug 的状态,只有很小部分的代码会丢失

持续集成的目的是提高代码质量,让产品快速的更新迭代。它的核心措施是,代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。

与持续集成相关的还有持续交付和持续部署

2.2 持续交付 

持续交付在持续集成的基础上,将集成后的代码部署到更贴近真实运行环境的「类生产环境」 (production-like environments)中。交付给质量团队或者用户,以供评审。如果评审通过,代码就进入生产阶段

如果所有的代码完成之后一起交付,会导致很多问题爆发出来,解决起来很麻烦,所以持续集成, 也就是没更新一次代码,都向下交付一次,这样可以及时发现问题,及时解决,防止问题大量堆积。

2.3 持续部署

持续部署是指当交付的代码通过评审之后,自动部署到生产环境中。持续部署是持续交付的最高阶段。

Puppet,SaltStack 和 Ansible 是这个阶段使用的流行工具。容器化工具在部署阶段也发挥着重要用。 Docker 和 k8s 是流行的工具,有助于在开发,测试和生产环境中实现一致性。 除此之外,k8s 还 可以实现自动扩容缩容等功能。

 三、K8S 在 DevOps 中的核心作用

Docker 和 K8S 的出现使 DevOps 变得更加普及,更加容易实现。在传统运维中我们服务时需要针对不 同的环境去安装不同的版本,部署方式也杂、多。那么有了 docker 之后,一次构建、到处运行,我们只 需要要构建一次镜像,那么只要有 docker 的主机,就可以基于镜像把应用跑起来。

互动:docker 可以实现 DevOps 的这个思想,但是存在一个问题,什么问题呢?

在众多微服务中,我们每天可能需要去处理各种服务的崩溃,而服务间的依赖调用关系也及其复 杂,这对我们解决问题带来了很大的复杂度。要很好的解决这个问题。我们就需要用到容器编排工具。

Kubernetes 的出现主宰了容器编排的市场,也进化了过去的运维方式,将开发与运维联系的更加紧 密。而且让 DevOps 这一角色变得更加清晰,它是目前可用的很流行的容器解决方案之一。

3.1 自动化

敏捷开发->持续集成->持续交付->持续部署

3.2 多集群管理

可以根据客户需求对开发,测试,生产环境部署多套 kubernetes 集群,每个环境使用独立的物理资 源,相互之间避免影响

3.3 多环境一致性

Kubernetes 是基于 docker 的容器编排工具,因为容器的镜像是不可变的,所以镜像把 OS、业务代 码、运行环境、程序库、目录结构都包含在内,镜像保存在我们的私有仓库,只要用户从我们提供的私有仓库拉取镜像,就能保证环境的一致性。

3.4 实时反馈和智能化报表

每次集成或交付,都会第一时间将结果通过多途径的方式反馈给你,也可以定制适合企业专用的报 表平台。

四、基于 Jenkins+K8S+harbor+git 等技术链助力 DevOps 在企业落地

1)神州泰岳 k8s+DevOps+微服务生态体系建设与实践

开发代码->提交代码到代码仓库->Jenkins 调 k8s API->动态生成 Jenkins Slave Pod->Slave Pod 拉取 git 上的代码->编译代码->打包镜像->推送镜像到镜像仓库 harbor 或者 docker hub->通过 k8s 编排服 务发布到测试、生产平台-> Slave Pod 工作完成之后自动删除>通过 Ingress 发布服务

2)百度:基于 k8s 构建亿级 PV 流量的 DevOps 平台

DevOps流程图:

五、基于 Jenkins+k8s+Git+DockerHub 等技术链构建企业级 DevOps 容器云平台

在 k8s 集群安装jenkins

  Jenkins 版本是 2.328

  Kubernetes 集群版本是 1.20.7/1.23

https://www.jenkins.io/zh/doc/book/installing/

 5.1 安装 nfs 服务

# 安装nfs服务,选择任意一台机器:master
[root@master ~]# yum install nfs-utils -y
[root@master ~]# systemctl start nfs && systemctl enable nfs
# 节点安装nfs
yum install nfs-utils -y
systemctl start nfs && systemctl enable nfs && systemctl status nfs
# 配置nfs共享目录
[root@master ~]# cat /etc/exports
/data/v1 *(rw,no_root_squash)
/data/v2 *(rw,no_root_squash)
[root@master ~]# exportfs -arv
[root@master ~]# systemctl restart nfs

5.2 在kubernetes中安装jenkins

# 1.创建名称空间
[root@master ~]# kubectl create namespace jenkins-k8s
namespace/jenkins-k8s created # 2.创建pv
[root@master jenkis]# cat pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: jenkins-k8s-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany # 多路读写
nfs:
server: 192.168.10.10
path: /data/v2 [root@master jenkis]# kubectl apply -f pv.yaml
persistentvolume/jenkins-k8s-pv created # 3.创建pvc
[root@master jenkis]# cat pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: jenkins-k8s-pvc
namespace: jenkins-k8s
spec:
resources:
requests:
storage: 10Gi
accessModes:
- ReadWriteMany [root@master jenkis]# kubectl apply -f pvc.yaml
persistentvolumeclaim/jenkins-k8s-pvc created [root@master jenkis]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
jenkins-k8s-pv 10Gi RWX Retain Bound jenkins-k8s/jenkins-k8s-pvc 87s
[root@master jenkis]# kubectl get pvc -n jenkins-k8s
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
jenkins-k8s-pvc Bound jenkins-k8s-pv 10Gi RWX 54s # 4.创建sa账号
[root@master jenkis]# kubectl create sa jenkins-k8s-sa -n jenkins-k8s
serviceaccount/jenkins-k8s-sa created # 5.将上面sa账号做rbac授权:将jenkins-k8s-sa通过clusterrolebinding绑定到clusterrole集群管理员上拥有该集群admin权限
[root@master jenkis]# kubectl create clusterrolebinding jenkins-k8s-sa-cluster -n jenkins-k8s --clusterrole=cluster-admin --serviceaccount=jenkins-k8s:jenkins-k8s-sa
clusterrolebinding.rbac.authorization.k8s.io/jenkins-k8s-sa-cluster created # 6.节点拉取jenkins镜像,下载最新的镜像,避免出现插件不可用的情况,jenkins-jnlp要用匹配的镜像
[root@node1 ~]# docker pull jenkins/jenkins:latest [root@node2 ~]# docker pull jenkins/jenkins:latest
[root@node1 ~]# docker load -i jenkins-jnlp.tar.gz
[root@node2 ~]# docker load -i jenkins-jnlp.tar.gz
# 7.通过deployment部署jenkins
[root@master jenkis]# kubectl apply -f jenkins-deployment.yaml
deployment.apps/jenkins created
[root@master jenkis]# kubectl get pods -n jenkins-k8s
NAME READY STATUS RESTARTS AGE
jenkins-578dc4f66f-5tzrx 0/1 CrashLoopBackOff 1 21s
[root@master jenkis]# kubectl logs jenkins-578dc4f66f-5tzrx -n jenkins-k8s
touch: cannot touch '/var/jenkins_home/copy_reference_file.log': Permission denied
Can not write to /var/jenkins_home/copy_reference_file.log. Wrong volume permissions
[root@master jenkis]# kubectl delete -f jenkins-deployment.yaml
deployment.apps "jenkins" deleted # 1000为jenkins属组
[root@master jenkis]# chown -R 1000.1000 /data/v2
[root@master jenkis]# kubectl apply -f jenkins-deployment.yaml
deployment.apps/jenkins created
[root@master jenkis]# kubectl get pods -n jenkins-k8s
NAME READY STATUS RESTARTS AGE
jenkins-578dc4f66f-dpmkb 0/1 Running 0 11s
# 60s后就绪
[root@master jenkis]# kubectl get pods -n jenkins-k8s
NAME READY STATUS RESTARTS AGE
jenkins-578dc4f66f-dpmkb 1/1 Running 0 108s

[root@master jenkis]# cat jenkins-deployment.yaml
kind: Deployment
apiVersion: apps/v1
metadata:
name: jenkins
namespace: jenkins-k8s
spec:
replicas: 1
selector:
matchLabels:
app: jenkins
template:
metadata:
labels:
app: jenkins
spec:
serviceAccount: jenkins-k8s-sa
containers:
- name: jenkins
image: docker.io/jenkins/jenkins:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: web
protocol: TCP
- containerPort: 50000
name: agent
protocol: TCP
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /login
port: 8080
initialDelaySeconds: 60
timeoutSeconds: 5
failureThreshold: 12
readinessProbe:
httpGet:
path: /login
port: 8080
initialDelaySeconds: 60
timeoutSeconds: 5
failureThreshold: 12
volumeMounts:
- name: jenkins-volume
subPath: jenkins-home
mountPath: /var/jenkins_home
volumes:
- name: jenkins-volume
persistentVolumeClaim:
claimName: jenkins-k8s-pvc

Jenkins-deployment.yaml

[root@master jenkis]# kubectl get pods -n jenkins-k8s -l app=jenkins -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
jenkins-578dc4f66f-dpmkb 1/1 Running 0 6m44s 10.244.166.130 node1 <none> <none> # 8.在jenkins前端加上service,提供外部网络访问
# 更新资源清单
[root@master jenkis]# cat jenkins-service.yaml
apiVersion: v1
kind: Service
metadata:
name: jenkins-service
namespace: jenkins-k8s
labels:
app: jenkins
spec:
selector:
app: jenkins # 通过标签找到jenkins的pod
type: NodePort
ports:
- name: web
port: 8080
targetPort: web
nodePort: 30002
- name: agent
port: 50000
targetPort: agent [root@master jenkis]# kubectl apply -f jenkins-service.yaml
service/jenkins-service created
[root@master jenkis]# kubectl get svc -n jenkins-k8s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
jenkins-service NodePort 10.99.12.14 <none> 8080:30002/TCP,50000:31417/TCP 11s

访问:http://192.168.10.10:30002/

5.3 安装jenkins插件

[root@master jenkis]# cat /data/v2/jenkins-home/secrets/initialAdminPassword
b838fa03a4c24f5cac632e18d4c09670

网络安装:失败也没关系,可以自己装,需要时间比较长

创建账号密码:admin/admin

 5.4 Jenkins安装blueocean和kubernetes插件

安装kubernetes插件

Manager Jenkins --> 插件管理--->可选插件中搜索“kubernetes” -->"Download now and install after restart"--->安装完成后重启登录jenkins;如果发现有插件安装失败或者一部分失败,可以再安装一次

安装blueocean插件

Manager Jenkins --> 插件管理--->可选插件中搜索“blueocean”--> "Download now and install after restart"

5.5 配置jenkins链接到k8s集群

系统管理Manager Jenkins --->系统配置--->Clount(The cloud configuration has moved to a separate configuration page.)--->点击“a separate configuration page.”--->配置集群"选择kubernetes" ---> 点击"Kubernetes Cloud"

内容填写:

填写“kubernetes地址”:https://192.168.10.10:6443

Kubernetes 命名空间:jenkins-k8s  ---->链接测试:Connected to Kubernetes v1.20.7

Jenkins 地址:http://jenkins-service.jenkins-k8s.svc.cluster.local:8080  (kubectl get svc -n jenkins-k8s查看名称)

点击pod模板:Pod Template details--->添加pod模板---->名称:test,标签列表:testhan --->添加卷:Host Path Volume-->主机路径:/var/run/docker.sock 挂载路径:/var/run/docker.sock --->继续添加卷:主机路径:"/root/.kube" ,挂载路径:"/home/jenkins/.kube"

点击“容器列表Container Template”---》名称:固定 "jnlp",Docker镜像拉取地址:jenkins-jnlp:v1,清空“运行的命令”和“命令参数”,勾选:分配伪终端

Service Account:jenkins-k8s-sa

点击:Apply应用---点击:save

 5.5 添加自己的dockhub凭据

系统管理---》Manage Credentials----->点击“全局”:添加凭据

这些内容之后会被引用,用户名为dockerhub的username,密码也是dockerhub密码,ID定义后,之后流水线Pipline Script脚本内会引用

 5.6 测试通过 Jenkins 部署应用发布到 k8s 开发环境、测试环境、生产环境

任务:

  开发提交代码到代码仓库 gitlab-→jenkins 检测到代码更新-→调用 k8s api 在 k8s 中创建 jenkins slave pod:

    Jenkins slave pod 拉取代码---→通过 maven 把拉取的代码进行构建成 war 包或者 jar 包--->上 传代码到 Sonarqube,进行静态代码扫描- -->基于 war 包构建 docker image-->把镜像上传到 harbor 镜像仓库-->基于镜像部署应用到开发环境-->部署应用到测试环境--->部署应用到生产环 境。

#下面在 Pipeline Script 输入的脚本内容

#在k8s的控制节点创建名称空间

[root@master jenkis]# kubectl create ns devlopment
namespace/devlopment created
[root@master jenkis]# kubectl create ns production
namespace/production created
[root@master jenkis]# kubectl create ns qatest
namespace/qatest created

回到Jenkins首页---》点击“新建任务”---》任务名称:jenkins-variable-test-deploy,流水线--->点击确定------定义:Pipline Script脚本---应用,保存

Fork:https://github.com/luckylucky421/jenkins-sample 到自己的github

Pipline Script脚本内容:

脚本中账号密码就是5.5中配置的dockerhub的凭据

  withCredentials([usernamePassword(credentialsId: 'dockerhub', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')])

// testhan 之前创建的模板
node('testhan') {
stage('Clone') {
echo "1.Clone Stage"
// 克隆代码,一般自建gitlab
git url: "https://github.com/simon-xiong/jenkins-sample.git"
script {
// 标识代码是否改变
build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
}
stage('Test') {
echo "2.Test Stage" }
// 构建镜像
stage('Build') {
echo "3.Build Docker Image Stage"
sh "docker build -t simonxiong/jenkins-demo:${build_tag} ."
}
stage('Push') {
echo "4.Push Docker Image Stage"
// 上传到镜像仓库:修改用户名密码和credentialsId
withCredentials([usernamePassword(credentialsId: 'dockerhub', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')]) {
sh "docker login -u ${dockerHubUser} -p ${dockerHubPassword}"
sh "docker push simonxiong/jenkins-demo:${build_tag}"
}
}
// 部署命令到开发环境
stage('Deploy to dev') {
echo "5. Deploy DEV"
sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s-dev.yaml"
sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s-dev.yaml"
// sh "bash running-devlopment.sh"
sh "kubectl apply -f k8s-dev.yaml --validate=false"
}
stage('Promote to qa') {
def userInput = input(
id: 'userInput', message: 'Promote to qa?',
parameters: [
[
$class: 'ChoiceParameterDefinition',
choices: "YES\nNO",
name: 'Env'
]
]
)
echo "This is a deploy step to ${userInput}"
if (userInput == "YES") {
sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s-qa.yaml"
sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s-qa.yaml"
// sh "bash running-qa.sh"
sh "kubectl apply -f k8s-qa.yaml --validate=false"
sh "sleep 6"
sh "kubectl get pods -n qatest"
} else {
//exit
}
}
stage('Promote to pro') {
def userInput = input( id: 'userInput',
message: 'Promote to pro?',
parameters: [
[
$class: 'ChoiceParameterDefinition',
choices: "YES\nNO",
name: 'Env'
]
]
)
echo "This is a deploy step to ${userInput}"
if (userInput == "YES") {
sh "sed -i 's/<BUILD_TAG>/${build_tag}/' k8s-prod.yaml"
sh "sed -i 's/<BRANCH_NAME>/${env.BRANCH_NAME}/' k8s-prod.yaml"
// sh "bash running-production.sh"
sh "cat k8s-prod.yaml"
sh "kubectl apply -f k8s-prod.yaml --record --validate=false"
}
}
}

Pipline Script内容

在首页可以看到

点击:jenkins-variable-test-deploy----》立即构建

# jenkins运行中会创建pod
[root@master jenkis]# kubectl get pods -n jenkins-k8s
NAME READY STATUS RESTARTS AGE
jenkins-578dc4f66f-dpmkb 1/1 Running 0 6h43m
test-xjw1j 1/1 Running 0 14m
[root@master jenkis]# kubectl get all -n qatest
NAME READY STATUS RESTARTS AGE
pod/jenkins-demo-7cf486ff75-x7h7d 1/1 Running 0 2m54s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/demo NodePort 10.107.65.122 <none> 18888:31891/TCP 2m54s NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/jenkins-demo 1/1 1 1 2m54s NAME DESIRED CURRENT READY AGE
replicaset.apps/jenkins-demo-7cf486ff75 1 1 1 2m54s
[root@master jenkis]# kubectl get all -n devlopment
NAME READY STATUS RESTARTS AGE
pod/jenkins-demo-7cf486ff75-jptpm 1/1 Running 0 14m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/demo NodePort 10.99.158.15 <none> 18888:31889/TCP 14m NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/jenkins-demo 1/1 1 1 14m NAME DESIRED CURRENT READY AGE
replicaset.apps/jenkins-demo-7cf486ff75 1 1 1 14m # 上面jenkins截图中没有点击yes,一直卡在这一步
# 点击yes前
[root@master jenkis]# kubectl get all -n production
No resources found in production namespace. # 点击yes后
[root@master jenkis]# kubectl get all -n production
NAME READY STATUS RESTARTS AGE
pod/jenkins-demo-7cf486ff75-65kjj 1/1 Running 0 13s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/jenkins-demo NodePort 10.100.137.104 <none> 18888:31890/TCP 13s NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/jenkins-demo 1/1 1 1 13s NAME DESIRED CURRENT READY AGE
replicaset.apps/jenkins-demo-7cf486ff75 1 1 1 13s

访问:http://192.168.10.10:31891/;dockerhub中可以查看到已经上传了的版本

研发、测试和正式环境中配置文件都不相同,可以使用configmap来处理

首页中有blueocean选项:

如果代码改变了,例如修改下github上首页内容:static/index.html中随便改点什么

# 原来的
[root@master jenkis]# kubectl get all -n devlopment
NAME READY STATUS RESTARTS AGE
pod/jenkins-demo-7cf486ff75-jptpm 1/1 Running 0 33m [root@master jenkis]# kubectl describe pod/jenkins-demo-7cf486ff75-jptpm -n devlopment
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 35m default-scheduler Successfully assigned devlopment/jenkins-demo-7cf486ff75-jptpm to node2
Normal Pulled 35m kubelet Container image "simonxiong/jenkins-demo:489e102" already present on machine
Normal Created 35m kubelet Created container jenkins-demo
Normal Started 35m kubelet Started container jenkins-demo

点击ocean---“重运行”---每一步都会重新走一次

# 现在的
[root@master jenkis]# kubectl get all -n qatest
NAME READY STATUS RESTARTS AGE
pod/jenkins-demo-c9f668766-4k2zr 1/1 Running 0 17s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/demo NodePort 10.107.65.122 <none> 18888:31891/TCP 58m [root@master jenkis]# kubectl describe pod/jenkins-demo-c9f668766-4k2zr -n qatest
Normal Scheduled 31s default-scheduler Successfully assigned qatest/jenkins-demo-c9f668766-4k2zr to node2
Normal Pulled 30s kubelet Container image "simonxiong/jenkins-demo:02f8909" already present on machine

访问:http://192.168.10.10:31891/

5.7 测试Jenkins按照指定版本回滚业务

回到首页: 新建一个任务------>输入一个任务名称处输入 jenkins-variable-test-deploy-rollout------>流水线------>确定------>在 Pipeline script 处输入如下内容


[root@master jenkis]# kubectl get all -n devlopment
NAME READY STATUS RESTARTS AGE
pod/jenkins-demo-c9f668766-5zjs6 1/1 Running 0 15h

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/demo NodePort 10.99.158.15 <none> 18888:31889/TCP 16h

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/jenkins-demo 1/1 1 1 16h

NAME DESIRED CURRENT READY AGE
replicaset.apps/jenkins-demo-7cf486ff75 0 0 0 16h
replicaset.apps/jenkins-demo-c9f668766 1 1 1 15h

# 查看可回滚的版本
[root@master jenkis]# kubectl rollout history deploy/jenkins-demo -n devlopment
deployment.apps/jenkins-demo
REVISION CHANGE-CAUSE
1 <none>
2 <none> # getVersion.sh 获取回滚版本号
kubectl rollout history deploy/jenkins-demo -n <namespace> | grep -v "deployment" | grep -v "REVISION" | awk '{print $1}' > version.csv # rollout.sh 进行回滚
kubectl rollout undo deployment jenkins-demo --to-revision=<version> -n <namespace>

node('testhan') {
stage('git clone') {
git url: "https://github.com/simon-xiong/jenkins-rollout"
sh "ls -al"
sh "pwd"
}
stage('select env') {
def envInput = input(
id: 'envInput',
message: 'Choose a deploy environment',
parameters: [
[
$class: 'ChoiceParameterDefinition',
choices: "devlopment\nqatest\nproduction",
name: 'Env'
]
]
)
echo "This is a deploy step to ${envInput}"
sh "sed -i 's/<namespace>/${envInput}/' getVersion.sh"
sh "sed -i 's/<namespace>/${envInput}/' rollout.sh"
sh "bash getVersion.sh"
// env.WORKSPACE = pwd()
// def version = readFile "${env.WORKSPACE}/version.csv"
// println version
}
stage('select version') {
env.WORKSPACE = pwd()
def version = readFile "${env.WORKSPACE}/version.csv"
println version
def userInput = input(id: 'userInput',
message: '选择回滚版本',
parameters: [
[
$class: 'ChoiceParameterDefinition',
choices: "$version\n",
name: 'Version'
]
]
)
sh "sed -i 's/<version>/${userInput}/' rollout.sh"
}
stage('rollout deploy') {
sh "bash rollout.sh"
}
}

回滚rollout-pipeline-script

构建任务成功后,就会回滚到指定版本的镜像

按照指定可以回滚从测试-----开发----生产:注意版本号可能一样但是

六、Jenkins Pipeline语法介绍

6.1 Jenkins Pipeline介绍

Jenkins pipeline (流水线)是一套运行于 jenkins 上的工作流框架,将原本独立运行于单个或者 多个节点的任务连接起来,实现单个任务难以完成的复杂流程编排与可视化。它把持续提交流水线 (Continuous Delivery Pipeline)的任务集成到 Jenkins 中。

  pipeline 是 jenkins2.X 最核心的特性, 帮助 jenkins 实现从 CI 到 CD 与 DevOps 的转变

持续提交流水线(Continuous Delivery Pipeline)会经历一个复杂的过程: 从版本控制、向用户 和客户提交软件,软件的每次变更(提交代码到仓库)到软件发布(Release)。这个过程包括以一种可 靠并可重复的方式构建软件,以及通过多个测试和部署阶段来开发构建好的软件(称为 Build)

总结:

  1.Jenkins Pipeline 是一组插件,让 Jenkins 可以实现持续交付管道的落地和实施。

  2.持续交付管道(CD Pipeline)是将软件从版本控制阶段到交付给用户或客户的完 整过程的自动化表现

  3.软件的每一次更改(提交到源代码管理系统)都要经过一个复杂的过程才能被发布。+

6.2 为什么要用Jenkins Pipeline

本质上,Jenkins 是一个自动化引擎,它支持许多自动模式。 Pipeline 向 Jenkins 中添加了一组 强大的工具, 支持简单的 CI 到全面的 CD pipeline。通过对一系列的相关任务进行建模,

用户可以利用 pipeline 的很多特性:

  1、代码:Pipeline 以代码的形式实现,使团队能够编辑,审查和迭代其 CD 流程。

  2、可持续性:Jenkins 重启或者中断后都不会影响 Pipeline Job。

  3、停顿:Pipeline 可以选择停止并等待人工输入或批准,然后再继续 Pipeline 运行

  4、多功能:Pipeline 支持现实复杂的 CD 要求,包括循环和并行执行工作的能力。

  5、可扩展:Pipeline 插件支持其 DSL 的自定义扩展以及与其他插件集成的多个选项。

DSL 是什么?

DSL 其实是 Domain Specific Language 的缩写,中文翻译为领域特定语言(下简称 DSL);而 与 DSL 相对的就是 GPL,这里的 GPL 并不是我们知道的开源许可证,而是 General Purpose Language 的简称,即通用编程语言,也就是我们非常熟悉的 Objective-C、Java、Python 以及 C 语言等等。

6.3 Jenkins pipeline入门

pipeline 脚本是由 groovy 语言实现的 但无需专门学习 groovy

pipeline 支持两种语法:

   -Declarative:声明式

  -Scripted pipeline :脚本式

声明式 pipeline 语法:

  所有有效的声明式流水线必须包含在一个 pipeline 块中, 比如:

pipeline {
/* insert Declarative Pipeline here */
}

官网: https://www.jenkins.io/doc/book/pipeline/syntax/

   https://www.jenkins.io/zh/doc/book/pipeline/syntax/

声明式语法包括以下核心流程:

  1.pipeline : 声明其内容为一个声明式的 pipeline 脚本

  2.agent: 执行节点(job 运行的 slave 或者 master 节点)

   3.stages: 阶段集合,包裹所有的阶段(例如:打包,部署等各个阶段)

  4.stage: 阶段,被 stages 包裹,一个 stages 可以有多个 stage

  5.steps: 步骤,为每个阶段的最小执行单元,被 stage 包裹

  6.post: 执行构建后的操作,根据构建结果来执行对应的操作

根据上面流程创建一个简单的 pipeline:

pipeline{
agent any # 任何节点可以执行
stages{
stage("This is first stage"){
steps("This is first step"){
echo "I am simon"
}
}
}
post{
always{
echo "The process is ending"
}
}
} Pipeline:
作用域:应用于全局最外层,表明该脚本为声明式 pipeline
是否必须:必须 agent:
作用域:可用在全局与 stage 内
agent 表明此 pipeline 在哪个节点上执行
是否必须:是
参数:any,none, label, node,docker,dockerfile agent any
#运行在任意的可用节点上 agent none
#全局不指定运行节点,由各自 stage 来决定 agent { label 'master' }
#运行在指定标签的机器上,具体标签名称由 agent 配置决定 agent {
node {
label 'my-defined-label'
customWorkspace 'xxxxxxx'
}
}
#node{ label 'master'} 和 agent { label 'master' }一样,但是 node 可以扩展节点信息,允许额外的选项 (比如 customWorkspace )。 agent { docker 'python' }
#使用指定的容器运行流水线
agent {
docker {
image 'maven:3-alpine'
label 'my-defined-label'
args '-v /tmp:/tmp'
}
}
#定义此参数时,执行 Pipeline 或 stage 时会动态的在具有 label 'my-defined-label'标签的node 提供 docker 节点去执行 Pipelines。 docker 还可以接受一个 args,直接传递给 docker run 调用。
agent {
// Equivalent to "docker build -f Dockerfile.build --build-arg version=1.0.2 ./build/
dockerfile {
filename 'Dockerfile.build'
dir 'build'
label 'my-defined-label'
additionalBuildArgs '--build-arg version=1.0.2'
}
}

6.4 Pipeline声明式语法Declarative

6.4.1 environment

environment 指令指定一系列键值对,这些键值对将被定义为所有 step 或 stage-specific step 的环境变量,具体取决于 environment 指令在 Pipeline 中的位置。该指令支持一种特殊的方法 credentials(),可以通过其在 Jenkins 环境中的标识符来访问预定义的凭据。对于类型为“Secret Text” 的凭据,该 credentials()方法将确保指定的环境变量包含 Secret Text 内容;对于“标准用户名和密码” 类型的凭证,指定的环境变量将被设置为 username:password 并且将自动定义两个附加的环境变量:MYVARNAME_USR 和 MYVARNAME_PSW。

可以构架后看看,会输出环境变量:

pipeline {
agent any
environment {
CC = 'clang'
}
stages {
stage('Example') {
steps {
sh 'printenv'
}
}
}
}

6.4.2 options

options 指令允许在 Pipeline 本身内配置 Pipeline 专用选项。Pipeline 本身提供了许多选项,例 如 buildDiscarder,但它们也可能由插件提供,例如 timestamps。

可用选项:

buildDiscarder: pipeline 保持构建的最大个数。用于保存 Pipeline 最近几次运行的数据,例如:options { buildDiscarder(logRotator(numToKeepStr: '1')) }

disableConcurrentBuilds: 不允许并行执行 Pipeline,可用于防止同时访问共享资源等。例如: options { disableConcurrentBuilds() }

skipDefaultCheckout:跳过默认设置的代码 check out。例如:options { skipDefaultCheckout() }

skipStagesAfterUnstable: 一旦构建状态进入了“Unstable”状态,就跳过此 stage。例如: options { skipStagesAfterUnstable() }

timeout: 设置 Pipeline 运行的超时时间,超过超时时间,job 会自动被终止,例如:options { timeout(time: 1, unit: 'HOURS') }

retry: 失败后,重试整个 Pipeline 的次数。例如:options { retry(3) }

timestamps: 预定义由 Pipeline 生成的所有控制台输出时间。例如:options { timestamps() }

pipeline {
agent any
options {
timeout(time: 1, unit: 'HOURS')
}
stages {
stage('Example') {
steps {
echo 'Hello World'
}
}
}
}

6.4.3 parameters

parameters 指令提供用户在触发 Pipeline 时的参数列表。这些参数值通过该 params 对象可用于 Pipeline stage 中,具体用法如下:

作用域:被最外层 pipeline 所包裹,并且只能出现一次,参数可被全局使用

好处:使用 parameters 好处是能够使参数也变成 code,达到 pipeline as code,pipeline 中设置 的参数会自动在 job 构建的时候生成,形成参数化构建

可用参数:
string:
A parameter of a string type, for example: parameters { string(name: 'DEPLOY_ENV', defaultValue: 'staging', description: '') }
booleanParam:
A boolean parameter, for example: parameters { booleanParam(name: 'DEBUG_BUILD', defaultValue: true, description: '') }
目前只支持[booleanParam, choice, credentials, file, text, password, run, string]这几种参数类型,其他高级参数化类型还需等待社区支持。

# 需要根据Build with Parameters进行构建,否则会报错
pipeline{
agent any
parameters {
string(name: 'simon', defaultValue: 'my name is simon', description: 'My name is simon')
booleanParam(name: 'simon43210', defaultValue: true, description: 'This is my wechat')
}
stages{
stage("stage1"){
steps{
echo "$simon"
echo "$simon43210"
}
}
}
}

如果不勾选simon43210,console output虽然输出内容,但是是false

 6.4.4 tiggers

triggers 指令定义了 Pipeline 自动化触发的方式。目前有三个可用的触发器:cron 和 pollSCM 和 upstream

用域:被 pipeline 包裹,在符合条件下自动触发 pipeline

cron 接受一个 cron 风格的字符串来定义 Pipeline 触发的时间间隔,

  例如: triggers { cron('H 4/* 0 0 1-5') }

pollSCM

接受一个 cron 风格的字符串来定义 Jenkins 检查 SCM 源更改的常规间隔。如果存在新的更改,则 Pipeline 将被重新触发。

  例如:triggers { pollSCM('H 4/* 0 0 1-5') }

pipeline {
agent any
triggers {
cron('H 4/* 0 0 1-5')
}
stages {
stage('Example') {
steps {
echo 'Hello World'
}
}
}
}

6.4.5 tools

通过 tools 可自动安装工具,并放置环境变量到 PATH。如果 agent none,这将被忽略。

Supported Tools(Global Tool Configuration)支持参数: maven jdk gradle

pipeline {
agent any
tools {
#工具名称必须在 Jenkins 管理 Jenkins → 全局工具配置中预配置。
maven 'apache-maven-3.0.1'
}
stages {
stage('Example') {
steps {
sh 'mvn --version'
}
}
}
}

# The tool name must be pre-configured in Jenkins under Manage Jenkins → Global Tool Configuration

6.4.6 input

stage 的 input 指令允许你使用 input step提示输入。 在应用了 options 后,进入 stage的agent 或评估 when条件前,stage将暂停。 如果 input 被批准, stage 将会继续。 作为 input 提交的一部分的任何参数都将在环境中用于其他 stage

配置项
message 必需的。 这将在用户提交 input 时呈现给用户。
id input 的可选标识符, 默认为 stage 名称。
ok `input`表单上的"ok" 按钮的可选文本。
submitter 可选的以逗号分隔的用户列表或允许提交 input 的外部组名。默认允许任何用户。
submitterParameter 环境变量的可选名称。如果存在,用 submitter 名称设置。
parameters 提示提交者提供的一个可选的参数列表。 pipeline {
agent any
stages {
stage('Example') {
input {
message "Should we continue?"
ok "Yes, we should."
submitter "simon,lucky"
parameters {
string(name: 'PERSON', defaultValue: 'simon', description: 'Who should I say hello to?')
}
}
steps {
echo "Hello, ${PERSON}, nice to meet you."
}
}
}
}
# 指定用户提交

 6.4.7 when

when 指令允许 Pipeline 根据给定的条件确定是否执行该阶段。该 when 指令必须至少包含一个 条件。如果 when 指令包含多个条件,则所有子条件必须为 stage 执行返回 true。这与子条件嵌套在一 个 allOf 条件中相同(见下面的例子)。

更复杂的条件结构可使用嵌套条件建:not,allOf 或 anyOf。嵌套条件可以嵌套到任意深度。

内置条件:

branch:
当正在构建的分支与给出的分支模式匹配时执行,例如:when { branch 'master' }。请注意,这仅适用于多分支 Pipeline。
environment:
当指定的环境变量设置为给定值时执行,例如: when { environment name: 'DEPLOY_TO', value: 'production' }
expression:
当指定的 Groovy 表达式求值为 true 时执行,例如: when { expression { return params.DEBUG_BUILD }
not:
当嵌套条件为 false 时执行。必须包含一个条件。例如:when { not { branch 'master' } }
allOf:
当所有嵌套条件都为真时执行。必须至少包含一个条件。例如:when { allOf { branch 'master'; environment name: 'DEPLOY_TO', value: 'production' } }
anyOf:
当至少一个嵌套条件为真时执行。必须至少包含一个条件。例如:when { anyOf { branch 'master'; branch 'staging' } } pipeline {
agent any
stages {
stage('Example Build') {
steps {
echo 'Hello World'
}
}
stage('Example Deploy') {
when {
allOf {
branch 'production'
environment name: 'DEPLOY_TO', value: 'production'
}
}
steps {
echo 'Deploying'
}
}
}
}

6.4.8 Parallel

Declarative Pipeline 近期新增了对并行嵌套 stage 的支持,对耗时长,相互不存在依赖的 stage 可以使用此方式提升运行效率。除了 parallel stage,单个 parallel 里的多个 step 也可以使用并行的方 式运行。

pipeline {
agent any
stages {
stage('Non-Parallel Stage') {
steps {
echo 'This stage will be executed first.'
}
}
stage('Parallel Stage') {
when {
branch 'master'
}
parallel {
stage('Branch A') {
agent {
label "for-branch-a"
}
steps {
echo "On Branch A"
}
}
stage('Branch B') {
agent {
label "for-branch-b"
}
steps {
echo "On Branch B"
}
}
}
}
}
}

6.5 Pipeline Scripted声明式语法与脚本式语法对比分析

Groovy 脚本不一定适合所有使用者,因此 jenkins 创建了 Declarative pipeline,为编写 Jenkins 管道提供了一种更简单、更有主见的语法。但是由于脚本化的 pipeline 是基于 groovy 的一种 DSL 语言,所以与 Declarative pipeline 相比为 jenkins 用户提供了更巨大的灵活性和可扩展性。

6.5.1 流程控制

pipeline 脚本同其它脚本语言一样,从上至下顺序执行,它的流程控制取决于 Groovy 表达式, 如 if/else 条件语句,举例如下:

node {
stage('Example') {
if (env.BRANCH_NAME == 'master') {
echo 'I only execute on the master branch'
} else {
echo 'I execute elsewhere'
}
}
}

6.5.2 .Declarative pipeline 和 Scripted pipeline 的比较

共同点:

  两者都是 pipeline 代码的持久实现,都能够使用 pipeline 内置的插件或者插件提供的 stage,两者 都可以利用共享库扩展。

区别:

  两者不同之处在于语法和灵活性。Declarative pipeline 对用户来说,语法更严格,有固定的组织 结构,更容易生成代码段,使其成为用户更理想的选择。但是 Scripted pipeline 更加灵活,因为 Groovy 本身只能对结构和语法进行限制,对于更复杂的 pipeline 来说,用户可以根据自己的业务进行 灵活的实现和扩展。

七、jenkins+k8s+harbor 实现 DevOps

7.1 添加凭据(和之前的dockerhub添加凭据步骤一样)

  首页------>系统管理→管理凭据------>点击 Stores scoped to Jenkins 下的第一行 jenkins,

 7.2 编写jenkins pipeline

因为镜像要上传到 harbor 私有镜像仓库,所以需要在 harbor 上创建一个项目,项目名称是 jenkins-demo,如下所示:

新建一个任务------>输入一个任务名称处输入 jenkins-harbor------>流水线------>确定------> 在 Pipeline script 处输入如下内容

#jenkins-pipeline-script的dockerhub地址改为自己搭建的hub地址
#登录地址也进行修改,指定IP
# 上传地址修改
# github中contianer的image地址也需要修改地址

应用------>保存------>立即构建即可,打开 blue ocean 会看到如下流程,可以手动点击确认

八、Jenkins介入Sonarqube(还没操作,等之后补全)

官网:https://www.sonarqube.org

8.1 sonarqube介绍

SonarQube 是一个开源的代码分析平台, 用来持续分析和评测项目源代码的质量。 通过SonarQube我们可以检测出项目中重复代码, 潜在bug, 代码规范,安全性漏洞等问题, 并通过SonarQube web UI展示出来。

用途优点:

  1.代码质量和安全扫描和分析平台

  2.多维度分析代码:代码量、安全隐患、编写规范隐患、重复度、复杂度、代码增量、测试覆盖率等

  3.支持25+编程语言的代码扫描和分析,包含java\python\C#\javascript\go\C++等。

  4.涵盖了编程语言的静态扫描规则: 代码编写规范+安全规范

  5.能够与代码编辑器、CI/CD平台完美集成。

  6.能够与SCM集成,可以直接在平台上看到代码问题是由哪位开发人员提交。

  7.帮助程序猿写出更干净、更安全的代码

静态扫描主要针对开发人员编写的源代码。

通过定义好的 代码质量和安全规则,对开发人员编写的代码进行扫描和分析。将分析的结果多维护的呈现出来,以方便开发人员进行代码的优化和规范编写。

8.2 sonarqube如何工作

sonar静态代码扫描由部分组成:sonarQube平台,sonar-scanner扫描器。

1.SonarQube服务器
2.SonarQube数据库来存储
3.多个插件
4.一个或多个SonarQube Scanners:可以与CI服务进行集成

8.3 安装

docker pull sonarqube

docker run -d --name postgres10 -p 5432:5432 -e POSTGRES_USER=sonar -e POSTGRES_PASSWORD=123456 postgres

docker run -d --name sonarqube7.9 -p 9000:9000 --link postgres10 -e SONARQUBE_JDBC_URL=jdbc:postgresql://postgres10:5432/sonar -e SONARQUBE_JDBC_USERNAME=sonar \
-e SONARQUBE_JDBC_PASSWORD=123456 \
-v sonarqube_conf:/opt/sonarqube/conf \
-v sonarqube_extensions:/opt/sonarqube/extensions \
-v sonarqube_logs:/opt/sonarqube/logs -v sonarqube_data:/opt/sonarqube/data sonarqube

在 jenkins 中安装 sonarqube 插件:

系统管理->插件管理->可选插件:搜索 sonar,找到 Sonarqube Scanner,选择 Sonarqube Scanner 直接安装,安装之后重启 jenkins 即可

在 sonarqube 的 web 界面创建一个 token:

把 copy 后面的一串 token 记录下来: 2b91be9d40a28385d8b2f61b0fc0a52efd5abf97

回到 k8s 的 master1 节点:

cd /root/microservic-test

mvn sonar:sonar -Dsonar.host.url=http://192.168.40.181:9000 - Dsonar.login=2b91be9d40a28385d8b2f61b0fc0a52efd5abf97

这样就可以把代码上传到 sonarqube 了

基于Jenkins+k8s+Git等技术构建DeOps平台的更多相关文章

  1. 基于 Jenkins+Docker+Git 的CI流程初探

    在如今的互联网时代,随着软件开发复杂度的不断提高,软件开发和发布管理也越来越重要.目前已经形成一套标准的流程,最重要的组成部分就是持续集成(Continuous Integration,CI)及持续部 ...

  2. jenkins 和 git 的每日构建

    没有太难的技术含量,只要按照步骤操作就可以成功 step 1:全局工具配置git.exe 首先,登录 Jenkins ,在首页找到 “系统管理 -> Global Tool Configurat ...

  3. 基于jenkins,tekton等工具打造kubernetes devops平台

    本贴为目录贴,将不断更新 目录 1.Docker在centos下安装以及常见错误解决 2.使用kubernetes 官网工具kubeadm部署kubernetes(使用阿里云镜像) 3.无法访问gcr ...

  4. 基于Kubernetes/K8S构建Jenkins持续集成平台(上)-2

    基于Kubernetes/K8S构建Jenkins持续集成平台(上)-2 Kubernetes实现Master-Slave分布式构建方案 传统Jenkins的Master-Slave方案的缺陷 Mas ...

  5. 基于Kubernetes/K8S构建Jenkins持续集成平台(上)-1

    基于Kubernetes/K8S构建Jenkins持续集成平台(上)-1 Jenkins的Master-Slave分布式构建 什么是Master-Slave分布式构建 Jenkins的Master-S ...

  6. 基于Kubernetes/K8S构建Jenkins持续集成平台(下)

    基于Kubernetes/K8S构建Jenkins持续集成平台(下) Jenkins-Master-Slave架构图回顾: 安装和配置NFS NFS简介 NFS(Network File System ...

  7. Jenkins日常运维笔记-重启数据覆盖问题、迁移、基于java代码发版(maven构建)

    之前在公司机房部署了一套jenkins环境,现需要迁移至IDC机房服务器上,迁移过程中记录了一些细节:1)jenkins默认的主目录放在当前用户家目录路径下的.jenkins目录中.如jenkins使 ...

  8. 基于Jenkins自动构建系统开发

    1  绪论 1.1 课题的研究背景 随着IT行业的不断发展,软件开发的复杂度也随着不断提高.与此同时,软件的开发团队也越来越庞大,而如何更好地协同整个团队进行高效准确的工作,从而确保软件开发的质量已经 ...

  9. 构建基于Jenkins + Github的持续集成环境

    搭建持续集成首先要了解什么是持续集成,带着明确的目标去搭建持续集成环境才能让我们少走很多弯路.持续集成(Continuous integration)简称CI,是一种软件开发的实践,可以让团队在持续集 ...

  10. Net 项目构建基于Jenkins + Github + Mono 的持续集成环境

    Net 项目构建基于Jenkins + Github + Mono 的持续集成环境 阅读目录 1 安装 2 配置 3 测试 在Redhat enterprise 6.5 的服务器上,为在gutub 上 ...

随机推荐

  1. KingbaseES V8R6 等待事件之CLogControlLock

    前言 Kingbase数据库的tuple行头部来标识这条记录的事务结束状态(未知.已提交.已回滚),在事务提交时如果并发更新100万行记录,需要对多个page的tuple进行更改,这种繁重的操作会对数 ...

  2. wordpress固定链接+宝塔nginx配置伪静态访问URL

    一.站点设置 打开站点设置,选择伪静态,选择wordpress 二.wordpress设置 打开wordpress后台,选择设置 --->固定链接 选择一个你喜欢的格式点击保存 之后打开你的文章 ...

  3. 关于 ThreadLocalRandom 随机数生成器

    ThreadLocalRandom 线程安全随机数获取. 示例随机整数:java.util.concurrent.ThreadLocalRandom.current().nextInt(); 线程Th ...

  4. 【放假第1天】采购季倒计时 2G 50/年,4G 618/3年 云服务器选购攻略 阿里云 腾讯云 京东云对比 搭建网站、数据分析

    ​ 更新日期:4月4日(阿里云价格回调,京东云采购季持续进行) <最新对比表>已更新在文章头部-腾讯云文档,文章具有时效性,请以腾讯文档为准! https://docs.qq.com/do ...

  5. #树状数组,CDQ分治#洛谷 4390 [BOI2007]Mokia 摩基亚

    题目 分析 考虑离线处理,那么询问区间和就可以转换为四个询问, CDQ分治按横坐标处理询问,树状数组维护前缀和就可以了 代码 #include <cstdio> #include < ...

  6. OpenHarmony创新赛丨报名倒计时,超强秘籍带你直通大奖!

      OpenHarmony创新赛报名倒计时开始啦! 设于开放原子全球开源大赛下的OpenHarmony创新赛,目前正在如火如荼地进行赛事招募中!这次大赛围绕创新应用.商显行业.金融行业三大赛题,邀请来 ...

  7. java内存模型(jmm)概念初探

    1.和java内存结构的区别: 很多人会把jmm和Java内存结构搞混,网上搜到的一些文章也是如此,java内存结构就是我们常说的堆,栈,方法区,程序计数器..., 当jvm虚拟机启动的时候,会初始化 ...

  8. FreeMarker 去除循环末尾的符号

    在使用 FreeMarker 模板引擎来生成文件时,经常会使用到 list 标签用于循环生成. 有时会遇到需要处理末尾符号的情况,比如 Json 文件,循环生成的标签中末尾是不需要 , 的,例如: & ...

  9. HMS Core Insights第九期直播预告——手语服务,助力沟通无障碍

    [导读] 你知道吗?全球有超5%的人群正在遭受听力损失的折磨.这些听障群体由于沟通不便,在日常生活中面对着很多的困难与挑战,建立沟通无障碍环境的需求十分迫切.随着科技的发展,越来越多的人们享受到技术进 ...

  10. centos部署Django一:环境搭建

    前言: 参考文档: https://www.cnblogs.com/djangocn/p/9538551.html https://www.icode9.com/content-3-546765.ht ...