Jenkins: использование shared libraries

Jul 30, 2018 07:04 · 689 words · 4 minute read jenkins CI scripts

При использовании Jenkins в компании с большим количеством проектов, рано или поздно вы заметите, что ваши описания пайплайнов (Pipeline) имеют много общего. И, возможно, вам захочется избавиться от избыточности и следовать принципу DRY (Don’t Repeat Yourself) - давайте разберемся!

Помимо принципа DRY, важна также возможность внести изменения в код пайплайна один раз и автоматически использовать обновленный пайплайн в 50-100 других проектах.

Для этой цели как нельзя лучше подходят Shared Libraries - общие библиотеки, которые могут быть определены в отдельном репозитории системы управления версиями и загружены в описании пайплайна.

Структура каталогов в репозитории общих библиотек должна выглядеть следующим образом:

+- src                     # Source files
|   +- org
|       +- foo
|           +- Bar.groovy  # for org.foo.Bar class
+- vars
|   +- foo.groovy          # for global 'foo' variable
|   +- foo.txt             # help for 'foo' variable
+- resources               # resource files (external libraries only)
|   +- org
|       +- foo
|           +- bar.json    # static helper data for org.foo.Bar

Примечание. В данной статье мы будем использовать только каталоги vars и resources.

Для нас наибольший интерес представляет директория vars - в ней можно разместить глобальные функции и переменные, доступные в пайплайнах. Согласно документации, имена файлов должны быть в camelCase формате (без дефисов/подчеркиваний и т.д. - это важно), и иметь расширение .groovy (именно эти файлы нас и интересуют) или .txt (для документации).

В каталоге resources можно разместить любые другие (не Java) файлы (например, .yaml или .json), которые будут загружаться в описании пайплайна с помощью шага libraryResource.

Итак, рассмотрим несколько примеров. Допустим, у нас есть простенький проект, в котором описание пайплайна (файл Jenkinsfile) выглядит так:

pipeline {
  agent any

  stages {
    stage('Checkout') {
      steps {
        checkout scm
      }
    }
    stage('Build') {
      steps {
        script {
          sh '''
            sudo docker version
            sudo docker build -t ealebed/hellonode:latest .
            sudo docker image ls
          '''
        }
      }
    }
  }
}

Кроме описания пайплайна в репозитории находится еще два файла - Dockerfile:

FROM node:6.9
COPY server.js .
EXPOSE 8080
CMD node server.js

и файл server.js следующего содержания:

var http = require('http');
var handleRequest = function(request, response) {
  response.writeHead(200);
  response.end("Hello World!");
}
var www = http.createServer(handleRequest);
www.listen(8080);

В первой итерации выделим в отдельные функции части пайплайна, которые можно будет использовать в других проектах. Для этого:

  • создаем отдельный git-репозиторий для наших общих библиотек;
  • в репозитории создаем каталог vars;
  • в каталоге vars размещаем скрипт dockerCmd.groovy.

Содержимое dockerCmd.groovy:

def call(args) {
  assert args != null
  sh(script: "sudo docker ${args}")
}

Настраиваем использование Shared Libraries на Jenkins (пример с картинками). Теперь в нашем проекте пайплайн можно переписать так:

@Library('jenkins-shared-libs@master') _

pipeline {
  agent any

  stages {
    stage('Checkout') {
      steps {
        checkout scm
      }
    }
    stage('Build') {
      steps {
        dockerCmd 'version'
        dockerCmd 'build -t ealebed/hellonode:latest .'
        dockerCmd 'image ls'
      }
    }
  }
}

Продолжаем. Во второй итерации избавимся от необходимости хранить Dockerfile в репозитории проекта. Для этого в git-репозитории с общими библиотеками создаем каталог resources и переносим в него Dockerfile из основного проекта. Далее, в каталоге vars размещаем скрипт createDockerfile.groovy следующего содержания:

def call() {
  def file = libraryResource 'Dockerfile'
  writeFile file: 'Dockerfile', text: file
}

Описание пайплайна в основном проекте изменяем на следующее:

@Library('jenkins-shared-libs@master') _

pipeline {
  agent any

  stages {
    stage('Checkout') {
      steps {
        checkout scm
      }
    }
    stage('Get Dockerfile') {
      steps {
        createDockerfile()
      }
    }
    stage('Build') {
      steps {
        dockerCmd 'version'
        dockerCmd 'build -t ealebed/hellonode:latest .'
        dockerCmd 'image ls'
      }
    }
  }
}

Но что, если мы хотим использовать в других проектах не только отдельные функции, а весь пайплайн целиком? Нет ничего проще!

В git-репозитории с общими библиотеками в каталоге vars создаем скрипт allPipeline.groovy следующего содержания:

def call(body) {
  def pipelineParams= [:]
  body.resolveStrategy = Closure.DELEGATE_FIRST
  body.delegate = pipelineParams
  body()

  pipeline {
    agent any

    stages {
      stage('Checkout') {
        steps {
          checkout scm
        }
      }
      stage('Get Dockerfile') {
        steps {
          script {
            def tmpFile = libraryResource 'Dockerfile'
            writeFile file: 'Dockerfile', text: tmpFile
          }
        }
      }
      stage('Build') {
        steps {
          script {
            sh '''
              docker version
              docker build -t ealebed/hellonode:latest .
              docker image ls
            '''
          }
        }
      }
    }
  }
}

Теперь, в содержимое файла Jenkinsfile (описание пайплайна) в основном репозитории проекта невероятно упрощается:

@Library('jenkins-shared-libs@master') _

allPipeline {}

Больше интересных примеров можно найти в официальной документации, а также здесь, здесь и здесь.

Don’t Repeat Yourself!

tweet Share