Jenkins Base Install
Using the public Docker image as a baseline https://github.com/jenkinsci/docker, the majority of the Dockerfile has been adjusted to support our initial base installation with the following components:
- JDK11 Image
- No Install wizard
- Preinstall Plugins for workflow
- Configure Credentials for SCM and Nexus3
version: '3.3'
services:
jenkins:
image: infra/jenkins:1.0.0
build:
context: .
network: host
args:
ARG_ART_URL: http://d1i-doc-ngbuild:3001
extra_hosts:
- "d1i-doc-ngbuild:172.22.90.2"
FROM infra/java/openjdk-11:1.0.0
ARG ARG_ART_URL
RUN sed -e "s|APT_URL|${ARG_ART_URL}|" /etc/apt/sources.list.base > /etc/apt/sources.list \
&& apt-get update \
&& apt-get install -y curl git unzip dos2unix \
&& apt-get clean \
&& rm /etc/apt/sources.list
ENV JENKINS_HOME /var/jenkins_home
ARG user=jenkins
ARG group=${user}
ARG uid=1000
ARG gid=1000
RUN groupadd -g ${gid} ${group} \
&& useradd -d "$JENKINS_HOME" -u ${uid} -g ${gid} -m -s /bin/bash ${user}
ARG ARG_ART_URL
###############################################################
## Install Jenkins & Plugin Manager
###############################################################
RUN mkdir -p /usr/share/jenkins \
&& curl $ARG_ART_URL/repository/dml/docker/jenkins/jenkins-war-2.346.3.war -o /usr/share/jenkins/jenkins.war \
&& curl $ARG_ART_URL/repository/dml/docker/jenkins/jenkins-plugin-manager-2.1.0.jar -o /usr/lib/jenkins-plugin-manager.jar
COPY jenkins-plugin-cli.sh /usr/local/bin/jenkins-plugin-cli.sh
RUN chmod +x /usr/local/bin/jenkins-plugin-cli.sh \
&& dos2unix /usr/local/bin/jenkins-plugin-cli.sh
###############################################################
## Core Structure and permissions
###############################################################
# `/usr/share/jenkins/ref/` for bundles, needed in jenkins.sh check
RUN mkdir -p /usr/share/jenkins/ref/init.groovy.d
RUN chown -R ${user} "$JENKINS_HOME" /usr/share/jenkins/ref
# startup scripts
COPY jenkins-support /usr/local/bin/jenkins-support
COPY jenkins.sh /usr/local/bin/jenkins.sh
RUN chmod +x /usr/local/bin/jenkins-support \
&& chmod +x /usr/local/bin/jenkins.sh \
&& dos2unix /usr/local/bin/jenkins-support \
&& dos2unix /usr/local/bin/jenkins.sh
# needed for jenkins.sh and jenkins-support
# specifically for volume persmission check
ENV COPY_REFERENCE_FILE_LOG $JENKINS_HOME/copy_reference_file.log
# for jenkins options logs
#RUN mkdir /var/log/jenkins
#RUN chown -R ${user}:${user} /var/log/jenkins
# for jenkins options cache
#RUN mkdir /var/cache/jenkins
#RUN chown -R ${user}:${user} /var/cache/jenkins
# needed for jenkins.sh execution
ENV JAVA_OPTS="-Xmx2048m -Djenkins.install.runSetupWizard=false"
#ENV JENKINS_OPTS="--logfile=/var/log/jenkins/jenkins.log --webroot=/var/cache/jenkins/war"
###############################################################
# get tini for docker processes
###############################################################
RUN curl $ARG_ART_URL/repository/dml/docker/tools/tini -o /bin/tini \
&& chmod +x /bin/tini
###############################################################
## Actual Plugins for this image, this will change frequently
## for each possible build base
## so putting it here at the end for image optimization
###############################################################
# plugins for this image
COPY plugins-01.txt /tmp/plugins-01.txt
RUN /usr/local/bin/jenkins-plugin-cli.sh -f /tmp/plugins-01.txt
# init files via groovy
COPY init-01.groovy /usr/share/jenkins/ref/init.groovy.d/init-01.groovy
RUN chmod +x /usr/share/jenkins/ref/init.groovy.d/init-01.groovy
###############################################################
## Final Steps
###############################################################
# Switch to the jenkins user
USER ${user}
# only needed if you connect to a repo with a non-public cert
RUN git config --global http.sslVerify "false"
ENTRYPOINT ["/bin/tini", "--", "/usr/local/bin/jenkins.sh"]
import hudson.model.*;
import jenkins.model.*;
import hudson.tasks.Maven.MavenInstallation;
import hudson.tools.InstallSourceProperty;
import hudson.tools.ToolProperty;
import hudson.tools.ToolPropertyDescriptor;
import hudson.util.DescribableList;
import com.cloudbees.plugins.credentials.impl.*;
import com.cloudbees.plugins.credentials.*;
import com.cloudbees.plugins.credentials.domains.*;
import com.cloudbees.jenkins.plugins.sshcredentials.impl.*
Thread.start {
println "--> starting init-01"
/*
* Step 1 - check if we are already configured based on credentials existing
*/
List<Credentials> existing = SystemCredentialsProvider.getInstance().getCredentials();
if (existing.size() > 0) {
println "Jenkins already configured, skipping."
return;
}
/*
* Step 2 - we want to use 3 sets of credentiuals, one for artifacts
* one for scm if needed, another for ssn if needed
*/
def env = System.getenv()
//https://github.com/jenkinsci/jenkins-scripts/blob/master/scriptler/addCredentials.groovy
def artifactsPassword = new File('/run/secrets/jenkins-passwords/artifacts-pwd.txt').text
Credentials artifactCredentials = (Credentials) new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, env['artifacts.id'], env['artifacts.id'], env['artifacts.user'], artifactsPassword)
SystemCredentialsProvider.getInstance().getStore().addCredentials(Domain.global(), artifactCredentials)
println "--> added artifacts credentials from env and secrets"
if (env['scm.id'] != null) {
def scmPassword = new File('/run/secrets/jenkins-passwords/scm-pwd.txt').text
Credentials scmCredentials = (Credentials) new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, env['scm.id'], env['scm.id'], env['scm.user'], scmPassword)
SystemCredentialsProvider.getInstance().getStore().addCredentials(Domain.global(), scmCredentials)
println "--> added scm credentials from env and secrets"
}
if (env['ssh.id'] != null) {
// https://github.com/jenkinsci/ssh-credentials-plugin/blob/master/src/main/java/com/cloudbees/jenkins/plugins/sshcredentials/impl/BasicSSHUserPrivateKey.java
def sshPassphrase = new File('/run/secrets/jenkins-passwords/ssh-passphrase.txt').text
// for proper env independance we need to worry about dos2unix for win to linux file issues
def sshPrivateKey = new File('/run/secrets/jenkins-passwords/ssh-private-key.txt').text.replaceAll("\r\n", "\n")
def source = new BasicSSHUserPrivateKey.DirectEntryPrivateKeySource(sshPrivateKey)
def ssh = new BasicSSHUserPrivateKey(CredentialsScope.GLOBAL, env['ssh.id'], env['ssh.id'], source, sshPassphrase, env['ssh.id'])
SystemCredentialsProvider.getInstance().getStore().addCredentials(Domain.global(), ssh)
println "--> added ssh credentials from env and secrets"
}
}
#!/usr/bin/env bash
java -jar /usr/lib/jenkins-plugin-manager.jar "$@"
#!/bin/bash -eu
# compare if version1 < version2
versionLT() {
local v1; v1=$(echo "$1" | cut -d '-' -f 1 )
local q1; q1=$(echo "$1" | cut -s -d '-' -f 2- )
local v2; v2=$(echo "$2" | cut -d '-' -f 1 )
local q2; q2=$(echo "$2" | cut -s -d '-' -f 2- )
if [ "$v1" = "$v2" ]; then
if [ "$q1" = "$q2" ]; then
return 1
else
if [ -z "$q1" ]; then
return 1
else
if [ -z "$q2" ]; then
return 0
else
[ "$q1" = "$(echo -e "$q1\n$q2" | sort -V | head -n1)" ]
fi
fi
fi
else
[ "$v1" = "$(echo -e "$v1\n$v2" | sort -V | head -n1)" ]
fi
}
# returns a plugin version from a plugin archive
get_plugin_version() {
local archive; archive=$1
local version; version=$(unzip -p "$archive" META-INF/MANIFEST.MF | grep "^Plugin-Version: " | sed -e 's#^Plugin-Version: ##')
version=${version%%[[:space:]]}
echo "$version"
}
# Copy files from /usr/share/jenkins/ref into $JENKINS_HOME
# So the initial JENKINS-HOME is set with expected content.
# Don't override, as this is just a reference setup, and use from UI
# can then change this, upgrade plugins, etc.
copy_reference_file() {
f="${1%/}"
b="${f%.override}"
rel="${b:23}"
version_marker="${rel}.version_from_image"
dir=$(dirname "${b}")
local action;
local reason;
local container_version;
local image_version;
local marker_version;
local log; log=false
if [[ ${rel} == plugins/*.jpi ]]; then
container_version=$(get_plugin_version "$JENKINS_HOME/${rel}")
image_version=$(get_plugin_version "${f}")
if [[ -e $JENKINS_HOME/${version_marker} ]]; then
marker_version=$(cat "$JENKINS_HOME/${version_marker}")
if versionLT "$marker_version" "$container_version"; then
action="SKIPPED"
reason="Installed version ($container_version) has been manually upgraded from initial version ($marker_version)"
log=true
else
if [[ "$image_version" == "$container_version" ]]; then
action="SKIPPED"
reason="Version from image is the same as the installed version $image_version"
else
if versionLT "$image_version" "$container_version"; then
action="SKIPPED"
log=true
reason="Image version ($image_version) is older than installed version ($container_version)"
else
action="UPGRADED"
log=true
reason="Image version ($image_version) is newer than installed version ($container_version)"
fi
fi
fi
else
if [[ -n "$TRY_UPGRADE_IF_NO_MARKER" ]]; then
if [[ "$image_version" == "$container_version" ]]; then
action="SKIPPED"
reason="Version from image is the same as the installed version $image_version (no marker found)"
# Add marker for next time
echo "$image_version" > "$JENKINS_HOME/${version_marker}"
else
if versionLT "$image_version" "$container_version"; then
action="SKIPPED"
log=true
reason="Image version ($image_version) is older than installed version ($container_version) (no marker found)"
else
action="UPGRADED"
log=true
reason="Image version ($image_version) is newer than installed version ($container_version) (no marker found)"
fi
fi
fi
fi
if [[ ! -e $JENKINS_HOME/${rel} || "$action" == "UPGRADED" || $f = *.override ]]; then
action=${action:-"INSTALLED"}
log=true
mkdir -p "$JENKINS_HOME/${dir:23}"
cp -pr "${f}" "$JENKINS_HOME/${rel}";
# pin plugins on initial copy
touch "$JENKINS_HOME/${rel}.pinned"
echo "$image_version" > "$JENKINS_HOME/${version_marker}"
reason=${reason:-$image_version}
else
action=${action:-"SKIPPED"}
fi
else
if [[ ! -e $JENKINS_HOME/${rel} || $f = *.override ]]
then
action="INSTALLED"
log=true
mkdir -p "$JENKINS_HOME/${dir:23}"
cp -pr "${f}" "$JENKINS_HOME/${rel}";
else
action="SKIPPED"
fi
fi
if [[ -n "$VERBOSE" || "$log" == "true" ]]; then
if [ -z "$reason" ]; then
echo "$action $rel" >> "$COPY_REFERENCE_FILE_LOG"
else
echo "$action $rel : $reason" >> "$COPY_REFERENCE_FILE_LOG"
fi
fi
}
#! /bin/bash -e
: "${JENKINS_WAR:="/usr/share/jenkins/jenkins.war"}"
: "${JENKINS_HOME:="/var/jenkins_home"}"
touch "${COPY_REFERENCE_FILE_LOG}" || { echo "Can not write to ${COPY_REFERENCE_FILE_LOG}. Wrong volume permissions?"; exit 1; }
echo "--- Copying files at $(date)" >> "$COPY_REFERENCE_FILE_LOG"
find /usr/share/jenkins/ref/ \( -type f -o -type l \) -exec bash -c '. /usr/local/bin/jenkins-support; for arg; do copy_reference_file "$arg"; done' _ {} +
# if `docker run` first argument start with `--` the user is passing jenkins launcher arguments
if [[ $# -lt 1 ]] || [[ "$1" == "--"* ]]; then
# read JAVA_OPTS and JENKINS_OPTS into arrays to avoid need for eval (and associated vulnerabilities)
java_opts_array=()
while IFS= read -r -d '' item; do
java_opts_array+=( "$item" )
done < <([[ $JAVA_OPTS ]] && xargs printf '%s\0' <<<"$JAVA_OPTS")
jenkins_opts_array=( )
while IFS= read -r -d '' item; do
jenkins_opts_array+=( "$item" )
done < <([[ $JENKINS_OPTS ]] && xargs printf '%s\0' <<<"$JENKINS_OPTS")
exec java -Duser.home="$JENKINS_HOME" "${java_opts_array[@]}" -jar ${JENKINS_WAR} "${jenkins_opts_array[@]}" "$@"
fi
# As argument is not jenkins, assume user want to run his own process, for example a `bash` shell to explore this image
exec "$@"
workflow-aggregator:2.6
git:4.8.2
pipeline-utility-steps:2.10.0
http_request:1.11
As we are disabling the installation wizard via -Djenkins.install.runSetupWizard=false, we will be using the jenkins-plugin-manager.jar in combination with the plugins.txt to install plugins (and their dependencies). For our installation the following plugins will be installed:
workflow-aggregator:2.6
git:4.8.2
pipeline-utility-steps:2.10.0
http_request:1.11
A new plugin Manager is available at https://github.com/jenkinsci/plugin-installation-manager-tool with the intend of replacing the above plugin process. In the future we may update our install to use the new CLI.
Important Note: While we try to maintain our core guidelines for Availability and Reproducibility, we are unable to do so here as plugin installations are done connecting to https://updates.jenkins.io. In the scenario that the URL is unavailable, or a plugin that we use no longer exists, our builds will not be fully reproducible. If you do end up using plugins that are not strongly supported, ensure that you keep a copy in your DML.
The majority of the installation and configuration is done via our init-01.groovy script responsible for taking our injected secrets and creating the appropriate Jenkins credentials for use in builds.
A typical docker compose provides the environment based variables that will be needed to faciliate the automated install and config.
services:
jenkins:
[...]
environment:
- artifacts.id=nexus
- artifacts.user=admin
- scm.id=bitbucket
- scm.user=admin
- ssh.id=github
secrets:
- jenkins-passwords
secrets:
jenkins-passwords:
#artifacts-pwd.txt
#scm-pwd.txt
#ssh-passphrase.txt
#ssh-private-key.txt
file: secrets/jenkins-passwords
The following secrets are generally needed when dealing with jenkins pipelines:
- (required) Credentials for Artifact Storage, in our case Nexus 3
- (optional) Credentials for User/Password based Source Control
- (optional) Credentials for SSH keys to support for example private Github repos
Building and Running the Base Image
Build the Base image.
docker-compose -f images/jenkins/docker-compose.yml build
#docker login docker-private.acme.com
images/docker-push.sh infra/jenkins:1.0.0 docker-private.acme.com
Running is done via a compose that includes secrets and optional configs for volumes.
admin123
admin123
version: '3.3'
services:
jenkins:
image: infra/jenkins:1.0.0
ports:
- "8080:8080"
#volumes:
# - jenkins-logs:/var/log/jenkins
# - jenkins-data:/var/jenkins_home
environment:
- artifacts.id=nexus
- artifacts.user=admin
- scm.id=bitbucket
- scm.user=admin
- ssh.id=github
secrets:
- jenkins-passwords
networks:
- ops-network
extra_hosts:
- "bitbucket.acme.com:172.22.90.1"
- "nexus.acme.com:172.22.90.1"
networks:
ops-network:
name: ops-network
volumes:
jenkins-data:
name: jenkins-data
jenkins-logs:
name: jenkins-logs
secrets:
jenkins-passwords:
#artifacts-pwd.txt
#scm-pwd.txt
#ssh-passphrase.txt
#ssh-private-key.txt
file: secrets/jenkins-passwords
Before running the Base Jenkins, we need to ensure secrets get added:
echo 'admin123' > composed/secrets/jenkins-passwords/scm-pwd.txt
echo 'admin123' > composed/secrets/jenkins-passwords/artifacts-pwd.txt
As part of our first run, we will use --force-recreate to ensure that we get a full re-installation each time Jenkins starts.
docker-compose -f composed/docker-compose-jenkins-base.yml up --force-recreate
Jenkins will now be accessible via http://localhost:8080.
Base Image Run Options
Using the Base image and the compose file provided, you do have the option to run this as your primary "always-on" Jenkins instance to use for manual builds, though keep in mind that this may not provide you with the "Infrastructure-as-Code" approach you are looking for. When running as always-on the following should be considered:
- Setup Security via the Jenkins Configure Global Security
- Adding another nginx config so you can run this behind our existing NGINX (*.acme.com)
- Adding Docker Volumes that are currently commented out to ensure that your data is kept during restarts *
* The init-01.groovy will assume that our configuration has been applied when credentials are available to allow restarts while persisting to volume.