Prerequisites
AWS Credential (provisioning scope)
An AWS credential whose role carries the Provisioning scope - the one write role for EKS and self-managed clusters - or the EC2-only Self-managed scope; access keys with the equivalent policy also work. A provisioning stack launched before the self-managed statements were added must be updated. See AWS Credentials.
SSH Key Credential
An SSH public key for node access. You can provide your own or let Ankra generate one. See SSH Key Credentials.
The network Ankra creates
Leavevpc_id out of the request (the wizard’s Create a new VPC (recommended)) and Ankra lays out, in the region you chose:
Preflight reports the plan before anything is built:
network_ownership: created, resolved_availability_zones, whether the CIDR holds a /20 and a /24 per zone, whether every zone exists and is available, the three-control-plane rule, and the VPC, NAT gateway and Elastic IP quotas. Teardown deletes all of it - route tables, subnets, gateways, NAT gateways with their Elastic IPs, the VPC - by the tags Ankra set, in reverse order.
Adopting an existing VPC
An account with an established network passesvpc_id (the wizard’s Use an existing VPC) and Ankra builds inside it instead. The target region then needs:
network_ip_range, availability_zones and nat_gateway_single_zone are refused alongside vpc_id; node_subnet_ids and bastion_subnet_id are refused without it. In an adopted VPC Ankra never creates, modifies or deletes your VPC, subnets, internet gateway, NAT gateways, DHCP options or existing route tables.
Egress modes
Nodes have no public IP, so every private subnet must route0.0.0.0/0 through something. The egress_mode field on the create request decides what, and Ankra never mixes modes on one cluster:
nat_gateway(created VPC only, the default there) - an Ankra-created NAT gateway with its own Elastic IP in every zone, each zone’s private route table pointing at its own gateway, so egress survives a zone outage.nat_gateway_single_zone: truecreates one gateway in the first zone that every private subnet routes through - cheaper, one egress failure domain. Cost: about USD 32 per month per NAT gateway plus per-GB data processing (see VPC pricing); a three-zone cluster runs three gateways unless you collapse them to one. The bastion is an SSH jump host only.bastion_nat(either network) - the bastion becomes the NAT instance, as it is on Hetzner and DigitalOcean: source/destination checking is switched off and it forwards node traffic to the internet gateway. In a created VPC the private route tables point their default route at the bastion; in an adopted VPC Ankra creates one tagged route table with0.0.0.0/0to the bastion and associates the node subnets with it, recording each subnet’s previous association so teardown restores it exactly. Cost: the bastion instance and its Elastic IP, nothing else - the cheapest option. The trade is that node egress is capped by the bastion’s network performance, shares its fate (a bastion resize interrupts it), and the bastion must stay running.existing(adopted VPC only) - your node subnets already route the default to a NAT gateway, a NAT instance or a transit path. Ankra checks the route and changes nothing. Cost: whatever you already pay for that path, shared with everything else in the subnet.
egress_mode resolves to nat_gateway for a created VPC. For an adopted VPC preflight resolves it: subnets that already have egress select existing; empty subnets without it select bastion_nat; subnets without egress that hold instances Ankra did not create are refused with a message naming them. bastion_nat is always refused in an adopted VPC when any node subnet carries a foreign instance, because re-pointing that subnet’s default route would hijack its egress. Create the NAT gateway yourself and use existing in that case.
What Ankra creates, and what it never touches
Every object Ankra creates is taggedankra.cloud/cluster-id=<id>, ankra.cloud/managed=true and kubernetes.io/cluster/<id>=owned, and the credential’s write permissions are scoped to those tags. In both modes Ankra creates two security groups (nodes, bastion), an imported key pair, one Elastic IP for the bastion, the bastion instance, the node instances with their network interfaces and encrypted gp3 root volumes, and two IAM roles with instance profiles (ankra-k3s-<cluster>-cp and ankra-k3s-<cluster>-node). In a created VPC it also owns the VPC, subnets, internet gateway, route tables, NAT gateways and their Elastic IPs; in an adopted VPC it adds at most one route table (bastion_nat mode) and never touches your VPC, subnets, internet gateway, NAT gateways, DHCP options or existing route tables. The full list, with tags and IAM scoping, is in the AWS Reference.
Creating an AWS Cluster
Via the Platform UI
1
Navigate to Clusters
Go to Clusters in the Ankra dashboard and click Create Cluster.
2
Select AWS - Self-managed
Choose AWS as the provider and pick the Self-managed action (the other action, Cloud Managed, creates EKS).
3
Select Credentials
Pick an AWS credential with the provisioning or self-managed scope and an SSH key credential. A read-only (cost) credential, or a provisioning role whose stack predates the self-managed statements, is listed but refused at preflight with the missing permission named. You can create either credential from the wizard.
4
Choose Region and Network
Select a region - the list loads live from your credential. Create a new VPC (recommended) is selected: keep the network range (
10.0.0.0/16) or set your own /16 to /20, and optionally name the availability zones - otherwise preflight picks one zone for a single control plane and three for a three-node control plane. Switch to Use an existing VPC to pick the VPC, one or more node subnets and the bastion subnet instead; the wizard shows each subnet’s availability zone and whether it has a public or private route, and offers only public subnets for the bastion.5
Egress and Bastion Access
For a new VPC choose NAT gateway (the default - one per zone, or tick single NAT gateway to share one) or bastion as NAT (cheapest); for an existing VPC choose existing, bastion as NAT, or leave it on Detect and let preflight resolve it. Then enter the allowed IPs that may reach the bastion on port 22 - at least one CIDR, and
0.0.0.0/0 is refused. Your current public address is offered as a starting point.6
Configure Nodes
Set your cluster topology:
- Bastion - instance type for the SSH bastion (a burstable type such as
t3.smallis enough for an SSH jump host; size it for bandwidth inbastion_natmode) - Control Plane - count (1 to 9 - at least three when the cluster spans more than one availability zone, and three is what makes a new VPC three-zone) and instance type
- Workers - one or more node groups, each with a name, instance type and count
amd64 types are offered in this release.7
Distribution, CNI and Image
Keep k3s (the wizard’s default) or pick kubeadm, which on AWS is Cilium only. The CNI defaults to Cilium for both distributions; a k3s cluster can pick Calico or flannel and the advanced features, but only Cilium and Calico can run the IMDS guard that keeps pods away from the control-plane instance role, so flannel is accepted with a preflight warning. The Ubuntu series defaults to 24.04 and the root volume is an encrypted gp3 volume of 40 GiB whose size you can raise (up to 2000 GiB).
The wizard and the CLI examples on this page create k3s clusters. An API request that omits
distribution follows the platform default, kubeadm, like every other self-managed provider - set "distribution": "k3s" explicitly when you want k3s from the API.8
GitOps, Networking & DNS
Optionally connect a GitHub repository for GitOps-driven deployment. Two checkboxes on this step are on by default:
- Include Networking Stack - deploys Traefik, cert-manager and a Let’s Encrypt ClusterIssuer. Traefik’s Service asks for a Network Load Balancer, which the AWS cloud controller creates in your account.
- Include Public DNS - gives the cluster its own delegated subdomain on
ankra.ccand installs external-dns with its credentials already wired, so an ingress hostname under that subdomain gets its DNS record and TLS certificate automatically.
9
Preflight, Create & Track Progress
The wizard runs the preflight before it submits and shows what it found - network ownership, the availability zones it resolved, the egress mode, the AMI it will use, and the vCPU, VPC, NAT gateway, Elastic IP and security-group quotas it checked. Click Create to start provisioning. A live progress view tracks IAM roles and instance profiles, Prepare VPC, Subnets & Routing, NAT gateways (in
nat_gateway mode), security groups, key pair, Elastic IP, bastion, route table (bastion_nat in an adopted VPC), node instances, Kubernetes installation, cloud controller and CSI driver, and Ankra Agent setup. The cluster appears offline until provisioning completes, then transitions to online.Managing from the Dashboard
Once the cluster is online, day-2 operations live where they do for every self-managed provider:- Nodes section - node groups, control plane, bastion health and resize, restart, diagnose and repair. See Nodes.
- Settings → General - Kubernetes upgrade, stop and start, power schedules, and the Danger Zone to deprovision.
- Settings → Access - SSH commands through the bastion and SSH key management.
Via the CLI
Create the credentials first. The AWS credential is the same one EKS uses: the provisioning role (AnkraProvisioning) builds both EKS and self-managed clusters, and the credential page walks through it in the dashboard, including how to update a stack launched before the self-managed statements were added. From the terminal, onboarding prints the external ID, the trust principal, the quick-create link for the stack and the create-role command to run once the stack shows CREATE_COMPLETE:
create-role stores must be the one onboarding printed and the stack was launched with; a role created at the cost scope is refused at cluster create.
Then look up what the credential can reach (vpcs and subnets matter only when you adopt a VPC):
Preflight
preflight takes the same flags as create and runs every check the create runs, without creating anything. For a created network: whether network_ip_range holds a private /20 and a public /24 per zone, whether every zone exists and is available in the region, the three-control-plane rule for more than one zone, and the VPC (L-F678F1CE), NAT gateway (L-FE5A380F) and Elastic IP quotas. For an adopted VPC: whether the VPC and subnets exist and sit in the region, which zones the node subnets cover, and the resolved egress mode (and why bastion_nat is refused, if it is). For both: the AMI resolved for the Ubuntu series and architecture, the vCPU quota for the chosen instance families, the security-groups-per-interface quota, and whether bastion_allowed_ips is valid. The result names network_ownership (created or adopted) and resolved_availability_zones. Every item is three-state - ok, warning (a read Ankra could not perform, or a caveat such as flannel not enforcing the IMDS guard) or error - and the result carries can_proceed and resolved_egress_mode, the mode the create will run under. Run it until there are no errors, then run create with the same flags.
Only --name, --credential-id, --ssh-key-credential-id, --region and --bastion-allowed-ips are required; without --vpc-id Ankra creates the network. Three control planes make it a three-zone VPC with a NAT gateway per zone:
Create
--network-ip-range (default 10.0.0.0/16), --availability-zones (comma-separated, in the order the spread walks them), --egress-mode nat_gateway|bastion_nat and --nat-gateway-single-zone. A single-zone development cluster on the cheapest egress:
Adopting an existing VPC from the CLI
Pass--vpc-id with the subnets, and --egress-mode existing|bastion_nat (or leave it for preflight to detect); the created-network flags are refused alongside it:
--cni and --cni-features pick the CNI and its feature toggles (kube_proxy_replacement, hubble, wireguard_encryption for Cilium, ebpf_dataplane for Calico); --environment and --criticality classify the cluster as on every provider; --gitops-repository, --gitops-credential-name and --gitops-branch wire GitOps at create. The full flag list is in the CLI reference.
Via the API
Preflight first - the endpoint takes the same body as create and answers with findings instead of a cluster. Withoutvpc_id the request describes a created network (network_ownership: created in the result, with the resolved_availability_zones):
{"cluster_id", "name", "kind": "aws", "state": "creating", "operation_id"}; the operation tracks provisioning.
Adopting an existing VPC from the API
Name the VPC and its subnets instead;network_ip_range, availability_zones and nat_gateway_single_zone are refused with 422 alongside vpc_id, and egress_mode is existing or bastion_nat:
create_aws_cluster tool takes the same fields and runs the preflight for you.
Accessing the Cluster
The bastion is the only instance with a public address, and its security group admits SSH only frombastion_allowed_ips. The nodes admit SSH and the Kubernetes API from the bastion’s security group, and everything from each other. Settings → Access shows copy-pasteable commands with the addresses filled in, and ankra cluster aws access-info <cluster_id> (GET /api/v1/clusters/aws/{cluster_id}/access-info) answers bastion_host, bastion_port (22), bastion_user and target_user (both ubuntu) and the control plane IPs:
ubuntu. For everyday kubectl you do not need the tunnel - the Kubernetes browser, ankra cluster get and the kubeconfig route through the Ankra Agent, which needs no inbound access at all.
Changing who may reach the bastion
bastion_allowed_ips is the bastion security group’s SSH ingress. Update it from Settings → Access or the API and the rule is rewritten in place; the list must keep at least one entry and never widen to 0.0.0.0/0. Ankra’s own provisioning path is always admitted.
Managing SSH keys
Add or remove SSH key credentials on a running cluster from Settings → Access, or withankra cluster ssh-keys set <cluster_id> --ssh-key-credential-ids <id>,<id>. Changes are written to authorized_keys on every node on the next reconciliation; the key pair imported into EC2 at create is the Ankra-managed key and is never removed. ankra cluster ssh-keys resync <cluster_id> forces the write.
Node Groups
Node groups organise workers into groups with independent instance types, counts, labels and taints, exactly as on the other self-managed providers. New nodes of a group are balanced across the cluster’s node subnets - each node takes the availability zone with the fewest instances cluster-wide - unless the group is pinned to one zone withavailability_zone. Pin a group that runs zonal storage: an EBS volume cannot attach from another availability zone.
topology.kubernetes.io/zone and topology.kubernetes.io/region, set by the cloud controller, so topologySpreadConstraints work without extra configuration.
Control Plane
Grow a single control plane to three, or change the control plane instance type, from the Nodes → Control plane tab or the CLI. Three control planes are required when the node subnets span more than one availability zone, so that etcd keeps quorum when a zone is lost; a multi-AZ cluster cannot be scaled below three.Restarting, Diagnosing and Repairing a Node
Restart any node - a control plane node, a worker, or the bastion - as a tracked operation from the Nodes section, the CLI or the API. A restart is an EC2 reboot; if the instance does not respond it falls back to a stop and start, which keeps the private IP.ssh_diagnose_node and ssh_repair_node AI tools.
Bastion health and resize
bastion status reports the last verdict of the bastion health loop - reachable or not, which hop a failed probe stopped at, and when - without probing anything, so it answers even while the bastion is down; bastion diagnose probes it now as a tracked operation. Both are shown on the Nodes → Bastion tab.
PUT /api/v1/clusters/aws/{cluster_id}/bastion/instance-type with {"instance_type": "t3.medium"}: the instance is stopped, its type changed and started again, keeping its Elastic IP.
Upgrading Kubernetes Version
Upgrades roll control planes first, then workers, one node at a time, each cordoned, drained and gated on beingReady at the target version. Both k3s and kubeadm clusters upgrade; downgrades and skipping a minor version are refused. The AWS cloud controller manager’s minor version is pinned to the cluster’s Kubernetes minor, and the upgrade path moves it with the nodes.
Stopping and Starting a Cluster
Stop a cluster to park it: every node and the bastion are stopped (not terminated), so the instances keep their root volumes and - because they live in a VPC - their private IPs. Start brings the same instances back and reconciles the cluster; embedded etcd rejoins on the same addresses, so a stopped cluster comes back with its state, not as a rebuild. The Elastic IP stays allocated and the bastion keeps its public address across a stop.nat_gateway egress is cheaper to deprovision. Compute is not billed. Ankra records each instance’s private IP at create and verifies it after every start; a difference is reported as drift rather than silently accepted. Stop and start also run on a timetable with power schedules, and a stopped cluster is where control plane instance type changes are applied.
Deprovisioning
Deprovisioning terminates the instances, releases the bastion’s Elastic IP, and deletes the security groups, key pair and per-cluster IAM roles. In a created VPC it then removes the network in reverse order - NAT gateways with their Elastic IPs, route tables, subnets, the internet gateway, the VPC - every object found by the tags Ankra set. In an adopted VPC it restores each node subnet’s previous route table association before deleting the one route table it created (bastion_nat mode), and your VPC, subnets, gateways and DHCP options are untouched. The cluster is removed from Ankra.
What happens to the volumes the EBS CSI driver provisioned and the load balancers the cloud controller created follows the cluster’s retention_policy, chosen at create (with one exception: when Ankra created the VPC, load balancers are always deleted with it, because a load balancer cannot outlive the VPC it lives in - volumes still follow the policy):
retain(the default) records and keeps them. EBS volumes and Network Load Balancers stay in your account and keep billing; the teardown result lists their provider IDs, and they are tagged with the cluster ID so they are easy to find in the EC2 and load balancing consoles.deletesweeps them: a volume or load balancer is deleted only when it carries this cluster’sankra.cloud/managed=truetag and the policy isdelete. Nothing without the tag is touched, whatever the policy.
Architecture
An AWS cluster provisions the following infrastructure - the first four rows only when Ankra creates the network:Cloud integration
- Cloud controller manager. Ankra deploys the AWS cloud controller manager as a vendored manifest with an exact image tag, matched to the cluster’s Kubernetes minor. Nodes register with
cloud-provider=external, a provider ID ofaws:///<availability-zone>/<instance-id>, and their EC2 private DNS name as the node name, so the controller can find each instance.KubernetesClusterIDis the Ankra cluster ID, and every instance carries the matchingkubernetes.io/cluster/<id>=ownedtag. - Load balancers. A
Serviceof typeLoadBalancergets an AWS load balancer from the cloud controller. The Traefik the networking stack installs asks for a Network Load Balancer (service.beta.kubernetes.io/aws-load-balancer-type: nlb), and Ankra sets the managed tag on it so the teardown sweep can find it. - Storage. The EBS CSI driver is installed from its upstream chart at a pinned version, after the cloud controller (it needs the provider ID and topology labels the controller sets). The default StorageClass provisions encrypted gp3 volumes.
- Credentials without keys. Neither the cloud controller nor the CSI driver holds AWS keys: each uses the instance profile of the node it runs on. The control-plane and worker profiles are distinct, following the upstream policy split, and the platform credential you gave Ankra is never copied into the cluster.
- Instance metadata. Every instance requires IMDSv2. Workers and the bastion use a hop limit of 1, so a pod on the pod network cannot reach the instance role and a workload does not inherit the node’s AWS permissions by accident. Control-plane instances use a hop limit of 3, because the upstream EBS CSI chart cannot run its controller on the host network and that controller needs the control-plane role. The compensating control is the IMDS guard shipped with the AWS cloud-provider stack: a cluster-wide network policy, rendered for Cilium and Calico, that denies pod egress to
169.254.169.254except from the EBS CSI controller. On a flannel cluster there is no policy engine to enforce it - the create result reportsimds_guard: unavailable, and a pod scheduled onto a control-plane node can reach the control-plane instance role. Prefer Cilium (the default) or keep the control planes tainted. A workload that needs AWS access brings its own credentials either way.
Troubleshooting
Common Issues
Cloud controller cluster ID and node names
The AWS cloud controller manager identifies a cluster’s instances by thekubernetes.io/cluster/<id>=owned tag and matches each Kubernetes node to an instance by name. Two things must therefore hold on every node, and Ankra sets both:
- The instance carries the
kubernetes.io/cluster/<id>tag, where<id>is the cluster ID the controller was configured with. Removing or editing that tag in the console makes the controller treat the instance as foreign; the node keeps theuninitializedtaint (or, on a running cluster, is removed from Kubernetes) until the tag is restored. - The node name equals the instance’s EC2 private DNS name, as reported by
DescribeInstances. k3s is started with--node-nameset to that value, so the name does not depend on what the OS derives from the VPC’s DHCP options set. A VPC whose DHCP options carry a customdomain-nameproduces hostnames such asip-10-0-1-23.corp.example, while EC2 reportsip-10-0-1-23.eu-north-1.compute.internal; the controller resolves the latter, which is why Ankra pins the node name rather than the hostname. Changing the DHCP options set after creation does not affect existing or new nodes.
Instance metadata (IMDS)
Every instance is launched with IMDSv2 required, so tools that use IMDSv1 (curl http://169.254.169.254/latest/... without a token) fail on the nodes. Workers and the bastion have a hop limit of 1, so a pod on the pod network fails on either version - that is the intended isolation of the instance role. Control-plane instances have a hop limit of 2 so the EBS CSI controller, which the upstream chart cannot run on the host network, can use the control-plane role; the IMDS guard - a cluster-wide network policy in the AWS cloud-provider stack, rendered for Cilium and Calico - denies every other pod’s egress to 169.254.169.254.
The guard needs a policy engine. On a flannel cluster the create result reports imds_guard: unavailable, and any pod scheduled onto a control-plane node can read the control-plane instance role’s credentials. Prefer Cilium (the default) or Calico, or keep the control planes tainted so no workload lands there. If a workload needs AWS credentials, give it its own (a secret, or a role it assumes with keys you manage); do not raise the hop limits or switch IMDSv1 back on, and note that ModifyInstanceMetadataOptions is one of the calls Ankra’s credential is allowed to make on its own instances, so Ankra restores the hardening on reconciliation.
AWS service quotas
Preflight checks the quotas that most often block a create, but a quota can still be consumed between preflight and create, and the per-cluster quota report (GET /api/v1/clusters/aws/{cluster_id}/quotas) answers 404 (Quota reporting is not available for aws clusters) until it ships. If provisioning fails with a quota error, check in the Service Quotas console for the region:
- Running On-Demand instances (vCPU) for the instance family
- VPCs per region (
L-F678F1CE) - a created network uses one - NAT gateways per availability zone (
L-FE5A380F) -nat_gatewayegress uses one per zone, or one in total withnat_gateway_single_zone - Elastic IPs per region - one for the bastion plus one per NAT gateway
- Security groups per network interface, and rules per security group
- Network Load Balancers per region