Skip to main content

16 posts tagged with "DevOps"

View All Tags

Parameters and Variables in Jenkinsfile

· One min read

Open in Notion

import groovy.transform.Field

@Field def globalV = ""

def myFunc() {
echo "${globalV}" // The @Field is required for this variable in definition
}

node {
stage('Start') {
echo 'Testing..'
print params
if (params.b) {
echo 'has b'
}
if (b == 'true') {
echo 'b == true'
}
print 's:' + s
echo 's: ' + s

if (params.nn) {
echo 'has nn'
} else {
echo 'nn undefined'
}
}

stage('Build') {
echo 'Building..'
}

stage('Test') {
echo 'Testing..'
}

stage('Deploy') {
echo 'Deploying....'
}

stage ("Prompt for input") {
steps {
script {
env.USERNAME = input message: 'Please enter the username',
parameters: [string(defaultValue: '',
description: '',
name: 'Username')]
env.PASSWORD = input message: 'Please enter the password',
parameters: [password(defaultValue: '',
description: '',
name: 'Password')]
}
echo "Username: ${env.USERNAME}"
echo "Password: ${env.PASSWORD}"
}
}
}

Docker Resource

· One min read

Open in Notion

docker pull node:14
docker pull node:<version>-alpine
docker pull node:<version>-slim

docker run --rm -v "$PWD":/home/node/app -w /home/node/app -it node:14-slim /bin/bash -c "npm build"

Zipkin

· One min read

Open in Notion

https://zipkin.io/

Zipkin is a distributed tracing system. It helps gather timing data needed to troubleshoot latency problems in service architectures. Features include both the collection and lookup of this data.

If you have a trace ID in a log file, you can jump directly to it. Otherwise, you can query based on attributes such as service, operation name, tags and duration. Some interesting data will be summarized for you, such as the percentage of time spent in a service, and whether or not operations failed.

web-screenshot.png

ELK Setup - Elasticsearch、Kibana、Logstash

· 10 min read

Open in Notion

ELK日志收集系统进阶使用,本文主要讲解如何打造一个线上环境真实可用的日志收集系统。有了它,你就可以和去服务器上捞日志说再见了!

ELK环境安装

ELK是指Elasticsearch、Kibana、Logstash这三种服务搭建的日志收集系统,具体搭建方式可以参考《SpringBoot应用整合ELK实现日志收集》。这里仅提供最新版本的docker-compose脚本和一些安装要点。

docker-compose脚本

version: '3'
services:
elasticsearch:
image: elasticsearch:6.4.0
container_name: elasticsearch
environment:
- "cluster.name=elasticsearch" #设置集群名称为elasticsearch
- "discovery.type=single-node" #以单一节点模式启动
- "ES_JAVA_OPTS=-Xms512m -Xmx512m" #设置使用jvm内存大小
- TZ=Asia/Shanghai
volumes:
- /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins #插件文件挂载
- /mydata/elasticsearch/data:/usr/share/elasticsearch/data #数据文件挂载
ports:
- 9200:9200
- 9300:9300
kibana:
image: kibana:6.4.0
container_name: kibana
links:
- elasticsearch:es #可以用es这个域名访问elasticsearch服务
depends_on:
- elasticsearch #kibana在elasticsearch启动之后再启动
environment:
- "elasticsearch.hosts=http://es:9200" #设置访问elasticsearch的地址
- TZ=Asia/Shanghai
ports:
- 5601:5601
logstash:
image: logstash:6.4.0
container_name: logstash
environment:
- TZ=Asia/Shanghai
volumes:
- /mydata/logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf #挂载logstash的配置文件
depends_on:
- elasticsearch #kibana在elasticsearch启动之后再启动
links:
- elasticsearch:es #可以用es这个域名访问elasticsearch服务
ports:
- 4560:4560
- 4561:4561
- 4562:4562
- 4563:4563

安装要点

  • 使用docker-compose命令运行所有服务:
docker-compose up -d
  • 第一次启动可能会发现Elasticsearch无法启动,那是因为/usr/share/elasticsearch/data目录没有访问权限,只需要修改/mydata/elasticsearch/data目录的权限,再重新启动;
chmod 777 /mydata/elasticsearch/data/
  • Logstash需要安装json_lines插件。
logstash-plugin install logstash-codec-json_lines

分场景收集日志

这里为了方便我们查看日志,提出一个分场景收集日志的概念,把日志分为以下四种。

  • 调试日志:最全日志,包含了应用中所有DEBUG级别以上的日志,仅在开发、测试环境中开启收集;
  • 错误日志:只包含应用中所有ERROR级别的日志,所有环境只都开启收集;
  • 业务日志:在我们应用对应包下打印的日志,可用于查看我们自己在应用中打印的业务日志;
  • 记录日志:每个接口的访问记录,可以用来查看接口执行效率,获取接口访问参数。

Logback配置详解

要实现上面的分场景收集日志,主要通过Logback的配置来实现,我们先来了解下Logback的配置吧!

完全配置

在SpringBoot中,如果我们想要自定义Logback的配置,需要自行编写logback-spring.xml文件,下面是我们这次要使用的完全配置。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration>
<configuration>
<!--引用默认日志配置-->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!--使用默认的控制台日志输出实现-->
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<!--应用名称-->
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="springBoot"/>
<!--日志文件保存路径-->
<property name="LOG_FILE_PATH" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/logs}"/>
<!--LogStash访问host-->
<springProperty name="LOG_STASH_HOST" scope="context" source="logstash.host" defaultValue="localhost"/>

<!--DEBUG日志输出到文件-->
<appender name="FILE_DEBUG"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--输出DEBUG以上级别日志-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<encoder>
<!--设置为默认的文件日志格式-->
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--设置文件命名格式-->
<fileNamePattern>${LOG_FILE_PATH}/debug/${APP_NAME}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<!--设置日志文件大小,超过就重新生成文件,默认10M-->
<maxFileSize>${LOG_FILE_MAX_SIZE:-10MB}</maxFileSize>
<!--日志文件保留天数,默认30天-->
<maxHistory>${LOG_FILE_MAX_HISTORY:-30}</maxHistory>
</rollingPolicy>
</appender>

<!--ERROR日志输出到文件-->
<appender name="FILE_ERROR"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--只输出ERROR级别的日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<!--设置为默认的文件日志格式-->
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--设置文件命名格式-->
<fileNamePattern>${LOG_FILE_PATH}/error/${APP_NAME}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<!--设置日志文件大小,超过就重新生成文件,默认10M-->
<maxFileSize>${LOG_FILE_MAX_SIZE:-10MB}</maxFileSize>
<!--日志文件保留天数,默认30天-->
<maxHistory>${LOG_FILE_MAX_HISTORY:-30}</maxHistory>
</rollingPolicy>
</appender>

<!--DEBUG日志输出到LogStash-->
<appender name="LOG_STASH_DEBUG" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<destination>${LOG_STASH_HOST}:4560</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>Asia/Shanghai</timeZone>
</timestamp>
<!--自定义日志输出格式-->
<pattern>
<pattern>
{
"project": "mall-tiny",
"level": "%level",
"service": "${APP_NAME:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger",
"message": "%message",
"stack_trace": "%exception{20}"
}
</pattern>
</pattern>
</providers>
</encoder>
<!--当有多个LogStash服务时,设置访问策略为轮询-->
<connectionStrategy>
<roundRobin>
<connectionTTL>5 minutes</connectionTTL>
</roundRobin>
</connectionStrategy>
</appender>

<!--ERROR日志输出到LogStash-->
<appender name="LOG_STASH_ERROR" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<destination>${LOG_STASH_HOST}:4561</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>Asia/Shanghai</timeZone>
</timestamp>
<!--自定义日志输出格式-->
<pattern>
<pattern>
{
"project": "mall-tiny",
"level": "%level",
"service": "${APP_NAME:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger",
"message": "%message",
"stack_trace": "%exception{20}"
}
</pattern>
</pattern>
</providers>
</encoder>
<!--当有多个LogStash服务时,设置访问策略为轮询-->
<connectionStrategy>
<roundRobin>
<connectionTTL>5 minutes</connectionTTL>
</roundRobin>
</connectionStrategy>
</appender>

<!--业务日志输出到LogStash-->
<appender name="LOG_STASH_BUSINESS" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${LOG_STASH_HOST}:4562</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>Asia/Shanghai</timeZone>
</timestamp>
<!--自定义日志输出格式-->
<pattern>
<pattern>
{
"project": "mall-tiny",
"level": "%level",
"service": "${APP_NAME:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger",
"message": "%message",
"stack_trace": "%exception{20}"
}
</pattern>
</pattern>
</providers>
</encoder>
<!--当有多个LogStash服务时,设置访问策略为轮询-->
<connectionStrategy>
<roundRobin>
<connectionTTL>5 minutes</connectionTTL>
</roundRobin>
</connectionStrategy>
</appender>

<!--接口访问记录日志输出到LogStash-->
<appender name="LOG_STASH_RECORD" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${LOG_STASH_HOST}:4563</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>Asia/Shanghai</timeZone>
</timestamp>
<!--自定义日志输出格式-->
<pattern>
<pattern>
{
"project": "mall-tiny",
"level": "%level",
"service": "${APP_NAME:-}",
"class": "%logger",
"message": "%message"
}
</pattern>
</pattern>
</providers>
</encoder>
<!--当有多个LogStash服务时,设置访问策略为轮询-->
<connectionStrategy>
<roundRobin>
<connectionTTL>5 minutes</connectionTTL>
</roundRobin>
</connectionStrategy>
</appender>

<!--控制框架输出日志-->
<logger name="org.slf4j" level="INFO"/>
<logger name="springfox" level="INFO"/>
<logger name="io.swagger" level="INFO"/>
<logger name="org.springframework" level="INFO"/>
<logger name="org.hibernate.validator" level="INFO"/>

<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
<!--<appender-ref ref="FILE_DEBUG"/>-->
<!--<appender-ref ref="FILE_ERROR"/>-->
<appender-ref ref="LOG_STASH_DEBUG"/>
<appender-ref ref="LOG_STASH_ERROR"/>
</root>

<logger name="com.macro.mall.tiny.component" level="DEBUG">
<appender-ref ref="LOG_STASH_RECORD"/>
</logger>

<logger name="com.macro.mall" level="DEBUG">
<appender-ref ref="LOG_STASH_BUSINESS"/>
</logger>
</configuration>

配置要点解析

使用默认的日志配置

一般我们不需要自定义控制台输出,可以采用默认配置,具体配置参考console-appender.xml,该文件在spring-boot-${version}.jar下面。

<!--引用默认日志配置-->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!--使用默认的控制台日志输出实现-->
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>

springProperty

该标签可以从SpringBoot的配置文件中获取配置属性,比如说在不同环境下我们的Logstash服务地址是不一样的,我们就可以把该地址定义在application.yml来使用。

例如在application-dev.yml中定义了这些属性:

logstash:
host: localhost

在logback-spring.xml中就可以直接这样使用:

<!--应用名称-->
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="springBoot"/>
<!--LogStash访问host-->
<springProperty name="LOG_STASH_HOST" scope="context" source="logstash.host" defaultValue="localhost"/>

filter

在Logback中有两种不同的过滤器,用来过滤日志输出。

ThresholdFilter:临界值过滤器,过滤掉低于指定临界值的日志,比如下面的配置将过滤掉所有低于INFO级别的日志。

<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level></filter>

LevelFilter:级别过滤器,根据日志级别进行过滤,比如下面的配置将过滤掉所有非ERROR级别的日志。

<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter>

appender

Appender可以用来控制日志的输出形式,主要有下面三种。

  • ConsoleAppender:控制日志输出到控制台的形式,比如在console-appender.xml中定义的默认控制台输出。
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder></appender>
  • RollingFileAppender:控制日志输出到文件的形式,可以控制日志文件生成策略,比如文件名称格式、超过多大重新生成文件以及删除超过多少天的文件。
<!--ERROR日志输出到文件-->
<appender name="FILE_ERROR"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--设置文件命名格式-->
<fileNamePattern>${LOG_FILE_PATH}/error/${APP_NAME}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<!--设置日志文件大小,超过就重新生成文件,默认10M-->
<maxFileSize>${LOG_FILE_MAX_SIZE:-10MB}</maxFileSize>
<!--日志文件保留天数,默认30天-->
<maxHistory>${LOG_FILE_MAX_HISTORY:-30}</maxHistory>
</rollingPolicy>
</appender>
  • LogstashTcpSocketAppender:控制日志输出到Logstash的形式,可以用来配置Logstash的地址、访问策略以及日志的格式。
<!--ERROR日志输出到LogStash-->
<appender name="LOG_STASH_ERROR" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${LOG_STASH_HOST}:4561</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>Asia/Shanghai</timeZone>
</timestamp>
<!--自定义日志输出格式-->
<pattern>
<pattern>
{
"project": "mall-tiny",
"level": "%level",
"service": "${APP_NAME:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger",
"message": "%message",
"stack_trace": "%exception{20}"
}
</pattern>
</pattern>
</providers>
</encoder>
<!--当有多个LogStash服务时,设置访问策略为轮询-->
<connectionStrategy>
<roundRobin>
<connectionTTL>5 minutes</connectionTTL>
</roundRobin>
</connectionStrategy>
</appender>

logger

只有配置到logger节点上的appender才会被使用,logger用于配置哪种条件下的日志被打印,root是一种特殊的appender,下面介绍下日志划分的条件。

  • 调试日志:所有的DEBUG级别以上日志;
  • 错误日志:所有的ERROR级别日志;
  • 业务日志:com.macro.mall包下的所有DEBUG级别以上日志;
  • 记录日志:com.macro.mall.tiny.component.WebLogAspect类下所有DEBUG级别以上日志,该类是统计接口访问信息的AOP切面类。

控制框架输出日志

还有一些使用框架内部的日志,DEBUG级别的日志对我们并没有啥用处,都可以设置为了INFO以上级别。

<!--控制框架输出日志-->
<logger name="org.slf4j" level="INFO"/>
<logger name="springfox" level="INFO"/>
<logger name="io.swagger" level="INFO"/>
<logger name="org.springframework" level="INFO"/>
<logger name="org.hibernate.validator" level="INFO"/>

Logstash配置详解

接下来我们需要配置下Logstash,让它可以分场景收集不同的日志,下面详细介绍下使用到的配置。

完全配置

input {
tcp {
mode => "server"
host => "0.0.0.0"
port => 4560
codec => json_lines
type => "debug"
}
tcp {
mode => "server"
host => "0.0.0.0"
port => 4561
codec => json_lines
type => "error"
}
tcp {
mode => "server"
host => "0.0.0.0"
port => 4562
codec => json_lines
type => "business"
}
tcp {
mode => "server"
host => "0.0.0.0"
port => 4563
codec => json_lines
type => "record"
}
}
filter{
if [type] == "record" {
mutate {
remove_field => "port"
remove_field => "host"
remove_field => "@version"
}
json {
source => "message"
remove_field => ["message"]
}
}
}
output {
elasticsearch {
hosts => ["es:9200"]
action => "index"
codec => json
index => "mall-tiny-%{type}-%{+YYYY.MM.dd}"
template_name => "mall-tiny"
}
}

配置要点

  • input:使用不同端口收集不同类型的日志,从4560~4563开启四个端口;
  • filter:对于记录类型的日志,直接将JSON格式的message转化到source中去,便于搜索查看;
  • output:按类型、时间自定义索引格式。

SpringBoot配置

在SpringBoot中的配置可以直接用来覆盖Logback中的配置,比如logging.level.root就可以覆盖<root>节点中的level配置。

  • 开发环境配置:application-dev.yml
logstash:
host: localhost
logging:
level:
root: debug
  • 测试环境配置:application-test.yml
logstash:
host: 192.168.3.101
logging:
level:
root: debug
  • 生产环境配置:application-prod.yml
logstash:
host: logstash-prod
logging:
level:
root: info

Docker Usage

· 3 min read

Open in Notion

docker build -t <image-name> --build-arg YOUR_ARG="<arg-value>" .

# -d: as deamon
# --rm: exit and remove
docker run -d -p 80:80 -p 443:443 --name <container-name> -v /opt/websites/app:/usr/share/nginx/html <image-name>

# enter the terminal
docker exec -it <container-name> /bin/bash

docker image remove <image-name>

docker container rm <container-name>


##
docker container rename <container-name> <new-container-name>
docker rm -f <container-name>

# build from github
docker build -t <image-name> https://github.com/bndynet/docker.nginx.git[#<branch/tag>:<dir-for-context>]


# Copy & Move
## Export and import containers
docker export container-name | gzip > container-name.gz
zcat container-name.gz | docker import - container-name
## Container image migration - Using this method, the data volumes will not be migrated, but it preserves the data of the application created inside the container.
docker commit container-id image-name
## Save and load images
docker save image-name > image-name.tar
cat image-name.tar | docker load
## copy files from container into host which does not need a running container
docker cp your-file-path-in-container your-host-location

# Publish
docker build -t bndynet/image-name:1.0 .
docker push bndynet/image-name:1.0

Networking

bookmark

# bridge: The default network driver
# host: use the host’s networking directly
docker --network bridge|host|overlay|ipvlan

Change the port for the existing container

You can change the port mapping by directly editing the hostconfig.json file at /var/lib/docker/containers/[hash_of_the_container]/hostconfig.json or /var/snap/docker/common/var-lib-docker/containers/[hash_of_the_container]/hostconfig.json, I believe, if You installed Docker as a snap.

You can determine the [hash_of_the_container] via the docker inspect <container_name> command and the value of the "Id" field is the hash.

  1. Stop the container (docker stop <container_name>).
  2. Stop docker service (per Tacsiazuma's comment)
  3. Change the file.
  4. Restart your docker engine (to flush/clear config caches).
  5. Start the container (docker start <container_name>).

Clear

docker system df # show disk space about docker
docker image prune -f # clear all images which is not used
# print the log for container
docker logs -f <container-name> 1>/dev/null

Example:

IMAGE_NAME = "bndynet/nginx"
CONTAINER_NAME = "web"

docker rm -f $CONTAINER_NAME || true
docker image rm $IMAGE_NAME || true
docker build -t $IMAGE_NAME .

docker run -d -p 80:80 -p 443:443 --name $CONTAINER_NAME -v /opt/website:/usr/share/nginx/html -v /etc/ssl:/etc/ssl $IMAGE_NAME

Mirrors

{
"registry-mirrors": [
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn"
]
}

Jenkins File Examples

· 4 min read

Open in Notion

Parameter Definitions

Scripted

properties([
parameters([
string(name: 'USERNAME', defaultValue: 'admin', description: 'Enter your username'),
booleanParam(name: 'ENABLE_FEATURE', defaultValue: false, description: 'Enable experimental feature'),
choice(name: 'DEPLOY_ENV', choices: 'dev\nqa\nprod', description: 'Choose deployment environment')
])
])

node {
echo "USERNAME: ${params.USERNAME}"
echo "ENABLE_FEATURE: ${params.ENABLE_FEATURE}"
echo "DEPLOY_ENV: ${params.DEPLOY_ENV}"
}

Declarative

pipeline {
agent any
parameters {
string(name: 'VERSION', defaultValue: '1.0.0', description: 'Version to deploy')
booleanParam(name: 'CONFIRM', defaultValue: true, description: 'Confirm the deployment')
choice(name: 'ENV', choices: ['staging', 'production'], description: 'Environment to deploy to')
}
stages {
stage('Print Params') {
steps {
echo "Version: ${params.VERSION}"
echo "Confirm: ${params.CONFIRM}"
echo "Environment: ${params.ENV}"
}
}
}
}

Parameter Usage

node {
print '✋' + params
print params.anything_to_undefined // output: null
if (params.MultiLines) {
params.MultiLines.split("\\r?\\n").each {
if (it.trim()) {
echo '✋-->' + it
}
}
}
// clean workspace
cleanWs()
try {
docker.image('node:20').inside('--network host') {
stage("Source code") {
checkout([
$class: 'GitSCM',
branches: [[name: '*/${Git_Branch}']],
userRemoteConfigs: [[
url: 'git@github.com:bndynet/typescript-lib-starter.git',
credentialsId: '${Cred_ID}'
]],
extensions: [
[$class: 'CloneOption', depth: 1, noTags: false, shallow: true],
[$class: 'CheckoutOption', timeout: 30]
]
])
}
stage('Prepare') {
sh """
echo "================================"
printenv | sort
echo "================================"
"""
sh 'npm i'
}
stage('Build') {
withEnv(['my_name=bing']) {
// TODO
sh 'Hi ${my_name}.'
}
}
stage('Test') {
if (params.Skip_Test != true) {
echo 'do test'
} else {
echo 'Skip test'
}
}
stage('Deploy') {
if (params.Skip_Deploy != true) {
// TODO
echo 'do deploy'
} else {
echo 'Skip deploy'
}
}
stage('Deploy to production') {
def toProdParams
def didTimeout = false
try {
timeout(time: 15, unit: 'SECONDS') {
// change to a convenient timeout for you
toProdParams = input(
id: 'deployToProd', message: 'Deploy to production?', parameters: [
choice(choices: ['prod-a.com/api', 'prod-b.com/api'], description: 'Backend endpoint to connect', name: 'Backend_Uri'),
booleanParam(defaultValue: false, description: 'Archive this package', name: 'Archive')
]
)
}
} catch (err) {
// timeout reached or input false
// def user = err.getCauses()[0].getUser() // required administrator
//if('SYSTEM' == user.toString()) { // SYSTEM means timeout.
didTimeout = true
//} else {
//toProd = false
//echo "Aborted by: [${user}]"
//}
}
echo '✋Your choice: ' + toProdParams // null if timeout
if (toProdParams) {
echo toProdParams.Backend_Uri
}
if (didTimeout) {
// do something on timeout
echo "no input was received before timeout"
} else if (toProdParams) {
// do something
echo "Deployed to production."
} else {
// do something else
echo "Skip deployment to production."
}
}
}
} catch (error) {
echo """❗Exception thrown:
${error.class}
${error.message}
"""
// error.printStackTrace() // required administrator
currentBuild.result = 'FAILURE'
} finally {
if (currentBuild.result == 'SUCCESS') {
// TODO
} else {
// TODO
}
echo currentBuild.result
}
}

Docker in Node

node ('your-server-node-label') {
stage('Test') {
docker.image('node:7-alpine').withRun() {
c -> sh 'node --version'
}
}
}

Maven and Deploy

pipeline {
agent any

tools {
maven 'Maven 3.5'
}

environment {
APP_NAME = 'wf'
DEPLOY_TO = '../../../../websites/demo/'
EMAIL_RECIPIENTS = 'zb@bndy.net'
HAS_DEPLOYMENT = 'false'
}

stages {
stage('Prepare') {
steps {
updateGithubStatus("PENDING", "Begin to build");
//sh 'printenv'
sh 'java -version'
sh 'mvn -v'
sh 'mkdir -p ../../email-templates/ && \\cp ./jenkins/my-groovy-html.template ../../email-templates/my-groovy-html.template'
}
}
stage('Build') {
steps {
echo 'Building...'
sh 'mvn -B -DskipTests clean package'
}
}
stage('Test') {
steps {
echo 'Skip tests'
}
}
stage('Deploy') {
when {
// following expression is just available for multi-branch project
branch 'master'
}
steps {
echo 'Deploying....'
sh 'rm -f ${DEPLOY_TO}/${APP_NAME}.war'
sh 'rm -Rf $DEPLOY_TO/${APP_NAME}'
sh 'mv ./target/wf-*.war ${DEPLOY_TO}/${APP_NAME}.war'
script {
HAS_DEPLOYMENT = 'true'
}
}
}
}

post {
success {
updateGithubStatus("SUCCESS", "Build complete");
sendEmail("SUCCESS");
}
unstable {
updateGithubStatus("UNSTABLE", "Build complete");
sendEmail("UNSTABLE");
}
failure {
updateGithubStatus("FAILURE", "Build complete");
sendEmail("FAILURE");
}
}
}

// get change log to be send over the mail
@NonCPS
def getChangeString() {
MAX_MSG_LEN = 100
def changeString = ""

echo "Gathering SCM changes"
def changeLogSets = currentBuild.changeSets
for (int i = 0; i < changeLogSets.size(); i++) {
def entries = changeLogSets[i].items
for (int j = 0; j < entries.length; j++) {
def entry = entries[j]
truncated_msg = entry.msg.take(MAX_MSG_LEN)
changeString += "<li>${truncated_msg} [${entry.author}]</li>"
}
}

if (!changeString) {
changeString = "<li>No new changes</li>"
}
return changeString
}

def sendEmail(status) {
def subject = "[" + status + "] ${currentBuild.fullDisplayName}"
if (HAS_DEPLOYMENT == 'true') {
subject += " - deployed"
}

// Default Email Notification Plugin: email(...), but below supports html
emailext(
mimeType: "text/html",
to: "$EMAIL_RECIPIENTS",
subject: "${subject}",
body: '''${SCRIPT, template="my-groovy-html.template"}''',
//body: "Changes:<ul>" + getChangeString() + "</ul><br />Check console output at: ${BUILD_URL}console" + "<br />",
recipientProviders: [[$class: 'CulpritsRecipientProvider'], [$class: 'RequesterRecipientProvider']]
)
}

// set GitHub status
def getRepoURL() {
sh "git config --get remote.origin.url > .git/remote-url"
return readFile(".git/remote-url").trim()
}

def getCommitSha() {
sh "git rev-parse HEAD > .git/current-commit"
return readFile(".git/current-commit").trim()
}

def updateGithubStatus(status, message) {
// status: pending, success, failure or error
repoUrl = getRepoURL()
commitSha = getCommitSha()

step ([
$class: 'GitHubCommitStatusSetter',
reposSource: [$class: "ManuallyEnteredRepositorySource", url: repoUrl],
commitShaSource: [$class: "ManuallyEnteredShaSource", sha: commitSha],
errorHandlers: [[$class: 'ShallowAnyErrorHandler']],
statusResultSource: [
$class: 'ConditionalStatusResultSource',
results: [
[$class: 'AnyBuildResult', state: status, message: message]
]
]
])
}