0x01:前提概要
本篇做了两种方案,一种将AWS-ECR作为镜像仓库来使用,另外一种是自己搭建的Harbor仓库。(测试环境跑在内网虚拟机,线上环境跑在公网AWS云主机,所以采用了两套仓库部署)
0x02:关键名词解释
github: 代码托管工具
docker: 容器操作工具
kubectl: k8s集群客户端
jenkins: CI/CD工具
rancher: k8s集群管理工具
harbor: docker开源镜像仓库
aws-cli: AWS命令行工具
aws-ecr: 亚马逊云镜像仓库
0x03:服务器资源介绍
本次实验使用了2台4c8g的虚拟服务器,操作系统为centos7.3,服务器资源描述如下:
172.100.10.13 【关闭selinux、安装docker、docker-compose、kubectl】
172.100.10.23 【关闭selinux、安装docker】
我们选用172.100.10.13这台服务器作为master节点,它上面会部署jenkins、rancher以及harbor,整个服务部署情况如下图所示:
我们预先在172.100.10.13上创建好如下的目录结构:
opt/
├── docker
│ ├── jenkins
│ │ └── home
│ └── rancher_home
│ │ ├── auditlog
│ │ └── rancher
│ └── harbor
注意把/opt/docker/jenkins/home目录用户设置给jenkins用户,否则容器内无法写入该目录,~/.docker 目录也是如此,因为jenkins容器内部需要登录镜像仓库,登录操作是需要修改~/.docker/config.json文件的(参考资料)
chown -R 1000 home
chown -R 1000 /root/.docker
0x04:打通AWS服务,安装配置aws-cli (如果走的是Harbor仓库,这步跳过)
由于我们需要依赖aws的镜像仓库服务,所以需要有能操作aws的客户端工具,AWS官方提供了两种使用方式,一种是在宿主机安装,另外一种直接通过docker来完成,因为宿主机要配置aws,所以我先采用的第一种方式,参考连接:
aws-cli安装
aws-cli配置
通过以上,我们已经可以对aws进行基本的操作,通过以下命令来检测,如果能正常回显说明配置完成。
aws configure get region
配置完成以后我们回到用户目录下,看一下生成了.aws目录,里面包含了两个文件,这里先放一遍,后面我们会用到。
.aws/
├── config
└── credentials
另外一种方式可以通过docker来启动aws-cli,这里我们也可以尝试一下,后面在容器内部使用aws-cli就是用的这种方式:
docker run --rm -it -v /root/.aws:/root/.aws amazon/aws-cli configure get region
可以看到跟上面执行的结果是一样的。
0x05:安装Harbor (如果走的是AWS-ECR仓库,这步跳过)
1.进入 /opt/docker/harbor 目录,下载harbor程序包并解压,本次实验用的版本是v1.10.3;
2.进入harbor目录,找到harbor.yml,修改 hostname、port并且注释掉https访问(没有证书);
3.执行 ./install.sh ,等待下载镜像,安装完毕以后当前目录下会生成 docker-compose.yml 文件,后面可以通过docker-compose来启停harbor服务;
4.安装启动完成以后,访问 http://172.100.10.13:9090/ 即可看到harbor登录界面。
整体的安装步骤可以参考 https://www.jianshu.com/p/6fcacc2020d5
【注意】这里有个需要注意的坑,因为我们的harbor是用的http协议,在执行docker pull的时候会提示:
[root@cpe-172-100-10-13 docker]# docker pull 172.100.10.13:9090/test/demo1:20200623102639
Error response from daemon: Get https://172.100.10.13:9090/v2/: http: server gave HTTP response to HTTPS client
怎么解决该问题,网上目前搜到的方式是加docker配置文件: /etc/docker/daemon.json,文件内容具体如下:
{
"insecure-registries": ["172.100.10.13:9090"]
}
然后重启docker即可。(生产环境还是推荐配置https)
0x06: 部署Rancher,创建K8S集群
我们采用docker的方式部署,我这里选择的rancher版本是2.4.3
sudo docker run -d --restart=unless-stopped -v /opt/docker/rancher_home/rancher:/var/lib/rancher/ -v /opt/docker/rancher_home/auditlog:/var/log/auditlog/ --name rancher -p 80:80 -p 443:443 rancher/rancher:v2.4.3
启动成功以后就可以通过浏览器访问了,如果访问不成功的可以耐心等待一会,启动过程需要一点时间,如果长时间没有成功,注意看一下 docker log,有防火墙限制的记得把80和443端口开放出来。
我这里通过访问 https://172.100.10.13:443/ 进入rancher ,选择语言,设置密码,然后就OK了。
这时我们还没有K8S集群,需要来创建一个,看到rancher主菜单有一个“集群”,点进去,找到添加集群按钮
由于我们是自己的服务器搭建集群,所以这里选择自定义,进去以后输入集群名称,进入下一步,
这一步把Etcd和Control都选上,然后复制下面的命令,到172.100.10.13服务器上面执行,没有报错的话就静静等待集群启动起来。
启动完成以后我们查看新创建的集群,看到状态已经是绿色的Active了,这个时候可以给集群添加主机,点击集群升级按钮,打开页面后滚动到最下方,找到添加主机命令,复制。
登录到172.100.10.23服务器,执行上面的命令,完成以后节点会自动加入集群。
集群创建好了,我们还需要一个命令行客户端来管理它,这里用到了kubectl,我们来配置一下,首先拿到kubeconfig文件,在这个位置:
复制好内容,保存到172.100.10.13服务器的 /root/.kube/config文件中,然后测试一下看看是不是生效
kubectl get pods --all-namespaces
或者利用docker操作kubectl
docker run --rm --name kubectl -v /root/.kube/config:/.kube/config bitnami/kubectl:latest get pods --all-namespaces
0x07: 部署Jenkins
docker启动jenkins脚本:
docker run -d -p 8081:8080 -p 50000:50000 \
-v /opt/docker/jenkins/home:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /usr/bin/docker:/usr/bin/docker \
-v /usr/lib64/libltdl.so.7:/usr/lib/x86_64-linux-gnu/libltdl.so.7 \
-v /root/.aws:/root/.aws \
-v /root/.docker:/root/.docker \
-v /root/.kube/config:/root/.kube/config \
-u root --name jenkins --restart=always --privileged=true jenkins/jenkins
看到我们挂载了好多宿主机上的文件,这里简单介绍一下:
/opt/docker/jenkins/home # jenkins 工作的主要目录,用于保存jenkins状态数据;
/var/run/docker.sock && /usr/bin/docker && /usr/lib64/libltdl.so.7 # 用于在容器内部调用宿主机docker命令;
/root/.aws # aws-cli配置文件
/root/.docker # docker仓库授权配置文件存放于此目录下
/root/.kube/config # k8s集群配置文件
启动完成以后,访问 http://172.100.10.13:8081/ ,按照引导进入jenkins。
接下来开始安装jenkins插件,这里我们主要用到了这些插件:
Localization: Chinese 、Git、GitHub、ssh、Blue Ocean、docker、docker-build-step、Pipeline
0x08: 通过Jenkins部署golang应用
我们的代码都托管在github上,首先在github上面创建仓库,然后把编写好的代码提交上去,注意在项目根目录下写好你的Dockerfile和Jenkinsfile,用来制作docker镜像以及jenkins发布的pipeline步骤。我这里是用branchName做环境区分,test分支发布测试环境,master分支发布线上环境。
Jenkinsfile文件内容:
// 支持发布 test pro
def createVersion() {
// 定义一个版本号作为当次构建的版本,输出结果 20200615175842
return new Date().format('yyyyMMddHHmmss')
}
def currentVersion = createVersion()
pipeline {
agent any
parameters {
string(
name: 'appVersion',
defaultValue: currentVersion,
description: '应用版本号'
)
}
stages {
stage('prepare') {
steps {
echo "workspace: ${WORKSPACE}"
echo "GIT_COMMIT: ${GIT_COMMIT}"
echo "APP_VERSION: ${params.appVersion} ."
echo "branch: ${BRANCH_NAME}"
}
}
stage('repository authorization') {
steps {
script {
echo "repository authorization stage ..."
if(env.BRANCH_NAME == 'test'){
sh "docker login -u admin -p Harbor12345 172.100.10.13:9090"
}
else if(env.BRANCH_NAME == 'master'){
sh "docker run --rm -v /root/.aws:/root/.aws amazon/aws-cli ecr get-login-password >> ecr-login.txt"
sh "cat ecr-login.txt | docker login --username ***** --password-stdin *****.dkr.ecr.*****.amazonaws.com"
}
}
}
}
stage('build') {
steps {
echo "build stage ..."
sh "docker build -t *****:${params.appVersion} ."
}
}
stage('tag') {
steps {
script {
echo "tag stage ..."
if(env.BRANCH_NAME == 'test'){
sh "docker tag *****:${params.appVersion} 172.100.10.13:9090/*****/*****:${params.appVersion}"
}else if(env.BRANCH_NAME == 'master'){
sh "docker tag *****:${params.appVersion} *****.dkr.ecr.*****.amazonaws.com/*****:${params.appVersion}"
}
}
}
}
stage('push') {
steps {
script {
echo "push stage ..."
if(env.BRANCH_NAME == 'test'){
sh "docker push 172.100.10.13:9090/*****/*****:${params.appVersion}"
}else if(env.BRANCH_NAME == 'master'){
sh "docker push *****.dkr.ecr.*****.amazonaws.com/*****:${params.appVersion}"
}
}
}
}
stage('clean') {
steps {
script {
echo "clean stage ..."
if(env.BRANCH_NAME == 'test'){
sh "docker rmi 172.100.10.13:9090/*****/*****:${params.appVersion}"
}else if(env.BRANCH_NAME == 'master'){
sh "docker rmi *****.dkr.ecr.*****.amazonaws.com/*****:${params.appVersion}"
}
sh "docker rmi *****:${params.appVersion}"
}
}
}
stage('k8s secret') {
steps {
script {
echo "k8s secret stage ..."
if(env.BRANCH_NAME == 'test' ){
sh "docker run --rm --name kubectl -w /.kube -v /root/.kube/config:/.kube/config bitnami/kubectl:latest delete --ignore-not-found=true secrets harbor-secret"
sh "docker run --rm -u root --name kubectl -w /.kube -v /root/.kube/config:/.kube/config -v /root/.docker/config.json:/root/.docker/config.json bitnami/kubectl:latest create secret generic harbor-secret --from-file=.dockerconfigjson=/root/.docker/config.json --type=kubernetes.io/dockerconfigjson"
}else if(env.BRANCH_NAME == 'master'){
// 不完美,不能做到无缝升级,查阅了官方文档,没有 exist for update 机制
sh "docker run --rm --name kubectl -w /.kube -v /root/.kube/config:/.kube/config bitnami/kubectl:latest delete --ignore-not-found=true secrets aws-ecr-secret"
sh "docker run --rm -u root --name kubectl -w /.kube -v /root/.kube/config:/.kube/config -v /root/.docker/config.json:/root/.docker/config.json bitnami/kubectl:latest create secret generic aws-ecr-secret --from-file=.dockerconfigjson=/root/.docker/config.json --type=kubernetes.io/dockerconfigjson"
}
}
//sh "kubectl delete secrets aws-ecr-secret"
//sh "kubectl create secret generic aws-ecr-secret --from-file=.dockerconfigjson=/root/.docker/config.json --type=kubernetes.io/dockerconfigjson"
}
}
stage('deploy') {
steps {
script {
echo "deploy stage ..."
if(env.BRANCH_NAME == 'test'){
sh '''
cat > *****.yaml << EOF
# 声明一个Deployment资源对象
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-*****
spec:
# 通过replicas声明pod个数是2
replicas: 2
# 通过标签选择被控制的pod
selector:
matchLabels:
app: *****
# 在template中定义pod
template:
metadata:
labels:
# 给pod打上标签app=*****
app: *****
spec:
imagePullSecrets:
- name: harbor-secret
containers:
# 声明容器名称,注意不是pod名称,pod名称应该定义在metadata中
- name: *****
image: 172.100.10.13:9090/*****/*****:${appVersion}
args: ["test"]
ports:
- name: http
containerPort: 8898
resources:
limits:
cpu: 1000m
memory: 2Gi
requests:
cpu: 500m
memory: 1Gi
# 在一个yaml文件中通过---分割多个资源对象
---
apiVersion: v1
# 声明一个Service资源对象
kind: Service
metadata:
name: service-*****
spec:
ports:
- name: http
# Service监听端口
port: 8898
# 转发到后端Pod的端口号
targetPort: 8898
# 外部访问端口
#nodePort: 30076
# service-*****将选择标签包含app=*****的pod
selector:
app: *****
type: NodePort
EOF
'''
sh "docker run --rm --name kubectl -w /.kube -v /root/.kube/config:/.kube/config -v /opt/docker/jenkins/home/workspace/*****_test/*****.yaml:/.kube/*****.yaml bitnami/kubectl:latest apply -f ./*****.yaml"
}else if(env.BRANCH_NAME == 'master'){
sh '''
cat > *****.yaml << EOF
# 声明一个Deployment资源对象
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-*****
spec:
# 通过replicas声明pod个数是2
replicas: 2
# 通过标签选择被控制的pod
selector:
matchLabels:
app: *****
# 在template中定义pod
template:
metadata:
labels:
# 给pod打上标签app=*****
app: *****
spec:
imagePullSecrets:
- name: aws-ecr-secret
containers:
# 声明容器名称,注意不是pod名称,pod名称应该定义在metadata中
- name: *****
image: *****.dkr.ecr.*****.amazonaws.com/*****:${appVersion}
args: ["pro"]
ports:
- name: http
containerPort: 8898
resources:
limits:
cpu: 1000m
memory: 2Gi
requests:
cpu: 500m
memory: 1Gi
# 在一个yaml文件中通过---分割多个资源对象
---
apiVersion: v1
# 声明一个Service资源对象
kind: Service
metadata:
name: service-*****
spec:
ports:
- name: http
# Service监听端口
port: 8898
# 转发到后端Pod的端口号
targetPort: 8898
# 外部访问端口
#nodePort: 30076
# service-*****将选择标签包含app=*****的pod
selector:
app: *****
type: NodePort
EOF
'''
sh "docker run --rm --name kubectl -w /.kube -v /root/.kube/config:/.kube/config -v /opt/docker/jenkins/home/workspace/*****_master/*****.yaml:/.kube/*****.yaml bitnami/kubectl:latest apply -f ./*****.yaml"
}
//sh "docker run --rm --name kubectl -w /.kube -v /root/.kube/config:/.kube/config -v ${WORKSPACE}/*****.yaml:/.kube/*****.yaml bitnami/kubectl:latest apply -f ./*****.yaml"
//sh "kubectl apply -f *****.yaml"
}
}
}
}
}
下面我们来部署应用,打开Blue Ocean,进去之后点击“创建流水线”,代码仓库选择GitHub,首次会提示你输入access_token,输入进去即可,下一步选择组织,接着选择项目仓库,最后点击创建流水线,完成以后就会按照项目中配置的Jenkinsfile开始执行发布流程,我们上面分拆了这几个步骤:预备、编译打包镜像、生成tag、推送镜像、部署镜像、清理镜像,当然这些不是固定的,可以按照你的需求自由增减。
0x09: 总结归纳
目前整体流程下来基本上没什么大问题,就是还有几个做法不是太完美:
1.部署jenkins依赖宿主机的docker,最好能在jenkins容器内直接操作docker【我看目前没有合适的镜像,可能需要自己单独打镜像】;
2.【已解决】kubectl也是同样的问题,我试了kubtctl镜像,在宿主环境下可以执行kubectl apply,但是到container环境中会报错,已经反馈issue给到作者,看看后面能不能解决;https://github.com/bitnami/bitnami-docker-kubectl/issues/16
3.Jenkinsfile 因为用到了参数化构建,第一次会构建错误,镜像拿不到版本号,但是第二次以后就都可以了,不知道是何原因