Yes, We’re Actually Deploying It This Time! 🚀

Welcome back! If you joined me in Part 1, you already know we’re on a mission to prove that PowerShell can do a whole lot more than run scripts on a Windows machine. It can be containerized, deployed, scaled, and orchestrated, right inside Azure Kubernetes Service (AKS).

Missed part1 ? Here you can find it:

PowerShell Microservices on AKS Part 1

In this second part of the series, we’re finally rolling up our sleeves and getting hands-on. No more theory, now we’re actually building our first PowerShell microservice, packaging it into a container, pushing it to an Azure Container Registry (ACR), and deploying it onto AKS using Helm. 🎉

Still the same disclaimer as before:

👉 This blog is not about debating architecture, cost models, or whether PowerShell microservices are a “good idea.”

We’re here for one reason only: “To explore what’s possible and have fun doing it.” 😄

Throughout this post, you’ll again see the 🎬 icon highlighting steps you should actively follow along with, as well as 💡 notes that provide helpful context and tips so you don’t get lost on the way.

By the end of this walkthrough, you’ll have your first PowerShell service running inside AKS, and you’ll be able to test it live. Yes, really. PowerShell. In Kubernetes. Responding to requests.

Alright, enough talking. Let’s build something! 🚀

What we’ll be using

To help you get started I’ve prepared some material you’ll need throughout this blog which you can find here.

DockerFile

# Use official PowerShell image based on Alpine Linux (smaller size)
FROM mcr.microsoft.com/powershell:latest

# Set working directory
WORKDIR /app

# Install Pode module
RUN pwsh -Command "Install-Module -Name Pode -Force -Scope AllUsers -AllowClobber"

# Copy the PowerShell scripts
COPY scripts/ /app/scripts/

# Expose port 8080
EXPOSE 8080

# Run the Pode server
CMD ["pwsh", "-File", "/app/scripts/server.ps1"]

Server (server.ps1)

# Demo Service - Pode Web Server
# This service returns simple static data as JSON

Write-Host "========================================" -ForegroundColor Cyan
Write-Host "  PowerShell as a Service - Demo" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] Starting demo-service..." -ForegroundColor Green
Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] PowerShell Version: $($PSVersionTable.PSVersion)" -ForegroundColor Yellow
Write-Host ""

# Import Pode module
Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] Importing Pode module..." -ForegroundColor Yellow
Import-Module Pode
Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] Pode version: $((Get-Module -Name Pode).Version)" -ForegroundColor Yellow
Write-Host ""

# Start the Pode server
Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] Initializing Pode server..." -ForegroundColor Green
Start-PodeServer {
    
    Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] Configuring HTTP endpoint on port 8080..." -ForegroundColor Yellow
    # Add HTTP endpoint listening on all interfaces
    Add-PodeEndpoint -Address * -Port 8080 -Protocol Http
    
    Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] Enabling request logging..." -ForegroundColor Yellow
    # Enable request logging
    New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging
    
    Write-Host ""
    Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] Registering API endpoints..." -ForegroundColor Green
    Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')]   -> GET /health" -ForegroundColor Cyan
    # Health check endpoint
    Add-PodeRoute -Method Get -Path '/health' -ScriptBlock {
        Write-PodeJsonResponse -Value @{
            status = "healthy"
            service = "demo-service"
            timestamp = (Get-Date -Format "o")
        }
    }
    
    Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')]   -> GET /api/demo" -ForegroundColor Cyan
    # Main demo endpoint - returns static data
    Add-PodeRoute -Method Get -Path '/api/demo' -ScriptBlock {
        Write-PodeJsonResponse -Value @{
            service = "demo-service"
            message = "Hello from PowerShell as a Service!"
            data = @{
                items = @(
                    @{ id = 1; name = "Item One"; value = 100 }
                    @{ id = 2; name = "Item Two"; value = 200 }
                    @{ id = 3; name = "Item Three"; value = 300 }
                )
                totalCount = 3
                generatedAt = (Get-Date -Format "o")
            }
            version = "1.0.0"
        }
    }
    
    Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')]   -> GET /api/info" -ForegroundColor Cyan
    # Info endpoint - returns service information
    Add-PodeRoute -Method Get -Path '/api/info' -ScriptBlock {
        Write-PodeJsonResponse -Value @{
            serviceName = "demo-service"
            description = "A demo PowerShell service running on Pode"
            powershellVersion = $PSVersionTable.PSVersion.ToString()
            podeVersion = (Get-Module -Name Pode).Version.ToString()
            platform = $PSVersionTable.Platform
            os = $PSVersionTable.OS
            endpoints = @(
                @{ method = "GET"; path = "/health"; description = "Health check endpoint" }
                @{ method = "GET"; path = "/api/demo"; description = "Demo endpoint with static data" }
                @{ method = "GET"; path = "/api/info"; description = "Service information" }
            )
        }
    }
    
    Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')]   -> GET /" -ForegroundColor Cyan
    # Root endpoint
    Add-PodeRoute -Method Get -Path '/' -ScriptBlock {
        Write-PodeJsonResponse -Value @{
            message = "Welcome to PowerShell as a Service - Demo Service"
            availableEndpoints = @(
                "/health"
                "/api/demo"
                "/api/info"
            )
        }
    }
}

Helmchart (chart.yaml)

apiVersion: v2
name: demo-service
description: A Helm chart for PowerShell Demo Service running on Pode
type: application
version: 1.0.0
appVersion: "1.0.0"
keywords:
  - powershell
  - pode
  - microservice
maintainers:
  - name: PowerShell as Service Team

Values (values.yaml)

# Default values for demo-service Helm chart
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

replicaCount: 2

image:
  repository: acrbpas.azurecr.io/powershellservice
  pullPolicy: IfNotPresent
  tag: "latest"

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

service:
  type: ClusterIP
  port: 80
  targetPort: 8080

ingress:
  enabled: false
  className: ""
  annotations: {}
    # kubernetes.io/ingress.class: nginx
    # cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: demo-service.local
      paths:
        - path: /
          pathType: Prefix
  tls: []
  #  - secretName: demo-service-tls
  #    hosts:
  #      - demo-service.local

resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 250m
    memory: 256Mi

autoscaling:
  enabled: false
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80
  # targetMemoryUtilizationPercentage: 80

nodeSelector: {}

tolerations: []

affinity: {}

# Liveness and readiness probes
livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5
  timeoutSeconds: 3
  failureThreshold: 3

Service (service.yaml)

apiVersion: v1
kind: Service
metadata:
  name: {{ include "demo-service.fullname" . }}
  labels:
    {{- include "demo-service.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: {{ .Values.service.targetPort }}
      protocol: TCP
      name: http
  selector:
    {{- include "demo-service.selectorLabels" . | nindent 4 }}

Deployment (deployment.yaml)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "demo-service.fullname" . }}
  labels:
    {{- include "demo-service.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "demo-service.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "demo-service.selectorLabels" . | nindent 8 }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        ports:
        - name: http
          containerPort: {{ .Values.service.targetPort }}
          protocol: TCP
        livenessProbe:
          {{- toYaml .Values.livenessProbe | nindent 10 }}
        readinessProbe:
          {{- toYaml .Values.readinessProbe | nindent 10 }}
        resources:
          {{- toYaml .Values.resources | nindent 10 }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}

You should end up with something like this:

First docker image

Before our PowerShell service can even think about running on AKS, it needs a proper vessel. Kubernetes does not deploy scripts, folders, or good intentions. It deploys container images. So the very first real step in this journey is turning our PowerShell code into something AKS understands and can run. In this section, we will build that first image, give it a name, and send it off to Azure Container Registry so it is ready and waiting when AKS comes calling.

🎬Follow the steps below to create the first image

  • Go to the directory where the dockerfile is located (check previous section for files needed)
  • Run the command “docker build -t powershellasaservice:latest .” Do not forget the ‘.’ In the end!

You should see the result as shown below:

  • Tag the image with the command “docker tag powershellasaservice:latest acrbpas.azurecr.io/powershellservice:latest

💡In my situation I have to use “acrbpas.azurecr.io” but make sure you place your own Azure Container Registry path here which you can find in the Azure Portal on your deployed Image repository.

  • Now push the image to the remote repository with: “docker push acrbpas.azurecr.io/powershellservice:latest”

Again, make sure you update the path accordingly

You now should see that the image is pushed

  • You can validate it by going to your image repository which should now look like below:

Connect ACR and AKS

Our container image is safely stored in Azure Container Registry, but right now AKS has no idea it even exists. Before the cluster can pull and run our PowerShell service, we need to introduce the two to each other. In this step, we will grant AKS permission to access ACR so it can securely fetch the image whenever it needs to start or scale our service. Think of this as giving AKS the keys to the registry. Once that trust is in place, everything else starts to fall into place.

🎬 Run the commands below to connect the ACT to AKS

  • “az aks update -n bpasaks -g az-dev-weu-aks-001 –attach-acr acrbpas

💡 Make sure you update the bold typed values to your own values!

On the first place the name of the AKS cluster, second place the resource-group where the AKS is located in and on the third placeholder the name of the container registry.

You should get the result as shown below

  • Validate with “az aks show -n bpasaks -g az-dev-weu-aks-001 –query “servicePrincipalProfile””

You should see:

Deploy with Helm

Before we run anything, we need to introduce one more character in this story: Helm.

As soon as applications in Kubernetes grow beyond the very basics, things can get messy. A deployment here, a service there, maybe some configuration values sprinkled in between. You could apply all of those files one by one, but that quickly turns into a fragile and error-prone ritual. This is where Helm earns its place.

Think of Helm as the instruction manual Kubernetes actually understands. Instead of handing Kubernetes a pile of loose papers, Helm bundles everything together into a single package called a chart. That chart describes what should be deployed, how it should be configured, and how all the pieces fit together. With one command, Helm reads the chart and takes care of creating the right resources in the right order.

In this step, we will use Helm to install our PowerShell service into the cluster. Helm will translate our intent into running pods and services, and once it is done, Kubernetes takes over and keeps everything running. This is the moment where our PowerShell code officially becomes a Kubernetes workload.

🎬 follow the steps below

  • First we need to get credentials for the AKS cluster
az aks get-credentials --resource-group az-dev-weu-aks-001 --name bpasaks

💡 Make sure you update the bold typed values to your own values

  • Now make sure you are in the root directory of the project
  • Run the following command
helm install powershellasaservice .\helm\demo-service\

When the command is done deploying the helm chart we can validate the services:

  • Run the command below you should see the result;
kubectl get pods

We can see the same result in Azure:

Run the command below you should see the result;

kubectl get services

Test the service

🎬 Run the commands below so we can temporarily test the service

kubectl port-forward service/powershellasaservice-demo-service 8080:80

You should see:

 💡See how the port-forward service value matches with the output from kubectl get services?

  • Now run the command “curl http://localhost:8080” in a separate shell you should see the result below

In the shell we have the port-forwarding service enabled we see the incoming connection;

Success! We just validated that we get a response from our deployed pod.

Summary

We didn’t melt our GPU building images, we didn’t turn into Docker ninjas overnight, and we definitely didn’t summon Helm dragons from the depths of Kubernetes…

But we did take a very real step into running PowerShell as an actual microservice inside AKS, and that’s something most people never expect to see. 🐙⚙️🚀

Here’s what you walk away with:

You built your first container image:

From a humble Dockerfile to a tagged and pushed container in ACR. No magic, no mystery, just clean PowerShell packaged into a portable, cloud-ready box.

You connected AKS and ACR:

Your AKS cluster can now pull images straight from your registry without complaining, negotiating, or throwing errors like a moody teenager.

You deployed your first Helm chart:

Yes, Helm, the package manager of Kubernetes, the “apt-get” of the cloud. You installed your first release, created your service, and watched your pod spring to life. That’s a real microservice running PowerShell in a real cluster.

🎉 And the big picture:

This wasn’t theory anymore, this was execution.

Part 2 took you from “I have code” to “I have a running microservice in Kubernetes,” and that’s a massive leap in the containerized PowerShell world.

Because next time?

🔥 We scale beyond a single service

🔥 We explore more patterns for PowerShell microservices

🔥 And we start shaping a real, modular microservice architecture

Strap in… the adventure is far from over. On to part 3!!😉

Leave a Reply

Your email address will not be published. Required fields are marked *