Mit einer zuverlässigen Build-Pipeline kann bei der Softwareentwicklung Zeit gespart werden. Hier ist kurz aufgezeigt, wie dies mit Docker und Bamboo für .Net-Builds konfiguriert wird. Mit dieser Build-Pipeline kann jeder Entwickler sein Projekt ohne grossen Aufwand builden, der Build wird auf jedem Client und CI-Server gleich erstellt, die Dependencies werden reduziert und der Source wird in den Container gemounted. Als Nebenprodukt kann die Frontend-Entwicklung im Container gemacht werden und somit wird NVM obsolet.
Client-Voraussetzungen
- Windows 1903 (1803 geht auch aber dann kann man nicht die ltsc2019 Container verwenden, die auf dem Server benötigt werden und hat so unterschiedliche Container für lokale und CI-Server-Builds)
- Docker Desktop for Windows (benötigt einen Docker-Hub-Account, der kostenlos ist)
- Docker switched to Windows Containers (keine Angst man kann auch wieder zurück zu Linux switchen, einfach Parallelbetrieb ist nicht möglich)
Container für den .Net-Build
- Dockerfile und Build-Script in dasselbe Verzeichnis kopieren
- Im Dockerfile die MSBuild Settings und WORKDIR überprüfen
- Im Build-Script MyProject durch den Projektnamen austauschen und out_dir überprüfen
Dockerfile:
FROM mcr.microsoft.com/dotnet/framework/sdk:4.8-windowsservercore-ltsc2019
WORKDIR C:/src/Server
CMD nuget restore; \
cd MyProject.Web; \
msbuild /p:DeployOnBuild=true \
/p:PublishProfile=MyFileSystemProfile \
/p:Configuration=Debug \
/p:DebugSymbols=true \
/p:VisualStudioVersion=16.0 \
/p:SolutionDir=C:/src/Server \
/verbosity:minimal \
/p:publishUrl=c:/out
Build-Script:
#!/bin/sh
set -e
root_dir="pwd | sed -E 's/^\/(.)\//\1:\//'
"
out_dir="$root_dir/Server/publish"
# needed for bamboo
if [ ! -z "${DOCKER_PATH}" ]; then
echo "adding docker to path"
docker_path="$(echo ${DOCKER_PATH} | sed 's/\\/\//g' | sed -E 's/^(.):\//\/\L\1\//' | sed -E 's/\/[^/]*$//')"
PATH="${docker_path}:${PATH}"
fi
echo "building server into $out_dir"
docker build -t MyProject-server-builder .
docker run --rm -i -v $root_dir:C:\\src -v $out_dir:C:\\out MyProject-server-builder
Container für Client-Build
- Dockerfile, Build-Script und set-dependency-versions.sh in dasselbe Verzeichnis kopieren
- Im Dockerfile WORKDIR überprüfen und Git-Section entfernen, falls git für den Build nicht benötigt wird
- Im Build-Script MyProject durch den Projektnamen austauschen und out_dir überprüfen
- Im set-dependency-versions.sh Versionen und Checksums anpassen
Dokerfile:
FROM mcr.microsoft.com/windows/servercore:ltsc2019
ARG NODE_VERSION
ARG NODE_SHA256
ENV NPM_CONFIG_LOGLEVEL info
ENV NODE_VERSION ${NODE_VERSION}
ENV NODE_SHA256 ${NODE_SHA256}
RUN powershell -Command \
wget -Uri https://nodejs.org/dist/v%NODE_VERSION%/node-v%NODE_VERSION%-x64.msi -OutFile node.msi -UseBasicParsing ; \
if ((Get-FileHash node.msi -Algorithm sha256).Hash -ne $env:NODE_SHA256) {exit 1} ; \
Start-Process -FilePath msiexec -ArgumentList /q, /i, node.msi -Wait ; \
Remove-Item -Path node.msi
# Add git if needed
ARG GIT_DOWNLOAD_URL
ARG GIT_SHA256
ENV GIT_DOWNLOAD_URL ${GIT_DOWNLOAD_URL}
ENV GIT_SHA256 ${GIT_SHA256}
RUN powershell -Command \
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; \
Invoke-WebRequest -UseBasicParsing $env:GIT_DOWNLOAD_URL -OutFile git.zip; \
if ((Get-FileHash git.zip -Algorithm sha256).Hash -ne $env:GIT_SHA256) {exit 1} ; \
Expand-Archive git.zip -DestinationPath C:\git; \
Remove-Item git.zip
RUN powershell -Command \
$env:PATH = 'C:\git\cmd;C:\git\mingw64\bin;C:\git\usr\bin;{0}' -f $env:PATH ; \
[Environment]::SetEnvironmentVariable('PATH', $env:PATH, [EnvironmentVariableTarget]::Machine)
# End add git
WORKDIR C:/src/Client
CMD npm install && npm run build
Build-Script:
#!/bin/sh
set -e
root_dir="pwd | sed -E 's/^\/(.)\//\1:\//'
"
out_dir="$root_dir/Client/dist"
source ./set-dependency-versions.sh
echo "building container with node version $NODE_VERSION and git version $GIT_VERSION"
# needed for bamboo
if [ ! -z "${DOCKER_PATH}" ]; then
echo "adding docker to path"
docker_path="$(echo ${DOCKER_PATH} | sed 's/\\/\//g' | sed -E 's/^(.):\//\/\L\1\//' | sed -E 's/\/[^/]*$//')"
PATH="${docker_path}:${PATH}"
fi
echo "building client into $out_dir"
docker build --build-arg NODE_VERSION=$NODE_VERSION --build-arg NODE_SHA256=$NODE_SHA256 --build-arg GIT_DOWNLOAD_URL=$GIT_DOWNLOAD_URL --build-arg GIT_SHA256=$GIT_SHA256 -t MyProject-client-builder-$NODE_VERSION .
docker run -i --rm -v $root_dir:C:/src -v $out_dir:C:/out MyProject-client-builder-$NODE_VERSION
set-dependency-versions.sh:
#!/bin/sh
# this file sets the versions and checksums for node and git
set -e
export NODE_VERSION=6.8.1
export NODE_SHA256=4600ed0e30a2497b6add2f53cdfccc727cf1cf9c5de6f90e89037085f489e5f4
export GIT_VERSION=2.20.1
export GIT_DOWNLOAD_URL="https://github.com/git-for-windows/git/releases/download/v${GIT_VERSION}.windows.1/MinGit-${GIT_VERSION}-busybox-64-bit.zip"
export GIT_SHA256=9817ab455d9cbd0b09d8664b4afbe4bbf78d18b556b3541d09238501a749486c
Client-Entwicklung im Container
Die Entwicklung des Clients im Container bringt den Vorteil, dass alle globalen Dependencies bei jedem Entwickler gleich sind. Zudem wird NVM nicht mehr benötigt, da jeder Container seinen eigene Node-Version haben kann.
Der Container ist fast identisch zum Build-Container. Wichtig ist, dass der Client nicht auf localhost hört, sondern auf der Container IP. Ich habe mir es einfach gemacht und 0.0.0.0 verwendet (aber nur im Container!, gesteuert durch eine ENV-Variable).
- Dockerfile, Build-Script und set-dependency-versions.sh (vom Build) in dasselbe Verzeichnis kopieren
- Im Dockerfile WORKDIR überprüfen und Git-Section entfernen, falls git für den Build nicht benötigt wird
- Im Build-Script MyProject durch den Projektnamen austauschen
- Im set-dependency-versions.sh Versionen und Checksums anpassen
Dokerfile:
FROM mcr.microsoft.com/windows/servercore:ltsc2019
ARG NODE_VERSION
ARG NODE_SHA256
ENV NPM_CONFIG_LOGLEVEL info
ENV NODE_VERSION ${NODE_VERSION}
ENV NODE_SHA256 ${NODE_SHA256}
RUN powershell -Command \
wget -Uri https://nodejs.org/dist/v%NODE_VERSION%/node-v%NODE_VERSION%-x64.msi -OutFile node.msi -UseBasicParsing ; \
if ((Get-FileHash node.msi -Algorithm sha256).Hash -ne $env:NODE_SHA256) {exit 1} ; \
Start-Process -FilePath msiexec -ArgumentList /q, /i, node.msi -Wait ; \
Remove-Item -Path node.msi
# Add git if needed
ARG GIT_DOWNLOAD_URL
ARG GIT_SHA256
ENV GIT_DOWNLOAD_URL ${GIT_DOWNLOAD_URL}
ENV GIT_SHA256 ${GIT_SHA256}
RUN powershell -Command \
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; \
Invoke-WebRequest -UseBasicParsing $env:GIT_DOWNLOAD_URL -OutFile git.zip; \
if ((Get-FileHash git.zip -Algorithm sha256).Hash -ne $env:GIT_SHA256) {exit 1} ; \
Expand-Archive git.zip -DestinationPath C:\git; \
Remove-Item git.zip
RUN powershell -Command \
$env:PATH = 'C:\git\cmd;C:\git\mingw64\bin;C:\git\usr\bin;{0}' -f $env:PATH ; \
[Environment]::SetEnvironmentVariable('PATH', $env:PATH, [EnvironmentVariableTarget]::Machine)
# End add git
WORKDIR C:/src/Client
EXPOSE 4200
CMD npm install && npm start
Run-Script:
#!/bin/sh
set -e
root_dir="pwd | sed -E 's/^\/(.)\//\1:\//'
"
source ./set-dependency-versions.sh
echo "building container with node version $NODE_VERSION and git version $GIT_VERSION"
echo "starting client for development"
docker build --build-arg NODE_VERSION=$NODE_VERSION --build-arg NODE_SHA256=$NODE_SHA256 --build-arg GIT_DOWNLOAD_URL=$GIT_DOWNLOAD_URL --build-arg GIT_SHA256=$GIT_SHA256 -t MyProject-client-runner-$NODE_VERSION .
docker run -it --rm -p 4200:4200 -v $root_dir:C:/src MyProject-client-runner-$NODE_VERSION
Bamboo-Server
Wer den Build auf dem Bamboo-Server einrichten möchte, kann dies am einfachsten mit einer Spec machen. Wir benötigen einen Agent, der Docker, git bash und powershell als Capability hat. Dieser wird automatisch gewählt, wenn diese Ressourcen als Requirements definiert werden.
Eigentlich wollte ich die Spec im Repository ablegen und diese automatisch durch den Bamboo erkennen lassen. Leider ist aber in unserer Version noch ein Bug vorhanden, sodass dies leider nicht möglich ist. Aus diesem Grund erfolgt das Publish manuell mit Maven.
Da wir alle Build-Scripts als Bashfiles haben, benötige ich git bash. Da der Bamboo-Agent ein Windows-Server ist, ist die default Shell cmd. Aus diesem Grund wird hier ein CommandTask erstellt, sodass bash ausgeführt werden kann. Der Nachteil ist, dass dann alle Executables nicht mehr im Pfad sind. Aus diesem Grund werden die benötigten Executables als ENV-Variablen an den Task gebunden und anschliessend durch die Bash-Scripts wieder an den Pfad gehängt (siehe Build-Scripts), sodass das ganze auch lokal läuft. Powershell benötigen wir zum Zippen, ansonsten wäre diese Dependency für unseren Build nicht notwendig (vereinfacht das Herunterladen der Artefakte).
Hier ein Beispiel:
package ch.beispiel;
import com.atlassian.bamboo.specs.api.BambooSpec;
import com.atlassian.bamboo.specs.api.builders.plan.Plan;
import com.atlassian.bamboo.specs.api.builders.plan.PlanIdentifier;
import com.atlassian.bamboo.specs.api.builders.project.Project;
import com.atlassian.bamboo.specs.util.BambooServer;
import com.atlassian.bamboo.specs.api.builders.permission.Permissions;
import com.atlassian.bamboo.specs.api.builders.permission.PermissionType;
import com.atlassian.bamboo.specs.api.builders.permission.PlanPermissions;
import com.atlassian.bamboo.specs.api.builders.plan.Job;
import com.atlassian.bamboo.specs.api.builders.plan.Stage;
import com.atlassian.bamboo.specs.api.builders.plan.artifact.Artifact;
import com.atlassian.bamboo.specs.api.builders.repository.VcsRepository;
import com.atlassian.bamboo.specs.builders.repository.git.GitRepository;
import com.atlassian.bamboo.specs.builders.task.ScriptTask;
import com.atlassian.bamboo.specs.builders.task.VcsCheckoutTask;
import com.atlassian.bamboo.specs.builders.repository.bitbucket.server.BitbucketServerRepository;
import com.atlassian.bamboo.specs.api.builders.applink.ApplicationLink;
import com.atlassian.bamboo.specs.builders.task.CommandTask;
import com.atlassian.bamboo.specs.api.builders.requirement.Requirement;
/**
* Plan configuration for Bamboo. Learn more on: <a href=
* "https://confluence.atlassian.com/display/BAMBOO/Bamboo+Specs">https://confluence.atlassian.com/display/BAMBOO/Bamboo+Specs</a>
*/
@BambooSpec
public class PlanSpec {
/**
* Run main to publish plan on Bamboo
*/
public static void main(final String[] args) throws Exception {
// By default credentials are read from the '.credentials' file.
//BambooServer bambooServer = new BambooServer("https://localhost:8085");
BambooServer bambooServer = new BambooServer("https://bamboo.edorex.ch");
Plan devPlan = new PlanSpec().createPlan("develop", "BEISPIELDEV", "Beispiel development plan");
Plan prodPlan = new PlanSpec().createPlan("master", "BEISPIELPROD", "Beipsiel production plan");
bambooServer.publish(devPlan);
bambooServer.publish(prodPlan);
PlanPermissions devPlanPermission = new PlanSpec().createPlanPermission(devPlan.getIdentifier());
PlanPermissions prodPlanPermission = new PlanSpec().createPlanPermission(prodPlan.getIdentifier());
bambooServer.publish(devPlanPermission);
bambooServer.publish(prodPlanPermission);
}
PlanPermissions createPlanPermission(PlanIdentifier planIdentifier) {
Permissions permission = new Permissions()
.groupPermissions("bamboo-admin", PermissionType.ADMIN).loggedInUserPermissions(PermissionType.VIEW)
.anonymousUserPermissionView();
return new PlanPermissions(planIdentifier.getProjectKey(), planIdentifier.getPlanKey()).permissions(permission);
}
Project project() {
return new Project().name("Beispiel").key("BSP");
}
Plan createPlan(String branch, String key, String desc) {
return new Plan(project(), branch, key).description(desc)
.planRepositories(gitRepository(branch)).stages(new Stage("Stage 1").jobs(
new Job("Build", "BEISPIELBUILD")
.requirements(new Requirement("system.docker.executable"))
.requirements(new Requirement("system.builder.command.GitBash"))
.requirements(new Requirement("system.builder.command.powershell"))
.tasks(gitRepositoryCheckoutTask(), commandTask())
.artifacts(artifact())));
}
VcsRepository gitRepository(String branch) {
return new BitbucketServerRepository()
.name("beispiel")
.server(new ApplicationLink()
.name("Stash"))
.projectKey("BSP")
.repositorySlug("beispiel")
.branch(branch);
}
VcsCheckoutTask gitRepositoryCheckoutTask() {
return new VcsCheckoutTask().addCheckoutOfDefaultRepository();
}
CommandTask commandTask() {
return new CommandTask()
.executable("GitBash")
.argument("./build.sh")
.environmentVariables("DOCKER_PATH=\"${bamboo.capability.system.docker.executable}\" POWERSHELL_PATH=\"${bamboo.capability.system.builder.command.powershell}\"");
}
Artifact artifact() {
return new Artifact("Build results").location("build").copyPattern("*.zip");
}
}
Hinweise
- Verwendet für das Ausführen der Container –rm, damit die Container nach dem Run entfernt werden
Lessons learned
Die VisualStudioVersion in MSBuild ist relevant
Die VisualStudioVersion muss dem Visual Studio im Container entsprechen (ist momentan 2019 bzw. 16.0 für mcr.microsoft.com/dotnet/framework/sdk:4.8-windowsservercore-ltsc2019 ) .
Andernfalls werden bei einem Web-Projekt zwar die dlls gebuildet aber der Rest nicht.
Nuget nimmts genau
Funktioniert:
cd C:/src/Server/EZT
nuget restore
Installiert nicht alle Packages:
nuget restore -SolutionDirectory C:/src/Server/EZT
Server 2016 ist nicht brauchbar für Docker
Server 2016 unterstützt zwar Docker aber Volumes gehen leider nicht.
Versionen der Windows Container Images müssen <= OS-Version sein
https://hub.docker.com/_/microsoft-windows-servercore#full-tag-listing “Full Tag Listing” gibt eine Übersicht