Add EC2 instances to the mesh
Onboard workloads that run on Amazon EC2 instances to your ambient mesh.
About
As you build your ambient mesh, you might want to add a workload that runs on an Amazon EC2 instance to your cluster environment. For example, you might run an app or service on EC2 that must communicate with services in the Istio ambient mesh of your Kubernetes cluster. Additionally, you might need to secure the connections between EC2 and in-cluster services with mTLS, and centrally manage routing and policies by using Istio APIs.
In the Solo distribution of Istio version 1.30 and later, you can extend the mesh to include workloads running on EC2 instances by leveraging the istioctl ec2 add-workload command. This command bootstraps the connection from the EC2 instance to istiod, and uses the EC2 instance’s IAM instance profile and the EC2 Instance Metadata Service (IMDS) for authentication. The EC2 workloads can then communicate with in-mesh services in your Kubernetes cluster as well as securely communicate over mTLS with other mesh workloads.
EC2 discovery is opt-in. To allow istiod to discover an EC2 instance, you tag the instance with ec2.solo.io/* metadata tags. The discovery controller in istiod auto-discovers tagged EC2 instances and automatically creates the WorkloadEntry, Service, and gateway WorkloadEntry resources to represent the instances in the mesh.
The following diagram shows the EKS and EC2 architecture, in which EKS services and EC2 workloads can communicate through mTLS-secured connections in your ambient mesh. The steps in this guide enable bi-directional communication, both from and to your EC2 workloads.
Version and license requirements
This feature requires your mesh to be installed with the Solo distribution of Istio and an Enterprise-level license for Solo Enterprise for Istio. Contact your account representative to obtain a valid license.
EC2 integration into an ambient mesh is an alpha feature in the Solo distribution of Istio 1.30 and later. Alpha features are likely to change, are not fully tested, and are not supported for production. For more information, see Solo feature maturity.
Platform considerations
The east-west gateway of your ambient mesh must be accessible to all EC2 instances that are integrated into the mesh. The easiest way to accomplish this setup is to run your ambient mesh in an Amazon Elastic Kubernetes Service (EKS) cluster and run EC2 instances in the same Virtual Private Cloud (VPC) as the EKS nodes. However, the cluster can be hosted anywhere as long as the east-west gateway is accessible to the EC2 instances.
Multi-region support
In Solo Enterprise for Istio version 1.30 and later, istiod supports discovering and managing EC2 instances across multiple AWS accounts and regions. You can configure specific regions for each AWS account, allowing your ambient mesh to span EC2 instances in different regions. If no regions are specified for an account, discovery operates on the default region of the configured AWS role.
Multi-workload instances
EC2 integration supports multi-workload instances, in which a single EC2 instance runs multiple distinct workloads. Each workload runs with its own identity. The ztunnel on the EC2 instance runs as a shared proxy with a SOCKS5 handler to route outbound traffic per workload identity.
Step 1: Set up tools
Install the following CLI tools.
aws, the AWS command line tool.eksctl, the CLI tool for creating and managing EKS clusters.kubectl, the Kubernetes command line tool. Download thekubectlversion that is within one minor version of the EKS cluster you plan to use.helm, the Kubernetes package manager.jq, a command-line JSON processor.envsubst, a command-line tool for substituting environment variables in files. This tool is typically available via thegettextpackage, such as throughbrew install gettexton macOS, orapt-get install gettext-baseon Ubuntu.
Clone the
doc-examplesrepository, which contains scripts and manifests for this guide, and navigate to theec2directory.git clone https://github.com/solo-io/doc-examples.git cd doc-examples/istio/ec2Get the Solo distribution of Istio binary and install
istioctl, which you use for theec2 add-workloadcommand in this guide.Save the Solo distribution of Istio version and Helm repository location.
export ISTIO_IMAGE=1.30.3-solo export HELM_REPO=us-docker.pkg.dev/soloio-img/istio-helmDownload the Solo distribution of Istio binary and install
istioctl. This script automatically detects your OS and architecture, downloads the appropriate Solo distribution of Istio binary, and verifies the installation.scripts/build/install-istioctl.sh export PATH=${HOME}/.istioctl/bin:${PATH}
Step 2: Set up the EKS cluster with Istio
Create IAM roles for Istio. Then, either create a new EKS cluster and deploy an Istio ambient mesh to the cluster, or update an existing EKS cluster where a Solo distribution of Istio ambient mesh runs. The ambient mesh setup must include an east-west gateway to facilitate traffic between the EKS cluster and EC2 instances.
Save the following details in environment variables.
AWS_ACCOUNT: The ID of the AWS account that you want to use.AWS_REGION: The AWS region for your EKS cluster. You can configure the regions for EC2 instance discovery later on.CLUSTER_NAME: A name for the EKS cluster. If you already created a cluster where you installed the Solo distribution of Istio ambient mesh, use that cluster name. The cluster name must start with a letter or number and can contain only letters, numbers, hyphens, and underscores.
export AWS_ACCOUNT=<ID> export AWS_REGION=us-east-1 export CLUSTER_NAME=demoCreate IAM permissions for istiod to read from the EC2 API, which allow istiod to perform automatic discovery of EC2 instances.
The following script checks if the required IAM roles and policies already exist in your AWS account. If they do not exist, the script creates the following resources that are automatically associated with the
istio-system/istiodKubernetes service account when you create the EKS cluster:istiodIAM role with permissions to read from the EC2 API to be assumed by EKS podsistiod-ec2IAM role that provides read-only access to the EC2 APIec2-read-onlyIAM policy attached to theistiod-ec2roleistiod-${AWS_ACCOUNT}IAM policy that allows theistiodrole to assume theistiod-ec2role
scripts/build/iam-istiod.shExample output:
Starting creation of Istiod IAM roles and policies... Creating IAM role 'istiod'... Successfully created istiod role. Waiting for IAM role to propagate... Creating IAM role 'istiod-ec2'... Successfully created istiod-ec2 role. Creating IAM policy 'ec2-read-only'... Successfully created ec2-read-only policy. Successfully attached ec2-read-only policy to istiod-ec2 role. Creating IAM policy 'istiod-123456789012'... Successfully created istiod-123456789012 policy. Successfully attached istiod-123456789012 policy to istiod role. Istiod IAM setup completed successfully. Created resources: - IAM Role: istiod - IAM Role: istiod-ec2 - IAM Policy: ec2-read-only (attached to istiod-ec2) - IAM Policy: istiod-123456789012 (attached to istiod)The
istiod-ec2role requires only theec2:DescribeInstancespermission. This guide requires that you create the EKS cluster and EC2 instances in the same account. For cross-account setups where EC2 instances run in a different AWS account than the EKS cluster, theistiodrole also requiressts:AssumeRolepermission to assume theistiod-ec2role in each remote account. Thests:AssumeRolepermission is not needed when the EKS cluster and EC2 instances are in the same account.Create or update an existing EKS cluster with a pod identity association that links the
istio-system/istiodKubernetes service account with the istiod IAM role. If you follow the steps to create an EKS cluster, you also deploy an ambient mesh into the cluster.Create an EKS cluster, deploy the Solo distribution of Istio ambient mesh, and create an east-west gateway.
Set configuration details for the EKS cluster. Update the values as needed. For more information, see the EKS documentation.
OWNER_NAME: The name of the cluster owner.EKS_VERSION: The EKS version for the cluster to run.NUMBER_NODES: The number of nodes.NODE_TYPE: The instance type for the nodes.
export OWNER_NAME=$(whoami) export EKS_VERSION=1.33 export NUMBER_NODES=2 export NODE_TYPE="t2.medium"Set your Enterprise-level license for Solo Enterprise for Istio as an environment variable. If you do not have one, contact an account representative. If you prefer to specify license keys in a secret instead of an environment variable, see Licensing.
export SOLO_ISTIO_LICENSE_KEY=<enterprise_license_key>Set the Solo distribution of Istio version and repository details.
export ISTIO_IMAGE=1.30.3-soloCreate an EKS cluster. This configuration includes a pod identity association that links the
istio-system/istiodKubernetes service account with the IAM role you created earlier. To review the configuration before applying it, open themanifests/eks-cluster.yamlfile.envsubst < manifests/eks-cluster.yaml | eksctl create cluster --config-file -Apply the CRDs for the Kubernetes Gateway API to your cluster, which are required to create components such as waypoint proxies for L7 traffic policies, gateways with the
Gatewayresource, and more.kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.5.0/standard-install.yamlInstall the Istio ambient mesh components in your EKS cluster. This script performs the following actions:
- Installs the following Istio Helm charts:
- The Istio base chart with CRDs and cluster roles required for the Istio control plane
- The istiod control plane chart, configured for EC2 integration
- The Istio CNI node agent daemonset
- The ztunnel chart that deploys the ztunnel daemonset
- Waits for each component to be ready before proceeding.
- Labels the
istio-systemnamespace with the cluster network name, which is set to your EKS cluster’s name in theglobal.networkfield of the istiod installation. The ambient mesh uses this label for multi-network routing. - Verifies all deployments and provides detailed progress output.
To review the Helm values for each component before installing, open the files in the
manifests/directory.scripts/build/istio-ambient.shExample output:
Detected Istio 1.29 or later, using updated repository locations. ========================================= Installing Istio ambient mesh components ========================================= Cluster: demo Istio Version: 1.30.3-solo Helm Repository: us-docker.pkg.dev/soloio-img/istio-helm Step 1/4: Installing istio-base chart... ✓ istio-base installed successfully Step 2/4: Installing istiod control plane... ✓ istiod installed successfully Waiting for istiod to be ready... ✓ istiod is ready Step 3/4: Installing Istio CNI node agent daemonset... ✓ istio-cni installed successfully Waiting for CNI daemonset to be ready... ✓ istio-cni is ready Step 4/4: Installing ztunnel daemonset... ✓ ztunnel installed successfully Waiting for ztunnel daemonset to be ready... ✓ ztunnel is ready ========================================= Verifying ambient mesh components ========================================= NAME READY STATUS RESTARTS AGE istio-cni-node-pr5rl 1/1 Running 0 45s istio-cni-node-pvmx2 1/1 Running 0 45s istiod-85c4dfd97f-mncj5 1/1 Running 0 90s ztunnel-vtpjm 1/1 Running 0 30s ztunnel-hllxg 1/1 Running 0 30s ========================================= Labeling istio-system namespace ========================================= Labeling istio-system namespace with network: demo... ✓ Namespace labeled successfully ========================================= Installation complete! ========================================= Istio ambient mesh has been successfully installed and configured. Next steps: Tag EC2 instances with ec2.solo.io/* labels to enable discovery.- Installs the following Istio Helm charts:
Create an east-west gateway in the
istio-eastwestnamespace. The east-west gateway facilitates traffic between services in the EKS cluster and EC2 instances.- Solo distribution of
istioctl: For more information about this command, see the CLI reference.kubectl create namespace istio-eastwest istioctl multicluster expose --namespace istio-eastwest --generate > ew-gateway.yaml kubectl apply -f ew-gateway.yaml - Helm: For more information, see the Helm values reference. For recommendations on customizing the east-west gateway for resiliency and availability with the Helm chart, see the best practices for multicluster peering.
helm upgrade -i peering-eastwest oci://${HELM_REPO}/peering \ --version ${ISTIO_IMAGE} \ --namespace istio-eastwest \ --create-namespace \ -f - <<EOF eastwest: create: true cluster: ${CLUSTER_NAME} # The network that the istio-system namespace is labeled with. # In prod environments, network and cluster are likely not the same value. network: ${CLUSTER_NAME} deployment: {} EOF
- Solo distribution of
Verify that the east-west gateway is successfully deployed.
kubectl get pods -n istio-eastwestExample output:
NAME READY STATUS RESTARTS AGE istio-eastwest-5d4f757664-6hw7b 1/1 Running 0 9s
If you already installed the Solo distribution of Istio ambient mesh in an existing EKS cluster, update that cluster with the pod identity association that links the
istio-system/istiodKubernetes service account with the istiod IAM role.Install the EKS Pod Identity Agent addon, which is required for pod identity associations to inject AWS credentials into pods. If the addon is already installed, this command returns an error that you can ignore.
aws eks create-addon \ --cluster-name ${CLUSTER_NAME} \ --addon-name eks-pod-identity-agent \ --region ${AWS_REGION} aws eks wait addon-active \ --cluster-name ${CLUSTER_NAME} \ --addon-name eks-pod-identity-agent \ --region ${AWS_REGION}Add a pod identity association to link the
istio-system/istiodservice account with theistiodIAM role. This association allows istiod to assume the necessary IAM permissions for EC2 discovery. The following script checks if the pod identity association already exists. If it does not exist, the script creates it.scripts/build/add-pod-identity.shExample output:
========================================= Adding pod identity association for istiod ========================================= Cluster: demo Namespace: istio-system Service Account: istiod IAM Role: arn:aws:iam::123456789012:role/istiod Checking for existing pod identity association... Creating new pod identity association... ✓ Pod identity association created successfully (ID: a-abcdef1234567890) ========================================= Pod identity association setup complete! ========================================= The istio-system/istiod service account can now assume the arn:aws:iam::123456789012:role/istiod role. This enables istiod to discover EC2 instances.Restart istiod so that the pod identity agent injects the AWS credential environment variables into the new pod.
kubectl rollout restart deployment/istiod -n istio-system kubectl rollout status deployment/istiod -n istio-systemVerify that AWS credentials were injected into the istiod pod. The
AWS_CONTAINER_CREDENTIALS_FULL_URIenvironment variable must be present for istiod to authenticate with AWS and perform EC2 discovery.kubectl exec -n istio-system deployment/istiod -- env | grep AWS_CONTAINER_CREDENTIALS_FULL_URIIf the environment variable is present, the pod identity agent is injecting credentials correctly. If it is absent, verify that the
eks-pod-identity-agentaddon is installed and all of its pods are running.aws eks describe-addon \ --cluster-name ${CLUSTER_NAME} \ --addon-name eks-pod-identity-agent \ --region ${AWS_REGION} \ --query 'addon.status' kubectl get pods -n kube-system -l app.kubernetes.io/name=eks-pod-identity-agentUpdate your istiod Helm installation to enable EC2 instance discovery. The
--reuse-valuesflag preserves your existing Helm values and applies only the EC2 platform configuration.helm upgrade istiod oci://${HELM_REPO}/istiod \ -n istio-system \ --reuse-values \ --version ${ISTIO_IMAGE} \ -f - <<EOF platforms: aws: ec2Discovery: true accounts: - role: arn:aws:iam::${AWS_ACCOUNT}:role/istiod-ec2 EOFBefore continuing to the next section, verify that an east-west gateway exists in the cluster.
kubectl get pods -n istio-eastwestIf no east-west gateway exists, use the following commands to create one in the
istio-eastwestnamespace. The east-west gateway facilitates traffic between services in the EKS cluster and EC2 instances.- Solo distribution of
istioctl: For more information about this command, see the CLI reference.kubectl create namespace istio-eastwest istioctl multicluster expose --namespace istio-eastwest --generate > ew-gateway.yaml kubectl apply -f ew-gateway.yaml - Helm: For more information, see the Helm values reference. For recommendations on customizing the east-west gateway for resiliency and availability with the Helm chart, see the best practices for multicluster peering.
helm upgrade -i peering-eastwest oci://${HELM_REPO}/peering \ --version ${ISTIO_IMAGE} \ --namespace istio-eastwest \ --create-namespace \ -f - <<EOF eastwest: create: true cluster: ${CLUSTER_NAME} # The network that the istio-system namespace is labeled with. # In prod environments, network and cluster are likely not the same value. network: ${CLUSTER_NAME} deployment: {} EOF
- Solo distribution of
Step 3: Set up the EC2 instance
On each EC2 instance that you want to add to the mesh, ensure that the security group allows HBONE traffic, that an IAM instance profile is attached, and that the ztunnel binary is installed.
Save the EC2 instance ID for the instance you want to add to the mesh.
export EC2_INSTANCE_ID=<instance-id>Configure the security group rules required for HBONE traffic (port 15008) in both directions. The following script checks whether the rules already exist and adds them if not:
- EC2 instance security group: Allows inbound port 15008 from the VPC CIDR so that EKS ztunnel can send encrypted HBONE traffic to the EC2 instance.
- EKS node security group: Allows inbound port 15008 from the VPC CIDR so that the EC2 ztunnel can send encrypted HBONE traffic to EKS pods.For this demo, the security group settings are relaxed to simplify testing and ensure connectivity between EC2 and EKS workloads. In a production environment, be sure to configure security group settings to restrict access based on specific CIDR ranges, protocols, and ports. Limiting access helps to maintain security and prevent unwanted traffic between EC2 and EKS environments.
scripts/build/security-groups.shExample output:
========================================= Configuring security groups for EC2 ambient mesh ========================================= Cluster: demo Instance: i-0abc1234567890def Region: us-east-1 VPC CIDR: 10.0.0.0/16 EC2 SG: sg-0abc1234567890def EKS node SG: sg-0def1234567890abc Adding port 15008 inbound rule to sg-0abc1234567890def (EC2 instance, allows EKS-to-EC2 HBONE)... ✓ Added port 15008 inbound rule to sg-0abc1234567890def (EC2 instance, allows EKS-to-EC2 HBONE) Adding port 15008 inbound rule to sg-0def1234567890abc (EKS nodes, allows EC2-to-EKS HBONE)... ✓ Added port 15008 inbound rule to sg-0def1234567890abc (EKS nodes, allows EC2-to-EKS HBONE) ========================================= Security group configuration complete! =========================================Ensure that the EC2 instance has an IAM instance profile attached. The
istioctl ec2 add-workloadcommand automatically discovers the IAM role ARN from the instance profile, and ztunnel uses the instance profile for credential retrieval.Check whether an instance profile is already attached.
aws ec2 describe-instances \ --instance-ids ${EC2_INSTANCE_ID} \ --query 'Reservations[0].Instances[0].IamInstanceProfile'If a profile ARN is returned, continue to the next step. If the output returns
null, no profile is attached and you must complete the following sub-steps.- Create an instance profile and associate the IAM role that represents the workload.
aws iam create-instance-profile --instance-profile-name <profile-name> aws iam add-role-to-instance-profile \ --instance-profile-name <profile-name> \ --role-name <role-name> - Attach the instance profile to the EC2 instance.
aws ec2 associate-iam-instance-profile \ --instance-id ${EC2_INSTANCE_ID} \ --iam-instance-profile Name=<profile-name>
- Create an instance profile and associate the IAM role that represents the workload.
Start an application on the EC2 instance. For this guide, use Docker to run the Istio demo app on the ports that you configure in later steps.
SSH into the EC2 instance.
Set the workload name. You use this name as the Docker container name and later as the workload identity in the mesh.
export EC2_WORKLOAD_NAME=web-apiRun the demo app.
docker run -d -p 8080:8080 --name ${EC2_WORKLOAD_NAME} gcr.io/istio-testing/appVerify that the application is running and responding.
curl localhost:8080Example output:
ServiceVersion= ServicePort=8080 Host=localhost:8080 URL=/ Method=GET Proto=HTTP/1.1 IP=172.17.0.1 RequestHeader=Accept:*/* RequestHeader=User-Agent:curl/8.17.0 Hostname=<container-id>
Step 4: Tag EC2 instances and add them to the ambient mesh
Add each EC2 workload to the ambient mesh.
Configure EC2 workloads
In your cluster, create the namespace for your EC2 workloads and label it for ambient mesh inclusion.
kubectl create namespace ec2-workloads kubectl label namespace ec2-workloads istio.io/dataplane-mode=ambientSave the name of the workload you run on the EC2 instance.
export EC2_WORKLOAD_NAME=web-apiAdd each EC2 workload to the ambient mesh. You can use the
istioctl ec2 add-workloadcommand, which automates the tagging, Kubernetes resource creation, and bootstrap token generation. Alternatively, you can perform these steps manually if you prefer to use IaC tools like Terraform or do not haveistioctlavailable.Run the
istioctl ec2 add-workloadcommand for each workload on each EC2 instance. This command applies theec2.solo.io/discovery-enabledand workload metadata tags to the EC2 instance, which triggers istiod to discover it; creates the Kubernetes resources for the workload identity, including the namespace if it does not already exist; and generates the bootstrap token to start ztunnel on the instance. If a single instance runs multiple workloads, see Multi-workload instances. For more information about this command, see the CLI reference.- If your EC2 instance is in a different VPC or network than the EKS cluster, add the
--externalflag so that traffic is routed through the east-west gateway, and add--use-public-ipif there is no VPC peering between the networks. - If your EC2 instance is in a different AWS region than the EKS cluster, set
AWS_DEFAULT_REGIONto the EC2 instance’s region before running the command. The command uses the AWS SDK default region to call the EC2 API, and does not have a--regionflag.
istioctl ec2 add-workload ${EC2_WORKLOAD_NAME} \ --instance ${EC2_INSTANCE_ID} \ --namespace ec2-workloads \ --ports http:80:8080Review the following flags that you can specify in the
istioctl ec2 add-workloadcommand.Option Required? Default Description <workload-name>✅ The name of the workload running on the EC2 instance. Used as the workload identity and as the default Kubernetes ServiceAccount name. --instance✅ The EC2 instance ID (for example, i-0abc1234567890def).--namespaceistio-systemThe Kubernetes namespace to associate the workload with. The ServiceAccount and WorkloadEntry are created in this namespace. --service-accountworkload name The Kubernetes ServiceAccount for the workload identity. Defaults to the workload name. --portshttp:80Port configuration as a /-separated list ofprotocol:port[:targetPort]pairs, such ashttp:80:8080/grpc:9090.--use-public-ipfalseUse the instance’s public IP for routing instead of the private IP. --externalfalseMark the workload as external to the cluster network, so that traffic is routed through the east-west gateway. Use this when the EC2 instance is in a different VPC or network than the EKS cluster. --role-arnauto-discovered IAM role ARN to associate with the workload. Auto-discovered from the EC2 instance profile if not specified. --bootstrapfalseForce re-generation of the bootstrap token, even if the instance already has discovery tags applied. --profileAWS CLI profile to use. Defaults to the default AWS CLI profile. Example output:
• Found EC2 instance i-0abc1234567890def (AZ: us-east-1f) • Using IAM role arn:aws:iam::123456789012:role/istio-ec2-role • Tagged instance i-0abc1234567890def with workload "web-api" • Configured service account ec2-workloads/web-api with role annotation • Configured gateway service account ec2-workloads/ec2-i-0abc1234567890def-gateway • Tagged instance i-0abc1234567890def with gateway config • Generating a bootstrap token for ec2-workloads/ec2-i-0abc1234567890def-gateway... • Fetched Istiod Root Cert • Fetched Istio network (my-cluster) • Fetching Istiod URL... • Service "istio-eastwest" provides Istiod access on port 15012 • Fetching Istiod URL (https://a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4-1234567890.us-east-1.elb.amazonaws.com:15012) • Workload is authorized to run as role "arn:aws:iam::123456789012:role/istio-ec2-role" • Start ztunnel on the VM with: BOOTSTRAP_TOKEN=<generated_token> ztunnelThe following steps can be repeated for any EC2 workload to include it in the mesh. You can automate these steps with IaC tools like Terraform.
Tag the EC2 instance for discovery by istiod. The
ec2.solo.io/discovery-enabledtag opts the instance in for automatic discovery. Workload metadata tags use the formatec2.solo.io/workload/<name>for the workload name andec2.solo.io/workload/<name>/<property>for each property.aws ec2 create-tags \ --resources ${EC2_INSTANCE_ID} \ --tags \ Key=ec2.solo.io/discovery-enabled,Value=true \ Key=ec2.solo.io/workload/${EC2_WORKLOAD_NAME},Value=true \ Key=ec2.solo.io/workload/${EC2_WORKLOAD_NAME}/namespace,Value=ec2-workloads \ Key=ec2.solo.io/workload/${EC2_WORKLOAD_NAME}/ports,Value="http:80:8080" \ Key=ec2.solo.io/workload/${EC2_WORKLOAD_NAME}/service-account,Value=${EC2_WORKLOAD_NAME}Get the IAM role ARN from the instance profile attached to the EC2 instance.
INSTANCE_PROFILE_ARN=$(aws ec2 describe-instances \ --instance-ids ${EC2_INSTANCE_ID} \ --query 'Reservations[0].Instances[0].IamInstanceProfile.Arn' \ --output text) INSTANCE_PROFILE_NAME=$(echo ${INSTANCE_PROFILE_ARN} | awk -F'/' '{print $NF}') ROLE_ARN=$(aws iam get-instance-profile \ --instance-profile-name ${INSTANCE_PROFILE_NAME} \ --query 'InstanceProfile.Roles[0].Arn' \ --output text)Create the Kubernetes ServiceAccount for the workload and annotate it with the IAM role ARN.
kubectl create serviceaccount ${EC2_WORKLOAD_NAME} -n ec2-workloads kubectl annotate serviceaccount ${EC2_WORKLOAD_NAME} -n ec2-workloads \ ec2.solo.io/role-arn=${ROLE_ARN}Create a Kubernetes ServiceAccount for the gateway and annotate it with the IAM role ARN.
kubectl create serviceaccount ec2-${EC2_INSTANCE_ID}-gateway -n ec2-workloads kubectl annotate serviceaccount ec2-${EC2_INSTANCE_ID}-gateway -n ec2-workloads \ ec2.solo.io/role-arn=${ROLE_ARN}Create a bootstrap token for the EC2 workload. The bootstrap token contains the information ztunnel needs to connect to istiod.
ISTIOD_EXPOSE_HOSTNAME=https://$(kubectl get service -n istio-eastwest istio-eastwest -o jsonpath='{.status.loadBalancer.ingress[0].hostname}') ISTIOD_EXPOSE_PORT=$(kubectl get service -n istio-eastwest istio-eastwest -o jsonpath='{.metadata.labels.istio\.io\/expose-istiod}') # Adjust this command when using a custom root cert ISTIOD_CA_ROOT_CERT=$(kubectl get configmap -n istio-system istio-ca-root-cert -o json | jq '.data."root-cert.pem"') NAMESPACE="ec2-workloads" SERVICE_ACCOUNT="ec2-${EC2_INSTANCE_ID}-gateway" NETWORK=$(kubectl get namespace istio-system -o jsonpath='{.metadata.labels.topology\.istio\.io\/network}') # Set to true when the EC2 instance is in a different VPC or network than the EKS cluster # (equivalent to the --external flag in istioctl ec2 add-workload) REMOTE=false BOOTSTRAP_TOKEN=$(echo -n "{\"url\":\"${ISTIOD_EXPOSE_HOSTNAME}:${ISTIOD_EXPOSE_PORT}\",\"caCert\":${ISTIOD_CA_ROOT_CERT},\"namespace\":\"${NAMESPACE}\",\"serviceAccount\":\"${SERVICE_ACCOUNT}\",\"platform\":\"ec2\",\"network\":\"${NETWORK}\",\"remote\":${REMOTE}}" | base64 | base64)The bootstrap token JSON contains the following fields:
Field Description urlThe east-west gateway URL that ztunnel uses to reach istiod. caCertThe istiod root CA certificate for TLS verification. namespaceThe Kubernetes namespace for the workload identity. serviceAccountThe Kubernetes ServiceAccount (gateway SA) for the workload. platformMust be ec2for EC2 workloads.networkThe Istio network name, matching the istio-systemnamespace label.remoteSet to trueto route traffic through the east-west gateway (equivalent to the--externalflag inistioctl). Set tofalsewhen the EC2 instance is in the same VPC as the EKS cluster.Repeat these steps for each workload on each EC2 instance. If a single instance runs multiple workloads, see Multi-workload instances.
- If your EC2 instance is in a different VPC or network than the EKS cluster, add the
Start ztunnel and verify
SSH into the EC2 instance and save the
BOOTSTRAP_TOKENvalue from the previous step’s command output as an environment variable.export BOOTSTRAP_TOKEN=<generated_token>On the EC2 instance, start ztunnel with the bootstrap token.
docker run -d \ --name ztunnel \ --network host \ -e BOOTSTRAP_TOKEN="${BOOTSTRAP_TOKEN}" \ us-docker.pkg.dev/soloio-img/istio/ztunnel:1.30.3-solo \ proxyIn your cluster, verify that istiod automatically discovered the EC2 instance and created the mesh resources.
kubectl get workloadentry -n ec2-workloads kubectl get service -n ec2-workloadsExample output, including the
web-apiworkload:NAME AGE ADDRESS ec2-<instance-id>-gateway 7m12s 10.0.77.33 ec2-<instance-id>-web-api 9m38s 127.0.0.1 NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE web-api ClusterIP 172.20.226.38 <none> 80/TCP 9m39sVerify that the ztunnel workload is registered and using HBONE for mTLS-secured communication.
istioctl ztunnel-config workloads | grep ec2-workloadsExample output: The
PROTOCOLvalue ofHBONEconfirms that the workload can communicate with other services in the mesh using mTLS.ec2-workloads ec2-<instance-id>-gateway 10.0.1.42 <instance-id> None HBONE cluster1 istio-eastwest/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1111111111.us-east-1.elb.amazonaws.com ec2-workloads ec2-<instance-id>-web-api 127.0.0.1 <instance-id> None HBONE cluster1 cluster1/10.0.1.42
Step 5: Test connectivity
Verify that all traffic between EC2 and EKS workloads is secured with mTLS, ensuring that it is encrypted, verified, and routed through the Istio ztunnel.
EKS to EC2
After the EC2 workload joins the mesh, applications in the EKS ambient mesh can reach it by calling its Kubernetes service hostname, ${EC2_WORKLOAD_NAME}.ec2-workloads. The EC2 discovery controller automatically creates a Kubernetes Service for each discovered workload in the workload namespace. All traffic is encrypted with mTLS to the ztunnel binary on the EC2 instance, which then proxies requests to the application. Test this by making HTTP requests from an example EKS shell pod to the EC2 workload.
Label the default namespace in the EKS cluster for ambient mesh inclusion.
kubectl label namespace default istio.io/dataplane-mode=ambientDeploy shell and echo applications to the default namespace.
kubectl apply -f manifests/eks-echo.yaml kubectl apply -f manifests/eks-shell.yamlSend a curl request from the EKS shell pod to the EC2 workload. Because you use the EC2 workload’s Kubernetes service hostname, the request is routed through the mesh.
kubectl exec deploy/eks-shell -- curl -s ${EC2_WORKLOAD_NAME}.ec2-workloads:80Example output:
ServiceVersion= ServicePort=8080 Host=web-api.ec2-workloads URL=/ Method=GET Proto=HTTP/1.1 IP=172.17.0.1 RequestHeader=Accept:*/* RequestHeader=User-Agent:curl/8.7.1 Hostname=08b73388c4cb
EC2 to EKS
Test connectivity from the EC2 instance to the EKS echo service. Traffic from the EC2 instance is routed through ztunnel’s SOCKS5 proxy on port 15080. The SOCKS5 username selects which workload identity ztunnel uses for the outbound connection.
SSH into the EC2 instance.
Send a request to the EKS echo service using the SOCKS5 proxy, specifying the workload identity as the username.
ALL_PROXY=socks5h://${EC2_WORKLOAD_NAME}.ec2-workloads:pass@127.0.0.1:15080 curl eks-echo.default:8080Example output:
ServiceVersion= ServicePort=8080 Host=eks-echo.default:8080 URL=/ Method=GET Proto=HTTP/1.1 IP=10.0.1.42 RequestHeader=Accept:*/* RequestHeader=User-Agent:curl/8.17.0 Hostname=eks-echo-a1b2c3d4-x5y6z
Optional: EC2 to EC2
Test connectivity between two workloads on the same EC2 instance. Even though both workloads share an instance, traffic between them flows through the mesh and is protected by mTLS. Each workload uses its own SPIFFE identity, selected via the SOCKS5 username.
To run this test, add a second workload to the same EC2 instance by following the Multi-workload instances steps. The following example assumes two workloads, web-api and grpc-backend, both running on the same instance in the ec2-workloads namespace.
SSH into the EC2 instance.
Send a request from
web-apitogrpc-backend.ALL_PROXY=socks5h://web-api.ec2-workloads:pass@127.0.0.1:15080 curl grpc-backend.ec2-workloads:9090Send a request in the opposite direction, from
grpc-backendtoweb-api.ALL_PROXY=socks5h://grpc-backend.ec2-workloads:pass@127.0.0.1:15080 curl web-api.ec2-workloads:80Successful responses in both directions confirm that per-workload identity selection is working and that each workload’s traffic is independently secured by mTLS.
Step 6: Control access with authorization policies
Test access control by applying Layer 4 authorization policies to both EKS and EC2 workloads in the ambient mesh.
EC2 workloads use their Kubernetes ServiceAccount as their SPIFFE identity (spiffe://cluster.local/ns/<namespace>/sa/<workload-name>), the same as in-cluster pods.
L4 authorization for EKS workloads
Apply a deny-all L4 authorization policy to block all traffic to the EKS echo service.
kubectl apply -f manifests/eks-l4-deny.yamlSSH into the EC2 instance and run the following command to test that EC2 to EKS communication is now blocked by the L4 policy.
ALL_PROXY=socks5h://${EC2_WORKLOAD_NAME}.ec2-workloads:pass@127.0.0.1:15080 curl eks-echo.default:8080The request fails with a connection error, indicating that the L4 policy is blocking the traffic.
curl: (56) Recv failure: Connection reset by peerVerify that EKS to EC2 communication still succeeds, because no policy is blocking it.
kubectl exec deploy/eks-shell -- curl -s ${EC2_WORKLOAD_NAME}.ec2-workloads:80Example output:
ServiceVersion= ServicePort=8080 Host=web-api.ec2-workloads URL=/ Method=GET Proto=HTTP/1.1 IP=172.17.0.1 RequestHeader=Accept:*/* RequestHeader=User-Agent:curl/8.20.0 Hostname=08b73388c4cbRemove the EKS authorization policy to restore traffic flow.
kubectl delete -n default authorizationpolicies eks-echo-deny
L4 authorization for EC2 workloads
Apply a deny-all L4 authorization policy to the
ec2-workloadsnamespace to block all traffic to EC2 workloads.kubectl apply -f manifests/ec2-l4-deny.yamlSSH into the EC2 instance and run the following command to verify that EC2 to EKS communication is now allowed, since no L4 policy applies to EKS workloads.
ALL_PROXY=socks5h://${EC2_WORKLOAD_NAME}.ec2-workloads:pass@127.0.0.1:15080 curl eks-echo.default:8080Example output:
ServiceVersion= ServicePort=8080 Host=eks-echo.default:8080 URL=/ Method=GET Proto=HTTP/1.1 IP=10.0.77.33 RequestHeader=Accept:*/* RequestHeader=User-Agent:curl/8.17.0 Hostname=eks-echo-a1b2c3d4-x5y6zVerify that EKS to EC2 communication is now blocked by the L4 policy applied to EC2 workloads.
kubectl exec deploy/eks-shell -- curl -s ${EC2_WORKLOAD_NAME}.ec2-workloads:80The request fails with a connection error, indicating that the L4 policy is blocking traffic to the EC2 workload.
command terminated with exit code 56Clean up the authorization policy.
kubectl delete -n ec2-workloads authorizationpolicies ec2-deny
Per-workload authorization
For EC2 instances that run multiple workloads, you can write authorization policies that target a specific workload using the ec2.solo.io/workload label. Istiod automatically applies this label to each WorkloadEntry it creates during EC2 discovery, using the workload name as the value.
Apply a deny-all policy targeting only
${EC2_WORKLOAD_NAME}by using theec2.solo.io/workloadlabel selector. Other workloads on the same instance are unaffected.kubectl apply -f - <<EOF apiVersion: security.istio.io/v1 kind: AuthorizationPolicy metadata: name: deny-ec2-workload namespace: ec2-workloads spec: selector: matchLabels: ec2.solo.io/workload: ${EC2_WORKLOAD_NAME} action: DENY rules: - {} EOFVerify that traffic to
${EC2_WORKLOAD_NAME}is blocked but other workloads on the same instance remain reachable.kubectl exec deploy/eks-shell -- curl -s ${EC2_WORKLOAD_NAME}.ec2-workloads:80The request fails with a connection error, indicating that the policy is blocking traffic to
${EC2_WORKLOAD_NAME}.command terminated with exit code 56Clean up the policy.
kubectl delete -n ec2-workloads authorizationpolicies deny-ec2-workload
Further configuration
Using waypoints with EC2 workloads
To apply Layer 7 policies to EC2 workloads, deploy a waypoint proxy to the namespace where your EC2 WorkloadEntries and Services are created. EC2 workloads are represented as WorkloadEntries rather than native Kubernetes pods, so the waypoint must be scoped to all using the istio.io/waypoint-for: all label. Without this label, the default namespace scope only processes traffic to Kubernetes Services and does not include WorkloadEntries.
Deploy a waypoint in the EC2 workloads namespace with the
istio.io/waypoint-for: alllabel.kubectl apply -f - <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: waypoint namespace: ec2-workloads labels: istio.io/waypoint-for: all spec: gatewayClassName: istio-waypoint listeners: - name: mesh port: 15008 protocol: HBONE EOFLabel the namespace to use the waypoint.
kubectl label namespace ec2-workloads istio.io/use-waypoint=waypointApply your Layer 7 policies, such as HTTPRoute or AuthorizationPolicy resources, to the namespace. The waypoint proxy enforces these policies on all traffic to EC2 workloads in the namespace.
Updating workload configuration
After you integrate an EC2 workload, you might need to modify its routing and configuration. Workload properties use the tag format ec2.solo.io/workload/<name>/<property>. Istiod picks up tag changes automatically during its next discovery cycle.
| Tag | Default | Description |
|---|---|---|
ec2.solo.io/workload/<name>/ports | http:80 | Port configuration as a /-separated list of protocol:port[:targetPort] pairs, such as http:80:8080/grpc:9090. |
ec2.solo.io/workload/<name>/namespace | istio-system | The Kubernetes namespace to associate the workload with. The ServiceAccount and WorkloadEntry are created in this namespace. |
ec2.solo.io/workload/<name>/service-account | workload name | The Kubernetes ServiceAccount for the workload identity. |
ec2.solo.io/workload/<name>/hostname | <name>.<domain> | Override the hostname used as the WorkloadEntry selector. |
ec2.solo.io/use-public-ip | false | Use the instance’s public IP for routing instead of the private IP. |
Example to update the ports for an EC2 workload:
aws ec2 create-tags \
--resources ${EC2_INSTANCE_ID} \
--tags Key=ec2.solo.io/workload/${EC2_WORKLOAD_NAME}/ports,Value="http:80:8080/grpc:9090"AWS tag passthrough
In addition to the ec2.solo.io/* configuration tags, istiod passes through AWS resource tags as labels on the Kubernetes objects it creates during EC2 discovery. Only tags whose key contains one of the following domain prefixes are passed through:
*.istio.io/*.solo.io/*.kubernetes.io/
Both the tag key and value must be valid Kubernetes label key/value pairs.
Multi-account and multi-region EC2 discovery
You can configure istiod to discover EC2 instances across multiple AWS accounts and regions by adding entries to the platforms.aws.accounts list in your istiod configuration. Specify an array of regions for each account, or leave regions empty to use the account’s default region.
platforms:
aws:
ec2Discovery: true
accounts:
- domain: example1.ec2
role: arn:aws:iam::111111111111:role/istiod-ec2
regions: ["us-east-1", "us-west-2"]
- domain: example2.ec2
role: arn:aws:iam::222222222222:role/istiod-ec2
regions: ["eu-west-1"]When using multi-account discovery, ensure that the istiod role in your EKS account has permission to assume the istiod-ec2 role in each additional account.
Public and private IP addresses
By default, EC2 WorkloadEntries use the private IP address of the instance. If your EC2 instances are not in the same VPC as the EKS cluster or if VPC peering is not configured, you can configure EC2 WorkloadEntries to use the public IP address of the instance.
To use the public IP address, add the ec2.solo.io/use-public-ip=true tag to the EC2 instance.
aws ec2 create-tags \
--resources ${EC2_INSTANCE_ID} \
--tags Key=ec2.solo.io/use-public-ip,Value=trueAlternatively, pass --use-public-ip to istioctl ec2 add-workload to set the tag automatically. If the ec2.solo.io/use-public-ip=true tag is set but the instance does not have a public IP assigned, the WorkloadEntry is created with a ec2.solo.io/Configured=false condition and traffic is not routed to the instance until the address can be resolved.
Multi-workload instances
A single EC2 instance can host multiple distinct workloads, each with its own identity. To add multiple workloads to the same EC2 instance, run istioctl ec2 add-workload once per workload, specifying the same --instance ID each time. On the second and subsequent workloads, the instance is already bootstrapped, so no new bootstrap token is generated.
# First workload — also bootstraps ztunnel on the instance
istioctl ec2 add-workload web-api \
--instance ${EC2_INSTANCE_ID} \
--namespace ec2-workloads \
--ports http:80:8080
# Second workload — instance already bootstrapped, only tags and K8s resources are created
istioctl ec2 add-workload grpc-backend \
--instance ${EC2_INSTANCE_ID} \
--namespace ec2-workloads \
--ports grpc:9090Each workload uses its own Kubernetes ServiceAccount and SPIFFE identity. On the EC2 instance, ztunnel runs as a shared proxy and uses the SOCKS5 proxy (port 15080) with per-workload identity selection for outbound traffic. To send outbound traffic as a specific workload identity, set the SOCKS5 username to the workload’s service account.
IAM roles and credentials
EC2 ztunnel instances authenticate with istiod using the IAM instance profile attached to the EC2 instance. ztunnel fetches credentials through the EC2 Instance Metadata Service (IMDS), so no separate bootstrap token distribution is required for credential management. Ensure that:
- Each EC2 instance has an IAM instance profile attached with a role that the Kubernetes ServiceAccount annotation references.
- The
istioctl ec2 add-workloadcommand auto-discovers the role ARN from the instance profile. No additional IAM permissions are required on the instance role itself — ztunnel authenticates with istiod using the role ARN annotated on the Kubernetes ServiceAccount.
Rotating Istio certificates
Istio workload certificates for EC2 workloads are automatically rotated by istiod, which is the same as for in-cluster workloads. Ztunnel on the EC2 instance handles workload certificate rotation automatically.
However, when you rotate the Istio control plane root certificate (the istiod root CA), the ztunnel on the EC2 instance uses the istiod root cert embedded within the BOOTSTRAP_TOKEN. Because of this, EC2 ztunnels do not automatically pick up a rotated root certificate, and you must push out the new configuration to EC2 workloads as part of your root certificate rotation process.
BOOTSTRAP_TOKEN values using one of the following methods so EC2 ztunnels are bootstrapped with the new root as well. After all EC2 and Kubernetes ztunnels are rotated to the new root certificate and traffic is healthy, remove the old root certificate from the control plane.Update EC2 configuration
The simplest and recommended approach is to re-run the istioctl ec2 add-workload command with --bootstrap that you originally used to add the workload. This command fetches the new istiod root certificate and generates a fresh BOOTSTRAP_TOKEN that embeds the new cert.
istioctl binary at the same version that you want to use.Regenerate the bootstrap token.
istioctl ec2 add-workload <workload-name> \ --instance ${EC2_INSTANCE_ID} \ --namespace ec2-workloads \ --bootstrapNote the new
BOOTSTRAP_TOKENvalue from the command output.SSH into the EC2 instance and restart ztunnel with the new token.
pkill ztunnel && BOOTSTRAP_TOKEN=<new_token> ztunnel &
The following steps can be repeated for any EC2 workload to update its bootstrap token after a root certificate rotation. You can automate these steps with IaC tools like Terraform.
Create a new bootstrap token for the EC2 workload.
ISTIOD_EXPOSE_HOSTNAME=https://$(kubectl get service -n istio-eastwest istio-eastwest -o jsonpath='{.status.loadBalancer.ingress[0].hostname}') ISTIOD_EXPOSE_PORT=$(kubectl get service -n istio-eastwest istio-eastwest -o jsonpath='{.metadata.labels.istio\.io\/expose-istiod}') # Adjust this command when using a custom root cert ISTIOD_CA_ROOT_CERT=$(kubectl get configmap -n istio-system istio-ca-root-cert -o json | jq '.data."root-cert.pem"') NAMESPACE="ec2-workloads" SERVICE_ACCOUNT="ec2-${EC2_INSTANCE_ID}-gateway" NETWORK=$(kubectl get namespace istio-system -o jsonpath='{.metadata.labels.topology\.istio\.io\/network}') # Set to true when the EC2 instance is in a different VPC or network than the EKS cluster # (equivalent to the --external flag in istioctl ec2 add-workload) REMOTE=false BOOTSTRAP_TOKEN=$(echo -n "{\"url\":\"${ISTIOD_EXPOSE_HOSTNAME}:${ISTIOD_EXPOSE_PORT}\",\"caCert\":${ISTIOD_CA_ROOT_CERT},\"namespace\":\"${NAMESPACE}\",\"serviceAccount\":\"${SERVICE_ACCOUNT}\",\"platform\":\"ec2\",\"network\":\"${NETWORK}\",\"remote\":${REMOTE}}" | base64 | base64)SSH into the EC2 instance and restart ztunnel with the new token.
pkill ztunnel && BOOTSTRAP_TOKEN=<new_token> ztunnel &Repeat these steps for each EC2 workload in your mesh.
Verification
Confirm that the EC2 workload is reconnected and healthy.
kubectl get workloadentry -n ec2-workloads
istioctl ztunnel-config workloads | grep ec2-workloadsExample output:
ec2-workloads ec2-<instance-id>-web-api 10.0.1.42 None HBONEBOOTSTRAP_TOKEN that contained the old root certificate.Upgrading the Istio version of ztunnel on EC2
When you upgrade Istio, ztunnels running in Kubernetes are upgraded automatically. However, ztunnels on EC2 instances use the binary version that was installed manually. You must upgrade them as part of your upgrade process.
The simplest and recommended approach is to re-run the istioctl ec2 add-workload command with --bootstrap to regenerate the bootstrap token, then replace the ztunnel binary and restart it on the EC2 instance with the new token.
istioctl binary at the same version that you want to upgrade to.Regenerate the bootstrap token.
istioctl ec2 add-workload <workload-name> \ --instance ${EC2_INSTANCE_ID} \ --namespace ec2-workloads \ --bootstrapNote the new
BOOTSTRAP_TOKENvalue from the command output.SSH into the EC2 instance to stop ztunnel, download the new binary, and restart ztunnel with the new token.
pkill ztunnel curl -sSL https://storage.googleapis.com/soloio-istio-binaries/release/1.30.3-solo/ztunnel-1.30.3-solo-linux-amd64 \ -o /usr/local/bin/ztunnel chmod +x /usr/local/bin/ztunnel BOOTSTRAP_TOKEN=<new_token> ztunnel &
SSH into the EC2 instance.
Stop the running ztunnel process.
pkill ztunnelDownload the upgraded ztunnel binary.
curl -sSL https://storage.googleapis.com/soloio-istio-binaries/release/1.30.3-solo/ztunnel-1.30.3-solo-linux-amd64 \ -o /usr/local/bin/ztunnel chmod +x /usr/local/bin/ztunnelStart ztunnel with the same bootstrap token.
BOOTSTRAP_TOKEN=<token> ztunnel &Repeat for each EC2 instance in the mesh.
Verification
Confirm that the upgraded ztunnel is connected and reports HBONE connectivity.
istioctl ztunnel-config workloads | grep ec2-workloadsExample output:
ec2-workloads ec2-<instance-id>-web-api 10.0.1.42 None HBONECleanup
Remove the resources that you created in this guide.
On each EC2 instance, stop and remove the ztunnel container and any sample workload containers you started.
docker kill ztunnel && docker rm ztunnel docker kill ${EC2_WORKLOAD_NAME} && docker rm ${EC2_WORKLOAD_NAME}Remove EC2 instances from the mesh by deleting the
ec2.solo.io/discovery-enabledtag or by terminating the instances.aws ec2 delete-tags \ --resources ${EC2_INSTANCE_ID} \ --tags Key=ec2.solo.io/discovery-enabledDelete the Kubernetes namespace for the EC2 workloads.
kubectl delete namespace ec2-workloadsDelete the pod identity association and IAM roles and policies. This script performs the following cleanup actions:
- Deletes the pod identity association for the
istio-system/istiodservice account - Detaches and deletes the
istiod-${AWS_ACCOUNT}policy from theistiodrole - Deletes the
istiodIAM role - Detaches and deletes the
ec2-read-onlypolicy from theistiod-ec2role - Deletes the
istiod-ec2IAM role
scripts/cleanup/iam.sh- Deletes the pod identity association for the
If you created an EKS cluster for this guide, delete the cluster. The security group rules added for HBONE traffic are removed automatically when the cluster and its VPC are deleted. This script performs the following cleanup actions:
- Deletes the
${CLUSTER_NAME}EKS cluster - Removes all associated node groups
- Deletes the VPC and networking resources created by
eksctl
scripts/cleanup/eks-cluster.shThis step can take several minutes to complete aseksctlremoves all cluster resources.- Deletes the