Installing Dell Automation Platform 2.0 on a Single EC2 Instance

AI note: Mostly written by AI off my notes

Dell Automation Platform (DAP) 2.0 ships with no documented AWS path. This guide is that path: a complete single-node install on EC2 with Ubuntu 24.04 and RKE2. Follow it top to bottom. Where a command deviates from Dell’s documentation, a short note says so, and every deviation is listed at the end.

What you need

  • Ubuntu Server 24.04 LTS x86 AMI (the Support Matrix requires 24.04.3; apt upgrade on the current AMI reaches it)
  • m7i.4xlarge or larger. Dell’s minimum is 16 vCPU, 32 GiB RAM, 1 TiB per node
  • One 1100 GiB gp3 root volume. Do not split root and data
  • The DellAutomationPlatform_v2.0.0.1.zip bundle (about 19 GB) from Dell Drivers & Downloads
  • Component versions used throughout: RKE2 v1.34 channel, Helm 4.0.0, Docker CE from get.docker.com, Longhorn, HAProxy ingress chart 1.44.3 (controller 3.0.12), Harbor

Step 1: Launch the instance

Launch the 24.04 AMI on an m7i.4xlarge with a single 1100 GiB gp3 root volume.

  • Create a dedicated security group: 443 inbound from your admin ranges, all outbound.
  • Attach an IAM role with AmazonSSMManagedInstanceCore at launch.
  • Tag everything you create.

Step 2: Establish and verify network access

If the instance sits in a private subnet, create all three SSM interface endpoints. Registration uses ssm; sessions use ssmmessages. One endpoint alone gives you nothing.

Then prove egress from a session before proceeding:

for u in https://get.rke2.io https://archive.ubuntu.com https://get.helm.sh https://dl.dell.com; do
curl -sm 10 $u -o /dev/null -w "$u: %{http_code}\n"
done

Step 3: Private DNS

The installer’s preflight logs in to the registry by FQDN and the deployed components call the portal by FQDN, so the zone must exist before the script runs.

Create a Route 53 private hosted zone (this guide uses dap.example.internal) associated with the VPC, and add five A records pointing at the instance’s private IP:

portal.dap.example.internal
orch.dap.example.internal
registry.dap.example.internal
mtls-orch.dap.example.internal
mtls-recovery-orch.dap.example.internal

The cookie domain is the shared parent, dap.example.internal. Never use local.edge; it is reserved and the install fails on it. Verify with getent hosts on each name.

Step 4: OS preparation

sudo -i
apt update && apt upgrade -y
apt install -y unzip tmux open-iscsi nfs-common
systemctl enable --now iscsid
echo 'net.ipv4.ip_unprivileged_port_start=0' > /etc/sysctl.d/90-ingress-lowports.conf
cat > /etc/sysctl.d/91-inotify.conf <<'EOF'
fs.inotify.max_user_instances=1024
fs.inotify.max_user_watches=1048576
EOF
sysctl --system
swapon --show # must print nothing
reboot

The first sysctl lets the unprivileged HAProxy controller bind ports 80 and 443. The second raises Ubuntu’s default inotify limits, which are too low for this stack and otherwise fail the image loader mid-run with exit status 125. After the reboot, lsb_release -d must show 24.04.3 or later.

Step 5: RKE2

Write the config before first start. disable: rke2-ingress-nginx frees host ports 80 and 443 for Dell’s HAProxy. max-pods=180 satisfies an installer preflight gate that appears in no Dell document; the default of 110 fails it.

mkdir -p /etc/rancher/rke2
cat > /etc/rancher/rke2/config.yaml <<'EOF'
disable: rke2-ingress-nginx
kubelet-arg:
- "max-pods=180"
EOF
curl -sfL https://get.rke2.io | INSTALL_RKE2_CHANNEL=v1.34 sh -
systemctl enable --now rke2-server
ln -s /var/lib/rancher/rke2/bin/kubectl /usr/local/bin/kubectl
mkdir -p /root/.kube && ln -s /etc/rancher/rke2/rke2.yaml /root/.kube/config
kubectl get nodes # Ready
kubectl get node -o jsonpath='{.items[0].status.capacity.pods}{"\n"}' # 180
kubectl get pods -A # no ingress-nginx pods anywhere

RKE2 bundles the CNI (Canal), metrics-server, and the CSI snapshot controller, which covers three components Dell’s guide has you install by hand.

One more step that exists only because this is RKE2: Dell’s portal nginx hardcodes resolver kube-dns.kube-system.svc.cluster.local, the conventional kubeadm service name, and RKE2 names its CoreDNS service rke2-coredns-rke2-coredns. Without an alias, the portal’s nginx fails config parse and crash-loops later while every other portal pod runs clean. Create the alias now:

kubectl -n kube-system get svc rke2-coredns-rke2-coredns -o json \
| jq '.metadata = {name:"kube-dns", namespace:"kube-system"}
| del(.spec.clusterIP, .spec.clusterIPs, .status)' \
| kubectl apply -f -
kubectl -n kube-system get svc kube-dns

(apt install -y jq if you skipped it; it is also a Dell preflight requirement.)

Step 6: Helm 4.0.0 and Docker

Pin Helm to the version Dell names; the installer branches on the detected major version. Docker is only the installer’s image-push tool; the cluster runs RKE2’s containerd, and the two coexist.

curl -LO https://get.helm.sh/helm-v4.0.0-linux-amd64.tar.gz
tar -xzf helm-v4.0.0-linux-amd64.tar.gz && mv linux-amd64/helm /usr/local/bin/helm
curl -fsSL https://get.docker.com | sh

Step 7: Registry CA, trusted in two places

mkdir -p /root/certs && cd /root/certs
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
-keyout ca.key -out ca.crt -subj "/CN=DAP Lab CA/O=ExampleCorp"
openssl req -newkey rsa:4096 -nodes -keyout registry.key -out registry.csr \
-subj "/CN=registry.dap.example.internal/O=ExampleCorp"
cat > registry.ext <<'EOF'
basicConstraints=CA:FALSE
keyUsage=digitalSignature,keyEncipherment
extendedKeyUsage=serverAuth
subjectAltName=DNS:registry.dap.example.internal
EOF
openssl x509 -req -in registry.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out registry.crt -days 3650 -sha256 -extfile registry.ext
# Trust store 1: OS and Docker, for the installer's pushes
cp ca.crt /usr/local/share/ca-certificates/dap-lab-ca.crt
update-ca-certificates
systemctl restart docker
# Trust store 2: RKE2's containerd, for the cluster's pulls
cat > /etc/rancher/rke2/registries.yaml <<'EOF'
configs:
"registry.dap.example.internal":
tls:
ca_file: /usr/local/share/ca-certificates/dap-lab-ca.crt
EOF
systemctl restart rke2-server

Both stores are required. Skipping the second produces ImagePullBackOff on every pod when the charts deploy. Wait for kubectl get nodes to show Ready before continuing.

Step 8: HAProxy ingress

Dell’s guide prints an ha-proxy.sh with embedded values. The script does not ship in the bundle, and the printed values do not work on chart 1.44.3: the container ports default to 8080/8443, the hostNetwork: true key is invalid for this chart and silently ignored by Helm, and no MetalLB exists on EC2 to mask either problem. Use these values instead:

cat > /root/haproxy-values.yaml <<'EOF'
controller:
image:
repository: haproxytech/kubernetes-ingress
tag: "3.0.12"
pullPolicy: Always
kind: DaemonSet
dnsPolicy: ClusterFirstWithHostNet
config:
ssl-passthrough: "false"
containerPort:
http: 80
https: 443
daemonset:
useHostNetwork: true
useHostPort: true
hostPorts:
http: 80
https: 443
service:
enabled: false
defaultTLSSecret:
enabled: false
EOF
kubectl create namespace haproxy
helm repo add haproxytech https://haproxytech.github.io/helm-charts && helm repo update
helm install haproxy haproxytech/kubernetes-ingress \
-n haproxy --version 1.44.3 -f /root/haproxy-values.yaml

ssl-passthrough is off, reversing Dell’s setting, so HAProxy terminates TLS for Harbor. Dell’s own portal, orchestrator, and mTLS endpoints still need passthrough, applied per host by annotation during step 12; Harbor’s ingress stays unannotated and terminated, and the two models coexist.

Verify behavior, not Helm’s exit status:

kubectl -n haproxy get pods -o wide # pod IP must equal the node IP
ss -tlnp | grep haproxy # haproxy holding :80 and :443

A curl https://127.0.0.1/ returning 000 here is expected; no routes exist yet.

Step 9: Harbor

kubectl create namespace harbor
kubectl -n harbor create secret tls harbor-tls \
--cert=/root/certs/registry.crt --key=/root/certs/registry.key
helm repo add harbor https://helm.goharbor.io && helm repo update
helm install harbor harbor/harbor -n harbor \
--set expose.type=ingress \
--set expose.ingress.hosts.core=registry.dap.example.internal \
--set expose.ingress.className=haproxy \
--set expose.tls.certSource=secret \
--set expose.tls.secret.secretName=harbor-tls \
--set externalURL=https://registry.dap.example.internal \
--set harborAdminPassword='<registry-password>' \
--set persistence.persistentVolumeClaim.registry.size=200Gi

Wait for all pods in the harbor namespace to reach Running (jobservice restarting two or three times during startup is normal), then:

docker login registry.dap.example.internal -u admin -p '<registry-password>'
curl -u admin:'<registry-password>' -X POST \
https://registry.dap.example.internal/api/v2.0/projects \
-H 'Content-Type: application/json' -d '{"project_name":"dap","public":false}'

If docker login reports a transport-style error, verify with curl -v https://registry.dap.example.internal/v2/. A correct certificate plus an HTTP 401 with a bearer realm means the transport is fine and the credentials are wrong; Docker renders a 401 on the token endpoint with misleading errors.

Step 10: The bundle

dl.dell.com refuses non-browser clients; a plain curl downloads a stub. Send a browser User-Agent:

mkdir -p /opt/dap && cd /opt/dap
curl -L -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" \
-o DellAutomationPlatform_v2.0.0.1.zip "<direct-download-url>"
ls -lh DellAutomationPlatform_v2.0.0.1.zip # ~19G; a few hundred bytes means you got the stub

Verify and extract. The key ships as dell_public.key, lowercase, not the Dell_public.key the docs name:

unzip DellAutomationPlatform_v2.0.0.1.zip
openssl dgst -sha384 -verify dell_public.key \
-signature DellAutomationPlatform_v2.0.0.1-*.zip.signed.bin \
DellAutomationPlatform_v2.0.0.1-*.zip # must print: Verified OK
unzip DellAutomationPlatform_v2.0.0.1-*.zip -d bundle/
cd bundle && chmod +x install-upgrade.sh
rm /opt/dap/DellAutomationPlatform_v2.0.0.1.zip

Step 11: Preflight and snapshot

kubectl get nodes && helm list && docker ps \
&& docker login registry.dap.example.internal -u admin -p '<registry-password>'

Empty helm list and docker ps output still passes; the script checks that the tools work.

aws ec2 create-snapshots --instance-specification InstanceId=<instance-id> \
--description "DAP pre-install"

Step 12: Run the installer

Run it as root, inside tmux; the install outlasts most SSM sessions, and tmux sessions are per-user, so sudo -i before any tmux command.

sudo -i
tmux new -s dap
cd /opt/dap/bundle
./install-upgrade.sh EO_HOST=orch.dap.example.internal \
IMAGE_REG_URL=registry.dap.example.internal/dap \
IMAGE_REG_USERNAME=admin IMAGE_REG_PASSWORD='<registry-password>' \
REGISTRY_CERT_FILE_PATH=/root/certs/ca.crt \
NAMESPACE=hzp PORTAL_NAMESPACE=dapp \
PORTAL_COOKIE_DOMAIN=dap.example.internal \
PORTAL_INGRESS_CLASS_NAME=haproxy \
PORTAL_HOST=portal.dap.example.internal \
ORG_NAME=ExampleCorp ORG_DESC="DAP lab" \
FIRST_NAME=<first> LAST_NAME=<last> \
USERNAME=administrator EMAIL=<email>

REGISTRY_CERT_FILE_PATH is the CA certificate, not the server certificate. The loader pushes 126 images (the docs say 119). Optional: add IMAGE_LOADER_THREAD_COUNT=2 to halve peak load during the push phase. Detach with Ctrl-b then d; reattach with tmux attach -t dap. If a run fails partway, rerun it; the loader skips images already in the registry.

Watch from a second pane:

watch -n 10 'kubectl get pods -n hzp; echo; kubectl get pods -n dapp'

Expect one scripted intervention. After the portal chart deploys, the installer fetches an access token from the portal over HTTPS, and with our terminated-TLS ingress it fails with nginx’s 400 “The plain HTTP request was sent to HTTPS port.” Dell’s stack serves TLS end to end, so its ingresses need per-host passthrough. Annotate, verify, and rerun the same installer command; it resumes, skips the loader, and retries the token:

kubectl -n dapp annotate ingress --all haproxy.org/ssl-passthrough=true --overwrite
curl -sk -o /dev/null -w '%{http_code}\n' https://portal.dap.example.internal/ # 302 means fixed
docker login registry.dap.example.internal -u admin -p '<registry-password>' # Harbor unaffected

Then, during the orchestrator phase, annotate its ingresses the moment they appear in hzp so the orchestrator’s own token exchanges and the mtls hosts never hit the same 400:

kubectl -n hzp annotate ingress --all haproxy.org/ssl-passthrough=true --overwrite

Step 13: Finish and log in

The script runs preflight, pushes 126 images, opens the Helm values file (defaults are correct; save with :wq), deploys, and ends by printing the portal URL, orchestrator URL, administrator, and a one-time password. Record the password immediately. Init jobs failing once or twice while state converges is expected per Dell’s own troubleshooting section; sustained ImagePullBackOff means the containerd trust from step 7 is missing.

To log in before corporate DNS reaches the VPC, add the portal and orchestrator names to your laptop’s hosts file pointing at 127.0.0.1 and forward through SSM. Forward to local port 443, not an alternate like 8443: the portal’s Keycloak validates OIDC redirect URIs against the registered portless hostnames, and a port in the browser URL gets you “Invalid parameter: redirect_uri” at the login screen. The shorthand parameter form below also sidesteps shell quoting differences between bash, PowerShell, and cmd:

aws ssm start-session --target <instance-id> \
--document-name AWS-StartPortForwardingSession \
--parameters portNumber=443,localPortNumber=443

Binding local 443 needs the port free; if the session fails to bind, find the squatter with netstat -ano | findstr :443 on Windows or sudo lsof -i :443 elsewhere (IIS, Docker Desktop, and VMware are the usual suspects) and stop it for the session.

Browse to https://portal.dap.example.internal/ with no port, accept the self-signed certificate warning, set a real password, accept the terms, and complete licensing: Dynamic Licensing if the box can reach Dell, otherwise the offline license-file flow from Dell My Account.

Every difference from Dell’s documentation

  1. The supported OS version (Ubuntu 24.04.3) appears only in the Support Matrix, not the Deployment Guide; 22.04 is not supported.
  2. The preflight requires roughly 850 GiB of node ephemeral storage, measured on the filesystem holding /var/lib/kubelet. Documented nowhere; error code 0x01601000B. The single large root volume satisfies it.
  3. The preflight requires kubelet max-pods=180. Also documented nowhere.
  4. ha-proxy.sh is referenced in the guide but does not ship in the bundle.
  5. The guide’s HAProxy values contain an invalid hostNetwork key that Helm silently ignores; the working keys are controller.daemonset.useHostNetwork and useHostPort, plus explicit container ports 80/443.
  6. The HAProxy 3.0.x controller runs unprivileged and needs net.ipv4.ip_unprivileged_port_start=0 to bind 80/443 on the host network.
  7. MetalLB is in Dell’s requirements but does not apply on EC2; host networking replaces it.
  8. ssl-passthrough: "true" from the guide breaks TLS-terminated backends such as Harbor; run with it off, and revisit per host if you onboard mTLS devices.
  9. The signature key ships as dell_public.key, not Dell_public.key.
  10. dl.dell.com refuses non-browser User-Agents; scripted downloads need one.
  11. The v2.0.0.1 loader pushes 126 images, not the documented 119.
  12. REGISTRY_CERT_FILE_PATH takes the CA certificate, though the doc example implies the server certificate.
  13. Ubuntu’s default inotify limits are too low for this stack and fail the image loader with exit status 125; no Dell prerequisite mentions kernel limits.
  14. Dell’s portal nginx hardcodes the resolver name kube-dns.kube-system.svc.cluster.local; on RKE2, whose CoreDNS service is named differently, the portal nginx crash-loops until a kube-dns alias Service exists, even though “RKE cluster x single VM” is a validated Support Matrix row.
  15. Dell’s portal and orchestrator serve TLS end to end and require ingress passthrough; the installer’s token fetch fails with an nginx 400 behind a TLS-terminating ingress, fixed per host with the haproxy.org/ssl-passthrough=true annotation while Harbor stays terminated.

Leave a comment