Chapter 19Lesson 03~225 minutes

Docker, kubectl, Helm, and Other DevOps CLIs from PowerShell

Integrate Docker, kubectl, Helm, and other DevOps CLIs safely by passing arguments as data, preferring JSON/machine output, validating context, using only documented dry-run features, and normalizing vendor results.

DockerkubectlHelmMachine output

Learning objectives

  • Apply one safe native-CLI invocation pattern across DevOps tools.
  • Prefer JSON and documented machine formats over human tables.
  • Treat context, namespace, and environment selection as safety boundaries.
  • Use kubectl/Helm dry-run modes only where documented.
  • Design live-or-mock workflows that work without local infrastructure.
  • Normalize tool output into stable PowerShell objects.

1. External DevOps CLIs are APIs with process contracts

Docker, kubectl, Helm, Terraform, cloud CLIs, and many other tools are native executables from PowerShell's point of view. Treat them consistently: discover the executable, pass arguments as data, capture machine-readable output, inspect the exit code, and convert only the fields you need into stable PowerShell objects.

External DevOps CLIs are APIs with process contracts
flowchart TD
    PS[PowerShell orchestration] --> A[argument array]
    A --> CLI[native DevOps CLI]
    CLI --> J[JSON/YAML/machine output]
    CLI --> E[exit code]
    J --> O[PowerShell objects]
    E --> D[success/failure decision]
    O --> D

2. Never solve native argument construction with string evaluation

When values contain spaces, punctuation, selectors, or paths, build an argument array. PowerShell then knows which parts are arguments; you do not need to assemble executable source code.

$kubectl = Get-Command kubectl -CommandType Application -ErrorAction SilentlyContinue
$namespace = 'dev-team'

if ($kubectl) {
    $args = @('get','pods','--namespace',$namespace,'--output','json')
    $json = & $kubectl.Source @args
    if ($LASTEXITCODE -ne 0) {
        throw "kubectl get failed with exit code $LASTEXITCODE"
    }
    $pods = $json | ConvertFrom-Json
}

The same pattern works for Docker and Helm. Use the tool's documented escaping/argument rules where a single argument itself contains a template expression, but do not turn the whole command into a string and feed it to Invoke-Expression.

3. Ask the tool for JSON instead of scraping tables

Machine output is the most important integration habit in this lesson. Docker inspect emits JSON. Kubernetes supports -o json. Helm can render Kubernetes manifests locally. Convert that output into objects, then use normal PowerShell property access.

$dockerFixture = @'
[{"Id":"sha256:demo","Created":"2026-01-01T00:00:00Z","Architecture":"amd64"}]
'@
$raw = $dockerFixture
$source = 'fixture'

if (Get-Command docker -ErrorAction SilentlyContinue) {
    $candidate = & docker image inspect alpine:latest 2>$null
    if ($LASTEXITCODE -eq 0) {
        $raw = $candidate -join "`n"
        $source = 'docker'
    }
}

$images = $raw | ConvertFrom-Json
$images | Select-Object Id,Created,@{
    Name='Architecture'; Expression={$_.Architecture}
},@{Name='Source';Expression={$source}}
Stable contract rule: even JSON schemas can change across tool versions. Normalize the few fields your automation promises instead of forwarding an entire vendor object as your own long-term contract.

4. Context is a safety boundary, not a convenience

Many DevOps CLIs can target multiple environments. A Kubernetes context selects a cluster/user/namespace combination; Docker can have multiple contexts; Helm operates against the Kubernetes context. Automation must make the intended target visible before any mutating command.

function Get-KubeTarget {
    if (-not (Get-Command kubectl -ErrorAction SilentlyContinue)) {
        return [pscustomobject]@{ Available=$false; Context=$null; Namespace=$null }
    }

    $context = [string](& kubectl config current-context 2>$null)
    $context = $context.Trim()
    $namespace = [string](& kubectl config view --minify --output 'jsonpath={..namespace}' 2>$null)
    $namespace = $namespace.Trim()
    if (-not $namespace) { $namespace = 'default' }

    [pscustomobject]@{
        Available = $true
        Context   = $context
        Namespace = $namespace
    }
}

$target = Get-KubeTarget
$target

A production wrapper can compare this object with an allowlist and require an explicit -Environment or -Context. Never make “whatever context happens to be active” an invisible deployment input.

5. Use each tool’s real preview feature—when it has one

PowerShell's -WhatIf does not magically apply to native tools. Use the external tool's own preview behavior only when documented. For example, Kubernetes kubectl apply supports --dry-run=client and --dry-run=server; Helm supports client/server dry-run modes for simulated installs.

# Client-side Kubernetes validation/rendering example:
# kubectl apply --dry-run=client -f ./manifests/app.yaml -o yaml

# Server-side validation without persistence (requires cluster access):
# kubectl apply --dry-run=server -f ./manifests/app.yaml -o yaml

# Helm local rendering without a cluster connection:
# helm template demo ./chart --namespace dev --dry-run=client

Do not invent a --dry-run flag for a command that does not document one. If a CLI lacks preview semantics, design your PowerShell wrapper to inspect/plan first and make mutation a separately authorized step.

6. Capability detection keeps the lesson useful without Docker or Kubernetes

Tool-specific labs should degrade gracefully. Detect installed commands, record versions when available, and provide fixture JSON when the executable or live service is absent. This is also good test design: parsing and normalization should be testable independently of the real tool.

$capabilities = foreach ($name in 'docker','kubectl','helm') {
    $cmd = Get-Command $name -CommandType Application -ErrorAction SilentlyContinue
    [pscustomobject]@{
        Tool      = $name
        Available = [bool]$cmd
        Path      = $cmd.Source
    }
}
$capabilities

7. Normalize vendor output into your own deployment-inspection contract

A deployment-inspection object should answer operational questions without leaking the whole raw object: target, workload name, desired/ready replicas, image, and source. This makes downstream code independent of whether data came from live kubectl, fixture JSON, or another provider.

function ConvertFrom-KubeDeploymentJson {
    param([Parameter(Mandatory)][string]$Json)

    $doc = $Json | ConvertFrom-Json
    foreach ($item in @($doc.items)) {
        [pscustomobject]@{
            Platform        = 'kubernetes'
            Namespace       = $item.metadata.namespace
            Name            = $item.metadata.name
            DesiredReplicas = [int]($item.spec.replicas ?? 0)
            ReadyReplicas   = [int]($item.status.readyReplicas ?? 0)
            Image           = $item.spec.template.spec.containers[0].image
        }
    }
}

8. Lab: deployment inspection with live-or-mock data

This lab never changes a cluster. If kubectl is installed and a context is available, it performs a read-only get deployments -o json. Otherwise it uses a local fixture with the same shape.

$fixture = @'
{
  "items": [
    {
      "metadata": {"namespace":"dev","name":"api"},
      "spec": {"replicas":3,"template":{"spec":{"containers":[{"image":"example/api:1.4.0"}]}}},
      "status": {"readyReplicas":2}
    },
    {
      "metadata": {"namespace":"dev","name":"worker"},
      "spec": {"replicas":2,"template":{"spec":{"containers":[{"image":"example/worker:1.4.0"}]}}},
      "status": {"readyReplicas":2}
    }
  ]
}
'@

$source = 'fixture'
$json = $fixture

if (Get-Command kubectl -ErrorAction SilentlyContinue) {
    $context = [string](& kubectl config current-context 2>$null)
    $context = $context.Trim()
    if ($LASTEXITCODE -eq 0 -and $context) {
        $candidate = & kubectl get deployments --all-namespaces --output json 2>$null
        if ($LASTEXITCODE -eq 0) {
            $json = $candidate -join "`n"
            $source = "kubectl:$context"
        }
    }
}

$inventory = ConvertFrom-KubeDeploymentJson -Json $json | ForEach-Object {
    $_ | Add-Member -NotePropertyName Source -NotePropertyValue $source -PassThru
}

$inventory | Sort-Object Namespace,Name | Format-Table
$inventory | ConvertTo-Json -Depth 5 | Set-Content ./deployment-inventory.json

Expected observations

You receive the same normalized object contract whether the data came from a live read-only query or the fixture. The API deployment is visibly under-ready in the mock data.

Verification checklist

  • No apply/install/delete command is executed.
  • The data source is recorded.
  • Raw JSON becomes typed numeric replica fields.
  • The output can be persisted as clean JSON without parsing a human table.

Cleanup

Remove-Item ./deployment-inventory.json -ErrorAction SilentlyContinue

9. The pattern generalizes beyond three tools

Once the boundary is clear, the same method applies to Terraform plan JSON, cloud CLIs, package managers, scanners, and observability tools: prefer a documented machine format; validate context; bound timeouts; check exit codes; normalize output; and keep mutation behind an explicit decision.

10. Common mistakes

  • Scraping decorative CLI tables because they look convenient.
  • Assuming active Kubernetes/Docker context is safe for mutation.
  • Passing production names through string evaluation.
  • Claiming an external command supports -WhatIf.
  • Failing the whole lesson because an optional CLI is absent instead of providing a mock fixture.
  • Returning huge vendor JSON objects when downstream code only needs five stable fields.

11. Knowledge check

Question 1. Why use kubectl -o json instead of the default table?

Question 2. Does PowerShell -WhatIf protect native Docker/kubectl/Helm commands automatically?

Question 3. Why record context and namespace?

Question 4. Why provide mock JSON?

Question 5. What should your wrapper return?

12. Summary and bridge

You can now integrate native DevOps CLIs without turning PowerShell into fragile command-string glue. Lesson 4 compares that model with cloud-native PowerShell modules, where the integration returns objects directly but introduces equally important identity, tenant/account/subscription/region, pagination, throttling, and version concerns.

13. Authoritative references

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.