🚀 Mastering DevOps: HashiCorp Tool Installation with Cross-Platform PowerShell and the Perfect "Golden Image" Architecture 💻⚙️

Managing Infrastructure as Code (IaC) is no longer a luxury but a necessity in the modern technology world. IaC merges infrastructure management with software development disciplines, ensuring repeatability, version control, and agility. The pioneers of this revolution are undoubtedly Packer and Terraform.

While we create fundamental, reliable virtual machine images with Packer, we use those images with Terraform to provision complex infrastructures in minutes. However, the manual overhead of installing and keeping these critical tools up-to-date across multiple operating systems (Windows 🗔, Linux 🐧, macOS 🍎) fundamentally contradicts the philosophy of automation. Installation automation is the first and most foundational step of a successful IaC lifecycle.

In this article, I have prepared a script that fully automates this installation process by leveraging PowerShell's cross-platform capabilities.

In this comprehensive guide, we will delve into the depths of installation automation, explain why we chose this path, and analyze step-by-step how we use the Packer we installed to create a real-world scenario: an Ubuntu "Golden Image" loaded with PowerShell Core.

Part 1: The Foundation of IaC - The Installation Automation Script 🐍🛠️

For a DevOps engineer, consistency and reproducibility are essential. Installation automation is the first link in this chain of consistency; it guarantees that the same tools, with the same versions, exist across Development, Test, and Production environments.

A. Our Cross-Platform Installation Script (PowerShell) 📜

The following PowerShell script utilizes cross-platform management capabilities to the fullest extent when installing HashiCorp tools (Packer and Terraform).

# Clear the console screen
Clear-Host

Set-Location -Path $PSScriptRoot

# List of apps to install (packer commented out, terraform active)
$apps = @('packer', 'terraform') 

if ($IsWindows) {
    # Set the installation directory for Windows
    $installPath = "C:\hashicorp"

    # Loop through each app to download and install
    $apps | ForEach-Object {
        $app = $_
        # Get metadata JSON from HashiCorp releases API
        $metadataUrl = "https://releases.hashicorp.com/$($app)/index.json"
        $metadata = Invoke-RestMethod -Uri $metadataUrl

        # Extract available version strings
        $versionStrings = $metadata.versions.PSObject.Properties.Name

        # Filter only versions matching semantic versioning pattern (digit.digit.digit)
        $standardVersions = $versionStrings | Where-Object { $_ -match '^\d+\.\d+\.\d+$' }

        # Convert versions to [version] objects and sort descending
        $versions = $standardVersions | ForEach-Object { [version]$_ } | Sort-Object -Descending

        # Select the latest stable version
        $version = ($versions | Select-Object -First 1).ToString()

        # Exit with error if no version found
        if (!$version) {
            Write-Error "Could not get the latest version"
            exit 1
        }

        # Set parameters to download the zip archive for the app
        $param = @{
            uri     = "https://releases.hashicorp.com/$($app)/$($version)/$($app)_$($version)_windows_$([System.Environment]::Is64BitOperatingSystem ? 'amd64' : '386').zip"
            OutFile = "$($env:Temp)\$($app).zip"
        }
        Invoke-WebRequest @param

        # Extract the downloaded zip file to the installation path
        $param = @{
            path            = $param.OutFile
            DestinationPath = $installPath
        }
        Expand-Archive @param -Force

        # Remove the downloaded zip file after extraction
        Remove-Item -Path $param.path -Force
    }

    # Get the current user PATH environment variable
    $currentPath = [Environment]::GetEnvironmentVariable("Path", "User")

    # Add installPath to PATH if not already present
    if ($currentPath -notlike "*$installPath*") {
        # Reminder: This script must be run as Administrator on Windows!
        if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
            Write-Warning "⚠️ Please run this script as Administrator to install applications and modify system settings."
            exit 1
        }
        Write-Host "Adding $installPath to system PATH..."
        [Environment]::SetEnvironmentVariable("Path", "$currentPath;$installPath", "User")
    }
    else {
        Write-Host "PATH already contains $installPath"
    }
}
elseif ($IsLinux) {
    # Detect Linux distribution info from /etc/os-release
    $os = @{ }
    Get-Content /etc/os-release | ForEach-Object { if ($_ -match '^(.*?)=(.*)$') { $os[$matches[1]] = $matches[2].Trim('"') } }

    if ($os.Name -match 'Ubuntu|Debian') {
        # Add HashiCorp GPG key and repository for Debian-based systems
        wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
        echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list

        # Update package lists and install each app
        $apps | ForEach-Object {
            sudo apt update && sudo apt install $_
        }
    }
    elseif ($os.Name -match 'Centos|RHEL') {
        # Add HashiCorp repo and install packages on CentOS/RHEL
        sudo yum install -y yum-utils
        sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
        $apps | ForEach-Object {
            sudo yum -y install $_
        }
    }
    else {
        Write-Error "Unsupported OS: $($os["NAME"])"
    }
}
elseif ($IsMacOS) {
    # Use Homebrew to tap and install Terraform on macOS
    brew tap hashicorp/tap
    brew install hashicorp/tap/terraform
}
else {
    Write-Host 'Unknown operating system'
    exit 1
}        

B. Technical Analysis of the PowerShell Script: Platform Separation and Reliability 💡

1. Windows Installation: API Dynamism and Secure PATH Management 🗔

This approach eliminates dependency on static installation files:

  • Dynamic Version Detection: It consults HashiCorp's official API using Invoke-RestMethod. The latest stable (semantically versioned) version is guaranteed to be fetched using a Regular Expression ('^\d+\.\d+\.\d+$') and the [version] object type. This ensures the script remains current without continuous maintenance.
  • Smart Architecture Support: The machine's architecture is determined using the [System.Environment]::Is64BitOperatingSystem variable, ensuring the downloaded ZIP file is appropriate for either the amd64 or 386 (32-bit) architecture.
  • PATH Management: The user environment PATH is appended using [Environment]::SetEnvironmentVariable("Path", ...) to ensure system-wide accessibility of the application. ⚠️ Administrator Privileges: Since a PATH modification is required, the script checks for the necessary authority and alerts the user.

2. Linux Installation: Native Package Management Integration 🐧

The most correct and sustainable method on Linux is to use the distribution's own package manager:

  • Distribution Detection: The script reads the /etc/os-release file to detect the system (Ubuntu, Debian, CentOS, RHEL).
  • Repository Management: HashiCorp's GPG key is added (gpg --dearmor), and then the HashiCorp repository for APT or YUM is dynamically added based on the correct codename. This guarantees automatic dependency resolution and ensures the tools stay updated alongside system updates.

3. macOS Installation: Simplification with Homebrew 🍎

The industry standard for macOS, Homebrew, is utilized. The brew tap hashicorp/tap command installs HashiCorp tools quickly and securely.

Part 2: IaC Excellence in Practice - Automated "Golden Image" Production 🌟🏭

By using the Packer we automatically installed, we can now create our base image that will be ready for deployment with Terraform. This image is an Ubuntu 24.04 server loaded with PowerShell Core, which is necessary for automation.

A. Required Folder Structure 📁

Clean management of scripts and configuration files is critical for the project structure. All HCL and script files should reside in the same root directory, with the ISO file in a separate subdirectory:

📦 /packer-project
 ┣ 📜 packer-build.ps1        (Management script that runs Packer)
 ┣ 📜 ubuntu.pkr.hcl         (Packer configuration file)
 ┣ 📜 install_or_update_pwsh.sh (PowerShell installation script running inside the VM)
 ┣ 📄 meta-data              (cloud-init file)
 ┣ 📄 user-data              (cloud-init file)
 ┗ 📂 ISO                    (Folder containing the ISO file)
   ┗ 💿 ubuntu-24.04.3-live-server-amd64.iso        

B. The Complete Packer Configuration: ubuntu.pkr.hcl 🧱

The following Packer HCL file has been updated to reflect this change in the folder structure by modifying the iso_url parameter (it uses ./ISO/... instead of C:/ISO/... or your specific Windows file path):

packer {
  required_version = ">= 1.14.0"
  required_plugins {
    vmware-iso = {
      version = ">= 1.2.0"
      source  = "github.com/hashicorp/vmware"
    }
  }
}

source "vmware-iso" "ubuntu" {
  iso_url      = "C:/ISO/ubuntu-24.04.3-live-server-amd64.iso" # Your file path should be reflected here. The Windows path is kept in the example.
  iso_checksum = "none" # For security, it is highly recommended to fill this section with the SHA256 value!

  vm_name       = "ubuntu"
  cpus          = 8
  memory        = 16384
  headless      = false 

  cd_files = ["./meta-data", "./user-data"] # cloud-init files are transferred from the root directory to the VM
  cd_label = "cidata" 

  boot_wait = "5s"

  # Boot Command: Triggers the automated installation
  boot_command = [
    "<wait>",
    "c<wait>",
    "linux /casper/vmlinuz autoinstall ds=nocloud;s=/cdrom/ ---<enter><wait>",
    "initrd /casper/initrd<enter><wait>",
    "boot<enter>"
  ]

  ssh_username           = "ubuntu"
  ssh_password           = "1" 
  ssh_timeout            = "20m" 
  ssh_handshake_attempts = 50
}

build {
  sources = ["source.vmware-iso.ubuntu"]

  # Provisioner: Runs the PowerShell installation script after the VM is set up
  provisioner "shell" {
    script = "./install_or_update_pwsh.sh"
    # Secure Sudo Command: Executes the command with administrative privileges by piping the password.
    execute_command = "echo '{{ .Password }}' | sudo -S sh '{{ .Path }}'"
  }
}        

C. Cloud-init Files: meta-data and user-data Content ☁️

These are the most critical files that activate Packer's autoinstall feature and ensure the installation proceeds without manual intervention. The command cd_files = ["./meta-data", "./user-data"] presents these files to the virtual machine as a CD-ROM.

1. meta-data (Simple Content)

It is usually empty but can contain basic information like the instance-id. This simple format is sufficient for automated installation:

instance-id: ubuntu
local-hostname: ubuntu        

2. user-data (Ubuntu Installation Configuration)

This file defines Ubuntu's installation process (language, disk partitioning, user account, etc.) in YAML format. Remember that the username and password in this file must match the values used by Packer for the SSH connection.

#cloud-config
autoinstall:
  version: 1  
  identity:
    hostname: ubuntu
    username: ubuntu
    # Important: We were using a plain text password ("1") for Packer's SSH connection in the HCL file.
    # For a true Golden Image, the SHA-512 hash generated with mkpasswd should go here!
    password: $6$YCCM0tjftFGJ.E1C$Qn1HJGdVraSMJpozoqX1WOt8s61/XRXCCmNQPkK6IJWgEAfDQ/m12m9WLsXCNGnIVALukD/QFL6hXtUp2mgPG0
  ssh:
    install-server: true
    allow-pw: true
  storage:
    layout:
      name: direct
  packages:
    - open-vm-tools
  late-commands:
    - curtin in-target -- systemctl enable ssh        

D. Provisioning Script: PowerShell Core Installation (install_or_update_pwsh.sh) 🎯

This Bash script operates according to the principle of Idempotence (repeatability). In every execution, the script:

  • Checks if PowerShell is installed (command -v pwsh).
  • If installed, it detects the latest stable version from the GitHub API and updates only if necessary.
  • If not installed, it performs the secure initial installation by adding the Microsoft repository.

#!/bin/bash

# Define package and GitHub repo information
PACKAGE_NAME="powershell"
GITHUB_REPO="PowerShell/PowerShell"

# Check if the script is run as root
if [ "$EUID" -ne 0 ]; then 
    echo "Please run this script as root (use sudo)."
    exit 1
fi

# Check if pwsh is already installed
if command -v pwsh >/dev/null 2>&1; then
    echo "PowerShell is already installed. Checking for updates..."

    # Get the current installed version
    CURRENT_VERSION=$(pwsh -Command '$PSVersionTable.PSVersion.ToString()')

    # Fetch the latest release version from GitHub
    LATEST_VERSION=$(curl -s "https://api.github.com/repos/$GITHUB_REPO/releases/latest" | grep tag_name | cut -d '"' -f 4 | sed 's/^v//')

    # Compare versions
    if [ "$CURRENT_VERSION" != "$LATEST_VERSION" ]; then
        echo "Latest version: $LATEST_VERSION (current: $CURRENT_VERSION)"
        echo "Updating PowerShell..."

        # Update PowerShell using apt
        apt-get update
        apt-get install --only-upgrade -y $PACKAGE_NAME
    else
        echo "PowerShell is up to date (version: $CURRENT_VERSION)."
    fi
else
    echo "PowerShell is not installed. Installing..."

    # Add Microsoft package repository and install dependencies
    apt-get update && apt-get install -y wget apt-transport-https software-properties-common
    wget -q https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb
    dpkg -i packages-microsoft-prod.deb
    rm packages-microsoft-prod.deb

    # Install PowerShell
    apt-get update
    apt-get install -y $PACKAGE_NAME
fi

# Print installed version
echo "Done. Installed PowerShell version: $(pwsh -Command '$PSVersionTable.PSVersion.ToString()')"        

Script Analysis: Idempotence and Reliability Mechanisms

  • Idempotence Check: The if command -v pwsh command checks whether PowerShell is already installed. This ensures the script always runs safely: it either installs or just updates, and never performs a faulty re-installation.
  • Currency Guarantee: The GitHub API (curl -s "https://api.github.com/repos/$GITHUB_REPO/releases/latest") is used to always detect the latest stable version of PowerShell and install/update it on the system.
  • Secure Execution: The script enforces the presence of root privileges with the if [ "$EUID" -ne 0 ] check, ensuring that critical package manager (apt-get) commands run without errors.

E. Packer Management Flow: The packer-build.ps1 Script 📜

This PowerShell script manages the complex Packer process as a workflow. This is crucial for both early error detection and ensuring a clean start.

# Clear the console screen
Clear-Host

Write-Host "--- PACKER GOLDEN IMAGE MANAGEMENT STARTED ---" -ForegroundColor Green

# 1. Environment Cleanup: Ensures a clean start by removing previous outputs.
if (Test-Path .\output-ubuntu) {
    Write-Host "Cleaning up previous image outputs..." -ForegroundColor Yellow
    Remove-Item .\output-ubuntu -Recurse -Force
}

# Set the directory where the script is running
Set-Location -Path $PSScriptRoot

# 2. Preparation: Install Packer plugins
Write-Host "Packer init (Plugins) is loading..." -ForegroundColor Cyan
packer init .

# 3. Validation: Check the syntax and format of the HCL file
Write-Host "Packer validate (Configuration Check) is running..." -ForegroundColor Cyan
packer validate .

# 4. Start: Initiate the image creation process
Write-Host "Packer build (Image creation) process is starting..." -ForegroundColor Red
packer build .

Write-Host "--- PACKER GOLDEN IMAGE MANAGEMENT COMPLETED ---" -ForegroundColor Green        

Script Analysis: The Four Steps of Management

  • Cleanup (Remove-Item): Forcibly removes the .\output-ubuntu directory (where Packer is assumed to save its successful outputs). This prevents any leftover artifacts from affecting the new build process.
  • Preparation (packer init): Downloads the necessary plugins defined in the HCL file (in this case, vmware-iso) and prepares the working environment. This is mandatory when starting a project for the first time or when updating dependencies.
  • Validation (packer validate): Since the image creation process (build) can be lengthy, this step catches all syntax and formatting errors in the configuration file before the build operation. This is a critical checkpoint that prevents wasted time.
  • Initiation (packer build): Starts the actual image creation process after the preparation and validation steps have successfully completed.

F. Analysis of the Packer Build Output (Log) 🔍

The complete log output produced by Packer when running the packer build . command is provided below. This output shows both the successful steps of the process and any warnings that require attention:

The configuration is valid.
Warning: A checksum of 'none' was specified. Since ISO files are so big,
a checksum is highly recommended.

 on ubuntu.pkr.hcl line 11:
 (source code not available)

Warning: A shutdown_command was not specified. Without a shutdown command, Packer
will forcibly halt the virtual machine, which may result in data loss.

 on ubuntu.pkr.hcl line 11:
 (source code not available)
... (Start of the Process) ...
==> vmware-iso.ubuntu: Connected to SSH!
==> vmware-iso.ubuntu: Provisioning with shell script: ./install_or_update_pwsh.sh
==> vmware-iso.ubuntu: [sudo] password for ubuntu: /tmp/script_5512.sh: 8: [: Illegal number:
==> vmware-iso.ubuntu: PowerShell is not installed. Installing...
... (Package download and installation logs) ...
==> vmware-iso.ubuntu: Selecting previously unselected package powershell.
...
==> vmware-iso.ubuntu: Setting up powershell (7.5.3-1.deb) ...
...
==> vmware-iso.ubuntu: Done. Installed PowerShell version: 7.5.3
==> vmware-iso.ubuntu: Forcibly halting virtual machine...
... (Cleanup and VMX operations) ...
Build 'vmware-iso.ubuntu' finished after 7 minutes 58 seconds.

==> Wait completed after 7 minutes 58 seconds

==> Builds finished. The artifacts of successful builds are:
--> vmware-iso.ubuntu: VM files in directory: output-ubuntu        

Detailed Analysis of the Log Output 🔬

Every step in this output validates a critical part of our IaC process:

1. Critical Warnings:

  • iso_checksum Warnings: Since iso_checksum = "none" was used, Packer alerts us. Using a checksum like SHA256 is a security and reliability necessity to ensure data integrity and prevent download errors in large ISO files.
  • shutdown_command Warnings: Since Packer was not given a secure shutdown command for the VM (sudo shutdown now), a forced halt occurred with the command Forcibly halting virtual machine.... This increases the risk of data loss. This command must be added in the next revision.

2. SSH Connection: The line Connected to SSH! indicates that the non-interactive installation performed with cloud-init was completed successfully and that Packer has moved on to the Provisioning stage.

3. Provisioning Success:

  • The Bash script is executed with Provisioning with shell script: ./install_or_update_pwsh.sh.
  • The output PowerShell is not installed. Installing... shows that our script passed the Idempotence check and selected the initial installation mode.
  • The installation steps (adding the Microsoft repo, downloading packages) are completed without issues.
  • The final output Done. Installed PowerShell version: 7.5.3 confirms that the image has been successfully equipped with PowerShell Core.

Conclusion: Bootstrapping Automation with Automation 🔗

The unified automation model we presented in this article represents the pinnacle of modern infrastructure management and achieves fundamental DevOps goals:

  • Foundation (PowerShell Script): HashiCorp tools (Packer and Terraform) are installed consistently, dynamically, and up-to-date across all platforms (Windows 🗔, Linux 🐧, macOS 🍎) in the Development environment.
  • Image Production (Packer): A reliable base image with automation capability (PowerShell Core) is created, ready for deployment, using cloud-init and provisioning scripts.
  • Principle: Idempotence: Both the installation and provisioning scripts operate according to the principle of repeatability (update if installed, install if not), reducing manual error to zero.

This cycle allows your team to build faster, more reliable, and more repeatable infrastructures. By bootstrapping automation with automation, you take a significant step toward eliminating human error.

Have you used PowerShell's cross-platform capabilities in your IaC projects like this? What was the most interesting technical challenge you faced with Packer and cloud-init integration? I'd love to hear your experiences and comments! 👇

To view or add a comment, sign in

More articles by Mustafa Sercan Sak

Others also viewed

Explore content categories