Installieren der Plugins
Jenkins → Manage Jenkins → Manage Plugins
AWS Pipeline Plugin
Github: https://github.com/jenkinsci/pipeline-aws-plugin
AWS SQS Plugin (2.0.1)
Github: https://github.com/jenkinsci/aws-sqs-plugin
Utility Pipeline Plugin
Github: https://github.com/jenkinsci/pipeline-utility-steps-plugin
Zugangsdaten einstellen
Unglücklicherweise verwenden die Plugins nicht dasselbe Format um die Zugangsdaten zu speichern. Daher müssen wir die Zugangsdaten mehrfach in verschiedenen Formaten definieren:
Jenkins → Credentials → System → Global Credentials → Add new credentials (http://localhost:8081/credentials/store/system/domain/_/newCredentials)
CodeCommit Zugang
Erstelle "Username with Password" Zugangsdaten und verwende die CodeCommit HTTPS Zugangsdaten

Simple Queue Service (SQS) Zugang
Erstelle "Secret text" Zugangsdaten und verwende den Access Key als ID und den dazugehörigen Secret Key als Secret.

Die Queue selbst wird an einem anderen Ort im Jenkins konfiguriert:
Jenkins → Manage Jenkins → Configure System → Configuration of Amazon SQS queues: http://localhost:8081/configure (ganz unten)

Die URL der Queue kann unter AWS SQS
gefunden werden. Klicke dazu auf die Queue. Im Details Tab wird dann die URL
angezeigt.

Important: Speichern nicht vergessen!
AWS Zugang
Erstelle "Username with Password" Zugangsdaten und verwende den Access Key als Username und den Secret Key als Password.

Pipeline erstellen
Jenkins → New Item

Build Parameter (Git Tags)
Erstelle einen String parameter namens BUILD_TAG

Build Trigger (SQS)
Nachdem wir den SQS Zugang konfiguriert haben, können wir die Queue verwenden um unseren Build auszulösen.

Pipeline schreiben
Nun da wir Jenkins und unseren AWS Account konfiguriert haben, können wir mit dem Schreiben der Pipeline beginnen.
Die Grundstruktur einer Declarative Pipeline sieht folgendermaßen aus:
Structure
pipeline {
agent any
environment {
}
stages {
stage("stage one") {
steps {
}
}
}
}
Important!
In den folgenden werden wir:
Ändere diese zu deinem Bucket und Repository.
Environment Variables (Umgebungvariablen)
Um die Pipeline wartbarer zu machen, werden wir 5 Umgebungsvariablen erstellen:
- ECR - URI des Repositories
- IMAGE - Imagename der Applikation
- VERSION - Version die in der pom.xml steht
- STACK - Stackname die in CloudFormation verwendet werden soll
- COMMIT - Hash des Commits der gebaut wird
Groovy implementation Quelle erweitern
environment {
//ecr uri
//example ECR = "222222222222.dkr.ecr.eu-central-1.amazonaws.com/springio/gs-spring-boot-docker"
ECR = "<your uri here>"
//image name
IMAGE = "springio/gs-spring-boot-docker"
//pom.xml version
VERSION = "0.0.0"
//Stack name
STACK = "MyWebServiceStack"
//Commit hash
COMMIT = ""
}
Dadurch ersparen wir uns das mehrfache Eingeben von Links/Namen.
Variablen in Strings verwenden
Jenkins verwendet für die String interpolation Regeln die identisch zu Groovy sind. Groovy’s String interpolation kann für Anfänger verwirrend sein.
Während Groovy Stringdeklarationen mit einfachen oder doppeltem Anführungszeichen unterstützt:
String declaration
def singlyQuoted = 'Hello'
<strong></strong>def doublyQuoted = "World"
Nur der letztere unterstützt die Dollarzeichen ($) basierte String interpolation, zum Beispiel:
String and GString
def username = 'Jenkins'
echo 'Hello Mr. ${username}' //single quote
<strong></strong>echo "I said, Hello Mr. ${username}" //double quote
Resultiert in:
Output
Hello Mr. ${username}
I said, Hello Mr. Jenkins
Quelle: https://jenkins.io/doc/book/pipeline/jenkinsfile/#string-interpolation
Stages
Checkout Stage
Diese Stage klont das git Repository aus CodeCommit und holt sich eine bestimmte Version, falls definiert.
Entscheiden was geholt werden soll
Wenn ein Tag definiert ist (manueller Build) und dieser existiert im Repository, holen wir diese Version. Andernfalls holen wir die neueste Version.
Falls kein Tag definiert wird (automatisierter Build) holen wir uns die neueste Version.
Nach dieser Entscheidung setzen wir die Umgebungsvariablen COMMIT und VERSION.
Beispiel Stage
Example Stage Quelle erweitern
stage('Checkout') {
steps {
git url: 'https://git-codecommit.eu-central-
1.amazonaws.com/v1/repos/tutorial_repo',
credentialsId: 'CodeCommitCredentials' // ID of HTTPS
CodeCommit credentials
script {
if("${BUILD_TAG}" != "") {
echo "Searching for tag: ${BUILD_TAG}"
//output returns:
// the tag if the tag exists
// "" if the tag does not exists
def output = bat(script: "@ git tag -l \"${BUILD_TAG}\"", returnStdout: true).trim();
echo output
if("${output}" == "${BUILD_TAG}") {
echo "Tag found"
echo "Building tag \"${BUILD_TAG}\""
bat "git checkout ${BUILD_TAG}"
} else {
echo "Tag \"${BUILD_TAG}\"not found"
BUILD_TAG = "";
echo "Building latest commit"
}
} else {
echo "Building latest commit"
}
//escape % with a second % -> %%
COMMIT = bat(script: "@ git log -n 1 --pretty=format:%%h", returnStdout: true)
//read version from pom.xml
VERSION = readMavenPom().getVersion()
}
}
}
Maven Build Stage
Diese Stage baut die .jar Datei und testet die Applikation.
Maven verwenden
Um unsere .jar Dateien zu unterscheiden fügen wir den Tag/Commit zum Imagenamen hinzu:
- gs-spring-boot-docker-1.0.1.jar → gs-spring-boot-docker-1.0.1-v1.0.1-RC.jar
- gs-spring-boot-docker-1.0.0.jar → gs-spring-boot-docker-1.0.0-b127d6b.jar
Dabei hilft uns das Maven Versions Plugin
script {
// VERSION is read from pom.xml
bat "mvn versions:set -DnewVersion=${VERSION}-${BUILD_TAG}"
}
Testergebnisse anzeigen
https://jenkins.io/doc/pipeline/steps/junit/
https://jenkins.io/doc/book/pipeline/syntax/#post
Das Anzeigen der Testergebnisse sollte immer geschehen, egal ob der Buildprozess fehlschlägt oder nicht. Deshalb wird dieser Step im post-condition block always definiert.
Archivieren der gebauten jar
Wenn der Buildprozess erfolgreich durchlaufen wurde, werden wir die gebaut .jar Datei in dem S3 Bucket archivieren.
Beispiel Stage
Example Stage Quelle erweitern
stage('Build jar with Maven') {
steps {
script {
// VERSION is read from pom.xml
if("${BUILD_TAG}" != "") {
bat "mvn versions:set -DnewVersion=${VERSION}-${BUILD_TAG}"
} else {
bat "mvn versions:set -DnewVersion=${VERSION}-${COMMIT}"
}
}
bat 'mvn clean install'
}
post {
always {
//show junit test results
junit 'target/surefire-reports/*.xml'
}
success {
//push artifacts to s3 bucket (.jar)
script {
withAWS(region: 'eu-central-1', credentials: 'AWSCredentials') { // ID of the AWS credentials
//upload built jar to s3 bucket
s3Upload(bucket:'qstutorialbucket', includePathPattern: '**/target/*.jar', path:'builds/')
}
}
}
}
}
Docker Build Stage
Diese Stage baut das Docker Image
Um das Docker Image mit einem eigenen Tag anstelle des Standard-Tags latest zu bauen müssen wir die Option dockerfile.tag im Buildbefehl verwenden:
Example Stage Quelle erweitern
stage('Build docker image') {
steps{
script {
//always tag docker image with commit hash
bat "mvn dockerfile:build -Ddockerfile.tag=${COMMIT}"
//if build tag exists, tag image
if("${BUILD_TAG}" != "") {
bat "mvn dockerfile:tag -Ddockerfile.tag=${BUILD_TAG}"
}
}
}
}
Docker Tag/Push Stage
Diese Stage fügt dem Docker Image einen weiteren Tag hinzu, sodass wir das Image in das ECR hochladen können
Example Stage Quelle erweitern
stage('Tag/Push docker image') {
steps {
script {
//login aws with credentials
withAWS(region: 'eu-central-1', credentials: 'AWSCredentials') { // ID of the AWS credentials
//get login for ecr (returns command to execute in terminal)
def ecrLogin = ecrLogin()
//execute command
bat ecrLogin
}
//tag image with commit hash for ecr
bat "docker tag ${IMAGE}:${COMMIT} ${ECR}:${COMMIT}"
bat "docker push ${ECR}:${COMMIT}"
//if build tag exists, tag image for ecr
if("${BUILD_TAG}" != "") {
bat "docker tag ${IMAGE}:${BUILD_TAG} ${ECR}:${BUILD_TAG}"
bat "docker push ${ECR}:${BUILD_TAG}"
}
}
}
}
Deploy Stage
Diese Stage wird unsere CloudFormation starten.
Wenn der Stack noch nicht existiert, starten wir zuerst eine stabile Version des Images in der CloudFormation (definiert im Repository) und versuchen dann das neu gebaute Image aufzuspielen.
Überprüfen ob der Stack existiert
Dieser Check überprüft ob der Stack mit dem Namen der in der Umgebungsvariable STACK definiert ist schon existiert.
retrunStatus gibt den error code des Scripts zurück:
- 0: erfolgreich
- jeder andere Code: fehlgeschlagen
Daher überprüfen wir lediglich, ob der Output gleich 0 ist um zu sehen ob der Stack existiert.
Groovy implementation Quelle erweitern
script {
output = bat(script: "aws cloudformation describe-stacks --stack-name ${STACK}", returnStatus: true)
if(output == 0) {
//Stack exists
}
if(output != 0) {
//Stack does not exist
}
}
Eine stabile Version starten
Wenn der Stack noch nicht existiert, starten wir zuerst eine stabile Version des Images. Wenn der Stack schon existiert, können wir diesen Schritt überspringen.
Die stabile Version des Docker Images sollte in der .yaml Datei im Repository definiert sein.
Die Pipeline wird dann diese Konfiguration in den S3 Bucket hochladen, sodass CloudFormation darauf zugreifen kann.
Groovy implementation Quelle erweitern
script {
withAWS('eu-central-1', credentials: 'AWSCredentials') {
//upload cloudformation folder to s3 bucket
s3Upload(file:'cloudformation/', bucket:'qstutorialbucket', path:'cloudformation/')
def output = bat(script: "aws cloudformation describe-stacks --stack-name ${STACK}", returnStatus: true)
//if stack doesn't exist, create
if(output != 0) {
//start cloudformation, deletes stack if failed
cfnUpdate(stack: "${STACK}", url:'https://s3.eu-central-1.amazonaws.com/qstutorialbucket/
cloudformation/master.yaml', onFailure: 'DELETE')
}
}
}
Image Version austauschen
Um die Version des Docker Images auszutauschen müssen wir die .yaml Datei modifizieren, welche die Referenz zum zu verwendenden Docker Image enthält.
Diese Datei ist service.yaml
Um in unserem Stack so eine Änderung durchzuführen, müssen wir folgendes machen:
- Aktuelle Konfiguration lesen
- Eine neue Konfiguration mit dem neuen Docker Image erstellen
- Die neue Konfiguration hochladen, sodass CloudFormation darauf zugreifen kann
- Den Stack mit der neuen Konfiguration updaten
Groovy implementation Quelle erweitern
script {
//read current service.yaml
def yamlAsText = readFile file: 'cloudformation/services/website-service/service.yaml'
//replace current image with new one
def modifiedYamlAsText = ''
//if build tag exists, replace image with newly built image tagged with build tag
//else, replace image with newly built image tagged with commit hash
if("${BUILD_TAG}" != "") {
modifiedYamlAsText = yamlAsText.replaceAll(/Image:\s.*/, "Image: ${ECR}:${BUILD_TAG}")
} else {
modifiedYamlAsText = yamlAsText.replaceAll(/Image:\s.*/, "Image: ${ECR}:${COMMIT}")
} println modifiedYamlAsText
//write changes to file
writeFile file: 'tmpChange.yaml', text: "${modifiedYamlAsText}"
//upload new service.yaml to s3
s3Upload(file:'tmpChange.yaml', bucket:'qstutorialbucket', path:'cloudformation/services/website-service/service.yaml')
//update stack
cfnUpdate(stack: "${STACK}", url:'https://s3.eu-central-1.amazonaws.com/qstutorialbucket/cloudformation/master.yaml', onFailure:
'ROLLBACK')
}
Beispiel Stage
Letztendlich sollte die Stage ähnlich wie diese aussehen:
Example Stage Quelle erweitern
stage('Upload S3/Deploy CloudFormation') {
steps {
script {
withAWS(region: 'eu-central-1', credentials: 'AWSCredentials') {
//upload cloudformation folder to s3 bucket
s3Upload(file:'cloudformation/', bucket:'qstutorialbucket', path:'cloudformation/')
def output = bat(script: "aws cloudformation describe-stacks --stack-name ${STACK}", returnStatus: true)
//if stack doesn't exist, create
if(output != 0) {
//start cloudformation, deletes stack if failed
cfnUpdate(stack: "${STACK}", url:'https://s3.eu-central-1.amazonaws.com/qstutorialbucket/
cloudformation/master.yaml', onFailure: 'DELETE')
}
output = bat(script: "aws cloudformation describe-stacks --stack-name ${STACK}", returnStatus: true)
//if stack exists
if(output == 0) {
//make change in service.yaml
//read current service.yaml
def yamlAsText = readFile file: 'cloudformation/services/website-service/service.yaml'
//replace current image with new one
def modifiedYamlAsText = ''
//if build tag exists, replace image with newly built image tagged with build tag
//else, replace image with newly built image tagged with commit hash
if("${BUILD_TAG}" != "") {
modifiedYamlAsText = yamlAsText.replaceAll(/Image:\s.*/, "Image: ${ECR}:${BUILD_TAG}")
} else {
modifiedYamlAsText = yamlAsText.replaceAll(/Image:\s.*/, "Image: ${ECR}:${COMMIT}")
}
println modifiedYamlAsText
//write changes to file
writeFile file: 'tmpChange.yaml', text: "${modifiedYamlAsText}"
//upload new service.yaml to s3
s3Upload(file:'tmpChange.yaml', bucket:'qstutorialbucket', path:'cloudformation/services/
website-service/service.yaml')
//update stack
cfnUpdate(stack: "${STACK}", url:'https://s3.eu-central-1.amazonaws.com/qstutorialbucket/
cloudformation/master.yaml', onFailure: 'ROLLBACK')
}
}
}
}
}
Nun da wir alles konfiguriert haben, löst ein Commit der in
das Repository hochgeladen wird unsere Jenkins Pipeline aus.