持续集成流程
大致流程说明:
- 开发人员每天把代码提交到Gitlab代码仓库。
- Jenkins从Gitlab中拉取项目源码,编译并打成jar包,然后构建成Docker镜像,将镜像上传到Harbor私有仓库。
- Jenkins发送SSH远程命令,让生产部署服务器到Harbor私有仓库拉取镜像到本地,然后创建容器。
- 最后,用户可以访问到容器
编译
stage('1_pull_and_compile') {
steps {
checkout([$class: 'GitSCM', branches: [[name: '*/${selected_branch}']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: "${git_token_id}", url: 'ssh://git@192.168.129.159:2222/D6573/web-demo.git']]])
sh "mvn clean compile"
}
}
代码检查
sonar静态代码检测
stage('2_sonar') {
steps{
script {
scannerHome = tool 'sonarqube-scanner'
}
withSonarQubeEnv('sonar-8.6') {
sh "${scannerHome}/bin/sonar-scanner"
}
}
}
sonar 质量门
stage("3_sonar_quality_gate") {
steps {
script{
timeout(time: 120, unit: 'SECONDS') {
def qg = waitForQualityGate('sonar-8.6')
if (qg.status != 'OK') {
withCredentials([string(credentialsId: sonar_auth_id, variable: 'SONAR_AUTH_KEY')]) {
// 打印项目状态信息
final String url = "http://192.168.129.44:9000/api/qualitygates/project_status?projectKey=${project_name}"
final String requestScript = "curl -u $SONAR_AUTH_KEY: -s ${url} "
final String response = sh(script: requestScript, returnStdout: true).trim()
echo response
// 解析错误项并打印。
def parser = new groovy.json.JsonSlurper()
def applicationResp = parser.parseText(response)
println("状态<span style=\"color:red\">$applicationResp.projectStatus.status</span>")
for(item in applicationResp.projectStatus.conditions){
if(item.status == 'ERROR'){
println("错误项:"+item)
}
}
}
error "未通过Sonarqube的代码质量阈检查,请及时修改!failure: ${qg.status}"
}
}
}
}
}
构建项目
安装Pipeline Utility Steps 插件
findFiles
等方法需要使用该插件
dockerfile
FROM openjdk:8-jdk-alpine
ARG JAR_FILE="Default_Value"
## 从构建命令获取参数
COPY ${JAR_FILE} /app.jar
## 每个项目公开的端口不一样
ARG SERVER_PORT=8080
ENV env_srver_port=$SERVER_PORT
EXPOSE $env_srver_port
ENTRYPOINT ["java","-jar","/app.jar","--server.port=$env_srver_port"]
构建脚本
stage('4_package_and_build') {
steps{
script{
//编译,打包
sh "mvn package -Dmaven.test.skip=true "
//Pipeline Utility Steps 插件 --- findFiles
def files = findFiles(glob: 'target/*.jar')
if(files.length < 0){
error "未找到输出物*.jar."
}
def jarFile = files[0]
echo "---------------${files[0]}"
//定义镜像名称
def imageName = "${project_name}:${tag}"
//构建本地镜像
sh "docker build --build-arg JAR_FILE=${jarFile} --build-arg SERVER_PORT=${serverPort} -t ${imageName} ."
//定义远程标签名称
def remoteImageName = "${harbor_url}/${harbor_project_name}/${imageName}";
//给镜像打标签
sh "docker tag ${imageName} ${remoteImageName}"
//登录Harbor,并上传镜像
withCredentials([usernamePassword(credentialsId: "${harbor_auth}",
passwordVariable: 'password', usernameVariable: 'username')]) {
//登录
sh "docker login -u ${username} -p ${password} ${harbor_url}"
//上传镜像
sh "docker push ${remoteImageName}"
}
//删除本地镜像
sh "docker rmi -f ${imageName}"
sh "docker rmi -f ${remoteImageName}"
}
}
}
远程部署
部署脚本
在远程服务器192.168.129.42
服务器,创建/home/docker/jenkins_shell
,脚本内容如下
#! /bin/bash
harbor_url=$1
harbor_project_name=$2
project_name=$3
tag=$4
port=$5
echo "请求参数:harbor_url=$1, harbor_project_name=$2, project_name=$3, tag=$4, port=$5"
imageName=$harbor_url/$harbor_project_name/$project_name:$tag
echo "imageName=$imageName"
# 查询容器是否存在
containerId=`docker ps -a | grep -w ${project_name}:${tag} | awk '{print $1}'`
if [ "$containerId" != "" ]; then
# 停掉容器
docker stop $containerId
# 删掉容器
docker rm $containerId
echo "成功删除容器"
fi
# 删除存在的镜像
docker images| grep -w $project_name | awk '{print $3}' | xargs docker rmi -f
# 登录Harbor私服
docker login -u jhs -p Jhs123456 $harbor_url
# 下载镜像
docker pull $imageName
# 启动容器
docker run -di -p $port:$port $imageName
echo "容器启动成功:${containId}"
测试脚本内容
在jenkins容器内
执行如下命令
[docker@s42 jenkins_shell]$ chmod +x deploy.sh
[docker@s42 jenkins_shell]$ sh deploy.sh 192.168.129.44:85 demo1 web-demo 1223 10086
....
容器启动成功:2a90fcb9e541a7f8d694e493e4cc39614674b7b8564abc34b948db3bad687c19
,测试执行成功。
远程登录
免密登录原理
免密登录,需要先在本机生成公钥,然后将公钥拷贝到远程主机,拷贝的过程,既可以手动(在远程主机根目录下创建.ssh目录,然后将公钥存入该目录下authorized_keys文件中即可),也可以直接命令操作ssh-copy-id,这个操作做完了,即可免密登录远程主机。
- 首先在 serverA 上生成一对秘钥(ssh-keygen)
- 将公钥拷贝到 serverB,重命名 authorized_keys
- serverA 向 serverB 发送一个连接请求,信息包括用户名、ip
- serverB 接到请求,会从 authorized_keys 中查找,是否有相同的用户名、ip,如果有 serverB 会随机生成一个字符串
- 然后使用使用公钥进行加密,再发送个 serverA
- serverA 接到 serverB 发来的信息后,会使用私钥进行解密,然后将解密后的字符串发送给 serverB
- serverB 接到 serverA 发来的信息后,会给先前生成的字符串进行比对,如果一直,则允许免密登录
jenkins容器内执行
# 1.生成秘钥
[root@4e9029ecaeca .ssh]# ssh-keygen
# 2. 将本机公钥复制到远程机器的 ~/.ssh/authorized_key.文件中
[root@4e9029ecaeca /]# ssh-copy-id -i ~/.ssh/id_rsa.pub docker@192.168.129.42
# 输入密码
docker@192.168.129.42s password: *******
## 3. 远程登录目标机器 --- 无需使用密码
[root@vm1 web_demo_cloud_pipeline]# ssh 'docker@192.168.129.42'
## 4. 测试、执行脚本成功
[docker@s42 ~]$ sh /ssh_deploy/deploy.sh 192.168.129.44:85 demo1 web-demo 1223 10086
jenkin配置
安装Publish over SSH插件
略.
jenkins配置
pipeline脚本配置
流水线语法片段
生成的脚本:
sshPublisher(
publishers: [
sshPublisherDesc(
configName: '192.168.129.42-ssh',
transfers: [
sshTransfer(
cleanRemote: false,
excludes: '',
execCommand: '/home/docker/jenkins_shell/deploy.sh 192.168.129.44:85 demo1 web-demo 1223 10086',
execTimeout: 120000,
flatten: false,
makeEmptyDirs: false,
noDefaultExcludes: false,
patternSeparator: '[, ]+',
remoteDirectory: '',
remoteDirectorySDF: false,
removePrefix: '',
sourceFiles: ''
)
],
usePromotionTimestamp: false,
useWorkspaceInPromotion: false,
verbose: false
)
]
)
完整脚本(parameter)
DockerFile
FROM openjdk:8-jdk-alpine
ARG JAR_FILE="Default_Value"
## 从构建命令获取参数
COPY ${JAR_FILE} /app.jar
## 每个项目公开的端口不一样
ARG SERVER_PORT=8080
ENV env_srver_port=$SERVER_PORT
EXPOSE $env_srver_port
#ENTRYPOINT ["java","-jar","/app.jar","--server.port=$env_srver_port"]
ENTRYPOINT java -jar /app.jar --server.port=$env_srver_port
JenkinsFile
def git_token_id = "6cac69be-bb16-49d4-a315-d71bc2883106"
def tag = "1225"
def git_project_ssh_url_base = "ssh://git@192.168.129.159:2222/D6573"
def git_project_url="${git_project_ssh_url_base}/${project_name}.git"
def harbor_url = "192.168.129.44:85"
def harbor_project_name = "demo1"
def harbor_auth = "ee84a7cd-098b-4890-97cb-609850851ee6"
def sonar_auth_id = "sonar-authentication-token-test"
def sonar_project_status_url = "http://192.168.129.44:9000/api/qualitygates/project_status?projectKey=${project_name}"
pipeline {
agent any
stages {
stage('0_print_envs') {
steps {
echo "selected_branch=${selected_branch} ,serverPort=${serverPort} "
}
}
stage('1_pull_and_compile') {
steps {
checkout([$class: 'GitSCM', branches: [[name: '*/${selected_branch}']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: "${git_token_id}", url: "${git_project_url}"]]])
sh "mvn clean compile"
}
}
stage('2_sonar') {
steps{
script {
scannerHome = tool 'sonarqube-scanner'
}
withSonarQubeEnv('sonar-8.6') {
sh "${scannerHome}/bin/sonar-scanner"
}
}
}
stage("3_sonar_quality_gate") {
steps {
script{
timeout(time: 120, unit: 'SECONDS') {
def qg = waitForQualityGate('sonar-8.6')
if (qg.status != 'OK') {
withCredentials([string(credentialsId: sonar_auth_id, variable: 'SONAR_AUTH_KEY')]) {
final String requestScript = "curl -u $SONAR_AUTH_KEY: -s ${sonar_project_status_url} "
final String response = sh(script: requestScript, returnStdout: true).trim()
echo response
def parser = new groovy.json.JsonSlurper()
def applicationResp = parser.parseText(response)
println("状态<span style=\"color:red\">$applicationResp.projectStatus.status</span>")
for(item in applicationResp.projectStatus.conditions){
if(item.status == 'ERROR'){
println("错误项:"+item)
}
}
}
error "未通过Sonarqube的代码质量阈检查,请及时修改!failure: ${qg.status}"
}
}
}
}
}
stage('4_package_and_build') {
steps{
script{
//编译,打包
sh "mvn package -Dmaven.test.skip=true "
//Pipeline Utility Steps 插件 --- findFiles
def files = findFiles(glob: 'target/*.jar')
if(files.length < 0){
error "未找到输出物*.jar."
}
def jarFile = files[0]
echo "---------------${files[0]}"
//定义镜像名称
def imageName = "${project_name}:${tag}"
//构建本地镜像
sh "docker build --build-arg JAR_FILE=${jarFile} --build-arg SERVER_PORT=${serverPort} -t ${imageName} ."
//定义远程标签名称
def remoteImageName = "${harbor_url}/${harbor_project_name}/${imageName}";
//给镜像打标签
sh "docker tag ${imageName} ${remoteImageName}"
//登录Harbor,并上传镜像
withCredentials([usernamePassword(credentialsId: "${harbor_auth}",
passwordVariable: 'password', usernameVariable: 'username')]) {
//登录
sh "docker login -u ${username} -p ${password} ${harbor_url}"
//上传镜像
sh "docker push ${remoteImageName}"
}
//删除本地镜像
sh "docker rmi -f ${imageName}"
sh "docker rmi -f ${remoteImageName}"
}
}
}
stage("5_ssh_deploy") {
steps {
sshPublisher(
publishers: [
sshPublisherDesc(
configName: '192.168.129.42-ssh',
transfers: [
sshTransfer(
cleanRemote: false,
excludes: '',
execCommand: "/home/docker/jenkins_shell/deploy.sh $harbor_url $harbor_project_name $project_name $tag ${serverPort}",
execTimeout: 120000,
flatten: false,
makeEmptyDirs: false,
noDefaultExcludes: false,
patternSeparator: '[, ]+',
remoteDirectory: '',
remoteDirectorySDF: false,
removePrefix: '',
sourceFiles: ''
)
],
usePromotionTimestamp: false,
useWorkspaceInPromotion: false,
verbose: false
)
]
)
}
}
}
}
deploy.sh
#! /bin/bash
harbor_url=$1
harbor_project_name=$2
project_name=$3
tag=$4
port=$5
echo "请求参数:harbor_url=$1, harbor_project_name=$2, project_name=$3, tag=$4, port=$5"
imageName=$harbor_url/$harbor_project_name/$project_name:$tag
echo "imageName=$imageName"
# 查询容器是否存在
containerId=`docker ps -a | grep -w ${project_name}:${tag} | awk '{print $1}'`
if [ "$containerId" != "" ]; then
# 停掉容器
docker stop $containerId
# 删掉容器
docker rm $containerId
echo "成功删除容器"
fi
# 删除存在的镜像
docker images| grep -w $project_name | awk '{print $3}' | xargs docker rmi -f
# 登录Harbor私服
docker login -u jhs -p Jhs123456 $harbor_url
# 下载镜像
docker pull $imageName
# 启动容器
docker run -di -p $port:$port $imageName
echo "容器启动成功:${containId}"