Getting Started

This guide creates an AWS cluster and deploys an example "hello" app on your cluster via the podmin CLI.

Prerequisites

Install Podmin with Homebrew:

brew install podmin-dev/tap/podmin

Or install the latest release with Go:

go install github.com/podmin-dev/podmin@latest

Prebuilt binaries are available from Podmin releases. You also need:

Confirm your AWS identity:

aws sts get-caller-identity --profile=demo

Connect to AWS

You will need to decide on a cluster ID, AWS region, and globally unique S3 bucket name.

For this guide, the examples use:

First, you need to create a local "context" - this will create the cluster bucket if it does not already exist:

podmin connect example \
  --provider aws \
  --region us-west-2 \
  --bucket example-podmin

Podmin stores contexts under the applicable XDG config directory, falling back to ~/.podmin.

podmin connect selects the new context automatically. Run podmin use to list connected contexts or podmin use <cluster-id> to select one.

Create the Cluster

Create one default NodeGroup containing one ARM64 VM:

podmin setup \
  --vpc-cidr 10.0.0.0/16 \
  --nodegroup default

Review the OpenTofu/Terraform plan before approving it. Setup creates or reuses a compatible VPC, compares runtime dependencies with the cluster manifest, transfers only the pending files and images, and starts the NodeGroup. It is safe to run again for upgrades or configuration changes, including from CI without a persistent local cache.

You can add/remove NodeGroups at any time. Add multiple by repeating --nodegroup:

podmin setup \
  --vpc-cidr 10.0.0.0/16 \
  --nodegroup default \
  --nodegroup workers,size=3,disk-size=100,instance-type=c8g.large,zone=b

NodeGroups use a 20 GiB root disk and the region's first available zone by default. Add disk-size=GIB to select an 8-16384 GiB root disk independently for a group. Add zone=b for the region's b zone or provide a full available AWS zone name.

The complete NodeGroup list is authoritative. Removing a NodeGroup from the command removes it after plan approval.

Optional Container Log Export

Podmin can run Fluent Bit on every node and export container stdout and stderr to an OTLP logs endpoint. If the provider requires headers, first store a JSON object in the context's default secrets provider, for example otel-logs-headers.json:

{
  "Authorization": "Basic <base64-credentials>",
  "stream-name": "podmin"
}
podmin secret create otel-logs-headers \
  --system \
  --file otel-logs-headers.json

podmin setup \
  --vpc-cidr 10.0.0.0/16 \
  --nodegroup default \
  --otel-logs endpoint=https://api.openobserve.ai/api/example/v1/logs,headers-secret=true

OTLP/HTTP with protobuf is the default (grpc=false). Specify grpc=true for a secure gRPC endpoint without a URL path. headers-secret=true uses the explicitly user-manageable otel-logs-headers system secret stored at /<cluster>/_system/otel-logs-headers in the context's default secrets provider.

For an exporter endpoint that requires mutual TLS, enable a renewable workload identity and publish its issuing trust bundle where the provider can consume it:

podmin setup \
  --vpc-cidr 10.0.0.0/16 \
  --nodegroup default \
  --otel-logs endpoint=https://logs.example.com/insert/opentelemetry/v1/logs,ca=s3://observability-trust/tls/logs-server-ca.pem,mtls=true \
  --workload-ca-publish s3://observability-trust/podmin/workload-ca.pem

The destination bucket must already exist. On AWS, Podmin grants its generated node role and S3 endpoint read-only access to the exact server CA object and read/write access to the exact workload CA object. A bucket policy in another account and any customer-managed KMS key policy must also permit those operations. Configure the proxy in front of the logging provider, or the logging provider itself, to trust the published PEM bundle and, if desired, authorize the node SPIFFE pattern spiffe://<cluster>.podmin.internal/system/otel-logs/node/*. This is designed to interoperate with MonVM.

Deploy an Application

Copy Podplane's multi-platform Hello image into the cluster image store under the short name hello:

podmin push ghcr.io/podplane/hello:v1.5.0 hello

Deploy it to the default NodeGroup with Podmin's default/built-in manifest:

podmin deploy hello --image hello --nodegroup default --service

The default/built-in manifest includes a DaemonSet. --service includes an opinionated TCP Service on port 443 targeting port 8443, with an HTTPS readiness probe at /healthz, and configures images that support TLS_CERT_FILE and TLS_KEY_FILE to serve the mounted Podmin workload certificate. Repeatable or comma-separated --port SERVICE:TARGET mappings replace that HTTPS default with custom TCP mappings; the first target receives a TCP readiness probe.

The application is now available inside the cluster (from other Pods) at:

https://hello.default.svc.cluster.local

List the cluster's committed desired state at any time:

podmin list
NAME   NAMESPACE  NODEGROUP  SERVICE  ORIGIN
hello  default    default    hello    workload

Note: Podmin does not have a control plane, so the podmin list command only tells you the desired state configured in your object storage bucket - this inventory does not claim that the asynchronously reconciled Pod is running or healthy.

Next Steps

Clean Up

When finished, you can remove the app you deployed from the cluster's desired state:

podmin delete hello --nodegroup default

Run podmin list again to confirm that the deployment is no longer committed. Nodes remove the static Pod asynchronously.

Remove compute and networking while retaining the cluster bucket and certificate authorities so the cluster can be recreated later:

podmin teardown

Permanently remove all cluster infrastructure, stored workloads, images, and certificate authorities, then disconnect the local context:

podmin destroy

Both commands show what they will remove and require confirmation before proceeding.