本文介绍如何使用 Jenkins 的声明式 pipeline 为一个简单的 Golang web 应用搭建 CI 环境。如果你还不太了解 Jenkins 及其声明式 pipeline,请先参考笔者的 Jenkins 系列文章,或者直接到 Jenkins 官网进行学习。说明:本文的演示环境为 ubuntu 16.04。

准备 Jenkins 环境

鉴于篇幅原因,本文不再介绍 Jenkins 环境的搭建。本文演示的 demo 只要求 Jenkins server 连接了一个带有 go 标签的 agent,该 agent 上安装了 docker:

如果你希望可以收到 CI 中的邮件通知,请配置 Jenkins 邮件通知中的 SMTP server。

demo 程序

笔者创建了一个简单的 Golang web 程序用于演示,大家可以从这里下载该程序。
app.go
app.go 文件包含主程序,其内容如下:

package main

import (
"fmt"
"net/http"
"strings"
) func getNameLen(name string) int {
return len(name)
} func sayHello(w http.ResponseWriter, r *http.Request) {
message := r.URL.Path
message = strings.TrimPrefix(message, "/")
message = "Hello " + message + " : " + fmt.Sprintf("%d", getNameLen(message))
w.Write([]byte(message))
} func main() {
http.HandleFunc("/", sayHello)
if err := http.ListenAndServe(":8088", nil); err != nil {
panic(err)
}
}

该程序的功能非常简单,如果你在 url 中域名后面的部分添加了自己的名字,它会向你问好并计算出你名字的长度:

app_test.go
app_test.go 文件包含了函数 getNameLen() 的单元测试:

package main

import (
"testing"
) func Test_GetNameLen_1(t *testing.T) {
if l := getNameLen("nick"); l != {
t.Error("test failed, the length of nick is not correct.")
} else {
t.Log("test passed.")
}
} func Test_GetNameLen_2(t *testing.T) {
if l := getNameLen(""); l != {
t.Error("test failed, the length of empty string is not correct.")
} else {
t.Log("test passed.")
}
}

Dockerfile
Dockerfile 文件用于构建 docker 镜像,其内容如下:

FROM golang:1.11.0
WORKDIR /go/src/gowebdemo/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o gowebdemo . FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/gowebdemo .
EXPOSE 8088
CMD ["./gowebdemo"]

在准备好上面的内容后,让我们开始 CI 的配置。

Jenkinsfile

为了实现 pipeline as code,我们把配置 Jenkins 的 pipeline 内容保存到 Jenkinsfile 文件中,并和代码一起 checkin 到代码中。该 demo 的 Jenkinsfile 内容如下:

pipeline {
agent {
label 'go'
}
stages {
stage('UnitTest') {
steps {
script {
if( sh(script: 'docker run --rm -v $(pwd):/go/src/gowebdemo -w /go/src/gowebdemo golang:1.11.0 /bin/bash -c "/go/src/gowebdemo/rununittest.sh"', returnStatus: true ) != ){
currentBuild.result = 'FAILURE'
}
}
junit '*.xml'
script {
if( currentBuild.result == 'FAILURE' ) {
sh(script: "echo unit test failed, please fix the errors.")
sh "exit 1"
}
}
}
}
stage('Build') {
steps {
sh './buildapp.sh'
}
}
stage('Deploy') {
steps {
sh './deployapp.sh'
}
}
}
post {
failure {
mail bcc: '', body: "<b>gopro build failed</b><br>Project: ${env.JOB_NAME} <br>Build Number: ${env.BUILD_NUMBER} <br> URL de build: ${env.BUILD_URL}", cc: '', charset : 'UTF-8', from: '', mimeType: 'text/html', replyTo: '', subject: "ERROR CI: Project name -> ${env.JOB_NAME}", to: "your email address";
}
success {
mail bcc: '', body: "<b>gopro build success</b><br>Project: ${env.JOB_NAME} <br>Build Number: ${env.BUILD_NUMBER} <br> URL de build: ${env.BUILD_URL}", cc: '', charset: 'UTF-8', from: '', mimeType: 'text/html', replyTo: '', subject: "SUCCESS CI: Project name -> ${env.JOB_NAME}", to: "your email address";
}
}
}

label 'go'
agent 中的 label 指定该 pipeline 运行在带有 go 标签的 agent 上。

stage('UnitTest')
该部分运行代码中的单元测试,并根据单元测试的结果确定是否继续执行后面的流水线操作。其中的脚本文件 rununittest.sh 内容如下:

#!/bin/bash

set -x
go get -d -v golang.org/x/net/html
go get -u github.com/jstemmer/go-junit-report
go test -v >& > tmp
status=$?
$GOPATH/bin/go-junit-report < tmp > test_output.xml exit ${status}

该脚本执行单元测试操作,并把运行单元测试命令的结果作为脚本运行的结果返回。这一点很重要,我们就是通过这种方式来知道单元测试是否完全通过,如果没有完全通过就让该次持续集成失败,而停止后续的操作。同时使用 go-junit-report 组件把单元测试的结果保存为 junit 格式的文件 test_output.xml。junit '*.xml' 则可以分析该单元测试的结果,并以图表的方式展示:

stage('Build')
该部分执行脚本 buildapp.sh,其内容如下:

#!/bin/bash

set -ex
docker build -t gowebdemo . # remove all none tag images
if [ ! -z "$(docker images -q --filter 'dangling=true')" ]; then
docker rmi $(docker images -q --filter "dangling=true")
fi

首先执行 docker build -t gowebdemo . 命令,以 Dockerfile 中的指令构建应用程序并打包为容器镜像。然后移除系统中没有标签的镜像释放磁盘空间。
在比较正式的环境中,一般会把构建好的容器镜像推送到私有的镜像库中,这里为了简化过程,就把镜像存放在 agent 上,并在下一步中在 agent 上部署一个应用的实例。

stage('DeployApp')
该部分执行脚本 deployapp.sh,脚本的内容如下:

#!/bin/bash

set -ex
# remove the current app instance
if [ -n "$(docker ps -aq -f name=nickwebdemo)" ]; then
docker rm -f nickwebdemo
fi # run a new app instance
docker run -d \
-p : \
--name nickwebdemo \
--restart=always \
gowebdemo

脚本先检查是不是已经有同名的容器实例,如果有就先删除掉该实例,然后运行一个新的实例。

在最后的 post 部分,我们根据该次持续集成的状态来发送不同的邮件通知,比如整个过程没有错误发生,单元测试也都通过了,就发送成功的通知,否则发送失败的通知。

配置 Jenkins Job

在 Jenkins 中创建 pipeline 类型的 Job,并设置从 SCM 获得 pipeline 脚本:

因为笔者放置代码的库是公开的,所以只用指定代码库的路径就可以了,不需要添加相关的认证信息。
现在就可以触发 CI 过程了,下图是笔者机器上运行完成后的截图:

虽然只有两个单元测试的 case,但显示的结果还不错!并且 web app 被成功的部署到了 agent 上。

checkin 代码进行演示

下面我们配置 Jenkins 每隔一分钟检查一次代码是否有变更,有的话就触发 CI。在 Build Triggers 中选择 Poll SCM,然后输入 5 个由空格分隔的 * 号:

保存该的配置,接下来让我们添加一个单元测试的 case:

func Test_GetNameLen_3(t *testing.T) {
if l := getNameLen("andrew"); l != {
t.Error("test failed, the length of andrew string is not correct.")
} else {
t.Log("test passed.")
}
}

这里笔者故意算错了字符串 "andrew" 的长度,checkin 这段代码,然后看看 Jenkins 中持续集成的过程:

持续集成的过程被自动触发了,但是由于单元测试中有失败的 case 导致整个过程都失败了,并且单元测试后面的过程都没有被执行:

如果你正确配置了邮件服务器并且把 Jenkinsfile 中的邮件地址改成的你自己的邮件地址,那么不管持续集成是成功还是失败你都会收到相关的通知。
现在把失败的单元测试修改正确,再提交一次,这样就开启了我们的持续集成之旅!

总结

本文只是介绍了一个非常简单的 demo 场景,但是一旦一个简单的环境能够运行起来了,你就可以不断的往上添砖加瓦,比如创建集成测试的环境,添加集成测试,并最终销毁集成测试环境等内容,最终让它成为一个能够满足需求的持续集成流水线。

参考:
Building a CI for Golang test
Building a CI system for Go, with Jenkins

为容器化的 Go 程序搭建 CI的更多相关文章

  1. Docker容器化【Dockerfile编写&&搭建与使用Docker私有仓库】

    # Docker 学习目标: 掌握Docker基础知识,能够理解Docker镜像与容器的概念 完成Docker安装与启动 掌握Docker镜像与容器相关命令 掌握Tomcat Nginx 等软件的常用 ...

  2. 一次容器化springboot程序OOM问题探险

    背景 运维人员反馈一个容器化的java程序每跑一段时间就会出现OOM问题,重启后,间隔大概两天后复现. 问题调查 一查日志 由于是容器化部署的程序,登上主机后使用docker logs Contain ...

  3. ASP.NET Core应用程序容器化、持续集成与Kubernetes集群部署(一)(转载)

    本文结构 ASP.NET Core应用程序的构建 ASP.NET Core应用程序容器化所需注意的问题 应用程序的配置信息 端口侦听 ASP.NET Core的容器版本 docker镜像构建上下文(B ...

  4. 【转帖】使用容器化和 Docker 实现 DevOps 的基础知识

    使用容器化和 Docker 实现 DevOps 的基础知识 https://www.kubernetes.org.cn/6730.html 2020-02-24 15:20 灵雀云 分类:容器 阅读( ...

  5. Java 服务 Docker 容器化最佳实践

    转载自:https://mp.weixin.qq.com/s/d2PFISYUy6X6ZAOGu0-Kig 1. 概述 当我们在容器中运行 Java 应用程序时,可能希望对其进行调整参数以充分利用资源 ...

  6. 利用 ELK 搭建 Docker 容器化应用日志中心

    利用 ELK 搭建 Docker 容器化应用日志中心 概述 应用一旦容器化以后,需要考虑的就是如何采集位于 Docker 容器中的应用程序的打印日志供运维分析.典型的比如SpringBoot应用的日志 ...

  7. 详解利用ELK搭建Docker容器化应用日志中心

    概述 应用一旦容器化以后,需要考虑的就是如何采集位于Docker容器中的应用程序的打印日志供运维分析.典型的比如SpringBoot应用的日志 收集.本文即将阐述如何利用ELK日志中心来收集容器化应用 ...

  8. .NET 7 SDK 开始 支持构建容器化应用程序

    微软于 8 月 25 日在.NET官方博客上,.NET 7 SDK 将包括对创建容器化应用程序的支持,作为构建发布过程的一部分,从而绕过需要.显式 Docker 构建阶段. 这一决定背后的基本认知是简 ...

  9. Atitit s2018.6 s6 doc list on com pc.docx Atitit s2018.6 s6 doc list on com pc.docx  Aitit algo fix 算法系列补充.docx Atiitt 兼容性提示的艺术 attilax总结.docx Atitit 应用程序容器化总结 v2 s66.docx Atitit file cms api

    Atitit s2018.6 s6  doc list on com pc.docx Atitit s2018.6 s6  doc list on com pc.docx  Aitit algo fi ...

随机推荐

  1. python学习笔记(6)

    第6章 组合数据类型 组合类型的三种表达形式:集合.序列.字典 集合类型及操作 定义:集合是多个元素的无序组合 集合类型与数学中的集合概念一致 集合元素之间无序,每个元素唯一,不存在相同元素 集合元素 ...

  2. MUI消息推送

    一.push通过H5+实现 简单实现方式:通过轮询服务器是否有新消息推送过来 mui.plusReady(function() { plus.navigator.closeSplashscreen() ...

  3. SpringCloud 在Feign上使用Hystrix(断路由)

    SpringCloud  在Feign上使用Hystrix(断路由) 第一步:由于Feign的起步依赖中已经引入了Hystrix的依赖,所以只需要开启Hystrix的功能,在properties文件中 ...

  4. RabbitMQ 官方demo1

    public class RabbitMqSend { public static void Test() { var factory = new ConnectionFactory() { Host ...

  5. 连接Redis_五种数据格式

    前面我们已经准备成功开启Redis服务,其端口号为6379,接下来我们就看看如何使用C#语言来操作Redis.就如MongoDB一样,要操作Redis服务,自然就需要下载C#的客户端,这里通过Nuge ...

  6. 微信小程序60秒倒计时

    微信小程序发送短信验证码后60秒倒计时功能,效果图: 完整代码 index.wxml <!--index.wxml--> <view class="container&qu ...

  7. Java 延迟队列使用

    延时队列,第一他是个队列,所以具有对列功能第二就是延时,这就是延时对列,功能也就是将任务放在该延时对列中,只有到了延时时刻才能从该延时对列中获取任务否则获取不到…… 应用场景比较多,比如延时1分钟发短 ...

  8. win7系统下dos界面无法自由调整大小

    刚开始在win7系统,在dos界面下做MySQL的实验,很多数据不能显示界面上,只能显示固定的大小,以为这是系统的原因,后来在网上查找了一些资料.终于发现可以自由调节dos界面大小的方法.下面给出截图 ...

  9. phpcms2008远程代码执行漏洞

    phpcms2008远程代码执行漏洞 描述: 近日,互联网爆出PHPCMS2008代码注入漏洞(CVE-2018-19127).攻击者利用该漏洞,可在未授权的情况下实现对网站文件的写入.该漏洞危害程度 ...

  10. [Swift]LeetCode102. 二叉树的层次遍历 | Binary Tree Level Order Traversal

    Given a binary tree, return the level order traversal of its nodes' values. (ie, from left to right, ...