Create Docker image with Packer, PowerShell, PowerCLI and Azure DevOps self-hosted agent
Today we will create a Docker image that will contain Packer, Windows Update Provisioner for Packer, PowerShell, PowerCLI and Azure DevOps self-hosted agent.
To automate Packer image creation process we will utilize Azure DevOps, which is a service hosted in public cloud. What we want to do is to create Packer image on-premise in our datacenter, so there is a problem with communication between vCenter server and Azure DevOps.
Azure DevOps agent
Why Azure DevOps agent is needed?
Normally Azure DevOps does not have any connection with our datacenter. To establish connection between on-premise vCenter server and Azure DevOps, a dedicated agent placed inside our datacenter is needed. That agent needs to have network connection with our vCenter server and ESXi hosts. It can be placed in different VLAN, but that VLAN needs to be routable with vCenter and ESXi VLAN.
What Azure Devops agent does?
It actually triggers a tasks initiated from Azure DevOps portal locally in our infrastructure. All files reuired to create our builds needs to be first copied on Azure Devops agent and agent triggers the build job. The files copied on agent from Azure DevOps server are called artifacts. Because agent triggers a build jobs, it needs to be equipped with all necessary tools that are required to create Packer template. Those are:
- Packer
- Packer Windows Update provisioner
- PowerShell
- PowerCLI
Self-hosted Azure DevOps agent
Azure DevOps agent can be hosted in Azure or it can be hosted locally in our DC. Obviously we will need self-hosted agent.
First of all it allows direct communication with our datacenter. Second thing - it's free. For Azure DevOps agent hosted in Azure you normally needs to pay some money.
Of course everything depends on subscription level you own, but my assumption is that you don't have any available subscription and want to minimize operational costs.Remember, or goal is to build secure templates and minimize operational costs!
To establish communication between DevOps server and on-premise vCenter server a dedicated token will be used. This token will have to be generated on Azure Devops portal.
Support platforms for DevOps agents
Azure DevOps agent can be installed on Windows or on Linux. We will choose Linux as our destination platform and there is few reasons for that:
- When utilizing Linux you don't need to pay for Windows license. Since Azure DevOps agent will just trigger our build tasks, there is no need to pay Windows license for such service.
- When using Linux as agent platform we can utlize Docker and automate process of creating a docker image will all necessary tools.
You could probably use some bash script to automate process of installing and configuring all necessary tools on Linux, but using Docker will be much faster and efficient way to achieve expected result.
- Since DevOps agent keeps all artifacts locally in clear text (including passwords), security is a major concern here. It's very easy to erase Windows password using some small 3rd party tool and get access to all artifacts and all passwords on that agent. Using Linux and Docker container make a things much harder for potential attacker. We will use other methods to secure data on agent machine but Linux is much better option in terms of security.
Build Ubuntu VM
Create Ubuntu 21.04 Packer image
We could use Windows Subsystem for Linux (WSL) to speedup Docker image creation process but we want to build our final solution which will utilize Ubuntu VM. For this reason we will use Ubuntu 21.04. To create Ubuntu VM Template using Packer use this link.
Customize Ubuntu VM
To make life easier first enable root account and setup new root account password.
1sudo passwd root
Once done switch to root account typing command:
1su root
Username will change from your existing user to root. This means we have elevated privileges on the system.
Set static IP
1cd /etc/netplan/
2ls
Once done a configuration file will be displayed
Edit the file using nano editor:
1nano 00-installer-config.yaml
Edit IP, gateway and DNS settings to match your network configuration:
1# This is the network config written by 'subiquity'
2network:
3 ethernets:
4 ens192:
5 addresses:
6 - 192.168.1.222/24
7 gateway4: 192.168.1.1
8 nameservers:
9 addresses: [192.168.1.1, 8.8.8.8]
10 search: [lab.local]
11 version: 2
Type Ctrl+x and confirm saving changes typing y.
1netplan apply
This will apply network settings that has been modified. From now putty can be used to interact with VM through SSH console
Change hostname
Perform fallowing command to change hostname to ld9-vcsabackups01
1sudo hostnamectl set-hostname devopsagent01
Provide server description using fallowing command:
1sudo hostnamectl set-hostname "Azure DevOps Self-hosted Agent" --pretty
Once applied verify that changes have been applied
1hostnamectl
Upgrade system
Perform server upgrade
1sudo apt-get update
2sudo apt-get-upgrade
Install Docker Engine and Docker Compose
Install Docker Engine
Docker Engine allow us to run standalone containers.
First remove existing Docker Engine if exists:
1sudo apt-get remove docker docker-engine docker.io containerd runc
Update the apt package index and install packages to allow apt to use a repository over HTTPS:
1 sudo apt-get update
2 sudo apt-get install \
3 ca-certificates \
4 curl \
5 gnupg \
6 lsb-release
Add Docker’s official GPG key:
1curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
Use the following command to set up the stable repository.
1echo \
2 "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
3 $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Install Docker Engine
1sudo apt-get update
2sudo apt-get install docker-ce docker-ce-cli containerd.io
Check Docker version:
1docker --version
2
3Docker version 20.10.10, build b485636
Install Docker Compose
Compose is a file format for describing distributed Docker apps, and it’s a tool for managing them. Docker Compose relies on Docker Engine for any meaningful work, so make sure you have Docker Engine installed either locally or remote.
On Linux, you can download the Docker Compose binary from the Compose repository release page on GitHub.
Run this command to download the current stable release of Docker Compose:
1sudo curl -L "https://github.com/docker/compose/releases/download/v2.2.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
Apply executable permissions to the binary:
1sudo chmod +x /usr/local/bin/docker-compose
Note: If the command docker-compose fails after installation, check your path. You can also create a symbolic link to /usr/bin or any other directory in your path.
1sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
Test the installation:
1docker-compose --version
2
3Docker Compose version v2.2.2
Once Docker Engine and Docker Compose will be installed we need to add existing, non-root user to docker group to perform docker tasks without privileged mode:
1sudo usermod -aG docker your-user-name
Once added, restart your SSH session to apply changes.
Preparing for Docker container image creation
Inside your home directory create new folder named "Azure-DevOps-Agent":
1mkdir Azure-DevOps-Agent
2cd Azure-DevOps-Agent/
Once we are inside devopsagent folder create new file named Dockerfile
:
1nano Dockefile
To build container image we need a file named Dockerfile
which will include fallowing content:
1# Indicates the base image.
2FROM ubuntu:18.04
3
4# Define Args for the needed to add the package
5ARG PACKER_VERSION="1.7.8"
6ARG PACKER_WINDOWSUPDATE_VERSION="0.14.0"
7ARG PS_VERSION=7.1.3
8ARG PS_PACKAGE=powershell_${PS_VERSION}-1.ubuntu.18.04_amd64.deb
9ARG PS_PACKAGE_URL=https://github.com/PowerShell/PowerShell/releases/download/v${PS_VERSION}/${PS_PACKAGE}
10ARG TARGETARCH=amd64
11ARG AGENT_VERSION="2.195.2"
12
13# Define ENVs for Localization/Globalization
14ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false \
15 LC_ALL=en_US.UTF-8 \
16 LANG=en_US.UTF-8 \
17 # set a fixed location for the Module analysis cache
18 PSModuleAnalysisCachePath=/var/cache/microsoft/powershell/PSModuleAnalysisCache/ModuleAnalysisCache \
19 POWERSHELL_DISTRIBUTION_CHANNEL=PSDocker-Ubuntu-18.04
20ENV PACKER_VERSION=${PACKER_VERSION}
21ENV PACKER_WINDOWSUPDATE_VERSION=${PACKER_WINDOWSUPDATE_VERSION}
22ENV DEBIAN_FRONTEND=noninteractive
23RUN echo "APT::Get::Assume-Yes \"true\";" > /etc/apt/apt.conf.d/90assumeyes
24
25# Install dependencies and clean up
26RUN apt-get update \
27 && apt-get install --no-install-recommends -y \
28 # curl is required to grab the Linux package
29 curl \
30 # less is required for help in powershell
31 less \
32 # requied to setup the locale
33 locales \
34 # required for SSL
35 ca-certificates \
36 gss-ntlmssp \
37 # PowerShell remoting over SSH dependencies
38 openssh-client \
39 # Install python
40 python3 \
41 python3-pip\
42 python3-boto\
43 # Install unzip
44 unzip \
45 # Install DevOps Agent packages
46 jq \
47 git \
48 iputils-ping \
49 libcurl4 \
50 libicu60 \
51 libunwind8 \
52 netcat \
53 libssl1.0 \
54 # Download the Linux package and save it
55 && echo ${PS_PACKAGE_URL} \
56 && curl -sSL ${PS_PACKAGE_URL} -o /tmp/powershell.deb \
57 && curl -LO https://releases.hashicorp.com/packer/${PACKER_VERSION}/packer_${PACKER_VERSION}_linux_amd64.zip \
58 && curl -LO https://github.com/rgl/packer-plugin-windows-update/releases/download/v${PACKER_WINDOWSUPDATE_VERSION}/packer-plugin-windows-update_v${PACKER_WINDOWSUPDATE_VERSION}_x5.0_linux_amd64.zip \
59 && unzip '*.zip' -d /usr/bin \
60 && chmod +x /usr/bin/packer-plugin-windows-update_v${PACKER_WINDOWSUPDATE_VERSION}_x5.0_linux_amd64 \
61 && rm *.zip \
62 && mv /usr/bin/packer-plugin-windows-update_v${PACKER_WINDOWSUPDATE_VERSION}_x5.0_linux_amd64 /usr/bin/packer-plugin-windows-update \
63 && curl -LsS https://aka.ms/InstallAzureCLIDeb | bash \
64 && apt-get install --no-install-recommends -y /tmp/powershell.deb \
65 && apt-get dist-upgrade -y \
66 && apt-get clean \
67 && rm -rf /var/lib/apt/lists/* \
68 && locale-gen $LANG && update-locale \
69 # remove powershell package
70 && rm /tmp/powershell.deb \
71 # intialize powershell module cache
72 # and disable telemetry
73 && export POWERSHELL_TELEMETRY_OPTOUT=1 \
74 && pwsh \
75 -NoLogo \
76 -NoProfile \
77 -Command " \
78 \$ErrorActionPreference = 'Stop' ; \
79 \$ProgressPreference = 'SilentlyContinue' ; \
80 while(!(Test-Path -Path \$env:PSModuleAnalysisCachePath)) { \
81 Write-Host "'Waiting for $env:PSModuleAnalysisCachePath'" ; \
82 Start-Sleep -Seconds 6 ; \
83 }"
84
85RUN apt-get clean && \
86 rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
87
88WORKDIR /azp
89RUN if [ "$TARGETARCH" = "amd64" ]; then \
90 AZP_AGENTPACKAGE_URL=https://vstsagentpackage.azureedge.net/agent/${AGENT_VERSION}/vsts-agent-linux-x64-${AGENT_VERSION}.tar.gz; \
91 else \
92 AZP_AGENTPACKAGE_URL=https://vstsagentpackage.azureedge.net/agent/${AGENT_VERSION}/vsts-agent-linux-${TARGETARCH}-${AGENT_VERSION}.tar.gz; \
93 fi; \
94 curl -LsS "$AZP_AGENTPACKAGE_URL" | tar -xz
95
96COPY ./start.sh .
97RUN chmod +x start.sh
98
99
100# Install the VMware.PowerCLI Module
101SHELL [ "pwsh", "-command" ]
102RUN Install-Module VMware.PowerCLI, PowerNSX -Force -Confirm:0;
103RUN Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -confirm:$false
104
105## Install DevOps Agnet
106#CMD [ "pwsh" ]
107#CMD ["/bin/bash"]
108ENTRYPOINT [ "./start.sh" ]
In same directory as Dockerfile
create new file named start.sh
1$ nano start.sh
and paste there fallowing content:
1#!/bin/bash
2set -e
3if [ -z "$AZP_URL" ]; then
4 echo 1>&2 "error: missing AZP_URL environment variable"
5 exit 1
6fi
7if [ -z "$AZP_TOKEN_FILE" ]; then
8 if [ -z "$AZP_TOKEN" ]; then
9 echo 1>&2 "error: missing AZP_TOKEN environment variable"
10 exit 1
11 fi
12 AZP_TOKEN_FILE=/azp/.token
13 echo -n $AZP_TOKEN > "$AZP_TOKEN_FILE"
14fi
15unset AZP_TOKEN
16if [ -n "$AZP_WORK" ]; then
17 mkdir -p "$AZP_WORK"
18fi
19export AGENT_ALLOW_RUNASROOT="1"
20cleanup() {
21 if [ -e config.sh ]; then
22 print_header "Cleanup. Removing Azure Pipelines agent..."
23 # If the agent has some running jobs, the configuration removal process will fail.
24 # So, give it some time to finish the job.
25 while true; do
26 ./config.sh remove --unattended --auth PAT --token $(cat "$AZP_TOKEN_FILE") && break
27 echo "Retrying in 30 seconds..."
28 sleep 30
29 done
30 fi
31 }
32 print_header() {
33 lightcyan='\033[1;36m'
34 nocolor='\033[0m'
35 echo -e "${lightcyan}$1${nocolor}"
36 }
37 # Let the agent ignore the token env variables
38 export VSO_AGENT_IGNORE=AZP_TOKEN,AZP_TOKEN_FILE
39 source ./env.sh
40 print_header "1. Configuring Azure Pipelines agent..."
41 ./config.sh --unattended \
42 --agent "${AZP_AGENT_NAME:-$(hostname)}" \
43 --url "$AZP_URL" \
44 --auth PAT \
45 --token $(cat "$AZP_TOKEN_FILE") \
46 --pool "${AZP_POOL:-Default}" \
47 --work "${AZP_WORK:-_work}" \
48 --replace \
49 --acceptTeeEula & wait $!
50 print_header "2. Running Azure Pipelines agent..."
51 trap 'cleanup; exit 0' EXIT
52 trap 'cleanup; exit 130' INT
53 trap 'cleanup; exit 143' TERM
54 # To be aware of TERM and INT signals call run.sh
55 # Running it with the --once flag at the end will shut down the agent after the build is executed
56 ./run.sh "$@" --once &
57 wait $!
Alternatively once you are under your home directory run fallowing command:
1git clone https://github.com/danonh/Azure-DevOps-Agent
Once done you should see Azure-DevOps-Agent folder which will contain three files:
1├── Dockerfile
2├── Readme.md
3└── start.sh
You can find both files on my GitHub account
Changing product versions
Within Dockerfile I've hardcoded particular product versions:
- Packer version 1.7.8
- Windows Update Provisioner version 0.14.0
- PowerShell version 7.1.3
- Azure DevOps agent version 2.195.2
Obviously you may change product versions according your needs. This can be done modifying ARG values directly in the Dockerfile or provide alternative values during building process which will be passed to Dockerfile.
Build Docker image
Finally we are at stage where building process can be initiated.
If product versions has been manually changed directly inside Dockerfile then you can simply run fallowing command:
1cd Azure-DevOps-Agent/
2docker build -t packercontainer:local .
This will initiate building process that may take up to 15 minutes.
Alternatively you can pass particular product versions into docker build command:
1docker build -t packercontainer:local --build-arg PS_VERSION="7.1.4" --build-arg PACKER_VERSION="1.7.7" .
With those arguments we are telling Docker to include PowerShell version 7.1.4 instead 7.1.3 and Packer version 1.7.7 instead 1.7.8.
Don't forget to include .
at the end of build command. This tells where Docker should look for Dockerfile.
Now sit down and relax.
If everything will go well you should see fallowing console output:
1Removing intermediate container 281635ad7c01
2 ---> fc1495d19d30
3Step 24/24 : ENTRYPOINT [ "./start.sh" ]
4 ---> Running in f2d498a10b01
5Removing intermediate container f2d498a10b01
6 ---> ae65ba27dd06
7Successfully built ae65ba27dd06
8Successfully tagged packercontainer:local
This would mean that our docker image has been created successfully. You can check if container image is visible running fallowing command:
1docker images
The output should look like this:
1docker images
2REPOSITORY TAG IMAGE ID CREATED SIZE
3packercontainer local ae65ba27dd06 15 minutes ago 2.25GB
To run the container we would have to pass few specific values from our Azure DevOps account, but that will be topic for some other blog entry.
Thanks for reading.