# Common Workflows Source: https://docs.tensor9.com/cli/common-workflows This guide shows common workflows for using the tensor9 CLI to manage your apps, stacks, and appliances. ## Prerequisites Before starting, ensure you have: * [Installed the tensor9 CLI](/cli/install) * Set your API key: `export T9_API_KEY=` * Configured AWS credentials for your Tensor9 AWS account ## Initial setup workflow Set up Tensor9 in your AWS account for the first time. Run the interactive setup: ```bash theme={null} tensor9 vendor setup ``` Or provide all parameters: ```bash theme={null} tensor9 vendor setup \ -cloud aws \ -region us-west-2 \ -awsProfile my-tensor9-profile ``` This creates your Tensor9 control plane in your AWS account. It takes several minutes to complete. Check that your control plane is ready: ```bash theme={null} tensor9 report ``` You should see your vendor information displayed. ```bash theme={null} tensor9 app create \ -name my-app \ -displayName "My Application" ``` Your app is now created and ready to have a stack bound to it. *** ## Publish and bind your origin stack Publish an origin stack and bind it to your app. ### For Terraform stacks From your Terraform workspace directory: ```bash theme={null} tensor9 stack publish \ -stackType TerraformWorkspace \ -stackS3Key my-app-stack \ -dir . ``` This uploads your Terraform files and returns a native stack ID like: ``` s3://t9-ctrl-000001/terraform-stacks/origins/my-app-stack.tf.tgz ``` ```bash theme={null} tensor9 stack bind \ -appName my-app \ -stackType TerraformWorkspace \ -nativeStackId "s3://t9-ctrl-000001/terraform-stacks/origins/my-app-stack.tf.tgz" ``` Your stack is now bound to your app. ### For Docker container stacks First, authenticate to your Tensor9 AWS account's ECR: ```bash theme={null} aws ecr get-login-password --region us-west-2 --profile my-tensor9-profile | \ docker login --username AWS --password-stdin .dkr.ecr.us-west-2.amazonaws.com ``` Tag and push your image: ```bash theme={null} docker tag my-app:latest .dkr.ecr.us-west-2.amazonaws.com/my-app:latest docker push .dkr.ecr.us-west-2.amazonaws.com/my-app:latest ``` The native stack ID is the image URI: ``` .dkr.ecr.us-west-2.amazonaws.com/my-app:latest ``` ```bash theme={null} tensor9 stack bind \ -appName my-app \ -stackType DockerContainer \ -nativeStackId ".dkr.ecr.us-west-2.amazonaws.com/my-app:latest" ``` Your container is now bound to your app. ### For Docker Compose stacks Before publishing your Docker Compose file, ensure all container images referenced in your compose file are pushed to their registries (ECR, Docker Hub, etc.). These images must be available when you create a release. ```bash theme={null} tensor9 stack publish \ -stackType DockerCompose \ -stackS3Key my-app-compose \ -file docker-compose.yml ``` Returns a native stack ID like: ``` s3://t9-ctrl-000001/my-app-compose.yml ``` ```bash theme={null} tensor9 stack bind \ -appName my-app \ -stackType DockerCompose \ -nativeStackId "s3://t9-ctrl-000001/my-app-compose.yml" ``` ### For CloudFormation stacks Deploy your CloudFormation stack using the AWS CLI: ```bash theme={null} aws cloudformation create-stack \ --stack-name my-app-stack \ --template-body file://template.yaml \ --region us-west-2 \ --profile my-tensor9-profile ``` ```bash theme={null} aws cloudformation describe-stacks \ --stack-name my-app-stack \ --region us-west-2 \ --query 'Stacks[0].StackId' \ --output text ``` This returns an ARN like: ``` arn:aws:cloudformation:us-west-2:123456789012:stack/my-app-stack/a1b2c3d4 ``` ```bash theme={null} tensor9 stack bind \ -appName my-app \ -stackType CloudFormation \ -nativeStackId "arn:aws:cloudformation:us-west-2:123456789012:stack/my-app-stack/a1b2c3d4" ``` *** ## Create a test appliance Create a test appliance for testing your releases before deploying to customers. ```bash theme={null} tensor9 test appliance create \ -appName my-app \ -name my-test-appliance ``` The test appliance will be created asynchronously. Check the status periodically: ```bash theme={null} tensor9 report ``` Wait until the test appliance status shows "Live". This typically takes 10-15 minutes. *** ## Deploy to a test appliance Deploy your stack to a test appliance to verify it works before releasing to customers. Create a release for your test appliance: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -testApplianceName my-test-appliance \ -vendorVersion "1.0.0" \ -description "Initial release" \ -notes "First production release" ``` After a few minutes, the deployment stack downloads to a directory named `my-test-appliance`, alongside a companion `my-test-appliance.audit` directory containing the [audit stack](/fundamentals/stack-audit) for pre-deployment review. Before deploying, you can scan the audit stack with your standard IaC security and policy tooling. The audit stack is a Tensor9-free copy of the deployment stack - same application infrastructure, without any Tensor9 providers or runtime plumbing - so scanners can plan and inspect it standalone: ```bash theme={null} cd my-test-appliance.audit tofu init tofu plan # Security / policy scans tfsec . checkov -d . ``` The audit stack is for review only. Do not `apply` it. Only the deployment stack (in `my-test-appliance`) provisions a working install. ```bash theme={null} cd my-test-appliance tofu init tofu apply ``` Your application is now deployed in the test appliance. Check the deployed resources: ```bash theme={null} kubectl get deployments kubectl get services kubectl get pods ``` For services with external ports, get the load balancer endpoint: ```bash theme={null} kubectl get service -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' ``` Access your application through the load balancer URL. *** ## Create a customer appliance Enable your customer to create an appliance for your app in their environment. Generate a signup link for your customer: ```bash theme={null} tensor9 app signup-link -appName my-app ``` Send the generated URL to your customer. They'll use it to create their appliance. The customer uses the signup link to: 1. Sign up and create their organization (if they haven't already) 2. Select their cloud provider and region 3. Complete the setup to create their appliance Their appliance will be provisioned automatically in their cloud account. Check when the customer's appliance is ready: ```bash theme={null} tensor9 report ``` Wait until their appliance status shows "Live". Once their appliance is ready: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -customerName acme-corp \ -vendorVersion "1.0.0" \ -description "Initial deployment for Acme Corp" \ -notes "Configured for production use" ``` The deployment stack downloads to a directory named after their appliance. Deploy the release to the customer's appliance: ```bash theme={null} cd acme-corp-appliance tofu init tofu apply ``` This deploys your application into the customer's appliance. *** ## Release to a customer appliance Release infrastructure or code changes to your customer appliances. Make changes to your Terraform files, docker-compose.yml, or CloudFormation template. For Terraform: ```bash theme={null} tensor9 stack publish \ -stackType TerraformWorkspace \ -stackS3Key my-app-stack \ -dir . ``` For Docker Compose: ```bash theme={null} tensor9 stack publish \ -stackType DockerCompose \ -stackS3Key my-app-compose \ -file docker-compose.yml ``` For CloudFormation: ```bash theme={null} aws cloudformation update-stack \ --stack-name my-app-stack \ --template-body file://template.yaml ``` You don't need to re-bind the stack. Tensor9 will use the updated version for new releases. Create a release to your test appliance: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -testApplianceName my-test-appliance \ -vendorVersion "1.1.0" \ -description "Added new features" \ -notes "Added caching layer and API improvements" ``` ```bash theme={null} cd my-test-appliance tofu apply ``` Verify the changes work as expected. After testing, release to your customers: ```bash theme={null} # Release to specific customer tensor9 stack release create \ -appName my-app \ -customerName acme-corp \ -vendorVersion "1.1.0" \ -description "Feature update" \ -notes "New caching layer and performance improvements" # Or release to all customers tensor9 stack release create \ -appName my-app \ -all \ -vendorVersion "1.1.0" \ -description "Feature update" \ -notes "New caching layer and performance improvements" ``` *** ## Manage multiple form factors Deploy your app to different cloud providers or connectivity modes. Create form factors for different environments: ```bash theme={null} # AWS connected tensor9 form-factor create \ -appName my-app \ -formFactorName aws-connected \ -description "AWS with internet connectivity" \ -env Aws \ -connectivity Connected # GCP connected tensor9 form-factor create \ -appName my-app \ -formFactorName gcp-connected \ -description "Google Cloud with internet connectivity" \ -env Gcp \ -connectivity Connected ``` Create test appliances for each form factor: ```bash theme={null} tensor9 test appliance create \ -appName my-app \ -name my-aws-test \ -formFactorName aws-connected tensor9 test appliance create \ -appName my-app \ -name my-gcp-test \ -formFactorName gcp-connected \ -cloudRegion gcp:us-central1 ``` ```bash theme={null} tensor9 stack release create \ -appName my-app \ -testApplianceName my-aws-test \ -vendorVersion "1.0.0" \ -description "AWS test" \ -notes "Testing AWS deployment" tensor9 stack release create \ -appName my-app \ -testApplianceName my-gcp-test \ -vendorVersion "1.0.0" \ -description "GCP test" \ -notes "Testing GCP deployment" ``` *** ## Onboard your vendor controller to Tailscale By default, the listeners your CLI and Terraform use to reach the vendor controller are exposed on a public network load balancer with mTLS protection. If you operate a [Tailscale](https://tailscale.com) tailnet for your engineering team, you can have the vendor controller join that tailnet and (optionally) remove the public path entirely. See [Connectivity](/fundamentals/connectivity#operator-to-control-plane) for the broader picture. This workflow rolls out in two phases: first attach the controller to your tailnet and verify operator access, then optionally enforce tunnel-only by removing the public listeners. ### Phase 1: attach the vendor controller to your tailnet This step is idempotent. It adds the `tag:tensor9-vctrl` (vendor controller) and `tag:tensor9-customer-ctrl` (appliance controller) tags, the `group:tensor9-operators` group, and the rules that allow operators and customer appliances to reach the vendor controller. Existing ACL entries are preserved. ```bash theme={null} export TAILSCALE_API_KEY=tskey-api-... tensor9 tailscale acl setup ``` If you maintain ACLs in HuJSON via the dashboard, the command prints a copy-pasteable snippet you can apply by hand. Otherwise answer `y` at the confirmation prompt to have the CLI POST the change for you (a local backup is written under `~/.tensor9/tailscale-acl-backups/`). See [`tensor9 tailscale acl setup`](/cli/reference#tailscale-acl-setup) for full options. The key is tagged so the ACL applies the right rules to the resulting node. Keep it single-use (the default) for production: ```bash theme={null} tensor9 tailscale key generate -tag VCtrl ``` Save the printed `tskey-auth-...` value; you will pass it to the next command. See [`tensor9 tailscale key generate`](/cli/reference#tailscale-key-generate) for full options. Add operators to the `group:operators` group in the Tailscale dashboard so they can reach the controller's listeners over the tailnet. ```bash theme={null} tensor9 vendor tailscale onboard \ -vctrlKey tskey-auth-XXXXXXXXXXXX ``` The command installs the Tailscale daemon on the vendor controller, joins it to the tailnet, and stamps the resulting tailnet hostname onto the controller's configuration. The controller's existing public listeners stay in place. See [`tensor9 vendor tailscale onboard`](/cli/reference#vendor-tailscale-onboard) for full options. With your operator account in `group:operators` and connected to the tailnet, run a read-only command: ```bash theme={null} tensor9 report ``` Your CLI should be able to reach the vendor controller over the tailnet without any extra flags. The controller is still reachable over the public path too, so this step proves the tailnet route is working while leaving you a fallback. The endpoint used with Terraform/OpenTofu `plan` and `apply` operations is defined in the `tensor9` provider block of the compiled deployment stack. Compilation emits the publicly available listener to this block when it is available, even if you have a tunnel configured, to be compatible with CI deployments. For testing, you can manually modify the compiled stack to change the endpoint address to be the vendor controller's address on the tailnet and try a `plan`. Follow phase 2, below, to remove the TF endpoint from the public load balancer and re-compile your deployment stacks to enforce using the Tailscale for communication between your local Terraform/OpenTofu CLI and the vendor controller. ### Phase 2: remove the public listeners Once you are satisfied that operators and Terraform-driven deploys work over the tailnet, you can enforce tunnel-only and tear the public listeners down. Roll this out per listener group rather than all at once so you can pause if something is missed. The `tunnel enforce` command mutates the controller's configuration; the listener teardown happens on the next infrastructure upgrade. ```bash theme={null} # CLI listeners first tensor9 vendor tunnel enforce -add CLI # After a soak period, the Terraform reactor too (make sure CI works!) tensor9 vendor tunnel enforce -add Terraform ``` See [`tensor9 vendor tunnel enforce`](/cli/reference#vendor-tunnel-enforce) for full options. If you also want to remove the appliance-facing public listener, the command checks first that no customer appliance still depends on it; see the pre-flight notes in the reference. Run an infrastructure upgrade to remove the now-redundant listeners from your network load balancer: ```bash theme={null} tensor9 vendor upgrade \ -kind Infrastructure \ -reason "remove public CLI and Terraform listeners after Tailscale cutover" ``` After this completes, the only way to reach the enforced listeners is over the tailnet. Make sure every operator and every CI runner that drives `tensor9` or `terraform` against the vendor controller is on the tailnet before you run `tunnel enforce -add CLI` / `Terraform`. After the next infrastructure upgrade, anything off the tailnet will be locked out. To roll back, run `tensor9 vendor tunnel enforce -remove CLI,Terraform` followed by `tensor9 vendor upgrade -kind Infrastructure`. The public listeners are recreated. *** ## Use stack tuning documents Customize resource allocations per customer or environment. Create a JSON file with resource overrides: ```json theme={null} { "version": "V1", "composeServices": { "web": { "replicas": 4, "resources": { "cpu": "2", "memory": "4Gi" } }, "api": { "replicas": 3, "resources": { "cpu": "1", "memory": "2Gi" } } } } ``` Save as `enterprise-tuning.json`. ```bash theme={null} tensor9 stack release create \ -appName my-app \ -customerName enterprise-customer \ -vendorVersion "1.0.0" \ -description "Enterprise deployment" \ -notes "High-performance configuration" \ -tuningDoc enterprise-tuning.json ``` The deployment stack will use the tuned resource specifications. *** ## Monitor appliances Check the status of your apps, appliances, and releases. ### View comprehensive report ```bash theme={null} tensor9 report ``` This shows: * All your apps and their stacks * All test appliances and their status * All customer appliances and their status * Active releases ### View detailed report ```bash theme={null} tensor9 report -detailed ``` ### List all appliances ```bash theme={null} tensor9 appliance list ``` ### Use the web portal Start a local web interface: ```bash theme={null} tensor9 portal ``` This opens a browser with a visual dashboard of your apps and appliances. *** ## Retire a test appliance Remove a test appliance when no longer needed. ```bash theme={null} tensor9 test appliance retire -testApplianceName my-test-appliance ``` This permanently deletes the test appliance and all its infrastructure. It may take several minutes to fully deprovision. *** ## Manage your vendor team Tensor9 distinguishes two kinds of vendor accounts: * The **root account** is created automatically when you run `vendor setup`. It is the singleton account for your vendor and the only one that can invite, list, or revoke other accounts. If you lose your root cert, you can re-issue it from your control plane's seed bundle in your AWS account (`iam account recover`). * **Operator accounts** are everyone else: teammates the root account invites by email. They get a long-lived mTLS cert pinned by your control plane after redeeming a one-time enrollment bundle. They can run ordinary vendor commands but cannot manage other accounts. If an operator loses their cert, the root account re-invites them. See [IAM Commands](/cli/reference#iam-commands) in the reference for the full surface and authorization details. ### Invite a teammate From your laptop (root account), invite by email: ```bash theme={null} tensor9 iam user invite -email alice@acme.com ``` This writes a single-use bundle to `./alice@acme.com-enrollment-bundle.json`. The bundle is valid for 48 hours by default. Use `-ttl PT24H` (or any ISO-8601 duration) to set a different window. Send the file to your teammate via Slack, encrypted email, or a similar channel. The bundle contains a one-time secret, so treat it as sensitive until redeemed. On their machine, your teammate runs: ```bash theme={null} tensor9 configure -enrollmentBundle ./alice@acme.com-enrollment-bundle.json ``` This trades the bundle for a long-lived mTLS cert pinned by your control plane, persists it locally, and writes a CLI profile so subsequent `tensor9` commands authenticate automatically. ### List your team ```bash theme={null} tensor9 iam user list ``` Shows the root account plus every invited operator with their enrollment status (`Enrolled`, `Invited`, or `Revoked`). ### Revoke access To remove an operator's access, find their account ID and revoke: ```bash theme={null} tensor9 iam user list tensor9 iam user revoke -accountId 00000000000000000000000000000003 ``` Revocation is two-stage on the server: any in-flight enrollment bundles are killed, and the leaf-fingerprint pin is dropped so the user's cached cert stops authenticating on its next handshake. The root account cannot revoke itself. Use `tensor9 iam account recover` to rotate the root cert. ### Recover the root account If you lose your laptop or wipe `~/.tensor9/`, recover by re-issuing the root cert from your control plane's seed bundle: ```bash theme={null} tensor9 iam account recover -vendorId ``` The command reads your control plane's seed bundle directly from your AWS Secrets Manager / Parameter Store. Access to that secret-store path is the actual gate, so make sure the IAM policy on it is scoped tightly. For non-root operator account recovery, ask your root account to re-invite you with `iam user invite`. Operator tokens are single-use, so there's no equivalent self-service recovery path on the operator side. *** ## Troubleshoot common issues **Problem**: `tofu apply` fails when deploying a release. **Solution**: 1. Check the Terraform error messages 2. Verify your origin stack is valid: `tofu validate` in your workspace 3. Check appliance status: `tensor9 report` 4. Review deployment stack variables and configuration 5. Check AWS credentials and permissions **Problem**: Test appliance remains in "Creating" status for a long time. **Solution**: 1. Wait 15-20 minutes (appliance creation can take time) 2. Check `tensor9 report -detailed` for error messages 3. Verify AWS quotas are sufficient for EKS, VPCs, etc. 4. Check CloudFormation console in AWS for stack creation issues 5. Contact support if stuck for more than 30 minutes **Problem**: `tensor9 stack publish` fails with upload errors. **Solution**: 1. Verify AWS credentials: `aws sts get-caller-identity --profile ` 2. Check that no .terraform directories are in your workspace 3. Ensure you have write permissions to the control plane S3 bucket 4. For large stacks, check your network connection **Problem**: Customer reports they can't reach their deployed application. **Solution**: 1. Verify deployment completed: Check with customer that `tofu apply` succeeded 2. Check load balancer: `kubectl get service` shows external IP/hostname 3. Verify DNS configuration if using custom domains 4. Check security groups/firewall rules in customer's cloud 5. Review application logs: `kubectl logs ` **Problem**: After creating a release, the deployment stack doesn't download. **Solution**: 1. Wait a few minutes (compilation takes time) 2. Check release status: `tensor9 report` 3. Verify appliance is in "Live" status 4. Check for stack validation errors in the report 5. Review control plane CloudWatch logs for compilation errors *** ## Best practices Use semantic versioning for your releases: * `1.0.0` - Initial release * `1.0.1` - Patch (bug fixes) * `1.1.0` - Minor (new features, backward compatible) * `2.0.0` - Major (breaking changes) This helps customers understand the impact of updates. Always create a release to a test appliance before releasing to customers: 1. Create release to test appliance 2. Deploy and verify functionality 3. Test upgrade path from previous version 4. Only then release to customers Write clear, customer-facing release notes: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -customerName acme-corp \ -vendorVersion "1.2.0" \ -description "Bug fixes and performance improvements" \ -notes "Fixed authentication timeout issue. Improved API response time by 40%. Added support for batch operations." ``` Your customers see these notes when deploying. Keep your origin stacks in version control and use consistent naming: * Use Git to track changes to origin stacks * Tag releases in Git: `git tag v1.0.0` * Use the same version in Git and Tensor9 releases * Document infrastructure changes in commit messages Regularly check appliance status: ```bash theme={null} # Weekly check tensor9 report # Monthly detailed review tensor9 report -detailed ``` Set up alerts for customer appliance issues. *** ## Next steps * **[CLI Reference](/cli/reference)**: Complete command reference * **[Quick Start Guides](/getting-started/quick-start-terraform)**: Step-by-step tutorials * **[Deployments](/fundamentals/deployments)**: Learn more about the deployment process * **[Testing](/fundamentals/testing)**: Best practices for testing releases # Install tensor9 CLI Source: https://docs.tensor9.com/cli/install The `tensor9` CLI is the primary tool for managing your Tensor9 control plane, publishing origin stacks, creating releases, and managing appliances. ## Prerequisites * **API Key**: Your Tensor9 API key (provided during onboarding, or email [hello@tensor9.com](mailto:hello@tensor9.com)) No separate Java installation is required - the CLI bundles its own runtime. ## Installation ### Homebrew (recommended - macOS and Linux) ```bash theme={null} brew tap tensor9ine/tensor9 brew install tensor9 ``` This installs the CLI with a bundled Java runtime. No other dependencies needed. ### Shell script Alternatively, install via the install script: ```bash theme={null} curl -sSL https://t9-artifacts-prod-1.s3.us-west-2.amazonaws.com/install-latest.sh | sh ``` ### Set your API key Set your Tensor9 API key as an environment variable: ```bash theme={null} export T9_API_KEY= ``` Add this to your shell profile (`~/.bashrc`, `~/.zshrc`, etc.) to persist it across sessions: ```bash theme={null} echo 'export T9_API_KEY=' >> ~/.zshrc source ~/.zshrc ``` ### Verify installation ```bash theme={null} tensor9 whoami ``` This should display your Tensor9 identity and environment information. ## Updating ### Homebrew ```bash theme={null} brew upgrade tensor9 ``` ### Manual update ```bash theme={null} tensor9 update ``` ## Uninstallation ### Homebrew ```bash theme={null} brew uninstall tensor9 brew untap tensor9ine/tensor9 ``` ### Manual ```bash theme={null} sudo rm /usr/local/bin/tensor9 rm -rf ~/.tensor9 ``` ## Platform support The tensor9 CLI is supported on: * **macOS** (Apple Silicon and Intel) * **Linux** (arm64 and amd64) Windows users can use the CLI through WSL (Windows Subsystem for Linux). ## Environment variables | Variable | Description | Required | | ------------ | -------------------- | ----------------------- | | `T9_API_KEY` | Your Tensor9 API key | Yes (for most commands) | ## Common installation issues **Problem**: `tensor9: command not found` after installation. **Solution**: If installed via Homebrew, try opening a new terminal. If the issue persists, ensure the Homebrew bin directory is in your PATH: ```bash theme={null} echo $PATH ``` For Homebrew on Apple Silicon, the default path is `/opt/homebrew/bin`. For Intel Macs and Linux, it is `/usr/local/bin`. **Problem**: "API key required" or authentication errors. **Solution**: 1. Verify your API key is set: `echo $T9_API_KEY` 2. If empty, set it: `export T9_API_KEY=` 3. Verify it works: `tensor9 whoami` 4. For persistence, add to your shell profile ## What's next? After installing the CLI: 1. **Set up your control plane**: Follow the [Quick Start](/getting-started/quick-start-terraform) guide 2. **Learn common workflows**: See [Common Workflows](/cli/common-workflows) for typical tasks 3. **Reference documentation**: Check the [CLI Reference](/cli/reference) for all commands ## Getting help If you encounter issues: * Run `tensor9 help` to see available commands * Run `tensor9 -help` for command-specific help * Contact support at [hello@tensor9.com](mailto:hello@tensor9.com) # CLI Reference Source: https://docs.tensor9.com/cli/reference This is a complete reference for all `tensor9` CLI commands. For installation instructions, see [Install](/cli/install). For common workflows and examples, see [Common Workflows](/cli/common-workflows). ## Usage ```bash theme={null} tensor9 [options] ``` Get help for any command: ```bash theme={null} tensor9 -help tensor9 help ``` ## Authentication Most commands require authentication via API key. Set your API key using the environment variable: ```bash theme={null} export T9_API_KEY= ``` Alternatively, pass it as a parameter: ```bash theme={null} tensor9 -apiKey ``` ## Command Groups The tensor9 CLI organizes commands into logical groups: * **[vendor](#vendor-commands)**: Vendor setup and management * **[iam](#iam-commands)**: Vendor account management (inviting teammates, recovery) * **[app](#app-commands)**: Application management * **[stack](#stack-commands)**: Stack and release management * **[form-factor](#form-factor-commands)**: Form factor management * **[appliance](#appliance-commands)**: Customer appliance management * **[install](#install-commands)**: Listing app installs across appliances * **[Kubernetes](#kubernetes-commands)**: Kubernetes RBAC roles and EKS access * **[tailscale](#tailscale-commands)**: Tailscale tailnet utilities (ACL setup, pre-auth key generation) * **[General commands](#general-commands)**: Help, update, install, report *** ## General Commands ### help Display help information about available commands. ```bash theme={null} tensor9 help tensor9 help ``` **Options:** * `-group` (optional): Show help for a specific command group **Examples:** ```bash theme={null} # Show all command groups tensor9 help # Show all vendor commands tensor9 help vendor # Show all stack commands tensor9 help stack ``` ### env Report on the current Tensor9 environment. ```bash theme={null} tensor9 env ``` Shows which Tensor9 stage you're connected to (production, staging, etc.). ### report Generate a comprehensive report about your vendor account. ```bash theme={null} tensor9 report ``` **Options:** * `-vendorId` (optional): The vendor ID to report on (auto-discovered if not specified) * `-detailed` (optional): Produce a detailed report * `-all` (optional): Show all items instead of truncating lists * `-outputFmt`: Output format: Human (default), Json, Raw, Csv. * `-raw`: Output raw value only (alias for `-outputFmt Raw`). * `-json`: Output as JSON (alias for `-outputFmt Json`). * `-csv`: Output as CSV (alias for `-outputFmt Csv`). **Examples:** ```bash theme={null} # Basic report tensor9 report # Detailed report tensor9 report -detailed ``` The report displays: * Vendor details * All apps and their stacks * Customer appliances * Active releases ### portal Start a local web portal for managing your apps and appliances. ```bash theme={null} tensor9 portal ``` **Options:** * `-vendorId` (optional): The vendor ID (auto-discovered from API key if not specified) * `-applianceName` (optional): Appliance name if you have multiple appliances (Customer) * `-port` (optional): Port to run the server on (default: 8080) * `-noBrowser` (optional): Don't automatically open browser **Examples:** ```bash theme={null} # Start portal on default port (8080) and open browser tensor9 portal # Use custom port tensor9 portal -port 9090 # Start without opening browser tensor9 portal -noBrowser ``` ### whoami Display information about the currently authenticated user. ```bash theme={null} tensor9 whoami ``` **Options:** * `-outputFmt`: Output format: Human (default), Json, Raw, Csv. * `-raw`: Output raw value only (alias for `-outputFmt Raw`). * `-json`: Output as JSON (alias for `-outputFmt Json`). * `-csv`: Output as CSV (alias for `-outputFmt Csv`). *** ## Vendor Commands ### vendor setup Set up Tensor9 in your AWS account. This creates your control plane for managing customer appliances. ```bash theme={null} tensor9 vendor setup [options] ``` **Options:** * `-region` (optional): The cloud region. Auto-detected if not provided. * `-awsProfile` (optional): AWS profile to use for credentials. * `-vendorId` (optional): The vendor ID to set up Tensor9 for (auto-discovered if not specified) * `-force` (optional): Force setup to proceed even if it would overwrite existing resources * `-noAwsPrivateLinkRdv` (optional): Suppress the AWS PrivateLink endpoint service that vendor setup provisions. With this flag set, PrivateLink will not be available as a network-path option for the appliance-to-control-plane channel on this vendor's form factors. AWS only; ignored on other clouds. **Examples:** ```bash theme={null} # Interactive setup (prompts for all inputs) tensor9 vendor setup # Non-interactive AWS setup tensor9 vendor setup \ -region us-west-2 \ -awsProfile my-profile ``` Your Tensor9 AWS account should be a dedicated AWS account used only for Tensor9. This reduces the risk of conflicts with other infrastructure. ### vendor report Generate a detailed report of the vendor's resources. ```bash theme={null} tensor9 vendor report [options] ``` **Options:** * `-vendorId` (optional): The vendor ID to report on. Auto-discovered if not specified. * `-detailed` (optional): Produce a detailed report. * `-all` (optional): Show all items instead of truncating lists. ### vendor upgrade Upgrade your Tensor9 vendor infrastructure or control-plane software. Use `Infrastructure` to re-apply the Tensor9 Terraform modules against your AWS account, or `Software` to deploy the latest control-plane software to your VCtrl instance. ```bash theme={null} tensor9 vendor upgrade -reason [options] ``` **Required:** * `-reason`: Operator note describing the motivation for this upgrade. **Optional:** * `-kind`: Upgrade kind. One of `Infrastructure` (default) or `Software`. * `-region`: AWS region. Auto-detected if not provided. * `-awsProfile`: AWS profile to use for credentials. * `-vendorId`: Vendor ID. Auto-discovered from API key if not specified. * `-targetDefId`: Specific upgrade definition to target (e.g., `fabric-2026.04.24`). Defaults to the latest registered definition for the chosen kind. * `-noAwsPrivateLinkRdv`: Suppress the AWS PrivateLink endpoint service for this upgrade run. Removes PrivateLink from the network-path options on the vendor's AWS form factors after the upgrade completes. **Examples:** ```bash theme={null} # Re-apply infrastructure modules (default kind) tensor9 vendor upgrade -reason "pick up new fabric module" # Upgrade control-plane software only tensor9 vendor upgrade \ -kind Software \ -reason "deploy latest control plane" # Pin to a specific upgrade definition tensor9 vendor upgrade \ -kind Infrastructure \ -reason "rollout new VPC layout" \ -targetDefId fabric-2026.04.24 ``` A vendor upgrade holds a 5-minute lease while running. If a previous upgrade was interrupted, wait for the lease to expire before retrying. ### vendor tailscale onboard Attach the vendor control plane to a Tailscale tailnet you operate. Once attached, your operator-side listeners are reachable at the control plane's tailnet hostname in addition to its public endpoint. See [Connectivity](/fundamentals/connectivity#operator-to-control-plane) for the broader picture. A step-by-step guide to onboarding is provided in [Common Workflows](/cli/common-workflows#onboard-your-vendor-controller-to-tailscale). ```bash theme={null} tensor9 vendor tailscale onboard -vctrlKey [options] ``` **Required:** * `-vctrlKey`: A Tailscale pre-auth key tagged for VCtrl use. Generate one with [`tensor9 tailscale key generate -tag VCtrl`](#tailscale-key-generate). **Optional:** * `-vctrlHostname`: Tailscale hostname for the control plane. Defaults to `vctrl-`. * `-region`: AWS region. Auto-detected if not provided. * `-awsProfile`: AWS profile to use for credentials. * `-vendorId`: Vendor ID. Auto-discovered from your Tensor9 profile if not specified. **Example:** ```bash theme={null} tensor9 vendor tailscale onboard \ -vctrlKey tskey-auth-XXXXXXXXXXXX \ -vctrlHostname vctrl-acme ``` After onboarding, your operators can run `tensor9` commands and your CI can run `terraform plan`/`apply` while connected to the same tailnet without needing the public control-plane endpoint. ### vendor tunnel enforce Remove the public-internet path for one or more vendor-side listener groups, leaving the Tailscale path as the only way to reach them. The change takes effect on the next `tensor9 vendor upgrade -kind Infrastructure` run. A step-by-step guide to onboarding is provided in [Common Workflows](/cli/common-workflows#onboard-your-vendor-controller-to-tailscale). ```bash theme={null} tensor9 vendor tunnel enforce [-add ] [-remove ] [-force] ``` **Listener groups:** | Group | Used by | | ------------ | ----------------------------------------------- | | `CLI` | `tensor9` CLI | | `Terraform` | Compiled stack deployment | | `Appliances` | Customer appliances reaching your control plane | **Options:** * `-add `: Comma-separated groups to require tunnel-only. * `-remove `: Comma-separated groups to restore the public path for. * `-force`: Bypass the soft pre-flight check (see below) when enforcing `Appliances`. **Examples:** ```bash theme={null} # Make the CLI listeners tunnel-only tensor9 vendor tunnel enforce -add CLI # Add Terraform after a soak period tensor9 vendor tunnel enforce -add Terraform # Roll back tensor9 vendor tunnel enforce -remove CLI,Terraform ``` **Pre-flight checks for `Appliances`.** Enforcing the `Appliances` group tears down the listener that customer appliances use, so the command checks first that nothing currently depends on it: * If AWS PrivateLink is enabled for appliance traffic, the command refuses (disable PrivateLink first; `-force` does not bypass). * If any customer appliance or appliance-setup link is still configured to use the public path, the command lists them and refuses. Re-run with `-force` to override. After the configuration write succeeds, run `tensor9 vendor upgrade -kind Infrastructure` to actually remove the listeners from your network load balancer. *** ## IAM Commands Manage who in your organization can act against your Tensor9 control plane. ### Root account vs operator accounts Tensor9 distinguishes two kinds of vendor accounts: | | **Root account** | **Operator account** | | ------------------ | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | **How created** | Automatically, when you run `vendor setup`. One per vendor. | The root account invites by email via `iam user invite`. Zero or more per vendor. | | **Authority** | Only account that can invite, list, or revoke other accounts. | Can call ordinary vendor commands (apps, stacks, appliances). Cannot manage other accounts. | | **Authentication** | Long-lived mTLS cert pinned by your control plane (no API key needed after `vendor setup`). | Long-lived mTLS cert pinned by your control plane (no API key needed after redeeming the invite). | | **Recovery** | Re-issue from the seed bundle stored in your AWS Secrets Manager / Parameter Store. Use `iam account recover`. | Ask the root account to re-invite via `iam user invite`. There is no self-service recovery path because operator tokens are single-use. | The root account is the seeded singleton, not a "permission level". Today it happens to be the only account with vendor-management capability, but rootness stays just the property of being the account provisioned at first boot. ### configure Redeem an enrollment bundle and enroll your local machine for a vendor. Run this on a fresh machine after a teammate sends you a bundle file, or to recover after losing your local cert. ```bash theme={null} tensor9 configure -enrollmentBundle ``` **Options:** * `-enrollmentBundle` (required for first-time enrollment): Path to the bundle JSON the inviting account produced. * `-vendorId` (optional): Vendor ID to enroll for. Auto-detected from the bundle. * `-profile` (optional): Profile name to use or create. Defaults to an auto-derived name based on the vendor and account type. * `-force` (optional): Re-enroll even if a valid cert is already cached locally. **Examples:** ```bash theme={null} # Redeem a bundle a teammate sent you tensor9 configure -enrollmentBundle ./alice@acme.com-enrollment-bundle.json # Re-enroll explicitly under a custom profile name tensor9 configure -enrollmentBundle ./bundle.json -profile work ``` After enrollment, your local config records a profile (visible via `tensor9 profile list`) and the just-enrolled vendor becomes the default for subsequent commands. ### iam user invite Invite a teammate. Provisions a vendor-scoped operator account and writes a single-use enrollment bundle for hand-off. ```bash theme={null} tensor9 iam user invite -email [options] ``` **Options:** * `-email` (required): Email address of the new teammate. * `-out` (optional): Path to write the enrollment bundle JSON. Defaults to `./-enrollment-bundle.json`. * `-ttl` (optional): Bundle expiration as an ISO-8601 duration (e.g. `PT24H`, `P7D`). Defaults to `PT48H`. * `-profile` (optional): Profile to authenticate with. Defaults to the active profile. **Authorization:** root-account-only. Operator accounts get an access-denied error. **Examples:** ```bash theme={null} # Invite Alice with default 48h TTL tensor9 iam user invite -email alice@acme.com # Custom TTL and output path tensor9 iam user invite \ -email alice@acme.com \ -out ./alice.json \ -ttl PT24H ``` After running, send the bundle file to the teammate out-of-band (Slack, encrypted email, etc.). They redeem it with `tensor9 configure -enrollmentBundle `. ### iam user list List the accounts known to your control plane as belonging to your vendor. ```bash theme={null} tensor9 iam user list ``` **Authorization:** root-account-only. The output includes the root account row, every invited operator, and each row's enrollment status: | Status | Meaning | | ---------- | ------------------------------------------------------------------------------------ | | `Enrolled` | Operator has redeemed their bundle and has a leaf cert pinned in your control plane. | | `Invited` | Bundle in flight, not yet redeemed. | | `Revoked` | Account has no pinned cert and no pending bundles. | ### iam user revoke Revoke a teammate's access. Two-stage on the server side: kills any in-flight enrollment bundles, then drops the leaf-fingerprint pin so the user's cached cert stops authenticating on its next handshake. ```bash theme={null} tensor9 iam user revoke -accountId ``` **Options:** * `-accountId` (required): The account ID of the user to revoke. Find this with `iam user list`. * `-profile` (optional): Profile to authenticate with. **Authorization:** root-account-only. The root account cannot revoke itself (use `iam account recover` for root-cert rotation). **Examples:** ```bash theme={null} tensor9 iam user revoke -accountId 00000000000000000000000000000003 ``` ### iam account recover Recover your root account's mTLS cert by re-issuing it from your control plane's seed bundle. Use this after losing your laptop, wiping `~/.tensor9/`, or when your cached cert no longer chains against your control plane's current root. ```bash theme={null} tensor9 iam account recover -vendorId [options] ``` **Options:** * `-vendorId` (required): Vendor ID to recover for. * `-region` (optional): AWS region of your control plane's secret store. Defaults to `us-west-2`. * `-awsProfile` (optional): AWS profile for credentials. Defaults to the standard AWS credential discovery chain. * `-force` (optional): Re-enroll even if a valid cert is already cached locally. * `-profile` (optional): Profile name to use or create. **Authorization:** This command does no auth on its own. Access is gated by the IAM policy on the seed-bundle path in your AWS account's secret store. Anyone who can read that path can recover the root cert, so treat it as security-sensitive and scope the policy tightly. **Examples:** ```bash theme={null} # Recover after a fresh laptop tensor9 iam account recover -vendorId 0000000000000042 # Override region and AWS profile tensor9 iam account recover \ -vendorId 0000000000000042 \ -region us-east-1 \ -awsProfile acme-prod ``` For non-root operator account recovery, ask your root account to re-invite you with `iam user invite`. Operator accounts have no seed-store entry because their tokens are single-use. *** ## App Commands ### app create Create a new app. ```bash theme={null} tensor9 app create -name -displayName [options] ``` **Required:** * `-name`: The app name (alphanumeric, underscores, hyphens; 3-64 characters) * `-displayName`: A friendly name for the app. **Optional:** * `-stackType`: The type of stack to bind to the app. Must be one of: CloudFormation, Terraform, TerraformWorkspace, DockerContainer, DockerCompose, Helm. * `-nativeStackId`: The native id of the stack to bind to the app (e.g. `arn:aws:cloudformation:us-west-2:1234:stack/my-stack/abcd` for CloudFormation). * `-vanityDomain`: A custom vanity domain name for this app. This is used to create endpoints used by your app, including per-appliance endpoints for appliances that install your app. e.g. `any-prem.vendor.co`. * `-json`: Output as JSON instead of human-readable text. **Examples:** ```bash theme={null} # Create app without binding a stack tensor9 app create \ -name my-app \ -displayName "My Application" # Create app and bind Docker Compose stack tensor9 app create \ -name my-app \ -displayName "My Application" \ -stackType DockerCompose \ -nativeStackId "s3://t9-ctrl-000001/my-app-compose.yml" # Create app with vanity domain tensor9 app create \ -name my-app \ -displayName "My Application" \ -vanityDomain anyprem.mycompany.com ``` ### app signup-link Generate a signup link for customers to sign up for your app. ```bash theme={null} tensor9 app signup-link -appName ``` **Required (one of):** * `-appId`: The app ID * `-appName`: The app name **Examples:** ```bash theme={null} # Generate signup link by app name tensor9 app signup-link -appName my-app # Generate signup link by app ID tensor9 app signup-link -appId 0000000000000123 ``` Returns a URL like `https://portal.tensor9.com/buyerSignup?appId=...` that you can send to customers. ### app list List applications by vendor. ```bash theme={null} tensor9 app list [options] ``` **Options:** * `-outputFmt`: Output format: Human (default), Json, Raw, Csv. * `-raw`: Output raw value only (alias for `-outputFmt Raw`). * `-json`: Output as JSON (alias for `-outputFmt Json`). * `-csv`: Output as CSV (alias for `-outputFmt Csv`). ### app retrieve Retrieve application details. ```bash theme={null} tensor9 app retrieve -appId ``` **Required:** * `-appId`: The ID of the app to retrieve. *** ## Stack Commands ### stack publish Upload a stack definition to your control plane. ```bash theme={null} tensor9 stack publish \ -appName \ -stackType \ -stackS3Key \ [options] ``` **Required:** * `-appName`: The name of the app to upload the stack for. * `-stackS3Key`: The name for the stack archive (without file extension). * `-stackType`: The type of stack. Must be one of: `CloudFormation`, `Terraform`, `TerraformJson`, `Kube`, `TerraformWorkspace`, `DockerContainer`, `DockerCompose`, `Helm`. **Optional:** * `-dir`: For `TerraformWorkspace`: The directory containing `.tf` files. Defaults to the current directory. * `-file`: For `DockerCompose`: The path to the `docker-compose.yml` file. (For `TerraformWorkspace`, use `-dir` instead). **Examples:** ```bash theme={null} # Publish Terraform stack tensor9 stack publish \ -appName my-app \ -stackType TerraformWorkspace \ -stackS3Key my-app-stack \ -dir ./terraform # Publish Docker Compose stack tensor9 stack publish \ -appName my-app \ -stackType DockerCompose \ -stackS3Key my-app-compose \ -file docker-compose.yml ``` Returns a native stack ID like `s3://t9-ctrl-000001/my-app-stack.tf.tgz`. ### stack bind Bind an origin stack (e.g., CloudFormation, Terraform, Docker, etc.) to your app. ```bash theme={null} tensor9 stack bind \ -stackType \ [options] ``` **Required:** * `-stackType`: The type of stack to bind. Must be one of: `CloudFormation`, `Terraform`, `TerraformJson`, `Kube`, `TerraformWorkspace`, `DockerContainer`, `DockerCompose`, `Helm`. * **Note**: You must provide either `-nativeStackId` or `-gitHubUrl`. **Optional:** * `-appName`: The name of the app to bind the stack to. * `-appVersion`: The version of the app to bind to (defaults to latest). * `-stackName`: A friendly name for the stack being bound. * `-nativeStackId`: The native ID of the stack. * **TerraformWorkspace**: `s3://my-bucket/my-stack.tf.tgz` * **DockerContainer**: `123456789012.dkr.ecr.us-west-2.amazonaws.com/my-container:latest` * **CloudFormation**: `arn:aws:cloudformation:us-west-2:0011223344556677:stack/my-stack/...` * `-gitHubUrl`: A GitHub URL pointing to a directory containing Terraform files (mutually exclusive with `-nativeStackId`). * `-subPath`: For `-gitHubUrl` with `TerraformWorkspace`: the subdirectory within the checkout containing the root module (`main.tf`). * `-credentialType`: The type of credential to use for cross-account or cross-cloud access. (e.g., `AwsRoleArn`, `GcpTrustedServiceAccountEmail`, `GitHubUserAndToken`, etc.) * `-credential`: The credential value. **Examples:** ```bash theme={null} # Bind Terraform stack via S3 tensor9 stack bind \ -appName my-app \ -stackType TerraformWorkspace \ -nativeStackId "s3://t9-ctrl-000001/my-app-stack.tf.tgz" # Bind Terraform stack via GitHub tensor9 stack bind \ -appName my-app \ -stackType TerraformWorkspace \ -gitHubUrl "https://github.com/owner/repo/tree/main/terraform" # Bind CloudFormation stack tensor9 stack bind \ -appName my-app \ -stackType CloudFormation \ -nativeStackId "arn:aws:cloudformation:us-west-2:123456789012:stack/my-stack/..." ``` You only need to bind once per app. Future publishes of the same stack don't require re-binding. ### stack unbind Unbind an origin stack (e.g., CloudFormation stack) from your app. ```bash theme={null} tensor9 stack unbind [options] ``` **Required (one of):** * `-appName`: The name of the app to unbind from. * `-appId`: The ID of the app to unbind from. **Required (one of):** * `-stackId`: The Tensor9 stack ID (found in `tensor9 report`). * `-stackName`: The friendly name of the stack to unbind. * **Note**: If neither is specified and only one stack is bound to the app, that stack will be automatically targeted. **Examples:** ```bash theme={null} # Unbind by app name (if only one stack bound) tensor9 stack unbind -appName my-app # Unbind specific stack by friendly name tensor9 stack unbind -appName my-app -stackName my-main-stack # Unbind specific stack by ID tensor9 stack unbind -appId 0000000000000123 -stackId 0000000000000456 ``` ### stack release create Release a stack to one or more appliances. ```bash theme={null} tensor9 stack release create [options] ``` **Options:** * `-appName`: The name of the app to release. * `-vendorVersion`: Version string (e.g., `v5.4.0`). * `-description`: A short description of the release. * `-notes`: A description of the release (detailed notes). **Target (exactly one required):** * `-projectionIds`: The IDs of the installs to release to. Must be JSON-encoded as a list of strings (e.g., `["0000000000000001:9a961d63e1120abe:0e4348b322904268", "0000000000000001:230d826ab554c3e7:6a40a3b5d3c807a5"]`). * `-testAppliance` or `-testApplianceName`: The name of a test appliance to use for this release. * `-customerName` or `-buyerName`: The name of the customer to release to. * `-all`: Release to all appliances. **Optional:** * `-stackId` or `-stackName`: Required if the app has multiple stacks bound to it. * `-tuningDoc`: A file containing the tuning document for this release. * `-tuningDocFmt`: The format of the tuning document file: `Json`, `Yaml`. (Defaults to `Json`). **Examples:** ```bash theme={null} # Release to specific installs tensor9 stack release create \ -appName my-app \ -projectionIds '["0000000000000001:9a961d63e1120abe:0e4348b322904268"]' \ -vendorVersion "v5.4.0" \ -description "Hotfix" \ -notes "Detailed notes here" # Release to customer tensor9 stack release create \ -appName my-app \ -customerName acme-corp \ -vendorVersion "v5.4.0" \ -description "Initial release" \ -notes "Production-ready version" # Release to test appliance tensor9 stack release create \ -appName my-app \ -testApplianceName my-test-appliance \ -vendorVersion "v5.4.0" \ -description "Testing" \ -notes "Internal verification" ``` After creating a release, the deployment stack downloads to a directory named after the appliance. Deploy it using: ```bash theme={null} cd terraform init terraform apply ``` ### stack release retire Retire a release of an app. ```bash theme={null} tensor9 stack release retire [options] ``` **Options:** * `-releaseId`: The ID of the release to retire. * OR: * `-vendorVersion`: The vendor version of the release to retire. * `-appName`: The name of the app to retire a release for. **Filters (apply when using -vendorVersion and -appName):** * `-stackName`: The name of the stack to retire releases for. * `-customerName` or `-buyerName`: The name of the customer to retire a release for. * `-reason`: The reason for retiring the release. **Examples:** ```bash theme={null} # Retire by release ID tensor9 stack release retire -releaseId 0000000000000789 # Retire by version tensor9 stack release retire \ -appName my-app \ -vendorVersion "v5.4.0" \ -customerName acme-corp \ -reason "Security vulnerability fixed in v5.4.1" ``` ### stack list List stack definitions for a vendor. ```bash theme={null} tensor9 stack list -vendorId ``` **Required:** * `-vendorId`: The vendor ID. ### stack retrieve Retrieve a specific stack definition. ```bash theme={null} tensor9 stack retrieve -vendorId -stackId ``` **Required:** * `-vendorId`: The vendor ID. * `-stackId`: The stack ID to retrieve. ### stack release list List stack releases for an app. ```bash theme={null} tensor9 stack release list [options] ``` **Required (one of):** * `-appName`: App name to list releases for. * `-appId`: App ID to list releases for. **Options:** * `-outputFmt`: Output format: Human (default), Json, Raw, Csv. * `-raw`: Output raw value only (alias for `-outputFmt Raw`). * `-json`: Output as JSON (alias for `-outputFmt Json`). * `-csv`: Output as CSV (alias for `-outputFmt Csv`). ### stack release download Download released stacks for one or more appliances. ```bash theme={null} tensor9 stack release download -appName -vendorVersion [options] ``` **Required:** * `-appName`: The name of the app to download the release for. * `-vendorVersion`: The vendor version to download (e.g., `v5.4.0`). Use `-latest` to download the most recent release. **Optional:** * `-stackId`: The stack ID to download. Required if the app has multiple stacks bound to it. * `-outDir`: The directory to download the stack(s) to. Defaults to the current directory. * `-historyRetention`: Number of previous release snapshots to keep in `.tensor9/history/`. Defaults to `10`. **Examples:** ```bash theme={null} # Download the latest release for an app tensor9 stack release download \ -appName my-app \ -vendorVersion -latest # Download a specific version to a custom directory tensor9 stack release download \ -appName my-app \ -vendorVersion v5.4.0 \ -outDir ./releases # Multi-stack app: specify which stack to download tensor9 stack release download \ -appName my-app \ -stackId 0000000000000456 \ -vendorVersion v5.4.0 ``` The download creates a directory per appliance containing the deployment stack. Deploy it with the appropriate IaC tool: ```bash theme={null} cd tofu init tofu apply ``` ### stack release-pending list List pending stack releases. ```bash theme={null} tensor9 stack release-pending list -vendorId -appId ``` **Required:** * `-vendorId`: The vendor ID. * `-appId`: App ID to filter pending releases. ### stack release-bundle download Download a release bundle. ```bash theme={null} tensor9 stack release-bundle download -releaseId -projectionId -outputFile ``` **Required:** * `-releaseId`: The release ID to download. * `-projectionId`: The install ID. * `-outputFile`: Path to save the downloaded bundle. ### stack release-bundle apply Apply a release bundle manually from a local file or presigned URL. ```bash theme={null} tensor9 stack release-bundle apply -releaseBundleFmt -bxEndpoint [options] ``` **Required:** * `-releaseBundleFmt`: Format (e.g., Tgz\_2024\_12\_07). * `-bxEndpoint`: Coordinator endpoint to use. **Required (exactly one of):** * `-releaseBundleFile`: Path to the bundle file. * `-releaseBundleUrl`: Presigned URL for air-gapped deployments. *** ## Terraform Commands ### tf import Import or adopt an existing resource into Tensor9 management. This command handles two scenarios: * **State recovery**: You lost your local Terraform state (e.g., crash, environment rebuild). The resource was previously deployed through Tensor9. Running `tf import` recovers your local state. * **Resource adoption**: A deployment failed because the resource already exists (e.g., "BucketAlreadyOwnedByYou", "resource already exists"). Running `tf import --nativeId` tells Tensor9 to adopt the existing resource instead of trying to create a new one. ```bash theme={null} tensor9 tf import \ --customerName \ --version \ --resource \ [--nativeId ] \ [--stackName ] \ [options] ``` **Required:** * `--version`: Vendor version of the release (e.g., `1.0.0`) * `--resource`: The twin resource name from your compiled stack, optionally module-qualified (e.g., `aws_s3_bucket_main_twin`, `module.vpc.aws_s3_bucket_main_twin`) **Scope (exactly one required):** * `--customerName`: Customer name (e.g., `acme-corp`) * `--customerId`: Customer ID * `--applianceName`: Appliance name * `--applianceId`: Appliance ID * `--installationId`: Installation ID (most specific; use when other scopes are ambiguous) **Optional:** * `--nativeId`: The resource's import ID. Required when a deployment failed because the resource already exists. The format depends on the resource type. See [Terraform provider docs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs) for the expected format. * `--stackName`: Stack name to disambiguate when multiple stacks contain a resource with the same name * `--dir`: Directory containing your Terraform configuration (defaults to current directory) * `--tfBin`: Path to the tofu/terraform binary (auto-discovered by default) **Examples:** ```bash theme={null} # Recover local state after losing terraform.tfstate tensor9 tf import \ --customerName acme-corp \ --version 1.0.0 \ --resource aws_s3_bucket_main_twin # Adopt an existing S3 bucket after a failed deployment tensor9 tf import \ --customerName acme-corp \ --version 1.0.0 \ --resource aws_s3_bucket_main_twin \ --nativeId my-existing-bucket # Adopt with module-qualified resource tensor9 tf import \ --customerName acme-corp \ --version 1.0.0 \ --resource module.vpc.aws_subnet_main_twin \ --nativeId subnet-0123456789abcdef0 # Multi-stack app: specify which stack tensor9 tf import \ --customerName acme-corp \ --version 1.0.0 \ --resource aws_s3_bucket_main_twin \ --nativeId my-existing-bucket \ --stackName my-vpc-stack ``` **What is `--nativeId`?** It's the value you would pass to `tofu import` for this resource type. The format varies by resource: * `aws_s3_bucket`: bucket name (e.g., `my-bucket`) * `aws_instance`: instance ID (e.g., `i-1234567890abcdef0`) * `aws_lb`: ARN (e.g., `arn:aws:elasticloadbalancing:...`) * `aws_ecs_service`: `cluster-name/service-name` Check the "Import" section in your Terraform provider docs for the expected format. After adopting a resource, run `tofu apply` to reconcile your deployment. This applies any configuration differences (such as tags) between your stack definition and the existing resource. *** ## Form Factor Commands ### svc-spec-doc template generate Generate a service specification document by analyzing your app's bound stacks and mapping discovered services to target environment equivalents. ```bash theme={null} tensor9 svc-spec-doc template generate \ -appName \ -targetEnv \ [options] ``` **Required:** * `-appName`: The app name (must have Terraform stacks bound) * `-targetEnv`: Customer environment (Aws, Gcp, Azure, Kube, Metal, BareMetal) **Optional:** * `-output`: Output file path. Use `-` or omit for stdout. This command: 1. Analyzes Terraform stacks bound to your app 2. Discovers AWS services (EKS, RDS, VPC, etc.) 3. Maps each service to its target environment equivalent 4. Generates a service specification document with dependencies and version constraints **Examples:** ```bash theme={null} # Generate spec for Kubernetes target tensor9 svc-spec-doc template generate \ -appName my-app \ -targetEnv Kube \ -output config.json ``` When multiple target options exist for a service, you'll be prompted to select which to offer customers: ``` Loading App 'my-app'... Generating service specs for Kube... Discovered 3 AWS service(s): - aws::1.0.0::eks::cluster (v1.29) - aws::1.0.0::rds::postgresql (v14.0) - aws::1.0.0::iam 1 service(s) have multiple target candidates. Select which options to offer customers (1 selection = Exact, 2+ = OneOf): aws::1.0.0::rds::postgresql → select target service(s): [x] cloudnative::1.0.0::postgresql [ ] gcp::1.0.0::cloudsql::postgresql Generated Exact for aws::1.0.0::rds::postgresql Generated 3 spec(s) - kubernetes::1.0.0::cluster - cloudnative::1.0.0::postgresql - tensor9::1.0.0::iam Config written to: config.json Next step - create form factor: tensor9 form-factor create \ -appName my-app \ -formFactorName kube-connected \ -env Kube \ -connectivity Connected \ -svcSpecDoc config.json ``` Selecting multiple options creates a OneOf requirement, letting customers choose their preferred service equivalent. After generating the service specification document, review the `tuning` fields for each service. Since scaling requirements for private deployments often differ from your cloud deployment, this is an opportunity to tune resource allocations (storage size, replicas, CPU, memory) for the specific form factor. ### form-factor create Create a new form factor for your app (e.g., `aws-connected`, `gcp-connected`). ```bash theme={null} tensor9 form-factor create \ -appName \ -formFactorName \ -env \ -connectivity \ [options] ``` **Required:** * `-appName`: The name of the app to create a form factor for. * `-formFactorName`: A name for the form factor (must match: `[a-z0-9_\-]{1,64}`). * `-env`: The environment for the form factor. One of: `Aws`, `Gcp`, `Azure`, `Kube`, `Metal`, `Local`, `BareMetal`. * `-connectivity`: The connectivity for the form factor. One of: `Connected`, `Disconnected`, `AirGapped`. **Optional:** * `-description`: A description for the form factor. * `-svcSpecDoc`: Path to JSON file containing service specification documents. Use `svc-spec-doc template generate` to generate a starter file. **Examples:** ```bash theme={null} # Create AWS connected form factor tensor9 form-factor create \ -appName my-app \ -formFactorName aws-connected \ -description "AWS with internet connectivity" \ -env Aws \ -connectivity Connected # Create form factor with service specifications tensor9 form-factor create \ -appName my-app \ -formFactorName kube-connected \ -env Kube \ -connectivity Connected \ -svcSpecDoc config.json ``` ### form-factor export Export an existing form factor as SvcSpecDoc JSON for modification and evolution. ```bash theme={null} tensor9 form-factor export \ -appName \ -formFactorName \ [options] ``` **Required:** * `-appName`: The name of the app that owns the form factor. * `-formFactorName`: The name of the form factor to export. **Optional:** * `-version`: Specific version to export (e.g., `1.2.0`). Defaults to current (latest) version. * `-output`: Output file path. Use `-` or omit for stdout. This command retrieves a form factor and converts it to the same SvcSpecDoc format used by `form-factor create`. The exported JSON can be modified and then used with `form-factor evolve` to create a new version. The export includes: * Service requirements (`Exact` and `OneOf`) * Service tunings (converted to tuning JSON) * Service dependency requirements `env` and `connectivity` are **not** included in the SvcSpecDoc as they cannot be changed during evolution. The export includes these as informational comments in the output. **Examples:** ```bash theme={null} # Export current (latest) version to stdout tensor9 form-factor export -appName my-app -formFactorName aws-connected # Export specific version to file tensor9 form-factor export -appName my-app -formFactorName aws-connected -version 1.2.0 -output config.json ``` After exporting, edit the JSON and use it to evolve the form factor: ```bash theme={null} tensor9 form-factor evolve -appName my-app -formFactorName aws-connected -svcSpecDoc config.json ``` ### form-factor promote Promote a form factor version to Preferred status. ```bash theme={null} tensor9 form-factor promote \ -appName \ -formFactorName \ -version ``` **Required:** * `-appName`: The name of the app that owns the form factor. * `-formFactorName`: The name of the form factor. * `-version`: The version to promote (e.g., `1.2.0`). The promoted version becomes the default for new installs. The previously Preferred version is automatically demoted to Active. **Examples:** ```bash theme={null} tensor9 form-factor promote -appName my-app -formFactorName aws-connected -version 1.2.0 ``` ### form-factor retire Retire a form factor version. ```bash theme={null} tensor9 form-factor retire \ -appName \ -formFactorName \ -version ``` **Required:** * `-appName`: The name of the app that owns the form factor. * `-formFactorName`: The name of the form factor. * `-version`: The version to retire (e.g., `1.0.0`). Retired versions cannot serve new installs, and existing installs using a retired version should be upgraded. Note: The **Preferred** version cannot be retired; you must promote another version first. **Examples:** ```bash theme={null} tensor9 form-factor retire -appName my-app -formFactorName aws-connected -version 1.0.0 ``` ### form-factor evolve Evolve an existing form factor to a new version using a modified SvcSpecDoc. ```bash theme={null} tensor9 form-factor evolve \ -appName \ -formFactorName \ -svcSpecDoc \ [options] ``` **Required:** * `-appName`: The name of the app that owns the form factor. * `-formFactorName`: The name of the form factor to evolve. * `-svcSpecDoc`: Path to the SvcSpecDoc JSON file (typically generated via `form-factor export`) with updated service requirements. **Optional:** * `-name`: New name for the form factor (defaults to current name). * `-description`: New description for the form factor (defaults to current description). This command creates a new version of the form factor. The version bump (Major, Minor, or Patch) is automatically detected based on the changes: * **Major**: Breaking changes (e.g., stricter constraints, new requirements, removed choices). * **Minor**: Compatible additions (e.g., relaxed constraints, new choices, removed requirements). * **Patch**: Metadata changes only (e.g., name or description updates). `env` and `connectivity` **cannot** be changed during evolution. Changing these requires creating a new form factor instead. **Examples:** ```bash theme={null} # Evolve with updated configuration tensor9 form-factor evolve -appName my-app -formFactorName aws-connected -svcSpecDoc config.json # Evolve with new name and description tensor9 form-factor evolve \ -appName my-app \ -formFactorName aws-connected \ -svcSpecDoc config.json \ -name "aws-connected-v2" \ -description "Updated for Kubernetes 1.30" ``` **Typical Workflow:** 1. **Export**: `tensor9 form-factor export -appName my-app -formFactorName aws-connected -output config.json` 2. **Edit**: Modify `config.json` with updated service requirements or tunings. 3. **Evolve**: `tensor9 form-factor evolve -appName my-app -formFactorName aws-connected -svcSpecDoc config.json` ### form-factor version lifecycle When you evolve a form factor, the new version starts as Active. Existing installs stay pinned to their original version until explicitly upgraded. See [Form factor versioning](/fundamentals/key-concepts#form-factor-versioning) for details on version statuses (Preferred, Active, Retiring, Retired). ### service specification document format The service specification document is a JSON file used with `form-factor create` and `form-factor evolve` to define service requirements and dependencies. ```json theme={null} { "services": [ { "svcId": "cloudnative::1.0.0::postgresql|abc123", "versionConstraints": ">=14.0.0", "tuning": { "overrideAllocatedStorageInGib": 100, "overrideNumReplica": 2, "overrideVCpu": 4, "overrideMemoryInGib": 8 }, "origin": "aws:tf:my-app-stack@module::database|aws_db_instance.main" } ], "dependencies": [ { "name": "cloudnative::1.0.0::cnpg::operator", "versionConstraints": ">=1.24.0", "installMethod": "Managed" } ] } ``` **Services** can be specified in two modes (see [Service requirements](/fundamentals/key-concepts#service-requirements)): **Exact mode** - A single, specific service requirement: ```json theme={null} { "svcId": "cloudnative::1.0.0::postgresql|abc123", "versionConstraints": ">=14.0.0", "tuning": { "overrideAllocatedStorageInGib": 100 }, "origin": "aws:tf:my-app-stack@module::database|aws_db_instance.main" } ``` **OneOf mode** - Customer chooses from multiple options: ```json theme={null} { "choices": [ { "svcId": "cloudnative::1.0.0::postgresql|abc123", "versionConstraints": ">=14.0.0", "tuning": { "overrideAllocatedStorageInGib": 100 } }, { "svcId": "byo::1.0.0::postgresql|def456", "versionConstraints": ">=14.0.0" } ], "origin": "aws:tf:my-app-stack@module::database|aws_db_instance.main" } ``` **Dependencies** are infrastructure components (like Helm charts or operators) that [service adapters](/form-factor/kubernetes#service-dependencies) may require. You specify which versions are acceptable using semver constraints, and Tensor9 installs the latest version that satisfies the constraint. * `name`: The dependency identifier * `versionConstraints`: Semver constraint you specify (e.g., `>=1.24.0`, `>=1.24.0 <2.0.0`) * `installMethod`: Either `Managed` (Tensor9 installs it) or `PreInstalled` (customer already has it). Note: private Kubernetes environments only support `PreInstalled`. Tensor9 manages dependencies using [reference counting](/form-factor/kubernetes#reference-counting) to ensure they're installed exactly once and cleaned up when no longer needed. *** ## Appliance Setup Commands ### appliance setup create Create a setup script that can be used to set up an appliance. ```bash theme={null} tensor9 appliance setup create \ -customerName \ -appName \ -cloud \ -formFactorName ``` **Required:** * `-customerName`: The name of the customer that will own the appliance. * `-appName`: The name of the app to be installed into that appliance. * `-cloud`: The cloud to set up the appliance in: Aws, Gcp, Azure, Private, Local. * `-formFactorName`: The name of the form factor the appliance will have. **Optional:** * `-privateCloudName`: The name of the private cloud the appliance will be in. Must be owned by the same customer that will own the appliance. * `-vendorMetadata`: JSON-encoded map of string key/value pairs to set on the resulting install once the appliance is set up and the app is installed. **Examples:** ```bash theme={null} # Create AWS setup script tensor9 appliance setup create \ -customerName acme-corp \ -appName my-app \ -cloud Aws \ -formFactorName aws-connected # Create setup script with metadata tensor9 appliance setup create \ -customerName acme-corp \ -appName my-app \ -cloud Gcp \ -formFactorName gcp-connected \ -vendorMetadata '{"tier":"enterprise","region":"us"}' ``` Returns a setup key and instructions to send to your customer. ### appliance setup list List appliance setups. ```bash theme={null} tensor9 appliance setup list -appId ``` **Optional:** * `-appId`: The ID of the app to list appliance setups for. * `-buyerId`: The ID of the customer to list appliance setups for. ### appliance setup retrieve Retrieve appliance setup info. ```bash theme={null} tensor9 appliance setup retrieve -applianceSetupKey ``` **Required:** * `-applianceSetupKey`: The appliance setup key of the appliance setup to retrieve. *** ## Appliance Commands ### appliance list List all appliances. ```bash theme={null} tensor9 appliance list ``` **Optional:** * `-outputFmt`: Output format: Human (default), Json, Raw, Csv. * `-raw`: Output raw value only (alias for `-outputFmt Raw`). * `-json`: Output as JSON (alias for `-outputFmt Json`). * `-csv`: Output as CSV (alias for `-outputFmt Csv`). ### appliance retrieve Retrieve appliance details. ```bash theme={null} tensor9 appliance retrieve -applianceName -customerName ``` You must specify either: * `-applianceName` + `-customerName`: Retrieve by appliance name. * `-applianceId`: Retrieve by appliance ID. **Optional:** * `-applianceName`: The name of the appliance to retrieve. Requires `-customerName`. Alternative: use `-applianceId`. * `-customerName`: The name of the customer that owns the appliance. Required with `-applianceName`. * `-applianceId`: The ID of the appliance to retrieve. Alternative: use `-applianceName` with `-customerName`. * `-outputFmt`: Output format: Human (default), Json, Raw, Csv. * `-raw`: Output raw value only (alias for `-outputFmt Raw`). * `-json`: Output as JSON (alias for `-outputFmt Json`). * `-csv`: Output as CSV (alias for `-outputFmt Csv`). ### appliance check Health check an appliance. ```bash theme={null} tensor9 appliance check -applianceId ``` **Required:** * `-applianceId`: The appliance ID of the appliance to health check. *** ## Install Commands ### install list List installs across apps, customers, or appliances. An install represents one of your apps running on a specific customer's appliance. ```bash theme={null} tensor9 install list [options] ``` **Filter (exactly one required):** * `-appName`: List all installs for an app. Alternative: `-appId`. * `-customerName`: List all installs for a customer. Alternative: `-customerId`. * `-applianceName`: List installs on a specific appliance. Requires `-customerName`. Alternative: `-applianceId`. **Examples:** ```bash theme={null} # List all installs of an app across all customers tensor9 install list -appName my-app # List all installs for a customer tensor9 install list -customerName acme # List installs on a specific appliance tensor9 install list \ -applianceName acme-prod \ -customerName acme ``` *** ## Kubernetes Commands ### kube role list List Tensor9 Kubernetes RBAC roles and their default state. Prints the `kubectl` commands to check current bindings on the cluster. Run by the customer using a customer API key. ```bash theme={null} tensor9 kube role list [options] ``` **Optional:** * `-applianceName`: Appliance name (required if the customer has multiple appliances). * `-namespace`: Override namespace (auto-discovered from appliance configuration). * `-clusterWide`: Use `ClusterRoleBinding` scope instead of namespace-scoped `RoleBinding`. The command lists 6 RBAC roles and their default state: | Role | Default | Description | | ------------------- | -------- | ----------------------------------------------------------------------------------------------------------- | | `install` | disabled | Full infrastructure provisioning. Required for Tensor9 to perform infrastructure provisioning and upgrades. | | `steady-state` | enabled | Read-only monitoring. Required for Tensor9 to monitor appliance health. | | `deploy` | disabled | Create or modify vendor workloads | | `operate-readonly` | disabled | Read-only troubleshooting | | `operate-readwrite` | disabled | Write troubleshooting | | `operate-admin` | disabled | Full troubleshooting | **Examples:** ```bash theme={null} # List roles for a single-appliance customer tensor9 kube role list # List roles for a specific appliance tensor9 kube role list -applianceName my-appliance # List roles using cluster-wide scope tensor9 kube role list -clusterWide ``` ### kube role enable Print the `kubectl apply` command needed to enable a Tensor9 RBAC role. This command does not mutate the cluster; it prints the command that the customer must run to apply the binding. ```bash theme={null} tensor9 kube role enable -role [options] ``` **Required:** * `-role`: The role to enable. One of: `install`, `steady-state`, `deploy`, `operate-readonly`, `operate-readwrite`, `operate-admin`. **Optional:** * `-applianceName`: Appliance name (required if multiple appliances). * `-namespace`: Override namespace (auto-discovered). * `-clusterWide`: Generate a `ClusterRoleBinding` instead of a namespace-scoped `RoleBinding`. **Examples:** ```bash theme={null} # Print the apply command for the deploy role tensor9 kube role enable -role deploy # Print the apply command for cluster-wide admin access tensor9 kube role enable -role operate-admin -clusterWide ``` ### kube role disable Print the `kubectl delete` command needed to disable a Tensor9 RBAC role. This command does not mutate the cluster; it prints the command that the customer must run to remove the binding. ```bash theme={null} tensor9 kube role disable -role [options] ``` **Required:** * `-role`: The role to disable. One of: `install`, `steady-state`, `deploy`, `operate-readonly`, `operate-readwrite`, `operate-admin`. **Optional:** * `-applianceName`: Appliance name (required if multiple appliances). * `-namespace`: Override namespace (auto-discovered). * `-clusterWide`: Target a `ClusterRoleBinding` instead of a namespace-scoped `RoleBinding`. Disabling `steady-state` prevents Tensor9 from monitoring the appliance's health. Disabling `install` prevents Tensor9 from performing infrastructure provisioning and upgrades. **Examples:** ```bash theme={null} tensor9 kube role disable -role deploy tensor9 kube role disable -role operate-admin -clusterWide ``` ### aws eks grant-access Grant an IAM role full admin access to an EKS cluster. This command mutates the cluster: it creates an access entry, associates `AmazonEKSClusterAdminPolicy`, and updates the local kubeconfig. ```bash theme={null} tensor9 aws eks grant-access -clusterName -roleArn [options] ``` **Required:** * `-clusterName`: The EKS cluster name. * `-roleArn`: The full IAM role ARN to grant access to (e.g., `arn:aws:iam::123456789012:role/MyRole`). **Optional:** * `-region`: AWS region. Defaults to `us-west-2`. * `-awsProfile`: AWS profile to use for credentials. * `-skipKubeconfig`: Skip the local kubeconfig update step. The command performs four steps: 1. Ensures the cluster's authentication mode is `API_AND_CONFIG_MAP`. 2. Creates an access entry for the IAM role. 3. Associates the `AmazonEKSClusterAdminPolicy` with the access entry. 4. Updates the local kubeconfig to point at the cluster (unless `-skipKubeconfig` is set). **Examples:** ```bash theme={null} # Grant access in the default region with the default AWS profile tensor9 aws eks grant-access \ -clusterName my-cluster \ -roleArn arn:aws:iam::123456789012:role/MyRole # Grant access in a specific region tensor9 aws eks grant-access \ -clusterName my-cluster \ -roleArn arn:aws:iam::123456789012:role/MyRole \ -region us-east-1 # Skip the kubeconfig update tensor9 aws eks grant-access \ -clusterName my-cluster \ -roleArn arn:aws:iam::123456789012:role/MyRole \ -skipKubeconfig ``` *** ## Tailscale Commands Utility commands for managing the Tailscale tailnet that backs the [operator-to-control-plane](/fundamentals/connectivity#operator-to-control-plane) and (optionally) [appliance-to-control-plane](/fundamentals/connectivity#appliance-to-control-plane) network paths. Both commands talk to the Tailscale API; set `TAILSCALE_API_KEY` in your environment before running them. ### tailscale acl setup Ensure the Tailscale ACL policy on a tailnet contains the tags, groups, and rules required for the vendor controller and customer appliances to communicate. The command is additive and idempotent: existing ACL entries are preserved. HuJSON comments are not supported. ```bash theme={null} tensor9 tailscale acl setup [options] ``` **Options:** * `-tailnet`: Tailscale tailnet identifier. Accepts a new-style tailnet ID, a domain, or `-` for the tailnet owned by the API key. Defaults to `-`. When additions are needed, the command prints a copy-pasteable JSON snippet and prompts for confirmation. You can either paste the snippet into the Tailscale dashboard (preserves any comments) or answer `y` to have the CLI POST the change for you (comments are lost, but a local backup is written to `~/.tensor9/tailscale-acl-backups/`). **Examples:** ```bash theme={null} export TAILSCALE_API_KEY=tskey-api-... # Update the tailnet owned by the API key tensor9 tailscale acl setup # Update a specific tailnet by domain tensor9 tailscale acl setup -tailnet acme.org ``` Run this command once before [`vendor tailscale onboard`](#vendor-tailscale-onboard). The ACL changes it ensures are what allow operators and customer appliances to actually reach the VCtrl over the tailnet. ### tailscale key generate Generate a Tailscale pre-auth key for a tailnet. The key is printed to stdout. Use the resulting key as the input to [`vendor tailscale onboard`](#vendor-tailscale-onboard) (for the vendor controller) or when creating an appliance-setup link (for a customer appliance). ```bash theme={null} tensor9 tailscale key generate -tag [options] ``` **Required:** * `-tag`: ACL tag applied to nodes that join with this key. `VCtrl` for the vendor controller, `Bx` for a customer appliance. **Options:** * `-tailnet`: Tailscale tailnet identifier. Same accepted forms as `tailscale acl setup`. Defaults to `-`. * `-reusable`: `true` or `false`. Allow the key to authenticate multiple nodes. Default `false`. Use `true` only for keys used in accounts you control (e.g. dev/test environments). * `-ephemeral`: `true` or `false`. Auto-remove the node when it goes offline. Default `false` (persistent). Ephemeral nodes require a reusable key to re-join after removal. * `-description`: Human-readable label for the key (e.g. `"vctrl for acme vendor"`). **Examples:** ```bash theme={null} # Production VCtrl key (single-use, persistent) tensor9 tailscale key generate -tag VCtrl # Production Bx key (single-use, persistent) tensor9 tailscale key generate -tag Bx # Dev/test Bx key (reusable, ephemeral) tensor9 tailscale key generate -tag Bx -reusable true -ephemeral true ``` By default, keys are single-use and nodes are persistent in the Tailscale dashboard. The key will be redeemed on the first join and the node will be able to rejoin the tailnet after reboots and shutdowns until its access is revoked. *** ## Advanced Commands ### appliance setup Set up an appliance (run by customer in their environment). ```bash theme={null} tensor9 appliance setup \ -setupKey \ -cloudRegion [options] ``` **Required:** * `-setupKey`: A single-use setup key identifying which appliance is being set up and how to set it up. * `-cloudRegion`: Cloud region (e.g., `aws:us-west-2`, `gcp:us-central1`). **Optional:** * `-autoApprove`: Automatically approve infrastructure changes. Required for non-interactive setups. * `-vanityDomain`: The vanity domain for the appliance (e.g., `app-name.company.com`). * `-domainSuffix`: Domain suffix for the appliance (e.g., `app-name.internal.company.com`). * `-privateZone`: Whether the domain suffix should be a private zone instead of a public zone. * `-gcpProjectId`: The project ID of the GCP project the appliance will live in. Only applies if setting up an appliance in GCP. Defaults to the project ID of the GCP environment running this action (if any). * `-resourceTags`: JSON-encoded key/value pairs to apply as tags on all cloud resources created by the appliance. * `-credentialType`: The type of credential to use to set up your appliance. Must be paired with `-credential`. * `-credential`: The credential to use to set up your appliance. Must be paired with `-credentialType`. * `-buyerSvcSpec`: Path to a service spec JSON file containing service selections and configurations. When provided, the setup record is updated with customer inputs before proceeding with setup. * `-json`: Output the created appliance as JSON instead of human-readable text. * `-kubeClusterMode`: Kubernetes cluster mode for Kube form factors: DedicatedCluster or VCluster. * `-kubeCfg`: Path to kubeconfig file for Kube form factors. * `-vClusterName`: vCluster name (required when `kubeClusterMode=VCluster`). * `-vClusterHostNamespace`: Host namespace for vCluster (required when `kubeClusterMode=VCluster`). **Examples:** ```bash theme={null} # Interactive AWS setup tensor9 appliance setup \ -setupKey abc123... \ -cloudRegion aws:us-west-2 # Non-interactive GCP setup tensor9 appliance setup \ -setupKey abc123... \ -cloudRegion gcp:us-central1 \ -gcpProjectId my-project \ -autoApprove \ -resourceTags '{"environment":"production"}' ``` ### machine setup Set up a machine in a private cloud. ```bash theme={null} tensor9 machine setup \ -type \ -ip [options] ``` **Required:** * `-type`: Machine type (Ctrl or Iso) * `-ip`: IP address exposed to other machines in the private cloud **Optional:** * `-zone`: Zone name (e.g., `datacenter-1`) * `-gpu`: GPU type (None, NvidiaT4, NvidiaA10, NvidiaA100, etc.) * `-force`: Force setup even if already set up **Examples:** ```bash theme={null} # Set up control machine tensor9 machine setup \ -type Ctrl \ -ip 10.0.1.10 \ -zone datacenter-1 # Set up isolated machine with GPU tensor9 machine setup \ -type Iso \ -ip 10.0.1.20 \ -zone datacenter-1 \ -gpu NvidiaA100 ``` *** ## Common Options These options are available on most commands: * `-apiKey`: Your Tensor9 API key (can also use `T9_API_KEY` environment variable) * `-vendorId`: Your vendor ID (usually auto-discovered from API key) * `-awsProfile`: AWS CLI profile for credentials * `-help`: Show help for the command ## Exit Codes * `0`: Success * `1`: General error * `2`: Command parsing error ## Environment Variables * `T9_API_KEY`: Your Tensor9 API key ## Getting Help For command-specific help: ```bash theme={null} tensor9 -help ``` For group-level help: ```bash theme={null} tensor9 help ``` For general help: ```bash theme={null} tensor9 help ``` ## Related Topics * [Install tensor9 CLI](/cli/install): Installation instructions * [Common Workflows](/cli/common-workflows): Step-by-step guides for common tasks * [Quick Start Guides](/getting-started/quick-start-terraform): Get started with Tensor9 # DNS and Domains Source: https://docs.tensor9.com/customer/configuration/dns-and-domains After the controller is online, you configure how users access the application. ## Domain Options ### Use Our Domain We can provide a subdomain automatically. This is the fastest way to get started - no DNS configuration required on your end. ### Bring Your Own Domain If you want the application available on your own domain (e.g., `app.yourcompany.com`), choose the custom domain option. You'll need: * A domain you control * Access to your DNS provider ## DNS Delegation When you bring your own domain, you need to delegate it to the hosted zone that the deployment creates in your cloud account. This is a one-time step. During setup, enter your root domain (e.g., `app.yourcompany.com`). The controller creates a hosted zone for your domain in your cloud account. The setup interface shows you the nameservers for this hosted zone. At whatever DNS provider currently hosts the parent domain (e.g., `yourcompany.com`), create an **NS record** for your chosen subdomain pointing to the nameservers shown in the setup interface. This delegates DNS for that subdomain to the hosted zone in your cloud account. Once delegation is in place, the controller creates all subdomain records, provisions TLS certificates, and configures any additional DNS records the application needs. The NS delegation step is the only manual DNS change you need to make. Everything else - subdomain records, certificates, email configuration - is handled automatically. ### Tips for Choosing a Domain * **Use a separate top-level domain** - e.g., `yourcompany.co` or `yourcompany.app` instead of a subdomain of `yourcompany.com`. This prevents browser cookies from being shared between the application and your primary domain, which is a security best practice. * **Keep it short** - subdomains are created under your root domain, and certificate Common Names have a 64-character limit. * **If using a subdomain**, choose something distinct - e.g., `tool.yourcompany.com` to keep it separate from your primary product domain. ## DNS Providers We support two DNS providers for managing your domain's records: ### Route 53 If your domain is managed in AWS Route 53, the system can configure DNS records automatically using your existing AWS credentials. ### Cloudflare If your domain is managed in Cloudflare, you'll provide a Cloudflare API token with DNS edit permissions for your zone. The system uses this to create the required records. ## TLS Certificates TLS certificates are provisioned automatically as part of DNS setup. You don't need to create, upload, or manage certificates. They are renewed automatically before expiration. # Private Ingress Source: https://docs.tensor9.com/customer/configuration/private-ingress By default, the application is accessible over the public internet. If your organization requires that it is only reachable over a private network, we offer private ingress options. ## Options | Option | Description | | -------------------- | ------------------------------------------------------------------- | | **Public** (default) | Accessible over the public internet | | **Allowlist** | Public-facing, but restricted to your corporate egress IP addresses | | **Tailscale** | Accessible only from your organization's Tailscale network | Regardless of which option you choose, the URL your users type stays the same. Only the network path changes. Contact us to discuss which option is right for your deployment. If you are reading this from the vendor side, the configuration-surface framing (how ingress fits into the broader set of customer-driven configuration knobs) lives at [Ingress Control](/customizations/ingress) under [Auto-Customizations](/customizations/overview). # Secrets Source: https://docs.tensor9.com/customer/configuration/secrets Some applications require credentials you provide - API keys, database passwords, webhook tokens, and similar. During configuration, the setup interface tells you exactly what's needed and how to create each secret. ## The Key Principle **Your secrets never leave your infrastructure.** You create them directly in your own cluster or cloud secret manager. The controller detects them automatically and shows their status in the setup interface. We never see or store your secret values. ## How It Works The setup interface lists every secret the application needs. For each secret, it shows: * What the secret is for (e.g., "GitHub Personal Access Token for CI") * Whether it's required or optional * The exact command to create it You create the secret in your infrastructure. The controller detects it and the status updates to a green checkmark. Optional secrets can be skipped. ## Secret Types ### Required Secrets These must be created before the deployment can proceed. The setup interface won't advance until all required secrets are detected. ### Optional Secrets These enable additional functionality but aren't required. You can skip them during setup and add them later if needed. ## What About Our Secrets? Some secrets are provided by us (for example, internal service credentials that the application needs). These are handled automatically during deployment - you don't need to create or manage them. ## Creating Secrets The setup interface generates ready-to-run `kubectl` commands. For example: ```bash theme={null} kubectl create secret generic github-token \ -n \ --from-literal=token= ``` Copy the command, replace the placeholder with your actual value, and run it. The secret goes directly into your cluster. AWS deployments store secrets in either **AWS Secrets Manager** or **AWS Systems Manager Parameter Store**, depending on what the origin stack uses. The setup interface tells you which service applies to each secret. **AWS Secrets Manager:** ```bash theme={null} aws secretsmanager create-secret \ --name \ --secret-string \ --region ``` **AWS Systems Manager Parameter Store:** ```bash theme={null} aws ssm put-parameter \ --name \ --value \ --type SecureString \ --region ``` ## Verifying Secrets Confirm your secrets were created in the correct namespace: ```bash theme={null} kubectl get secrets -n ``` The setup interface also shows a green checkmark next to each detected secret. **AWS Secrets Manager:** ```bash theme={null} aws secretsmanager list-secrets \ --filters Key=name,Values= \ --query "SecretList[].Name" ``` **AWS Systems Manager Parameter Store:** ```bash theme={null} aws ssm describe-parameters \ --parameter-filters "Key=Name,Option=BeginsWith,Values=" \ --query "Parameters[].Name" ``` The setup interface also shows a green checkmark next to each detected secret. ## Rotating Secrets ```bash theme={null} kubectl delete secret -n kubectl create secret generic \ -n \ --from-literal=key= ``` The controller detects the updated value automatically. No restart is needed in most cases. **AWS Secrets Manager:** ```bash theme={null} aws secretsmanager update-secret \ --secret-id \ --secret-string ``` **AWS Systems Manager Parameter Store:** ```bash theme={null} aws ssm put-parameter \ --name \ --value \ --type SecureString \ --overwrite ``` ## Adding Secrets Later If you skipped optional secrets during setup, you can add them at any time using the same creation command shown above. The controller polls periodically and will detect the new secret within 30-60 seconds. ## Common Issues | Symptom | Likely Cause | Fix | | ------------------- | --------------- | ------------------------------------------------------------------ | | Secret not detected | Wrong namespace | Verify you used `-n ` matching the deployment namespace | | Secret not detected | Wrong name | Copy the exact command from the setup interface to avoid typos | | Secret not detected | Detection delay | Wait 30-60 seconds and refresh the setup interface | | Symptom | Likely Cause | Fix | | ------------------- | --------------- | --------------------------------------------------------------------------------------- | | Secret not detected | Wrong region | Verify the secret is in the same region as the deployment | | Secret not detected | Wrong name | Copy the exact name from the setup interface to avoid typos | | Secret not detected | Wrong service | Confirm whether the secret belongs in Secrets Manager or Parameter Store | | Secret not detected | Detection delay | Wait 30-60 seconds and refresh the setup interface | | Permission denied | IAM issue | Verify your IAM user has `secretsmanager:CreateSecret` or `ssm:PutParameter` permission | ## Security Guarantees * Secret values are created in **your** infrastructure by **you** * They are transmitted over **your** network * We see only the **existence** of secrets (present or missing), never their **values** * The setup interface shows status indicators, not secret contents # Services Source: https://docs.tensor9.com/customer/configuration/services Depending on the application, your deployment may require service dependencies - databases, caches, message queues, or other infrastructure components. The setup interface guides you through any service decisions. ## How It Works The application defines what it needs (for example, "a relational database"). During configuration, the setup interface shows you the concrete options available in your environment and lets you choose. For example, if the application needs a relational database and you're deploying to AWS, you might see options like: * **Amazon RDS (PostgreSQL)** - Managed by AWS * **Self-hosted PostgreSQL** - You manage it yourself ## Service Categories ### Managed Services Cloud-provider-managed services (like RDS, ElastiCache, or Cloud SQL). These are provisioned automatically as part of the deployment. You choose the service, and the controller handles the rest. ### Self-Hosted Services Services you manage yourself. If you choose a self-hosted option, you'll provide connection details (hostname, port, credentials) during the secrets configuration step. ### Pre-Existing Services If you already have a compatible service running (for example, an existing PostgreSQL database), you can connect the application to it instead of provisioning a new one. ## Making Selections The setup interface shows only the options relevant to your environment and deployment. Select the services you want, and the rest happens automatically during deployment. If the application doesn't require any service selections, this step is skipped entirely. ## Changing Services Later Service selections can be modified after initial setup. Contact us to discuss changes - some service switches may require data migration. # Overview Source: https://docs.tensor9.com/customer/getting-started/overview **About this documentation** This guide walks you through installing and managing your vendor's application in your environment. Throughout these pages, **"we"** and **"us"** refer to your software vendor, and **"you"** and **"your"** refer to you, the customer. These pages cover multiple deployment environments (e.g. AWS, Kubernetes, on-prem) - your vendor may customize or limit the options shown to match the environments they support. Installing our application into your environment is a guided, step-by-step process. You'll use a web-based setup wizard that walks you through each stage, from selecting your environment to configuring DNS and secrets. You stay in control of your infrastructure and credentials at every stage. ## How It Works The installation flow has two parts, both guided by a web-based setup wizard: **Part 1 - Set up your environment.** You receive a setup link from us and open it in your browser. The setup wizard walks you through choosing your deployment environment, downloading infrastructure templates, and applying them in your own cloud account or cluster. This provisions the networking, permissions, and compute resources needed to run the application. The wizard tracks your progress and advances automatically as each step completes. **Part 2 - Configure your deployment.** Once the controller is running in your environment, the wizard moves to configuration. You configure DNS, select services, and create any required credentials. Sensitive data stays entirely within your infrastructure - it is never sent to our systems. After both parts are complete, we deploy the application into the environment you prepared. You don't need to do anything for the deployment step - the controller running in your environment handles it automatically. Once deployed, you can manage your appliance through your **Customer Portal**, a web interface where you can monitor health, review and approve operations, view deployed infrastructure, and configure release windows. ## What You'll Need * A dedicated cloud account (AWS) or Kubernetes cluster * Terraform installed on your workstation * The setup link we provide * Any credentials the application requires (API keys, tokens, etc.) See [Prerequisites](/customer/getting-started/prerequisites) for the full list. ## How Long Does It Take? Most installations complete in under an hour. The time depends on your environment and how many services need configuration. The process is designed so you can pause and resume at any point - your progress is saved automatically. ## What Happens to Your Credentials? Your credentials never leave your infrastructure. When the application needs secrets (API keys, database passwords, etc.), you create them directly in your own cluster or cloud secret manager. The controller detects them automatically. We never see or store your sensitive data. See [Security Model](/customer/security/security-model) for the full picture. # Prerequisites Source: https://docs.tensor9.com/customer/getting-started/prerequisites Before starting the installation, make sure you have the following ready. ## For All Environments * **Setup link** - We'll provide this when your deployment is ready to begin * **Terraform** - [Install Terraform](https://developer.hashicorp.com/terraform/install) (v1.0 or later) * **Web browser** - The setup flow runs in your browser ## Environment-Specific Requirements * **Dedicated AWS account** - We recommend deploying into a **new, dedicated AWS account** (not your existing production account). This provides clean isolation, avoids resource conflicts, and makes it easy to audit or tear down the deployment independently. If you use AWS Organizations, create a new member account for this purpose. * **AWS CLI** - [Install the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and configure credentials for the dedicated account * **Region selection** - Know which AWS region you want to deploy in * **EC2 vCPU quota** - Your account needs at least **32 vCPUs** for on-demand standard instances in your chosen region. New AWS accounts often have a default limit of 5. Check your current quota in the AWS console under **Service Quotas > Amazon EC2 > Running On-Demand Standard (A, C, D, H, I, M, R, T, Z) instances** and request an increase if needed. * **Kubernetes cluster** - A running cluster (v1.24 or later) with admin access, used exclusively for this application. The controller gets cluster-wide permissions. * **kubectl** - [Install kubectl](https://kubernetes.io/docs/tasks/tools/) and configure it to point at your cluster * **Cluster access** - Permissions to create namespaces, service accounts, roles, role bindings, and deployments ## Network Requirements The controller in your environment needs outbound internet access to communicate with our systems. No inbound ports are required - all communication is initiated from your side. If your environment has restricted outbound access, contact us for the list of endpoints to allow. ## Optional * **Custom domain** - If you want the application available on your own domain (e.g., `app.yourcompany.com`), have your DNS provider credentials ready (Route 53 or Cloudflare) * **Application credentials** - Any API keys, tokens, or passwords the application needs (we'll tell you exactly what's required during setup) # Choose Your Environment Source: https://docs.tensor9.com/customer/installation/choose-your-environment The first step in installation is selecting where you want to deploy. The setup interface adapts based on your choice. ## Available Environments Deploy on a dedicated EC2 instance in your AWS account. **What you'll need:** * An AWS account with permissions to create VPCs, subnets, IAM roles, security groups, S3 buckets, and EC2 instances * A preferred AWS region **Install flow:** Two-phase Terraform (infrastructure first, then controller). See [Installation Guide](/customer/installation/guide). Deploy into your existing Kubernetes cluster. **What you'll need:** * A running Kubernetes cluster (v1.24+) used exclusively for this deployment * kubectl configured with admin access **Install flow:** Single-phase Terraform. See [Installation Guide](/customer/installation/guide). The cluster you provide is dedicated to this deployment. The controller gets cluster-wide permissions, giving it full visibility and management capability within the cluster. ## Install Methods After choosing your environment, you select an install method. ### Terraform Currently the default for all environments. We generate standard `.tf` files tailored to your environment. You can review what will be created, understand every resource, and fold the configuration into your existing infrastructure-as-code workflow if you want. ### Helm (Coming Soon) For Kubernetes deployments, Helm chart support is on the way. ## What Happens Next After you select your environment and install method, the setup interface generates your Terraform configuration and walks you through downloading and applying it. * [Installation Guide](/customer/installation/guide) # Installation Guide Source: https://docs.tensor9.com/customer/installation/guide This guide walks through the installation step by step. ## Before You Begin Make sure you have: * Your setup link (provided by us) * AWS CLI installed and configured with appropriate permissions * Terraform installed (v1.0+) * Permissions to create VPCs, subnets, IAM roles, security groups, S3 buckets, and EC2 instances ## Step 1: Open Your Setup Link Open the setup link in your browser. This opens the setup wizard, a guided interface that walks you through each stage of the installation. ## Step 2: Select AWS and Your Region In the setup wizard, choose **AWS** as your environment. Select the AWS region where you want the infrastructure deployed. Pick the region closest to your users or the one that matches your compliance requirements. ## Step 3: Select Terraform as Your Install Method Choose **Terraform**. The wizard generates a Terraform configuration tailored to your selections. ## Step 4: Download and Apply the Infrastructure Template The first Terraform file creates your foundational infrastructure: * A VPC with public and private subnets * IAM roles for the controller * Security groups with minimal required access * An S3 bucket for deployment state **Download the file**, then run: ```bash theme={null} terraform init AWS_PROFILE= terraform plan # Review what will be created AWS_PROFILE= terraform apply ``` > **Important:** Replace `` with the name of the AWS CLI profile configured for your dedicated deployment account. Using ambient credentials (e.g., from a different account) can cause errors. You can check your available profiles with `aws configure list-profiles`. Review the plan output carefully - it shows exactly what resources will be created in your account. The setup wizard auto-advances when it detects the apply has completed. > **Tip:** You can review the `.tf` file before applying. Everything is standard Terraform - no custom providers or opaque modules. ## Step 5: Download and Apply the Controller Template Once the infrastructure is in place, a second Terraform file becomes available. This creates the EC2 instance that runs the controller. **Download the file**, then run: ```bash theme={null} terraform init AWS_PROFILE= terraform plan AWS_PROFILE= terraform apply ``` The controller instance: * Runs in your private subnet (no public IP) * Communicates outbound only - no inbound ports are opened * Uses the IAM role created in the previous step ## Step 6: Wait for the Controller to Come Online After the controller template is applied, the instance boots and connects to our systems. The setup wizard shows progress in real time. This typically takes 2-5 minutes. ## Step 7: Configure Your Deployment Once the controller is online, the setup wizard moves to configuration. You'll walk through each item directly in the wizard: 1. **DNS** - Choose a domain for the application. See [DNS and Domains](/customer/configuration/dns-and-domains). 2. **Services** - Select any service options relevant to your deployment. See [Services](/customer/configuration/services). 3. **Secrets** - Create any required credentials directly in your AWS account. See [Secrets](/customer/configuration/secrets). ## Step 8: We Deploy After configuration is complete, we deploy the application into the environment you prepared. The controller handles the deployment automatically - you don't need to do anything for this step. Once deployment completes, you'll have access to your **Customer Portal**, a web interface where you can monitor health, review and approve operations, view deployed infrastructure, and configure release windows. ## What Gets Created in Your Account | Resource | Purpose | | --------------- | ------------------------------------------- | | VPC | Isolated network for the deployment | | Public subnet | Load balancer and NAT gateway | | Private subnet | Controller instance (no public IP) | | NAT gateway | Outbound internet access for private subnet | | IAM roles | Permissions for the controller | | Security groups | Network access rules (minimal) | | S3 bucket | Deployment state storage | | EC2 instance | Runs the controller | All resources are tagged for easy identification in your AWS console. ## Understanding Permissions The controller's EC2 instance runs with an IAM instance profile. The role lives in your AWS account, and you can inspect it at any time: ```bash theme={null} aws iam get-role --role-name aws iam list-attached-role-policies --role-name ``` To revoke access, detach the policies from the role or delete the role entirely. See [Revoking Access](/customer/security/revoking-access) for details. ## Before You Begin Make sure you have: * Your setup link (provided by us) * A running Kubernetes cluster (v1.24+) * kubectl configured and pointing at your cluster * Terraform installed (v1.0+) * Cluster admin access (to create namespaces, service accounts, roles, and deployments) ## Step 1: Open Your Setup Link Open the setup link in your browser. This opens the setup wizard, a guided interface that walks you through each stage of the installation. ## Step 2: Select Kubernetes In the setup wizard, choose **Kubernetes** as your environment. The cluster you provide is dedicated to this deployment, and the controller gets cluster-wide permissions. ## Step 3: Select Terraform as Your Install Method Choose **Terraform**. The wizard generates a Terraform configuration tailored to your environment. ## Step 4: Download and Apply the Controller Template Kubernetes installation is a single Terraform apply. The template creates everything needed: * A dedicated namespace for the deployment * Service accounts for the controller * RBAC roles and role bindings * The controller deployment and service **Download the file**, then run: ```bash theme={null} terraform init terraform plan # Review what will be created terraform apply ``` > **Tip:** Review the plan output before applying. You'll see exactly what namespaces, roles, and service accounts will be created. Everything uses standard Kubernetes resources - no custom resource definitions or operators required. ### What the Template Creates | Resource | Purpose | | ------------------- | ------------------------------------- | | Namespace | Isolated space for the deployment | | ServiceAccounts | Identity for the controller | | ClusterRoles | Permission definitions (cluster-wide) | | ClusterRoleBindings | Bind permissions to service accounts | | Deployment | The controller itself | | Service | Network access to the controller | ## Step 5: Wait for the Controller to Come Online After applying, the controller pod starts and connects to our systems. The setup wizard shows progress in real time. This typically takes 1-3 minutes. You can also watch progress with kubectl: ```bash theme={null} kubectl get pods -n -w ``` The namespace name is shown in the Terraform output. ## Step 6: Configure Your Deployment Once the controller is online, the setup wizard moves to configuration. You'll walk through each item directly in the wizard: 1. **DNS** - Choose a domain for the application. See [DNS and Domains](/customer/configuration/dns-and-domains). 2. **Services** - Select any service options relevant to your deployment. See [Services](/customer/configuration/services). 3. **Secrets** - Create required credentials directly in your cluster. See [Secrets](/customer/configuration/secrets). ## Step 7: We Deploy After configuration is complete, we deploy the application into your cluster. The controller handles this automatically. Once deployment completes, you'll have access to your **Customer Portal**, a web interface where you can monitor health, review and approve operations, view deployed infrastructure, and configure release windows. ## Understanding Permissions The controller's service account is bound by `ClusterRole` and `ClusterRoleBinding` resources defined in the Terraform configuration you applied. Those bindings live in your cluster, and you can inspect, modify, or revoke them at any time. See [Permissions](/customer/security/permissions) and [Revoking Access](/customer/security/revoking-access). ## Next Steps * [Verify Your Installation](/customer/installation/verify) * [DNS and Domains](/customer/configuration/dns-and-domains) * [Secrets](/customer/configuration/secrets) # Installation Overview Source: https://docs.tensor9.com/customer/installation/overview The installation process is guided from start to finish. You make decisions about your environment, download standard Terraform configurations, and apply them in your own infrastructure. Your credentials never leave your environment. ## The Process Open setup link Choose your environment (AWS, Kubernetes) Choose your install method (Terraform) Download and apply infrastructure template Download and apply controller template Configure DNS, services, and secrets We deploy the application ## What Gets Created The Terraform templates create the infrastructure your deployment needs. You can review everything before applying. * A VPC with public and private subnets * IAM roles and security groups * An S3 bucket for state * An EC2 instance running the controller * A dedicated namespace * Service accounts for the controller * Role bindings for the controller * The controller deployment ## Progress Tracking The setup interface tracks progress across five phases. Completed phases get a checkmark, the active phase is highlighted, and future phases are grayed out. Both you and we can see where things stand at any time. ## Pausing and Resuming You can close the browser and come back later - your progress is saved. Just reopen the setup link and you'll pick up where you left off. ## Next Steps * [Choose Your Environment](/customer/installation/choose-your-environment) * [Installation Guide](/customer/installation/guide) # Verify Your Installation Source: https://docs.tensor9.com/customer/installation/verify After completing the installation and configuration steps, here's how to confirm everything is working. ## Check the Setup Interface The setup interface shows a progress tracker with five phases. When all phases show a green checkmark, installation is complete and the application is ready for deployment. ## Verify from Your Infrastructure ### Verify the Controller Pod ```bash theme={null} kubectl get pods -n ``` You should see the controller pod in a `Running` state with `1/1` containers ready. ### Check Controller Logs ```bash theme={null} kubectl logs -n ``` The logs should show successful startup and a connection established to our systems. ### Verify RBAC ```bash theme={null} kubectl get clusterrolebindings | grep ``` You should see the cluster role bindings created during installation. ### Verify Secrets (If Configured) ```bash theme={null} kubectl get secrets -n ``` Any secrets you created during configuration should appear here. ### Common Issues | Symptom | Likely Cause | Fix | | ---------------------------------------------- | ------------------------------- | ----------------------------------------------------- | | Pod stuck in `Pending` | Insufficient cluster resources | Check node capacity: `kubectl describe nodes` | | Pod in `CrashLoopBackOff` | Configuration error | Check logs: `kubectl logs -n ` | | Pod in `ImagePullBackOff` | Registry access issue | Verify your cluster can pull images from the internet | | Setup interface shows "Waiting for controller" | Controller hasn't connected yet | Wait 2-5 minutes; check outbound network access | ### Verify the EC2 Instance ```bash theme={null} aws ec2 describe-instances \ --filters "Name=tag:Name,Values=*controller*" \ --query "Reservations[].Instances[].{Id:InstanceId,State:State.Name,IP:PrivateIpAddress}" ``` The instance should be in a `running` state. ### Verify Connectivity The controller communicates outbound to our systems. You can verify it's connected by checking the setup interface - the controller status will show as online. ### Verify IAM Roles ```bash theme={null} aws iam list-roles --query "Roles[?contains(RoleName, 'controller')]" ``` The IAM role created during setup should appear with the expected permissions. ### Verify Secrets (If Configured) ```bash theme={null} aws secretsmanager list-secrets \ --filters Key=name,Values= \ --query "SecretList[].Name" ``` Any secrets you created during configuration should appear here. ### Common Issues | Symptom | Likely Cause | Fix | | ---------------------------------------------- | ------------------------------- | ------------------------------------------------------ | | Instance in `stopped` or `terminated` | IAM or quota issue | Check CloudTrail for errors | | Instance not starting | Insufficient instance quota | Request a limit increase in your AWS account | | Instance running but not connecting | Outbound access blocked | Verify security group allows outbound HTTPS (port 443) | | Setup interface shows "Waiting for controller" | Controller hasn't connected yet | Wait 2-5 minutes; check NAT gateway and route tables | For more detailed troubleshooting, see [Troubleshooting](/customer/operations/troubleshooting). ## Next Steps * [Monitoring and Health](/customer/operations/monitoring) - Track ongoing status * [Permissions](/customer/security/permissions) - Understand what the controller can access * [Updates and Upgrades](/customer/operations/updates-and-upgrades) - How we ship changes # Monitoring and Health Source: https://docs.tensor9.com/customer/operations/monitoring Once the deployment is running, here's how to check its status and health. ## From Your Customer Portal Your Customer Portal provides an at-a-glance view of appliance health, including status indicators (Healthy, Degraded, Unhealthy), recent activity, and quick actions. You can also view deployed infrastructure, review and approve operations, and configure release windows. ## From Your Infrastructure **Check controller status:** ```bash theme={null} kubectl get pods -n ``` **View controller logs:** ```bash theme={null} kubectl logs -n ``` **Check resource usage:** ```bash theme={null} kubectl top pods -n ``` **View recent events:** ```bash theme={null} kubectl get events -n --sort-by='.lastTimestamp' ``` **Check instance status:** ```bash theme={null} aws ec2 describe-instance-status --instance-ids ``` **View system logs:** Access via the AWS console under EC2 - Instances - your instance - Monitor and troubleshoot - Get system log. ## Application Telemetry If you and we agree to enable application telemetry, the controller can forward operational metrics to our monitoring systems. This enables us to proactively detect issues and provide better support. **What gets forwarded (when enabled):** * Application health metrics (uptime, error rates) * Resource utilization (CPU, memory) * Deployment status changes **What is never forwarded:** * Your secrets or credentials * Your other workloads' metrics * Network traffic data * User data processed by the application Telemetry is off by default and only enabled when both parties agree. ## Health Checks The controller performs regular health checks on the application: * **Pod readiness** - Are the application containers running and ready to serve traffic? * **Liveness** - Is the application responding to health probes? * **Connectivity** - Can the controller communicate with our systems? If a health check fails, the controller can take automatic remediation actions (like restarting a pod). ## Alerting We monitor the controller's connectivity from our side. If the controller goes offline or reports errors, we're aware and can reach out proactively. You don't need to set up monitoring for the controller itself - that's our responsibility. For monitoring the application's business logic and user-facing behavior, use your existing monitoring tools. The application runs in your infrastructure like any other workload. # Troubleshooting Source: https://docs.tensor9.com/customer/operations/troubleshooting Common issues and how to resolve them. ## General Issues These apply regardless of environment. ### Terraform Apply Fails **Common causes:** * **Permission denied** - Verify your cloud credentials have the required permissions (see [Prerequisites](/customer/getting-started/prerequisites)) * **Resource quota exceeded** - Check your cloud account's service limits * **State conflict** - If you re-run `terraform apply` after a partial failure, Terraform should pick up where it left off ### DNS Not Resolving **Symptom:** Domain configured but not resolving. **Check:** ```bash theme={null} dig nslookup ``` **Common causes:** * **Propagation delay** - DNS changes can take up to 48 hours to propagate (usually much faster) * **Incorrect DNS provider credentials** - Verify your Route 53 or Cloudflare credentials are correct * **Zone delegation** - For custom domains, verify you've delegated to the correct nameservers ### Can't Reach the Application Verify DNS is resolving: `dig ` Check that the load balancer / ingress is healthy. Verify TLS certificates are valid: `curl -v https://` ## Environment-Specific Issues ### Controller Pod Won't Start **Symptom:** Pod stuck in `Pending`, `CrashLoopBackOff`, or `ImagePullBackOff`. **Check pod status:** ```bash theme={null} kubectl describe pod -n ``` **Common causes:** | Status | Likely Cause | Fix | | ------------------ | ----------------------------------------- | ----------------------------------------------------------------- | | `Pending` | Insufficient CPU or memory on nodes | Scale up your cluster or free resources: `kubectl describe nodes` | | `CrashLoopBackOff` | Configuration error or missing dependency | Check logs: `kubectl logs -n ` | | `ImagePullBackOff` | Container registry access issue | Verify your cluster can pull images from the internet | ### Setup Wizard Shows "Waiting for Controller" The controller hasn't connected to our systems yet. This usually resolves in 2-5 minutes. **If it persists:** Verify the pod is running: `kubectl get pods -n ` Check logs for connection errors: `kubectl logs -n ` Verify outbound internet access from the pod's namespace. Check if network policies are blocking egress on port 443. ### Application Not Responding Check controller status: `kubectl get pods -n ` Check application pod status: `kubectl get pods -n -l app=` View application logs: `kubectl logs -n ` Check events: `kubectl get events -n --sort-by='.lastTimestamp'` Contact us with the error details. ### High Resource Usage ```bash theme={null} kubectl top pods -n kubectl top nodes ``` If the controller or application is consuming more resources than expected, contact us. It may indicate a configuration issue or a need to scale. ### Controller Instance Won't Start **Symptom:** EC2 instance in `stopped` or `terminated` state. **Check:** ```bash theme={null} aws ec2 describe-instance-status --instance-ids ``` **Common causes:** * **Insufficient instance quota** - Request a limit increase in your AWS account * **IAM role issues** - Verify the IAM role from the infrastructure template was created successfully * **Subnet issues** - Ensure the private subnet has a route to a NAT gateway for outbound access ### Setup Wizard Shows "Waiting for Controller" The controller hasn't connected to our systems yet. This usually resolves in 2-5 minutes. **If it persists:** Verify the instance is running: `aws ec2 describe-instances --instance-ids ` Check the system log: AWS console - EC2 - Instances - your instance - Monitor and troubleshoot - Get system log. Verify the security group allows outbound HTTPS (port 443). Verify the private subnet routes through a NAT gateway. ### Application Not Responding Verify the controller instance is running (see above). Check your Customer Portal for deployment errors. View the controller system log via the AWS console. Contact us with the error details. ### High Resource Usage Check CloudWatch metrics for the controller instance: ```bash theme={null} aws cloudwatch get-metric-statistics \ --namespace AWS/EC2 \ --metric-name CPUUtilization \ --dimensions Name=InstanceId,Value= \ --start-time $(date -u -v-1H +%Y-%m-%dT%H:%M:%S) \ --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \ --period 300 \ --statistics Average ``` If the controller is consuming more resources than expected, contact us. It may indicate a configuration issue or a need to resize the instance. ## Getting Help If you can't resolve an issue: * Controller logs * Controller status * Any error messages from the setup wizard or Customer Portal * When the issue started Send us the above information. The more detail you provide, the faster we can help. If we need to investigate, enable the appropriate [permission tier](/customer/security/permissions). You can revoke them as soon as the investigation is complete. # Updates and Upgrades Source: https://docs.tensor9.com/customer/operations/updates-and-upgrades After initial deployment, we ship updates to the application running in your environment. Here's how that works. ## How Updates Work We prepare a new release on our side. The controller in your environment receives the update. The controller applies the changes to the application. You don't need to do anything - updates are handled automatically. ## What Gets Updated * **Application code** - New features, bug fixes, performance improvements * **Application infrastructure** - If the update requires changes to supporting resources (a new database table, a modified IAM policy), the controller applies those via Terraform in your environment * **Configuration** - Updated environment variables, resource limits, or other application settings ## What Doesn't Change Without Notice * **Your infrastructure** - We don't modify your VPC, subnets, or security groups without telling you * **Your permissions** - We don't change the controller's role or bindings silently * **Your secrets** - We never touch secrets you created ## Your Control You always have the option to: * **Pause updates** - Freeze your deployment at a specific version for as long as needed * **Review changes** - See exactly what's in an update before it's applied * **Roll back** - If an update causes issues, the controller can revert to the previous version * **Configure release windows** - Schedule when updates can be applied to your environment * **Manage upgrades** - View available releases and control the upgrade process You can manage all of these from the **Releases**, **Release Windows**, and **Manage Upgrades** sections of your Customer Portal. ## Infrastructure Changes Occasionally, an update may require changes to supporting infrastructure (e.g., a new S3 bucket, an additional IAM policy). When this happens: * We coordinate with you before making the change * The controller applies the Terraform changes in your environment * You can review the Terraform plan before it's applied ## The Update Process The update process: a release flows from our systems to your environment, and your environment confirms back to us The update process: a release flows from our systems to your environment, and your environment confirms back to us No credentials are exchanged during this process. The controller already has the permissions it needs (from the initial installation) to deploy and manage the application. ## Upgrade Frequency Release cadence varies by application. Contact us for details about the update schedule for your specific deployment. # Frequently Asked Questions Source: https://docs.tensor9.com/customer/reference/faq ## Security ### Do you ever see my credentials or secrets? No. You create secrets directly in your own cluster or cloud secret manager. We see only whether a secret exists (a checkmark in the setup wizard), never its value. See [Credentials and Secrets](/customer/security/credentials-and-secrets). ### How do I know what permissions the controller has? The controller runs under an IAM role (AWS) or service account (Kubernetes) in your own account or cluster. You can inspect the Terraform templates before applying them - they contain the exact roles and bindings. After installation, you can list the active role and policies with kubectl or the AWS CLI. See [Permissions](/customer/security/permissions). ### Can I revoke access at any time? Yes. You own the role and bindings, and can revoke or delete them at any time without coordinating with us. Revocation is immediate. See [Revoking Access](/customer/security/revoking-access). ### How does the vendor get hands-on access if they need it? Routine support goes through commands you can review. For direct, one-off access to a specific resource - for example a live debugging session during an incident - your vendor uses break glass: they request it, you review and cryptographically approve (or reject) it, and the controller in your environment grants a scoped, time-boxed session that self-expires and is torn down afterward. Nothing happens without your signed approval. See [Break Glass](/customer/security/break-glass). ### Does the controller phone home? The controller communicates outbound to our systems for deployment instructions and to report health status. It does not forward your secrets, your data, or information about your other workloads. Application telemetry is off by default and only enabled with your agreement. ## Installation ### How long does installation take? Most installations complete in under an hour. The exact time depends on your environment, how many services need configuration, and how many secrets you need to create. ### Can I pause and resume? Yes. Your progress is saved automatically in the setup wizard. Close the browser and reopen the setup link whenever you're ready to continue. ### Can I review the Terraform before applying? Absolutely. The templates are standard `.tf` files with no custom providers or opaque modules. We encourage you to review them. See [Choose Your Environment](/customer/installation/choose-your-environment) for the AWS and Kubernetes install flows. ### What if Terraform apply fails partway through? Terraform tracks state. If an apply fails, re-running `terraform apply` will pick up where it left off. You won't get duplicate resources. ### Can I integrate the Terraform into my existing IaC workflow? Yes. The generated templates are standard Terraform. You can check them into your own repo, run them through your CI/CD pipeline, or manage them alongside your other infrastructure code. ## Configuration ### Can I use a custom domain? Yes. You can bring your own domain and configure it with Route 53 or Cloudflare. See [DNS and Domains](/customer/configuration/dns-and-domains). ### What if I don't have a custom domain? We can provide a subdomain automatically. No DNS configuration needed on your end. ### Can I skip optional secrets? Yes. Optional secrets can be skipped during setup and added later if needed. Required secrets must be created before the deployment can proceed. ### How do I rotate a secret? Replace the secret in your cluster or cloud secret manager. The controller detects the change automatically. See [Secrets](/customer/configuration/secrets). ## Operations ### How are updates applied? We prepare the release, and the controller in your environment applies it automatically. You don't need to take action for routine updates. See [Updates and Upgrades](/customer/operations/updates-and-upgrades). ### Can I pin to a specific version? Yes. You can manage version pinning and release windows from the **Manage Upgrades** and **Release Windows** sections of your Customer Portal. Contact us if you need additional help freezing at a specific version during an audit or compliance review. ### What monitoring do I need to set up? For the controller and deployment infrastructure - nothing. We monitor the controller's health from our side. For the application's business logic and user-facing behavior, use your existing monitoring tools. See [Monitoring](/customer/operations/monitoring). ### What happens if the controller goes offline? The application continues running - it doesn't depend on the controller for normal operation. We won't be able to deploy updates or monitor health until the controller reconnects. We'll be aware of the outage from our side. ## Network ### What outbound access does the controller need? The controller needs outbound HTTPS (port 443) to communicate with our systems. No inbound ports are required. Contact us for the specific endpoint list if your network has restricted outbound access. ### Does the controller need a public IP? No. The controller runs in a private subnet (AWS) or within your cluster's internal network (Kubernetes). All communication is outbound. # Break Glass Source: https://docs.tensor9.com/customer/security/break-glass Your vendor runs their software inside your environment with no standing access - no SSH key, no kubectl context, no database password. Most support work goes through vetted, reviewable commands. Occasionally, though, your vendor needs to reach one specific resource directly and interactively: a live `kubectl` session against a crash-looping cluster, or a database connection during an incident - work that can't be pre-scripted as a single command. Break glass is the consented, time-boxed path for that. Nothing happens without an approval **you** sign. You review each request, approve or reject it, hold the key that authorizes it, can end a session at any time, and control the network it runs over. Break glass flow: you review the request, approve and sign it with your own key, the controller in your environment verifies your signature, and a bounded, self-expiring session is created. Break glass flow: you review the request, approve and sign it with your own key, the controller in your environment verifies your signature, and a bounded, self-expiring session is created. ## What break glass is, and your role Reaching a resource takes two things: a network path to it, and a credential to authenticate once there. Break glass assembles both, only for the session you approved, and tears them down when it ends. * **The network path.** If the resource is already reachable, none is created. If it's private, a temporary path is stood up into your network for the session and removed at the end (see [Preparing your network](#preparing-your-network-for-a-private-target)). * **The credential.** For a Kubernetes cluster, the controller mints a fresh, short-lived token, and at session end revokes it by deleting the minted identity behind it (the token also self-expires on its own clock). For most other resources, a credential is produced by an operational command you approve (for example, a script that reads a database password from your secret store or creates a short-lived login). Sometimes your vendor authenticates with a credential they already hold, and break glass only opens the network path. Your role throughout is to **approve and govern** all of this: it is your signature that authorizes a session, your key that the controller verifies, the network isolation that bounds a session's reach, and your decision to end a session early. This is the same posture as the rest of your relationship with the vendor - see [Security Model](/customer/security/security-model) - applied to one-off, hands-on access. This is distinct from the standing [permission tiers](/customer/security/permissions) you grant the controller. Those are broad, ongoing IAM/RBAC grants you toggle on and off. Break glass is a single, cryptographically-approved, self-expiring session scoped to one resource. For routine investigation you might enable an [on-demand permission tier](/customer/security/permissions#on-demand-you-control-these) and revoke it afterward; break glass is for when the vendor needs direct, bounded access to one specific resource. ## Reviewing and approving a request When your vendor requests break glass, you're notified over your existing channel and given a link to an approval page. Opening it shows you the full request: | What you're approving | What it means | | --------------------- | -------------------------------------------------------------------------------------- | | Resource | The one resource your vendor may reach | | Privilege | The access level they will have on it | | Duration | How long the session may stay active | | Network path | How they will reach the resource | | Command (if any) | For a command-produced credential, the exact command and the exact inputs it runs with | You then **approve or reject**. Read the request against the questions below. Everything the session is allowed to do is in the fields above - there is nothing hidden. Reject to stop it cold (you can include a reason, which is sent back to your vendor), or continue to approve. Approving means signing the request with your own key. The approval page gives you a short script that signs locally; your private key never enters the browser or reaches us. See [Signing Keys](/customer/security/signing-keys) for the one-time key setup. The controller in your environment re-verifies your signature against your pinned public key, then assembles the session. It becomes active only once everything it needs is ready. **How to decide.** Before you approve, ask: * Does the stated reason justify direct access to this resource? * Is the privilege level acceptable? Be aware that the Kubernetes path mints a **cluster-admin-tier** credential - you are signing off on that level of access, scoped to that one cluster for that one session. * Is the requested duration reasonable for the work? * Is this a resource you're willing to expose for a hands-on session right now? **What your signature commits to.** The signature covers exactly the fields above and nothing else. After you sign, the request cannot be silently pointed at a different resource, widened in privilege, or extended in duration - any change breaks the signature and the controller rejects it. Approvals are single-use and valid only briefly, so a captured approval can't be replayed later. The requested duration is the length of the usable session and starts when the session becomes **active** (after you approve), so time spent waiting on your decision doesn't count against it. **Ending a session early.** Once a session is active, you can end it at any time; teardown runs immediately and your vendor loses access. Because the credential also self-expires on its own clock, ending early and letting it lapse reach the same result - no standing access remains. Ending a break glass session is not the same as [revoking access](/customer/security/revoking-access). Revoking access removes a *standing permission tier* you granted the controller. Ending a break glass session ends *one approved session*. Both are yours to control; they operate at different scopes. ## Preparing your network for a private target If the resource your vendor needs to reach isn't already reachable, break glass stands up a temporary, session-scoped path into your network - a Tailscale subnet router or a Twingate connector - created with single-use keys and removed when the session ends. It exists only for that one session. This is a deliberate, temporary exception to the otherwise outbound-only posture described under [No Inbound Network Access](/customer/security/security-model#no-inbound-network-access). It is created only after you approve a request, and only for that session. The temporary path is placed inside the network where your deployment runs, so a break glass session can reach whatever that network can reach, and no more. The tailnet the router joins is operated by your vendor, who manages its access controls; what's yours to control is the deployment's own network boundary. Your lever is **how you isolate the deployment**: keeping it in a dedicated account, VPC, or namespace (as recommended when you [set up your environment](/customer/getting-started/prerequisites)) keeps a break glass session walled off from your other resources. ## Guarantees and audit A few properties hold for every session: * **You are the only party that can authorize access.** Your vendor's control plane coordinates the workflow but is not trusted to grant access. The decision is made by the controller in your own account, which verifies your signature against a key only you hold. Even a fully compromised control plane cannot mint access, widen it, redirect it, or extend it - all of those require your key, which is never on the vendor's side. * **Least privilege means scoped and time-boxed.** A session reaches exactly the resource you approved, for the one session, for the duration you approved (your own environment - for example a cluster's maximum token lifetime - may shorten it further, but never extend it). It does *not* mean minimal permissions within the resource: the Kubernetes path is cluster-admin-tier. What's bounded is reach and lifetime, not the privilege level you signed off on. * **Nothing privileged is left behind.** Minted credentials are short-lived, delivered to your vendor once, never stored durably, and explicitly deleted at session end along with any temporary network path. Between sessions, nothing break glass created remains in your account. * **Fail-closed.** If your signature can't be verified, or the session can't be assembled, nothing is minted and the session does not go active. If teardown is interrupted, the credential still lapses on its own clock. **Your audit record.** The request, your signed approval, and teardown are recorded; credential delivery is logged on a best-effort basis. Your signed approval is the durable, non-repudiable evidence of what you consented to - an Ed25519 signature over the exact approved fields, re-verifiable against your pinned public key. This sits alongside the infrastructure-level audit you already keep ([Auditing Permission Changes](/customer/security/revoking-access#auditing-permission-changes), e.g. your cloud audit log). A turnkey command-line tool exists today for verifying *operational command* approvals; it does not yet cover break glass sessions. The cryptographic evidence - your signature over the approved request, preserved on the session record - is the same either way. ## Related * [Signing Keys](/customer/security/signing-keys) - set up the key you sign approvals with * [Security Model](/customer/security/security-model) - the trust boundary this builds on * [Permissions](/customer/security/permissions) - the standing access tiers, contrasted with a break glass session * [Revoking Access](/customer/security/revoking-access) - revoking standing access, and the audit log # Credentials and Secrets Source: https://docs.tensor9.com/customer/security/credentials-and-secrets This page covers how credentials flow through the system and the guarantees we make about their handling. ## Credential Ownership There are three categories of credentials in a deployment: ### Credentials You Provide These are secrets the application needs from you - API keys, tokens, database passwords, webhook URLs. During setup, the interface tells you exactly what's required and gives you the commands to create them. **How they flow:** You run a command on your workstation (kubectl or AWS CLI). The secret goes directly from your terminal into your cluster or cloud secret manager. The controller detects the secret exists. The setup interface updates to show a checkmark. At no point does the secret value pass through our systems. The path is: **your terminal - your infrastructure**. We see only the checkmark. ### Credentials We Provide Some secrets come from us - internal service credentials, license keys, or pre-configured tokens. These are injected automatically during deployment. You don't need to create or manage them. ### Credentials Generated at Deploy Time Some secrets are generated fresh during deployment - random passwords, internal tokens, auto-generated keys. These are created directly in your infrastructure by the controller. They never transit through our systems. ## Where Secrets Are Stored Kubernetes Secrets (in your namespace), created by you via kubectl. AWS Systems Manager Parameter Store or AWS Secrets Manager (in your account), created by you via AWS CLI or console. ## During Part 2 Configuration After the controller is online, some deployments have a second configuration step where you provide secrets and domain settings. This step uses a direct connection: You run a setup command on your workstation. A local server starts on your machine (localhost only). The server communicates directly with the controller **over your network**. Secrets travel from your workstation to your controller. The connection never leaves your infrastructure. This means even during interactive configuration, your secrets stay within your network perimeter. ## Secret Rotation You can rotate any secret you created by replacing it in your secret store: ```bash theme={null} kubectl delete secret -n kubectl create secret generic -n --from-literal=key= ``` **AWS Secrets Manager:** ```bash theme={null} aws secretsmanager update-secret --secret-id --secret-string ``` **AWS Systems Manager Parameter Store:** ```bash theme={null} aws ssm put-parameter --name --value --type SecureString --overwrite ``` The controller detects the change automatically. In most cases, no application restart is needed. ## What We Guarantee * **We never see your secret values** - only existence/absence status * **We never store your credentials** on our infrastructure * **We never transmit your secrets** over the network between your infrastructure and ours * **You can rotate at any time** without coordinating with us ## Related * [Secrets (Configuration)](/customer/configuration/secrets) - How to create secrets during setup * [Security Model](/customer/security/security-model) - The overall security architecture # Permissions Source: https://docs.tensor9.com/customer/security/permissions The controller operates with a tiered permission model. Some permissions are always active (the controller needs them to do its job), and others are on-demand - you control whether they're enabled. ## Permission Tiers ### Always Active These permissions are required for the controller to function: | Tier | What It Does | Why It's Needed | | ---------------- | -------------------------------------------------------------- | -------------------------------------------------------------------- | | **Steady-state** | Monitors the application's health, reads pod status and events | The controller needs to know if the application is running correctly | | **Install** | Creates and updates infrastructure resources | Required for initial deployment and upgrades | | **Deploy** | Creates and modifies application workloads | Required to deploy new versions of the application | ### On-Demand (You Control These) These permissions enable troubleshooting and operational support. They are off by default and can be enabled or disabled at any time: | Tier | What It Does | When You'd Enable It | | ------------------------- | ------------------------------------------------ | -------------------------------------------------------------- | | **Read-only operations** | View logs, describe resources, check events | When we need to diagnose an issue without making changes | | **Read-write operations** | Restart pods, apply patches | When we need to remediate an issue (e.g., restart a stuck pod) | | **Admin operations** | Execute commands inside containers, port-forward | For deep troubleshooting of complex issues | ## What the Controller Cannot Do Regardless of which permission tiers are enabled: * **Cannot access resources outside the deployment** - The controller is scoped to the cluster dedicated to this deployment * **Cannot read your secrets** - The controller can detect whether required secrets exist, but cannot read their values from outside its scope * **Cannot access your cloud account beyond the deployment** - IAM roles (AWS) or service accounts (Kubernetes) are scoped to the specific resources the deployment manages ## Implementation Details Each permission tier corresponds to: * A **ClusterRole** - defines what actions are allowed on what resources * A **ClusterRoleBinding** - grants those permissions to a service account * A **ServiceAccount** - the identity the controller uses This gives the controller cluster-wide visibility, which is appropriate because the entire cluster is dedicated to this deployment. The controller's EC2 instance runs with an IAM instance profile. The attached IAM role grants: * Access to the S3 bucket used for deployment state * Permissions to manage the specific resources the deployment created * No access to resources outside the deployment's scope ## Viewing Current Permissions List the active cluster role bindings: ```bash theme={null} kubectl get clusterrolebindings | grep ``` View the IAM role attached to the controller instance: ```bash theme={null} aws iam get-role --role-name aws iam list-attached-role-policies --role-name ``` ## Related * [Revoking Access](/customer/security/revoking-access) - How to disable specific permission tiers * [Security Model](/customer/security/security-model) - The overall security architecture # Revoking Access Source: https://docs.tensor9.com/customer/security/revoking-access You can disable individual permission tiers, re-enable them later, or remove all access entirely. Revocation is immediate - there is no delay or grace period. This page is about the **standing permission tiers** you grant the controller. Ending a single [break glass](/customer/security/break-glass) session is a separate control, at a different scope - see that page for one-off, approved sessions. ## What Happens When Permissions Are Revoked Regardless of environment, here's what to expect when you revoke a permission tier: | Revoked Tier | Impact | | --------------------- | ------------------------------------------------------------------------------------------------------ | | Steady-state | We lose visibility into application health. We won't know if the application is running or has issues. | | Install | We cannot provision infrastructure or perform upgrades. The current deployment continues running. | | Deploy | We cannot deploy new versions. The current version continues running. | | Read-only operations | We cannot view logs or events for troubleshooting. | | Read-write operations | We cannot restart pods or apply patches. | | Admin operations | We cannot execute commands in containers or port-forward. | ## Disabling Permissions To disable a specific permission tier (e.g., prevent us from restarting pods), delete the corresponding cluster role binding. ```bash theme={null} kubectl delete clusterrolebinding ``` Once deleted, the controller immediately loses that capability. We cannot perform actions that require that permission tier until you re-enable it. ### Example: Disable Admin Operations If you enabled admin operations for a troubleshooting session and want to revoke it: ```bash theme={null} kubectl delete clusterrolebinding ``` The controller can no longer execute commands inside containers or port-forward. Read-only and read-write operations (if enabled) are unaffected. The controller's EC2 instance uses an IAM instance profile with attached policies. To restrict what the controller can do, detach specific policies from its IAM role. ### View Current Policies ```bash theme={null} aws iam list-attached-role-policies --role-name ``` ### Detach a Policy ```bash theme={null} aws iam detach-role-policy \ --role-name \ --policy-arn ``` Once detached, the controller immediately loses the permissions granted by that policy. ## Re-Enabling Permissions To restore a permission tier you previously disabled, re-apply the role binding. The Terraform configuration you applied during installation contains the definitions. You can either: 1. Re-run `terraform apply` to restore all bindings to their original state 2. Create the specific role binding manually with kubectl Re-attach the policy to the role: ```bash theme={null} aws iam attach-role-policy \ --role-name \ --policy-arn ``` The Terraform configuration you applied during installation contains the full policy definitions. You can also re-run `terraform apply` to restore everything to its original state. ## Revoking All Access ### Option 1 - Delete all cluster role bindings ```bash theme={null} kubectl get clusterrolebindings | grep | awk '{print $1}' | xargs kubectl delete clusterrolebinding ``` ### Option 2 - Delete the namespace ```bash theme={null} kubectl delete namespace ``` This removes everything - the controller, its permissions, its service accounts, and all resources in the namespace. The application will stop running. Use this only if you need to completely remove the deployment. ### Option 1 - Detach all policies First, list all policies attached to the controller's IAM role: ```bash theme={null} aws iam list-attached-role-policies \ --role-name \ --query 'AttachedPolicies[].PolicyArn' \ --output text ``` Then, for each policy ARN returned, run: ```bash theme={null} aws iam detach-role-policy \ --role-name \ --policy-arn ``` The controller instance keeps running but can no longer call any AWS APIs. ### Option 2 - Stop the instance ```bash theme={null} aws ec2 stop-instances --instance-ids ``` This stops the controller entirely. ### Option 3 - Detach the instance profile ```bash theme={null} aws ec2 disassociate-iam-instance-profile \ --association-id ``` Once the instance profile is detached, the controller loses all IAM permissions immediately. ## Auditing Permission Changes Kubernetes records all RBAC changes in the audit log. You can verify when bindings were created, modified, or deleted: ```bash theme={null} kubectl get events -n --field-selector reason=Created ``` All IAM changes are recorded in AWS CloudTrail. You can verify when policies were attached, detached, or roles modified: ```bash theme={null} aws cloudtrail lookup-events \ --lookup-attributes AttributeKey=ResourceName,AttributeValue= \ --max-results 20 ``` ## Related * [Permissions](/customer/security/permissions) - What each permission tier allows * [Security Model](/customer/security/security-model) - The overall security architecture * [Break Glass](/customer/security/break-glass) - Ending a single approved session, distinct from revoking a standing tier # Security Model Source: https://docs.tensor9.com/customer/security/security-model Every design decision starts from the principle that your infrastructure, your credentials, and your data stay under your control. ## Core Principles ### Your Credentials Never Leave Your Infrastructure When the application needs secrets (API keys, passwords, tokens), you create them directly in your own cluster or cloud secret manager. The controller running in your environment detects them automatically. We never see, transmit, or store your secret values. This eliminates an entire class of security concerns: * No credential leaks on our side - we never had the credentials * No "rotate the credentials you gave us" conversations * No incident response for a credential breach we caused ### No Inbound Network Access The controller in your environment communicates outbound only. No inbound ports are opened, no public IPs are assigned (for VM deployments), and no ingress rules are created for the controller itself. All communication between the controller and our systems is initiated from your side. This is true during deployments of the application as well. The controller establishes a secure mutual TLS connection to our deployment orchestration service. Our infrastructure and your controller must both verify the authenticity of each other's certificate generated during setup in order to establish this connection. When a deployment is ready, our deployment orchestration sends requests over this connection to your controller to update the application's infrastructure. The outbound mutual TLS connection is established over the public internet by default. This works across every combination of vendor and customer environment. The vendor may additionally offer private network paths for this connection: * **AWS PrivateLink** (when both the vendor's control plane and your controller are on AWS). The mutual TLS handshake is identical, but the underlying route travels the private AWS backbone instead of the public internet. Cross-region attachments are supported, so your controller can live in any AWS region the vendor permits for the form factor. * **Vendor-provided Tailscale**. The vendor operates a tailnet; your controller joins it at boot using a pre-auth key the vendor supplies, and the mutual TLS handshake travels the tailnet. Works on any cloud where Tailscale can run. Which paths are available is a per-form-factor choice the vendor makes. Please notify your vendor if you have a preference on connectivity options. See [Connectivity](/fundamentals/connectivity#appliance-to-control-plane) for a comparison. There is one deliberate, temporary exception. If you approve a [break glass](/customer/security/break-glass) request for a resource that isn't already reachable, a short-lived, session-scoped network path is opened into your environment for that session and removed when it ends. It exists only after you sign an approval, only for the session you approved, and its reach is bounded by how you've isolated your deployment. Outside of a session you approve, the controller remains outbound-only. ## The Trust Boundary Trust boundary between your infrastructure and ours Trust boundary between your infrastructure and ours ## What We Can See * Whether the controller is online and healthy * Deployment status (succeeded, failed, in progress) * Which secrets exist (but not their values) * Application health metrics (if telemetry is enabled - see below) ## What We Cannot See * Your secret values (API keys, passwords, tokens) * Network traffic within your environment ## Telemetry If you and we agree to enable application telemetry, the controller can forward operational metrics (CPU usage, error rates, latency) to our monitoring systems. This is: * **Off by default** - only enabled when both parties agree * **Limited to application metrics** - no access to your infrastructure metrics * **Configurable** - you control what gets forwarded ## Detailed Topics * [Permissions](/customer/security/permissions) - What the controller can and cannot access, in detail * [Revoking Access](/customer/security/revoking-access) - How to disable permissions or remove access entirely * [Credentials and Secrets](/customer/security/credentials-and-secrets) - How credentials are handled, in detail * [Break Glass](/customer/security/break-glass) - How you approve and control one-off, hands-on vendor access * [Signing Keys](/customer/security/signing-keys) - The key you use to approve requests # Signing Keys Source: https://docs.tensor9.com/customer/security/signing-keys Some actions your vendor takes need your explicit, cryptographic consent: a [break glass](/customer/security/break-glass) request, or an operational command your vendor runs against your environment. You consent by **signing** the request with a private key that only you hold, and the controller running in your environment verifies your signature before it acts. This page covers how you set up and manage that signing key. Setup is one-time, and the same key is reused for every request you approve. ## Why you hold the key Your signature is your consent. The private key lives only in your environment and never travels to us, so: * We cannot forge an approval. The controller verifies every signature against the public key *you* pinned, using a key we never hold. * We cannot act without you. No signed request means no action - the controller refuses it. * Even if our systems were fully compromised, an attacker still could not approve anything in your name, because the key is not on our side to steal. ## The keypair | Property | Value | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Algorithm | Ed25519 | | Private key | Lives only in your own secret store - the same backends you already use for application secrets. Your browser never touches it. | | Public key | Pinned into the controller's secret store in your own environment, at a path the controller reads to verify your signatures. The public key is non-secret, but it never has to leave your environment - and we never see the private key. | ## Setting it up The first time you approve something, the approval page detects you have no key yet and walks you through these steps, generating the exact commands for your environment. You only do this once. On your workstation, generate an Ed25519 keypair. The private key stays on your machine in this step. ```bash theme={null} openssl genpkey -algorithm Ed25519 -out private.pem openssl pkey -in private.pem -pubout -out public.pem ``` Save the private key into your own secret store: it lives there from now on, not on your laptop, and never with us. Write the public key to the location the controller reads, so it can verify the requests you sign. The approval page polls until it sees the pinned key, then continues. Where the keys live, by environment: The private key goes into a Kubernetes Secret in your namespace; the public key is pinned as a separate Secret the controller reads. ```bash theme={null} # private key - stays in your cluster kubectl create secret generic signing-key -n --from-file=private.pem # public key - pinned for the controller to verify against kubectl create secret generic signing-pubkey -n --from-file=public.pem ``` The private key goes into AWS Systems Manager Parameter Store as a SecureString (KMS-encrypted at rest); the public key is pinned as a separate parameter the controller reads. ```bash theme={null} # private key - stays in your account, encrypted at rest aws ssm put-parameter --name --type SecureString --value "$(cat private.pem)" # public key - pinned for the controller to verify against aws ssm put-parameter --name --type String --value "$(cat public.pem)" ``` The exact secret names and paths are supplied by the approval page for your environment. These are the same secret stores described in [Credentials and Secrets](/customer/security/credentials-and-secrets); your signing key is stored exactly like your application secrets. Signing key setup is available on AWS and Kubernetes environments today. Support for other environments is on the roadmap. ## Signing a request When you approve a request, the approval page gives you a short script to run on a host that can read your key store. The script fetches your private key, signs the request's canonical bytes locally with `openssl pkeyutl`, and submits only the resulting signature. Your private key never enters the browser and never travels to us. We receive the signature, not the key. See [Break Glass](/customer/security/break-glass) for what a request looks like when you review and sign it. ## Rotating your key Rotate your signing key whenever you'd rotate any sensitive key - a retired workstation, a suspected laptop compromise, an employee with access leaving, or routine policy. Run the same generate step on the new workstation. Pin it the same way as the initial setup. You can pin the new key alongside the old one or replace it. Leave it pinned and prior approvals signed with it keep verifying until they expire, or unpin it and they stop verifying immediately. Either way, the historical record stays intact: each past signature records the key fingerprint it was signed with and continues to verify against that fingerprint, not whatever is pinned now. ## Recovery Because the private key lives in your own secret store (not only on a laptop), it survives workstation loss: any workstation with access to your secret store can fetch it and resume signing. If the secret itself is deleted, generate a fresh keypair and pin it - the setup flow is the same as the first time. ## What we guarantee * **Your private key never leaves your environment** - it lives in your secret store and is read only on the host where you sign. * **We never see or store your private key** - we receive signatures, never key material. * **You can rotate or remove it at any time** without coordinating with us. ## Related * [Break Glass](/customer/security/break-glass) - where you use this key to approve a vendor access request * [Credentials and Secrets](/customer/security/credentials-and-secrets) - the secret stores your signing key lives in * [Security Model](/customer/security/security-model) - the overall trust boundary # Controller Connectivity Source: https://docs.tensor9.com/customizations/connectivity Customer-chosen network path between the appliance and your control plane Every customer appliance establishes an outbound, mutually-authenticated connection to your control plane. Telemetry, deploy instructions, and [operations](/fundamentals/operations) commands all travel over that one connection. Your customer (within the form factor's permitted paths) chooses which network it runs on. Three controller-connectivity options: Public internet (default), AWS PrivateLink (aws-only), and Tailscale (tailnet-only). The customer's appliance reaches your Tensor9 Controller via one of these paths. Three controller-connectivity options: Public internet (default), AWS PrivateLink (aws-only), and Tailscale (tailnet-only). The customer's appliance reaches your Tensor9 Controller via one of these paths. This page is the customer-customizable knob for the **appliance-to-control-plane** link only. It is internal infrastructure: telemetry and management traffic travel over it, end-user requests do not. Your customer's end users never see it. The end-user-facing link is configured separately on [Ingress Control](/customizations/ingress). ## Controller connectivity options | Option | What it does | When customers pick it | Compliance lever | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Public internet** *(default)* | The appliance reaches your control plane over public HTTPS. The connection is mutually-TLS authenticated end-to-end. | Adoption-friendly default. The customer is comfortable with outbound HTTPS from their environment. | None directly. Common starting posture; the customer's egress proxy and TLS-inspection appliances handle policy. | | **AWS PrivateLink** | The appliance reaches your control plane over an AWS PrivateLink endpoint. Cross-region attachments supported. Requires both the customer environment and your control plane to be on AWS. | The customer wants no public-internet path between their environment and you. | Eliminates a common third-party-risk finding: there is no public route by which a compromised credential could reach your surface. The PrivateLink endpoint is documentation-friendly evidence for the customer's auditor. | | **Tailscale** | You operate a [Tailscale](https://tailscale.com) tailnet. Both your control plane and the customer's appliance join it, and the connection travels the tailnet. | The customer prefers Tailscale to public-internet egress, or the appliance lives somewhere outbound HTTPS is awkward (heavily-restricted on-prem environments, lab segments, etc.). | Eliminates the public path with a control the customer's security team already understands. Tailnet membership and ACLs become the authorization boundary. | ## Configuring an option Controller connectivity is set on the [form factor](/fundamentals/key-concepts#form-factor) you author. The form factor declares which of the three options it permits; your customer picks from those permitted options at appliance setup time. * **Public internet** is the default and requires no additional configuration beyond outbound HTTPS egress from the appliance's environment. * **AWS PrivateLink** requires you to expose a Service Endpoint in your control plane's AWS account and your customer to accept the corresponding Interface Endpoint in their account. Cross-region attachments are supported. * **Tailscale** requires you to operate a tailnet and provision an appliance auth key as part of the appliance setup configuration. ## How this fits into the bigger network picture Tensor9 manages four distinct network links in total: application ingress, operator to control plane, appliance to control plane (this page), and break-glass access. The full architectural view, including the your-side-only links, is on [Connectivity](/fundamentals/connectivity). This section's pages focus only on the two customer-customizable links: this one and [Ingress Control](/customizations/ingress). # Customer-Provided Services Source: https://docs.tensor9.com/customizations/customer-provided-services Customer-supplied services (managed or self-hosted) in place of default-shipped ones Three customer-provided services the customer can substitute into the deployed install (Temporal, PostgreSQL, and MongoDB), each available as managed or self-hosted. Arrows fan from the origin stack to each service card. Three customer-provided services the customer can substitute into the deployed install (Temporal, PostgreSQL, and MongoDB), each available as managed or self-hosted. Arrows fan from the origin stack to each service card. Some of your customers already operate the services (managed or self-hosted) that your application depends on, and want your application to use those instances instead of the default equivalent. Tensor9 lets your customer plug their own service into the install, and the compiler emits a deployment stack that wires to it. ## How this is different from service equivalents [Service adapters](/service-adapters/overview) covers the **automatic** substitution: when the compiler maps RDS to Cloud SQL because the appliance is on GCP. The mapping happens automatically based on the appliance's [form factor](/fundamentals/key-concepts#form-factor); your customer does not pick. Customer-provided services is the **customer-driven** substitution: your customer explicitly says "use my Temporal" and the compiler honors that. The substitution is a customer-supplied configuration knob, not a form-factor default. The two mechanisms compose. A customer on GCP can use the default-shipped Cloud SQL (service equivalent) while bringing their own Temporal (customer-provided service). ## Services customers can provide today | Service | Compliance lever | Page | | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | **Temporal** workflow orchestration | Data residency for workflow history; the customer's existing Temporal (Temporal Cloud or self-hosted) already lives inside their certified perimeter and their incident-response runbook. | [Temporal](/customizations/customer-provided-services/temporal) | | **PostgreSQL** relational database | Regulated data stays on the customer's already-certified database (RDS in their VPC, on-prem PostgreSQL behind their CA, etc.). Backup, patch cadence, and DBA ownership are the customer's. | [PostgreSQL](/customizations/customer-provided-services/postgresql) | | **MongoDB** document database | Document data stays in the customer's existing MongoDB (Atlas or self-hosted), under their existing access controls, network access, and backup configuration. PrivateLink to Atlas is the recommended production posture when the customer is on Atlas. | [MongoDB](/customizations/customer-provided-services/mongodb) | | **Elasticsearch** family (Elasticsearch, Lucenia, OpenSearch) | Search and log-aggregation indices stay on the customer's existing cluster, under their existing access controls, snapshot policy, and observability. Lucenia is the recommended commercial alternative (Tensor9 partner). | [Elasticsearch](/customizations/customer-provided-services/elasticsearch) | | **Kafka** event streaming | Event streams stay on the customer's existing brokers (Confluent Cloud, MSK, Strimzi, Redpanda), so retention policy, ACL administration, and cross-region replication remain the customer's responsibility. | [Kafka](/customizations/customer-provided-services/kafka) | | **Redis** cache, session store, pub/sub | Cache, session, and rate-limit state stay on the customer's existing Redis (ElastiCache, MemoryDB, Memorystore, self-hosted), under their existing eviction policy, snapshot schedule, and observability. | [Redis](/customizations/customer-provided-services/redis) | | **Valkey** cache (Linux Foundation BSD-licensed fork of Redis) | Customers who want the Redis API on a vendor-neutral OSS license. AWS ElastiCache for Valkey, GCP Memorystore for Valkey, and self-hosted Valkey are all fully supported. Separate from Redis Inc. licensing. | [Valkey](/customizations/customer-provided-services/valkey) | For each supported service, the customer can plug in either a managed equivalent (Temporal Cloud, Aurora, Atlas) or their self-hosted instance. The compiler emits the right deployment stack either way. The currently-supported set is what's listed here. The set is actively expanding as customer requirements drive new substitutions; if you have a customer who needs to bring their own service that is not on this list, [contact us](mailto:hello@tensor9.com). ## What your customer configures For each supported service, your customer supplies: * A connection endpoint or address. * The credentials the appliance needs to authenticate to the service. * Any service-specific configuration (namespace, database name, bucket name, and so on). Tensor9 stores this configuration as part of the appliance's setup and the compiler reads it to emit the right deployment stack. # Elasticsearch (and family) Source: https://docs.tensor9.com/customizations/customer-provided-services/elasticsearch Customer-provided Elasticsearch / Lucenia / OpenSearch deployment in place of default-shipped search Customer-provided Elasticsearch family: the origin stack uses the Elasticsearch API; the customer can plug in a managed cloud (Elastic Cloud or AWS OpenSearch Service), Lucenia (self-hosted with commercial license), or self-hosted OpenSearch / Elasticsearch OSS. Customer-provided Elasticsearch family: the origin stack uses the Elasticsearch API; the customer can plug in a managed cloud (Elastic Cloud or AWS OpenSearch Service), Lucenia (self-hosted with commercial license), or self-hosted OpenSearch / Elasticsearch OSS. If your origin stack uses the Elasticsearch / OpenSearch API for full-text search, log aggregation, or vector search (kNN), your customer can supply their own search cluster and the compiler will emit a deployment stack that talks to it instead of provisioning a default-shipped cluster. This page covers the wire-compatible Elasticsearch-family API surface. Three substitution paths are supported: 1. **Managed cloud** - [Elastic Cloud](https://www.elastic.co/cloud) (Elastic, Inc.'s SaaS) or [AWS OpenSearch Service](https://aws.amazon.com/opensearch-service/) (AWS's managed OpenSearch). Both are managed by their respective vendors; you supply only the endpoint + credentials. 2. **[Lucenia](https://lucenia.io/)** - self-hosted with a paid commercial license. Founded by the original OpenSearch creator at AWS, Lucenia is 100% wire-compatible with the Elasticsearch / OpenSearch API and adds multimodal search (vector + geospatial + numerics + timestamps + IP). Tensor9 has a [direct partnership with Lucenia](https://www.tensor9.com/resources/how-lucenia-is-building-consistent-self-hosted-deployments-with-tensor9/). 3. **Self-hosted OSS** - OpenSearch or Elasticsearch OSS that the customer runs themselves on Kubernetes or VMs / bare metal. ## What the customer provides | Field | Purpose | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | **Cluster URL** | The `https://host:port` endpoint of the coordinating nodes. | | **Username + password** *(or API key)* | The credential the appliance authenticates with. Elastic Cloud + Lucenia + OpenSearch all support API keys; basic auth works for legacy clients. | | **CA certificate** *(optional)* | The trust anchor for the cluster's TLS certificate when the customer uses a private CA. | | **Index prefix** *(optional)* | A namespace prefix so the appliance's indices do not collide with other tenants on the same cluster. | The customer provisions the cluster, creates the role mapping the appliance authenticates as, and grants index-level privileges (read/write on the appliance's index prefix; cluster monitor if your application reads health). ## What changes in the deployment stack When customer-provided Elasticsearch is selected, the compiler: * Drops the default-shipped OpenSearch cluster from the deployment stack (no managed-service resource, no in-cluster StatefulSet, no automated snapshot repository). * Wires the appliance's services to the customer's cluster URL. * Injects the customer's credentials as secrets the services read at startup. * Sets the index prefix the appliance writes under. Your application code does not change. The connection URL changes; the queries and indexing operations your application issues do not. ## Why Lucenia? Lucenia is highlighted on this page as the recommended commercial alternative for customers who want the search-API contract on their own infrastructure, without Elastic licensing concerns. It is source-available Apache code with a paid commercial license. Key properties: * **Wire-compatible** with the Elasticsearch / OpenSearch API. Your existing clients work unchanged. * **Multimodal search** built in: vector, geospatial, numerics, timestamps, and IP, all queryable in a single index. * **Founded by the original OpenSearch creator** (Nick Knize), the engineer who led OpenSearch at AWS for four years. * **Tensor9 partnership** - Tensor9 consolidates Lucenia's prior multi-mechanism deployment story into a single origin stack. See the [case study](https://www.tensor9.com/resources/how-lucenia-is-building-consistent-self-hosted-deployments-with-tensor9/). For self-hosted OSS deployments (OpenSearch / Elasticsearch OSS), the third card applies. ## What the customer takes on When your customer brings their own search cluster, they own: * Cluster provisioning, node sizing, and shard count planning. * Snapshot configuration and snapshot repository (S3, GCS, Azure Blob, or shared filesystem). * Patching, version upgrades, and major-version migrations. * High-availability replica configuration and rolling restarts. * Monitoring and alerting on the cluster (heap pressure, GC pauses, indexing rate, search latency). ## What stays your responsibility * All application-level concerns: index mappings, analyzer configuration, query design, relevance tuning, vector embedding generation. * Your services continue to be observable through Tensor9's normal mechanisms. * [Operations](/fundamentals/operations) commands that operate on the appliance's services still work normally. Operations commands cannot reach into your customer's search cluster. ## Authentication and rotation The username and password (or API key) are stored as [Customer-Supplied Secrets](/fundamentals/secrets#secret-ownership-models) - Tensor9 has no visibility into the underlying value. The appliance reads them at startup and on every reconnection. Two rotation paths: * **Username + password**: rotate the password in the customer's cluster (Elastic Cloud / Lucenia / OpenSearch admin UI) and update the secret in the customer's secret store; restart the affected services. * **API key**: API keys are independently revocable in all three families. Mint a new API key, update the secret, restart - then revoke the old one once the rollout settles. For AWS OpenSearch Service with SigV4 auth, the appliance assumes a customer-supplied IAM role and the underlying STS token refreshes automatically. ## Where this fits This page is one of the services covered by [Customer-Provided Services](/customizations/customer-provided-services). For automatic cross-cloud substitution (AWS OpenSearch Service to other clouds based on form factor), see [Service adapters](/service-adapters/overview). Partner documentation: [Elastic Cloud](https://www.elastic.co/cloud), [Lucenia](https://lucenia.io/), [OpenSearch](https://opensearch.org/). # Kafka Source: https://docs.tensor9.com/customizations/customer-provided-services/kafka Customer-provided Kafka cluster in place of default-shipped Kafka Customer-provided Kafka: the origin stack uses Kafka; the customer can plug in Confluent Cloud, AWS MSK, or a self-hosted Kafka cluster (Strimzi, Redpanda). Customer-provided Kafka: the origin stack uses Kafka; the customer can plug in Confluent Cloud, AWS MSK, or a self-hosted Kafka cluster (Strimzi, Redpanda). If your origin stack uses Kafka as its event-streaming backbone, your customer can supply their own Kafka cluster ([Confluent Cloud](https://www.confluent.io/confluent-cloud/), AWS MSK, self-hosted Strimzi, Redpanda, etc.) and the compiler will emit a deployment stack that talks to it instead of provisioning a default-shipped Kafka. ## What the customer provides | Field | Purpose | | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Bootstrap servers** | A comma-separated list of `host:port` endpoints for the Kafka brokers. | | **Security protocol** | `PLAINTEXT`, `SSL`, `SASL_PLAINTEXT`, or `SASL_SSL`. Confluent Cloud and MSK with IAM use `SASL_SSL`. | | **SASL mechanism + credentials** *(if SASL)* | `PLAIN`, `SCRAM-SHA-256`, `SCRAM-SHA-512`, `OAUTHBEARER`, or `AWS_MSK_IAM` plus the corresponding username/password (or IAM role reference). | | **mTLS client certificate + key** *(if mTLS)* | Alternative to SASL for client identity. | | **CA certificate** *(optional)* | The trust anchor for the brokers' TLS certificates when using a private CA. | | **Schema registry URL** *(optional)* | If your application uses Avro / Protobuf schemas, the Confluent Schema Registry or compatible endpoint. | | **Topic prefix or allow-list** *(optional)* | A namespace prefix for the appliance's topics, or an explicit list of topic names the appliance is permitted to use. | The customer creates the cluster, creates topics with appropriate partition counts and retention, and grants the appliance principal the ACLs it needs (read/write on the listed topics; create-on-demand if your application uses auto-create). ## What changes in the deployment stack When customer-provided Kafka is selected, the compiler: * Drops the default-shipped Kafka cluster from the deployment stack (no MSK cluster, no in-cluster Strimzi or Bitnami Kafka, no Zookeeper / KRaft controllers). * Wires the appliance's producers and consumers to the customer's bootstrap servers. * Injects the customer's credentials as secrets the services read at startup. * Configures the security protocol and SASL mechanism the clients use. Your application code does not change. The connection configuration changes; the topics, schemas, and event payloads your application produces and consumes do not. ## What the customer takes on When your customer brings their own Kafka, they own: * Cluster provisioning, broker sizing, and partition count planning. * Topic creation, retention policy, compaction settings, and replication factor. * ACL configuration so the appliance principal can read and write the topics it needs. * Backup, mirror topology (MirrorMaker 2, Confluent Replicator), and cross-region replication. * Patching, version upgrades, and major-version migrations. The customer's broker version must remain compatible with your client library's protocol version. * High availability, controller quorum, and disaster recovery. * Monitoring, alerting, and incident response for the brokers, controllers, and schema registry. ## What stays your responsibility * All application-level concerns: topic naming, schema design, producer/consumer correctness, idempotency, ordering guarantees, transactional semantics. * Your services continue to be observable through Tensor9's normal mechanisms. * [Operations](/fundamentals/operations) commands that operate on the appliance's services still work normally. Operations commands cannot reach into your customer's Kafka brokers. ## Deployment notes **Confluent Cloud** is the path of least friction for customers without an existing Kafka operations team. The appliance reaches the cluster over the public internet (or private peering if the customer enables it), authenticated by SASL/PLAIN with API keys. **AWS MSK** (including MSK Serverless) is common for customers who centralize Kafka inside AWS. IAM-auth (`AWS_MSK_IAM` SASL mechanism) is preferred for least-privilege; SASL/SCRAM is also supported. **Self-hosted Strimzi** is common for customers running Kubernetes who want Kafka native to their cluster. **Redpanda** is a drop-in Kafka-API alternative with simpler operations (single-binary, no Zookeeper). ## Authentication and rotation SASL credentials, mTLS client cert + key, and CA cert are stored as [Customer-Supplied Secrets](/fundamentals/secrets#secret-ownership-models) - Tensor9 has no visibility into the underlying values. The appliance reads them at startup. Rotation depends on the SASL mechanism: * **SASL/PLAIN, SASL/SCRAM**: rotate the password in the cluster's auth config and update the secret in the customer's secret store; restart the affected services so they reconnect. * **OAUTHBEARER**: the appliance fetches tokens from the customer's IdP on each connection; rotation is handled by the IdP. * **AWS\_MSK\_IAM**: the appliance assumes a customer-supplied IAM role and the underlying STS token refreshes automatically (no service restart needed). * **mTLS**: rotate the client cert + key in the customer's secret store and restart so the new identity is presented on reconnection. ## Where this fits This page is one of the services covered by [Customer-Provided Services](/customizations/customer-provided-services). For automatic cross-cloud substitution (MSK to GCP Pub/Sub via dialect mapping based on form factor), see [Service adapters](/service-adapters/overview). Partner documentation: [Confluent Cloud client quickstart](https://docs.confluent.io/cloud/current/client-apps/index.html), [MSK IAM access control](https://docs.aws.amazon.com/msk/latest/developerguide/iam-access-control.html), [Strimzi documentation](https://strimzi.io/documentation/), [Redpanda documentation](https://docs.redpanda.com/). # MongoDB Source: https://docs.tensor9.com/customizations/customer-provided-services/mongodb Customer-provided MongoDB deployment in place of default-shipped MongoDB Customer-provided MongoDB: the origin stack uses MongoDB; the customer can plug in MongoDB Atlas, Atlas with PrivateLink, or a self-hosted MongoDB cluster. Customer-provided MongoDB: the origin stack uses MongoDB; the customer can plug in MongoDB Atlas, Atlas with PrivateLink, or a self-hosted MongoDB cluster. If your origin stack uses MongoDB as its document store, your customer can supply their own MongoDB deployment and the compiler will emit a deployment stack that talks to it instead of provisioning a default-shipped MongoDB. [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) is the most common manifestation - a managed multi-cloud cluster - but it is not the only one. Self-hosted MongoDB on Kubernetes or on VMs is equally supported. ## What the customer provides | Field | Purpose | | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Connection string** | A `mongodb://` or `mongodb+srv://` URI for the cluster. The SRV form is typical for Atlas; the standard form is typical for self-hosted. | | **Database name** | The database the appliance's services should connect to. | | **Username** | The MongoDB user the appliance authenticates as. | | **Password** *(or workload-identity reference)* | The credential for the database user. Atlas Workload Identity is supported where the customer has it configured. | | **CA certificate** *(optional)* | The trust anchor for the cluster's TLS certificate. Atlas uses a public CA by default; customers using private peering or self-hosted MongoDB may need to supply a custom CA. | The customer creates the cluster (or chooses an existing one), creates the database user with the privileges your application needs, and configures network access so the appliance can reach the cluster. ## What changes in the deployment stack When customer-provided MongoDB is selected, the compiler: * Drops the default-shipped MongoDB instance from the deployment stack (no cluster provisioning, no automated backups, no scaling configuration). * Wires the appliance's services to the customer's connection string and database. * Injects the customer's credentials as secrets the services read at startup. Your application code does not change. The connection URI changes; the queries the application emits do not. ## What the customer takes on When your customer brings their own MongoDB, they own: * Cluster provisioning and tier sizing. * Network access configuration so the appliance can reach the cluster. For Atlas this is an IP allowlist, [PrivateLink](https://www.mongodb.com/docs/atlas/security-private-endpoint/), or VPC peering. For self-hosted MongoDB this is typically VPC/security-group configuration. * Backup configuration, including continuous backups and point-in-time recovery (Atlas tier feature, or the customer's own backup tooling for self-hosted). * Patching, version upgrades, and major-version migrations. * High-availability, replica-set topology, and cross-region replication. * Monitoring and alerting on the MongoDB side. ## What stays your responsibility * All application-level concerns: data model, query performance, application-managed migrations, index strategy. * Your services continue to be observable through Tensor9's normal mechanisms. * [Operations](/fundamentals/operations) commands that operate on the appliance's services still work normally. Operations commands cannot reach into your customer's MongoDB. ## Deployment notes **Atlas (public endpoint)** - the path of least friction. The appliance reaches the Atlas cluster over the public internet, authenticated by user/password and gated by an Atlas IP allowlist that includes the appliance's egress. **Atlas + [PrivateLink](https://www.mongodb.com/docs/atlas/security-private-endpoint/)** - the recommended production posture when the customer is on Atlas. PrivateLink removes the public-internet route to the cluster entirely; the appliance reaches Atlas via a private endpoint in the customer's VPC. **Self-hosted MongoDB** - the customer runs MongoDB themselves (commonly on Kubernetes via the MongoDB Operator, or on VMs/bare metal). The appliance treats it like any other MongoDB endpoint - only the connection string changes. The customer owns operations, backup, and upgrades. In all three cases, **read preferences and write concerns** stay defined by your application; the customer's deployment must support whatever your application requires. ## Authentication and rotation The username and password (or workload-identity reference) are stored as [Customer-Supplied Secrets](/fundamentals/secrets#secret-ownership-models) - Tensor9 has no visibility into the underlying value. The appliance reads them at startup. Two rotation paths: * **Static password**: rotate the database-user password in Atlas (or the customer's MongoDB) and update the secret in the customer's secret store; restart the affected services. * **Atlas Workload Identity**: the appliance authenticates to Atlas using a customer-supplied OIDC token; the underlying token is refreshed by the identity provider on its own cadence, so no service restart is needed for credential rotation. ## Where this fits This page is one of the services covered by [Customer-Provided Services](/customizations/customer-provided-services). Partner documentation: [Atlas Private Endpoints](https://www.mongodb.com/docs/atlas/security-private-endpoint/), [Atlas database user setup](https://www.mongodb.com/docs/atlas/security-add-mongodb-users/), [MongoDB Kubernetes Operator](https://www.mongodb.com/docs/kubernetes-operator/). # PostgreSQL Source: https://docs.tensor9.com/customizations/customer-provided-services/postgresql Customer-provided PostgreSQL database in place of default-shipped PostgreSQL Customer-provided PostgreSQL: the origin stack uses PostgreSQL; the customer can plug in an AWS-managed instance (RDS or Aurora), a cloud-managed instance (Cloud SQL, AlloyDB, etc.), or a self-hosted Postgres deployment. Customer-provided PostgreSQL: the origin stack uses PostgreSQL; the customer can plug in an AWS-managed instance (RDS or Aurora), a cloud-managed instance (Cloud SQL, AlloyDB, etc.), or a self-hosted Postgres deployment. If your origin stack uses PostgreSQL as its relational store, your customer can supply their own PostgreSQL instance (RDS, Cloud SQL, Aurora, self-hosted, etc.) and the compiler will emit a deployment stack that talks to it instead of provisioning a default-shipped database. ## What the customer provides | Field | Purpose | | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | **Hostname** | The `host` (or `host:port` if non-default) of the PostgreSQL endpoint. | | **Database name** | The PostgreSQL database the appliance's services should connect to. | | **Username** | The role the appliance authenticates as. | | **Password (or IAM role / managed-identity reference)** | The credential for the role. The appliance can use a static password or, where supported, an IAM-backed token. | | **CA certificate** *(optional)* | The trust anchor for the PostgreSQL server's TLS certificate when the customer uses a private CA. | The customer is responsible for creating the database, granting the appliance role the privileges it needs, and applying any schema migrations your application requires. ## What changes in the deployment stack When customer-provided PostgreSQL is selected, the compiler: * Drops the default-shipped PostgreSQL instance from the deployment stack (no RDS instance, no managed-instance provisioning, no automated backups). * Wires the appliance's services to the customer's hostname and database. * Injects the customer's credentials as secrets the services read at startup. Your application code does not change. The connection string changes; the SQL the application emits does not. ## What the customer takes on When your customer brings their own PostgreSQL, they own: * Provisioning, instance sizing, and capacity planning. * Backup, retention, and restore. Point-in-time recovery is the customer's PostgreSQL provider's feature, not Tensor9's. * Patching and version upgrades. The customer's PostgreSQL major version must remain compatible with your application's requirements. * High availability, read-replica topology, and failover. * Connection pooling, if the appliance's connection count exceeds what the customer's deployment can handle directly. * Monitoring, alerting, and incident response for the PostgreSQL instance itself. ## What stays your responsibility * All application-level database concerns: schema design, query performance, application-managed migrations, data correctness. * Your services continue to be observable through Tensor9's normal mechanisms (logs, metrics, traces) even though the database isn't. * [Operations](/fundamentals/operations) commands that operate on the appliance's services still work normally. Operations commands cannot reach into your customer's PostgreSQL. ## Authentication and rotation The username and password (or IAM role / managed-identity reference) are stored as [Customer-Supplied Secrets](/fundamentals/secrets#secret-ownership-models) - Tensor9 has no visibility into the underlying value. The appliance reads them at startup and on connection-pool establishment. Two rotation paths: * **Static password**: rotate the password in the customer's secret store and restart the affected services so they reconnect with the new credential. * **IAM-backed auth** (RDS IAM, Cloud SQL IAM): the appliance assumes the customer-supplied IAM role at runtime and the underlying STS token is refreshed automatically (15-minute TTL on AWS). The customer rotates the role / policy, not the credential. ## Where this fits This page is one of the services covered by [Customer-Provided Services](/customizations/customer-provided-services). For automatic cross-cloud substitution (RDS → Cloud SQL based on form factor), see [Service adapters](/service-adapters/overview). # Redis Source: https://docs.tensor9.com/customizations/customer-provided-services/redis Customer-provided Redis deployment in place of default-shipped Redis Customer-provided Redis: the origin stack uses Redis; the customer can plug in AWS ElastiCache or MemoryDB, Redis Enterprise (commercial Redis Inc. product), or a self-hosted Redis deployment. Customer-provided Redis: the origin stack uses Redis; the customer can plug in AWS ElastiCache or MemoryDB, Redis Enterprise (commercial Redis Inc. product), or a self-hosted Redis deployment. If your origin stack uses Redis as a cache, session store, or pub/sub bus, your customer can supply their own Redis instance and the compiler will emit a deployment stack that talks to it instead of provisioning a default-shipped Redis. This page covers the **Redis Inc.** product family (Redis 7.4+ under SSPL / RSAL, plus Redis Enterprise / Redis Cloud). For the BSD-licensed Linux Foundation fork, see [Valkey](/customizations/customer-provided-services/valkey). ## What the customer provides | Field | Purpose | | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Hostname** | The `host` (or `host:port` if non-default) of the Redis endpoint. For clustered Redis, the cluster configuration endpoint. | | **Password** *(or IAM auth token reference)* | The credential for AUTH. ElastiCache and MemoryDB support IAM-backed tokens; static passwords are supported everywhere. | | **Database number** *(optional)* | The Redis logical database (0-15). Defaults to 0. Ignored on clustered Redis. | | **TLS settings** *(optional)* | A custom CA certificate when the customer uses a private CA. Public-CA Redis (ElastiCache with TLS-in-transit, MemoryDB, Memorystore) does not need this. | | **Cluster mode** *(optional)* | Flag indicating whether the endpoint is cluster-mode (the application opens a cluster client) vs. standalone. | ## What changes in the deployment stack When customer-provided Redis is selected, the compiler: * Drops the default-shipped Redis instance from the deployment stack (no ElastiCache resource, no in-cluster Redis StatefulSet, no automated backups). * Wires the appliance's services to the customer's hostname. * Injects the customer's credentials as secrets the services read at startup. * Sets the database number and TLS configuration the services connect with. Your application code does not change. The connection address changes; the commands the application emits do not. ## What the customer takes on When your customer brings their own Redis, they own: * Provisioning, instance sizing, and capacity planning (memory headroom + connection limits). * Backup, snapshot, and restore. ElastiCache and Memorystore have built-in snapshot schedules; self-hosted Redis is the customer's responsibility. * Patching and version upgrades. The customer's Redis major version must remain compatible with your application's client library. * High availability, failover, and read-replica topology. * Eviction policy configuration (`allkeys-lru`, `volatile-ttl`, `noeviction`, etc.) appropriate for your application's use of Redis. * Monitoring, alerting, and incident response for the Redis instance itself. ## What stays your responsibility * All application-level concerns: key naming, TTLs, data model, command correctness. * Your services continue to be observable through Tensor9's normal mechanisms. * [Operations](/fundamentals/operations) commands that operate on the appliance's services still work normally. Operations commands cannot reach into your customer's Redis. ## Deployment notes **AWS ElastiCache for Redis** and **MemoryDB** are the most common managed paths for customers on AWS. ElastiCache supports both cluster-mode and standalone topologies; MemoryDB is durable (multi-AZ writes to a transaction log). **Other cloud managed Redis** (GCP Memorystore, Azure Cache for Redis) is supported the same way - hostname + password + TLS settings. **[Redis Enterprise](https://redis.io/enterprise/)** is the commercial Redis Inc. product, available as Redis Cloud (managed by Redis Inc.) or Redis Software (self-hosted by the customer with a commercial license). Active-active geo-replication and Redis on Flash are Redis Enterprise features. **Self-hosted Redis** (Redis OSS) is the customer running Redis themselves on Kubernetes or VMs. For the **BSD-licensed Linux Foundation fork**, see the separate [Valkey](/customizations/customer-provided-services/valkey) page. ## Authentication and rotation The AUTH password (or IAM auth token reference) is stored as a [Customer-Supplied Secret](/fundamentals/secrets#secret-ownership-models) - Tensor9 has no visibility into the underlying value. The appliance reads it at startup and on every reconnection. Two rotation paths: * **Static password / ACL user password**: rotate the password in the cluster's auth config (ElastiCache, MemoryDB, or Redis Enterprise admin UI) and update the secret in the customer's secret store; restart the affected services so they reconnect. * **IAM auth tokens** (ElastiCache, MemoryDB): the appliance assumes a customer-supplied IAM role and the underlying token refreshes automatically on the AWS-default 15-minute TTL. The customer rotates the role / policy, not the credential. ## Where this fits This page is one of the services covered by [Customer-Provided Services](/customizations/customer-provided-services). For automatic cross-cloud substitution (ElastiCache to Memorystore based on form factor), see [Service adapters](/service-adapters/overview). See also [Valkey](/customizations/customer-provided-services/valkey) for the OSS fork. # Temporal Source: https://docs.tensor9.com/customizations/customer-provided-services/temporal Customer-provided Temporal workflow orchestration in place of default-shipped Temporal Customer-provided Temporal: the origin stack uses Temporal; the customer can plug in Temporal Cloud, self-hosted Temporal on Kubernetes, or self-hosted Temporal on VMs. Customer-provided Temporal: the origin stack uses Temporal; the customer can plug in Temporal Cloud, self-hosted Temporal on Kubernetes, or self-hosted Temporal on VMs. If your origin stack uses Temporal as its workflow engine, your customer can supply their own Temporal cluster (self-hosted or [Temporal Cloud](https://temporal.io/cloud)) and the compiler will emit a deployment stack that talks to it instead of provisioning a default-shipped Temporal. ## What the customer provides | Field | Purpose | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | **Frontend address** | The `host:port` (or URL) of the customer's Temporal frontend service. | | **Namespace** | The Temporal namespace the appliance's workers should connect to. | | **mTLS client certificate + key** | The client identity the workers present when connecting. Required for Temporal Cloud and most customer-managed Temporal deployments. | | **CA certificate** | The trust anchor for the Temporal frontend's server certificate. | ## What changes in the deployment stack When customer-provided Temporal is selected, the compiler: * Drops the default-shipped Temporal cluster from the deployment stack (no server, no database, no UI). * Wires the appliance's worker pods to the customer's frontend address. * Injects the customer's mTLS material as secrets the workers read at startup. * Sets the namespace the workers register against. Your application code does not change. It opens a Temporal client the same way regardless of which Temporal is on the other end. ## What the customer takes on When your customer brings their own Temporal, they own: * Provisioning, sizing, and scaling of the Temporal cluster (or the Temporal Cloud namespace tier). * Worker host-pool capacity and any persistence backend (Cassandra, PostgreSQL, etc.) the Temporal cluster needs. * Patching and version upgrades. The customer's Temporal version must remain compatible with your Temporal SDK version; mismatches manifest as runtime errors. * Backup, point-in-time recovery, and disaster recovery for the workflow history. * High availability, failover, and cross-region replication. * Operational monitoring, alerting, and incident response for the Temporal cluster itself. Of the three customer-provided services Tensor9 supports today, Temporal is the heaviest operational lift. Meeting the backup, high-availability, and monitoring requirements above with self-hosted Temporal is non-trivial; many customers will prefer [Temporal Cloud](https://temporal.io/cloud) for this reason. ## What stays your responsibility * Your worker code, workflows, and activities are unchanged. * You remain responsible for SDK compatibility, retry policies, workflow correctness, and any application-level operational concerns the workers raise. * [Operations](/fundamentals/operations) commands that operate on the worker pods (logs, metrics, restart) still work normally. Operations commands cannot reach into your customer's Temporal cluster. ## Authentication and rotation The mTLS client certificate, private key, and CA certificate are stored as [Customer-Supplied Secrets](/fundamentals/secrets#secret-ownership-models) - Tensor9 has no visibility into the key material. Workers read the cert and key at startup and present them on every connection to the Temporal frontend. Rotation is the customer's responsibility: rotate the client cert in the customer's secret store (Temporal Cloud has a rotation flow in the Temporal UI), then restart the worker pods so they pick up the new material. CA-cert rotation follows the same pattern. ## Where this fits This page is one of the services covered by [Customer-Provided Services](/customizations/customer-provided-services). For the difference between customer-driven substitution (this) and automatic cross-cloud substitution, see [Service adapters](/service-adapters/overview). Partner documentation: [Temporal Cloud customer-namespace setup](https://docs.temporal.io/cloud/namespaces). # Valkey Source: https://docs.tensor9.com/customizations/customer-provided-services/valkey Customer-provided Valkey deployment in place of default-shipped Valkey Customer-provided Valkey: the origin stack uses Valkey; the customer can plug in AWS ElastiCache for Valkey, GCP Memorystore for Valkey, or a self-hosted Valkey deployment. Customer-provided Valkey: the origin stack uses Valkey; the customer can plug in AWS ElastiCache for Valkey, GCP Memorystore for Valkey, or a self-hosted Valkey deployment. [Valkey](https://valkey.io/) is a Linux Foundation project that forked Redis 7.2.4 in March 2024 after Redis Inc. changed the upstream Redis license to SSPL / RSAL. Valkey is wire-compatible with the Redis protocol, BSD-licensed, and governed by a vendor-neutral foundation (AWS, Google, Oracle, Snap, and Ericsson are founding members). If your origin stack uses Valkey as a cache, session store, or pub/sub bus, your customer can supply their own Valkey instance and the compiler will emit a deployment stack that talks to it instead of provisioning a default-shipped Valkey. ## Valkey vs Redis Valkey is API- and wire-compatible with Redis 7.2 and continues to evolve as a true OSS project. The two are separate products with separate governance, separate roadmaps, and separate licenses going forward: * **[Valkey](/customizations/customer-provided-services/valkey)** (this page): BSD-licensed, Linux Foundation governance. Use this page when your origin stack is built on Valkey or when your customer wants the Redis API without any Redis Inc. license entanglement. AWS ElastiCache, GCP Memorystore, Aiven, and others ship Valkey as a fully supported managed offering. * **[Redis](/customizations/customer-provided-services/redis)**: the Redis Inc. product (Redis 7.4+ under SSPL / RSAL, or Redis Enterprise commercial). Use that page when your origin stack is built on Redis specifically and your customer has a Redis Inc. license or runs on a Redis-branded managed service. ## What the customer provides | Field | Purpose | | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | **Hostname** | The `host` (or `host:port` if non-default) of the Valkey endpoint. For clustered Valkey, the cluster configuration endpoint. | | **Password** *(or IAM auth token reference)* | The credential for AUTH. ElastiCache for Valkey supports IAM-backed tokens; static passwords are supported everywhere. | | **Database number** *(optional)* | The Valkey logical database (0-15). Defaults to 0. Ignored on clustered mode. | | **TLS settings** *(optional)* | A custom CA certificate when the customer uses a private CA. Public-CA Valkey (ElastiCache with TLS-in-transit, Memorystore) does not need this. | | **Cluster mode** *(optional)* | Flag indicating whether the endpoint is cluster-mode vs. standalone. | ## What changes in the deployment stack When customer-provided Valkey is selected, the compiler: * Drops the default-shipped Valkey instance from the deployment stack (no ElastiCache for Valkey resource, no in-cluster Valkey StatefulSet, no automated snapshots). * Wires the appliance's services to the customer's hostname. * Injects the customer's credentials as secrets the services read at startup. * Sets the database number and TLS configuration the services connect with. Your application code does not change. The connection address changes; the commands the application emits do not. ## What the customer takes on When your customer brings their own Valkey, they own: * Provisioning, instance sizing, and capacity planning (memory headroom + connection limits). * Backup, snapshot, and restore. ElastiCache for Valkey and Memorystore have built-in snapshot schedules; self-hosted Valkey is the customer's responsibility. * Patching and version upgrades. The customer's Valkey major version must remain compatible with your application's client library (most Redis-protocol clients work unchanged against Valkey). * High availability, failover, and read-replica topology. * Eviction policy configuration (`allkeys-lru`, `volatile-ttl`, `noeviction`, etc.) appropriate for your application's use of Valkey. * Monitoring, alerting, and incident response for the Valkey instance itself. ## What stays your responsibility * All application-level concerns: key naming, TTLs, data model, command correctness. * Your services continue to be observable through Tensor9's normal mechanisms. * [Operations](/fundamentals/operations) commands that operate on the appliance's services still work normally. Operations commands cannot reach into your customer's Valkey. ## Deployment notes **[AWS ElastiCache for Valkey](https://aws.amazon.com/elasticache/valkey/)** has been available since late 2024 and is the path of least friction for customers on AWS, with the same managed-service ergonomics as ElastiCache for Redis and the OSS license guarantee. ElastiCache Serverless for Valkey is also available. **[GCP Memorystore for Valkey](https://cloud.google.com/memorystore/docs/valkey/valkey-overview)** is the GCP-native managed offering, available in both standard and cluster modes. Other providers (Aiven, Upstash) also offer managed Valkey; check the provider's docs for current feature coverage. **Self-hosted Valkey** runs anywhere Redis runs: Kubernetes via Helm charts (the Bitnami Valkey chart is widely used), VMs, or bare metal. ## Authentication and rotation The AUTH password (or IAM auth token reference) is stored as a [Customer-Supplied Secret](/fundamentals/secrets#secret-ownership-models) - Tensor9 has no visibility into the underlying value. The appliance reads it at startup and on every reconnection. Two rotation paths: * **Static password / ACL user password**: rotate in the cluster's auth config (ElastiCache for Valkey, Memorystore for Valkey, or self-hosted admin) and update the secret in the customer's secret store; restart the affected services so they reconnect. * **IAM auth tokens** (ElastiCache for Valkey): the appliance assumes a customer-supplied IAM role and the underlying token refreshes automatically on the AWS-default 15-minute TTL. The customer rotates the role / policy, not the credential. ## Where this fits This page is one of the services covered by [Customer-Provided Services](/customizations/customer-provided-services). The related [Redis](/customizations/customer-provided-services/redis) page covers the Redis Inc. product family. Partner documentation: [valkey.io](https://valkey.io/), [Valkey on GitHub](https://github.com/valkey-io/valkey). # Ingress Control Source: https://docs.tensor9.com/customizations/ingress Customer-chosen ingress posture for the deployed application Tensor9 lets each of your customers pick how their end users reach the deployed application. The choice is configuration, not source code: your same origin stack compiles into a deployment stack shaped to whatever posture the customer selects. The URL their end users type stays the same regardless of which option they pick; only the network path changes. Three ingress options: Public (default), Allowlist (CIDR-gated), and Tailscale (tailnet only). The customer picks one at appliance setup time. Three ingress options: Public (default), Allowlist (CIDR-gated), and Tailscale (tailnet only). The customer picks one at appliance setup time. ## Ingress options | Option | What it does | When customers pick it | Compliance lever | | ---------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | **Public** *(default)* | The application is reachable on the public internet. | Adoption-friendly default for customers who do not require a network-isolation boundary. | None directly. Common starting posture for non-regulated customers. | | **Allowlist** | The application is public-facing but restricted to a list of customer-supplied IPv4/IPv6 CIDRs. | The customer wants a public path but only from their corporate egress, partner networks, or other known sources. | Demonstrable network-boundary control. Audit-trail friendly: the CIDR list is the documented boundary. | | **Tailscale** | The application is reachable only over the customer's [Tailscale](https://tailscale.com) network. | The customer already runs Tailscale and wants end users on their tailnet to reach the application. | Eliminates the public path. Tailnet membership becomes the identity-and-authorization boundary the customer's security team already audits. | ## Configuring an option Ingress posture is set on the [form factor](/fundamentals/key-concepts#form-factor) you author. The form factor declares which of the three options it permits; your customer (or you on their behalf) picks from those permitted options at appliance setup time. * **Public, Allowlist, and Tailscale** are mechanical: declare them in the form factor, configure the customer's specifics (the allowlist CIDRs, or the Tailscale auth key) in the appliance setup link, and the compiler emits the right deployment stack. The customer-facing version of this summary lives at [Private ingress](/customer/configuration/private-ingress) for customers who land directly on that page. ## How this fits into the bigger network picture This page is the customer-customizable knob for **application ingress**, the link end users traverse to reach the deployed application. Tensor9 manages three other network links (operator-to-control-plane, appliance-to-control-plane, and break-glass) that are your-side concerns or covered separately. The full architectural view is on [Connectivity](/fundamentals/connectivity); the customer-customizable subset (this page + [Controller Connectivity](/customizations/connectivity)) is what shows up in this section. # How Auto-Customization Works Source: https://docs.tensor9.com/customizations/overview At appliance setup time, your customer declares three properties of their environment. The compiler reads those properties and emits a deployment stack that honors them. Your application code does not change between customers. Your origin stack compiles into per-customer installs. Each customer's install is shaped by the ingress posture, controller-connectivity path, and customer-provided services they declared at appliance setup time. Your origin stack compiles into per-customer installs. Each customer's install is shaped by the ingress posture, controller-connectivity path, and customer-provided services they declared at appliance setup time. | Surface | What your customer asserts | Why customers ask for this | Page | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | **Ingress posture** | How end users reach the deployed application. | Compliance boundary controls. A public-internet path is disqualifying for many regulated customers; allowlist and Tailscale are the levers their security teams accept. | [Ingress Control](/customizations/ingress) | | **Controller connectivity** | How the appliance reaches your control plane. | Third-party-risk findings. Outbound HTTPS to a third-party SaaS is a common audit finding; PrivateLink (or Tailscale) lets your customer eliminate the public-internet path entirely. | [Controller Connectivity](/customizations/connectivity) | | **Customer-provided services** | Which managed services the install should use your customer's existing instance of, instead of provisioning a default equivalent. | Data residency and existing controls. Your customer's Temporal, PostgreSQL, or MongoDB is already inside their certified perimeter, already on their patch schedule, already in their backup policy. | [Customer-Provided Services](/customizations/customer-provided-services) | Each surface is independent. Common combinations: public ingress + public controller + default services; allowlist or Tailscale ingress + PrivateLink controller + customer-provided PostgreSQL. The full Cartesian product is the design intent; untested combinations are work to validate, not guarantees. ## What stays the same regardless of customer choice These hold for every customer regardless of the choices above: * One codebase. No per-customer forks, branches, or porting. * New features ship to every customer through the same release pipeline. * One operational posture. The deployment stack changes; what you run and support does not. ## What this section is NOT * **Not application behavior.** This shapes the plumbing your application runs on. It does not change what your application does. Your customers cannot turn features on or off through this surface; that is your product roadmap, not this surface. * **Not version selection.** Your customers cannot pick alternate versions of your application or its dependencies. You ship one release per cadence to every customer. * **Not telemetry opt-out.** The appliance-to-controller link is required, and it is how the operational telemetry you need to support the install reaches you. Your customer customizes the *path* (see [Controller Connectivity](/customizations/connectivity)), not whether telemetry flows. * **Not unbounded substitution.** Each customer-provided service is an explicit, named substitution. The currently-supported set is what's documented here; the set is actively expanding as customer requirements drive new substitutions. * **Not a guarantee that every combination has been validated in production.** Composability is the design intent. Untested combinations are work to validate jointly with you and the customer. ## Form factor defines the permitted choices The choices on this page are not freely available to every customer. You author a [form factor](/fundamentals/key-concepts#form-factor) that defines the *permitted* set of choices for a given install template, and your customer (or you on their behalf) picks from what the form factor permits at appliance setup time. The form factor is also a security-review artifact: a customer's procurement team can read it to see every permitted path and substitution their install can use. For FedRAMP-adjacent customers, you might publish a form factor that requires Allowlist or Tailscale ingress and PrivateLink controller; for less-regulated customers, you might leave all three ingress options open with public as the default. ## How this relates to service adapters [Service adapters](/service-adapters/overview) covers the *automatic* substitution axis: the compiler maps RDS to Cloud SQL when the form factor's cloud is GCP. You author one stack against AWS primitives; the compiler emits a GCP-native deployment when the form factor calls for GCP. The substitution is automatic; your customer does not pick. This section covers the *customer-driven* substitution axis: your customer explicitly says "use my Temporal" and the compiler honors that. The two axes compose. A customer on GCP can use the default-shipped Cloud SQL (automatic, form-factor-driven) while bringing their own Temporal (customer-driven). ## Supported third-party auto-customizations Tensor9 supports these third-party services as choices on the corresponding surface in this section: | Service | Supported Variants | Surface | | ------------------------------------ | --------------------------------------------------------------------------------- | ------------------------------------------- | | **Tailscale** | - | Ingress posture and controller connectivity | | **AWS PrivateLink** | Cross-region attachments | Controller connectivity | | **Temporal** | Temporal Cloud, self-hosted | Customer-provided service | | **PostgreSQL** | RDS, Cloud SQL, Aurora, self-hosted | Customer-provided service | | **MongoDB** | Atlas, self-hosted (K8s, VMs) | Customer-provided service | | **Redis** | ElastiCache, MemoryDB, Redis Enterprise, self-hosted | Customer-provided service | | **Valkey** (BSD-licensed Redis fork) | ElastiCache for Valkey, Memorystore for Valkey, self-hosted | Customer-provided service | | **Kafka** | Confluent Cloud, MSK, Strimzi, Redpanda | Customer-provided service | | **Elasticsearch family** | Elastic Cloud, [Lucenia](https://lucenia.io/), OpenSearch, AWS OpenSearch Service | Customer-provided service | If you operate a managed service that should be a customer-provided substitution for installs running on Tensor9, [contact us](mailto:hello@tensor9.com). # Amazon Web Services Source: https://docs.tensor9.com/form-factor/aws Amazon Web Services (AWS) is Tensor9's primary deployment platform and the reference implementation for all service equivalents. Deploying to AWS provides the most complete feature set and serves as the baseline from which other form factors are derived. ## Overview When you deploy an application to AWS customer environments using Tensor9: * **Customer appliances** run entirely within the customer's AWS account * **Your control plane** orchestrates deployments from your dedicated Tensor9 AWS account * **Cross-account IAM roles** enable your control plane to manage customer appliances with customer-approved permissions * **Service equivalents** compile your origin stack into AWS-native resources (or preserve them if already AWS-based) AWS appliances use AWS-native services for compute, storage, networking, and observability, so they fit into the AWS environments your customers already run. ## Prerequisites Before deploying appliances to AWS customer environments, ensure: ### Your Control Plane * **Dedicated AWS account** for your Tensor9 control plane * **Control plane installed** - See [Installing Tensor9](/fundamentals/install) * **Origin stack published** - Your application infrastructure defined and uploaded ### Customer AWS Account Your customers must provide: * **AWS account** where the appliance will be deployed * **IAM roles configured** for the four-phase permissions model (Install, Steady-state, Deploy, Operate) * **VPC and networking** configured according to their requirements * **Sufficient service quotas** for your application's resource needs * **AWS region** where they want the appliance deployed ### Your Development Environment * **AWS CLI** installed and configured * **Terraform or OpenTofu** (if using Terraform origin stacks) * **AWS CloudFormation CLI** (if using CloudFormation origin stacks) * **Docker** (if deploying container-based applications) ## How AWS appliances work AWS appliances are deployed using AWS-native services orchestrated by your Tensor9 control plane. Your customer creates four IAM roles in their AWS account, each corresponding to a permission phase: Install, Steady-state, Deploy, and Operate. These roles define what the Tensor9 controller in the appliance can do within their environment. The customer configures trust policies that allow the Tensor9 controller to assume these roles with appropriate conditions (time windows, approval tags, etc.). You create a release targeting the customer's appliance: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -customerName acme-corp \ -vendorVersion "1.0.0" \ -description "Initial production deployment" ``` Your control plane compiles your origin stack into a deployment stack tailored for AWS, compiling any non-AWS resources to their AWS service equivalents. The deployment stack downloads to your local environment. The customer approves the deployment by granting temporary deploy access. This can be manual (updating IAM policy conditions) or automated (scheduled maintenance windows). Once approved, the Tensor9 controller in the appliance can assume the Deploy role in the customer's account. You run the deployment locally against the downloaded deployment stack: ```bash theme={null} cd acme-corp-production tofu init tofu apply ``` The deployment stack is configured to route resource creation through the Tensor9 controller inside the customer's appliance. The controller assumes the Deploy role and creates all infrastructure resources in the customer's AWS account: * VPCs, subnets, security groups * EKS clusters, Lambda functions * RDS databases, S3 buckets, ElastiCache clusters * CloudWatch log groups, IAM roles, Route 53 records * Any other AWS resources defined in your origin stack After deployment, your control plane uses the Steady-state role to continuously collect observability data (logs, metrics, traces) from the customer's appliance without requiring additional approvals. This data flows to your observability sink, giving you visibility into appliance health and performance. ## Permissions model AWS appliances use a four-phase IAM permissions model that balances operational capability with customer control. ### The four permission phases | Phase | IAM Role | Purpose | Access Pattern | | ---------------- | ----------------- | ----------------------------------------------- | ------------------------------- | | **Install** | `InstallRole` | Initial setup, major infrastructure changes | Customer-approved, rare | | **Steady-state** | `SteadyStateRole` | Continuous observability collection (read-only) | Active by default | | **Deploy** | `DeployRole` | Deployments, updates, configuration changes | Customer-approved, time-bounded | | **Operate** | `OperateRole` | Remote operations, troubleshooting, debugging | Customer-approved, time-bounded | ### IAM role structure Each role is created in the customer's AWS account with a trust policy that allows the Tensor9 controller in the appliance to assume it. **Example: Deploy role with conditional access** ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::VENDOR_ACCOUNT:role/ControlPlane" }, "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "aws:RequestTag/DeployAccess": "enabled" }, "DateLessThan": { "aws:CurrentTime": "2024-12-31T23:59:59Z" } } } ] } ``` The Tensor9 controller can only assume the Deploy role when: * The `DeployAccess` tag is set to "enabled" * The current time is within the allowed window Customers control when and how long deploy access is granted. **Example: Steady-state role (read-only observability)** ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:DescribeLogGroups", "logs:DescribeLogStreams", "logs:GetLogEvents", "cloudwatch:GetMetricData", "cloudwatch:ListMetrics", "ec2:Describe*", "rds:Describe*", "eks:Describe*", "s3:ListBucket", "s3:GetObject" ], "Resource": "*", "Condition": { "StringEquals": { "aws:ResourceTag/t9-appliance-id": "" } } }, { "Effect": "Deny", "Action": [ "iam:*", "*:Delete*", "*:Terminate*", "*:Update*", "*:Modify*" ], "Resource": "*" } ] } ``` The Steady-state role: * Can read observability data from resources tagged with the appliance's `t9-appliance-id` * Cannot modify, delete, or terminate any resources * Cannot change IAM policies ### Deployment workflow with IAM Customer approves a deployment by setting the `DeployAccess` tag to "enabled" and defining a time window. This can be done manually or through automated approval workflows. You run the deployment locally against the downloaded deployment stack: ```bash theme={null} cd acme-corp-production tofu init tofu apply ``` The deployment stack is configured to route resource creation through the Tensor9 controller in the appliance. For each resource Terraform attempts to create, the Tensor9 controller inside the appliance assumes the Deploy role and creates the resource in the customer's account. All infrastructure changes occur within the customer's account using their Deploy role permissions. After the time window expires, the Deploy role can no longer be assumed. Your control plane automatically reverts to using only the Steady-state role for observability. See [Permissions Model](/fundamentals/permissions-model) for detailed information on all four phases. ## Networking AWS appliances use an isolated networking architecture with a Tensor9 controller that manages communication with your control plane. ### Tensor9 controller VPC When an appliance is deployed, Tensor9 creates an isolated VPC containing the Tensor9 controller. This VPC is configured with: * **Internet Gateway**: Provides outbound internet connectivity * **Route to control plane**: Establishes a secure channel to your Tensor9 control plane * **No ingress ports**: The controller VPC does not accept inbound connections - all communication is outbound-only The Tensor9 controller uses this secure channel to: * **Receive deployments**: Deployment stacks are pushed from your control plane to the appliance * **Configure observability pipeline**: Set up log, metric, and trace forwarding to your observability sink * **Receive operational commands**: Execute remote operations initiated from your control plane ### Outbound-only security model The Tensor9 controller in your customer's appliance is designed to only make outbound connections and not require ingress ports to be opened in your customer's network perimeter: ```terraform theme={null} # Example: Controller VPC configuration (managed by Tensor9) resource "aws_vpc" "tensor9_controller" { cidr_block = "10.0.0.0/24" enable_dns_hostnames = true enable_dns_support = true tags = { Name = "tensor9-controller-000000007e" ManagedBy = "Tensor9" } } resource "aws_internet_gateway" "controller" { vpc_id = aws_vpc.tensor9_controller.id tags = { Name = "tensor9-controller-igw-000000007e" ManagedBy = "Tensor9" } } # Security group: egress only, no ingress resource "aws_security_group" "controller" { name = "tensor9-controller-sg-000000007e" description = "Tensor9 controller security group - outbound only" vpc_id = aws_vpc.tensor9_controller.id # No ingress rules - controller never accepts inbound connections egress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] description = "HTTPS to control plane" } tags = { Name = "tensor9-controller-sg-000000007e" ManagedBy = "Tensor9" } } ``` This architecture ensures that the customer's appliance cannot be compromised via inbound network attacks on the controller. ### Controller connectivity The appliance controller's outbound channel to your control plane is established over the public internet by default. Two private alternatives are additionally available for AWS form factors: * **AWS PrivateLink.** Traffic stays on the private AWS backbone instead of traversing the public internet. PrivateLink appears as a form-factor option unless your vendor controller infrastructure setup specifically opted out of AWS PrivateLink (see [`vendor setup -noAwsPrivateLinkRdv`](/cli/reference#vendor-setup)). * **Vendor-provided Tailscale.** The appliance joins a tailnet that you operate; the handshake travels the tailnet. The appliance-side join is bootstrapped from a pre-auth key you supply when generating the customer's setup link. See the [customer security model](/customer/security/security-model#no-inbound-network-access) for the underlying handshake, and the [Connectivity](/fundamentals/connectivity#appliance-to-control-plane) page for the full set of network paths. When you create an AWS form factor in the vendor portal, you select which of these transports the form factor permits. You may pick as many connectivity options as you are willing to support; when you construct an appliance setup link and assign a form factor to it, you may optionally reduce the set at that time, or defer the choice to the customer by leaving multiple options open. ### Application VPC topology Your application resources run in their own VPC(s), completely separate from the Tensor9 controller VPC. The application VPC topology is defined entirely by your origin stack - whatever VPC resources you define in your origin stack will be deployed into the appliance. **Example: Application VPC with internet-facing load balancer** If your origin stack defines a VPC with public subnets, an internet gateway, and a load balancer, that exact topology will be created in the customer's appliance: ```terraform theme={null} # Application VPC (defined in your origin stack) resource "aws_vpc" "application" { cidr_block = "10.1.0.0/16" enable_dns_hostnames = true enable_dns_support = true tags = { Name = "${var.namespace}myapp-vpc" } } # Internet gateway for application traffic resource "aws_internet_gateway" "application" { vpc_id = aws_vpc.application.id tags = { Name = "${var.namespace}myapp-igw" } } # Public subnets for load balancer resource "aws_subnet" "public" { count = 2 vpc_id = aws_vpc.application.id cidr_block = "10.1.${count.index}.0/24" availability_zone = data.aws_availability_zones.available.names[count.index] map_public_ip_on_launch = true tags = { Name = "${var.namespace}myapp-public-${count.index}" } } # Private subnets for application servers resource "aws_subnet" "private" { count = 2 vpc_id = aws_vpc.application.id cidr_block = "10.1.${count.index + 10}.0/24" availability_zone = data.aws_availability_zones.available.names[count.index] tags = { Name = "${var.namespace}myapp-private-${count.index}" } } # Application Load Balancer resource "aws_lb" "application" { name = "${var.namespace}myapp-alb" internal = false load_balancer_type = "application" security_groups = [aws_security_group.alb.id] subnets = aws_subnet.public[*].id tags = { Name = "${var.namespace}myapp-alb" } } # Security group for load balancer resource "aws_security_group" "alb" { name = "${var.namespace}myapp-alb-sg" description = "Security group for application load balancer" vpc_id = aws_vpc.application.id ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] description = "HTTPS from internet" } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] description = "All outbound traffic" } tags = { Name = "${var.namespace}myapp-alb-sg" } } ``` This application VPC topology is deployed alongside the Tensor9 controller VPC, but they remain completely separate. The controller VPC manages the control plane connection, while the application VPC handles your application's traffic and resources. ## Resource naming and tagging All AWS resources should incorporate the `@namespace` annotated variable to ensure uniqueness across multiple customer appliances. ### Parameterization pattern ```terraform theme={null} #@namespace(max_size=32, delimiter='-') variable "namespace" { type = string description = "Prefix that keeps globally-unique resource names distinct per install" default = "" } # S3 buckets resource "aws_s3_bucket" "data" { bucket = "${var.namespace}myapp-data" } # RDS databases resource "aws_db_instance" "postgres" { identifier = "${var.namespace}myapp-db" } # Lambda functions resource "aws_lambda_function" "api" { function_name = "${var.namespace}myapp-api" } # CloudWatch log groups resource "aws_cloudwatch_log_group" "api_logs" { name = "/aws/lambda/${var.namespace}myapp-api" } # IAM roles resource "aws_iam_role" "api_role" { name = "${var.namespace}myapp-api-role" } ``` ### Tags applied by Tensor9 You don't need to tag resources to identify the appliance. When Tensor9 compiles your origin stack it stamps `t9-app-name`, `t9-app-id`, `t9-buyer-name`, `t9-appliance-id`, `t9-projection-id`, `t9-release-id`, and `t9-release-version` onto every resource whose provider schema supports tags, merging them with any tags you set yourself: ```terraform theme={null} resource "aws_s3_bucket" "data" { bucket = "${var.namespace}myapp-data" tags = { Application = "my-app" ManagedBy = "Tensor9" } } ``` The `t9-appliance-id` tag: * Enables IAM condition keys to scope permissions to specific appliances * Allows CloudWatch filters to isolate telemetry by appliance * Helps customers track costs per appliance * Facilitates resource discovery by Tensor9 controllers ## Observability AWS appliances provide observability through CloudWatch, X-Ray, and CloudTrail. ### CloudWatch Logs Application and infrastructure logs flow to CloudWatch Log Groups: ```terraform theme={null} resource "aws_cloudwatch_log_group" "lambda_logs" { name = "/aws/lambda/${aws_lambda_function.api.function_name}" retention_in_days = 14 } resource "aws_cloudwatch_log_group" "eks_logs" { name = "/aws/eks/${aws_eks_cluster.main.name}/cluster" retention_in_days = 7 } ``` Your control plane uses the Steady-state role to continuously fetch logs: ```bash theme={null} aws logs get-log-events \ --log-group-name "/aws/lambda/myapp-api-000000007e" \ --log-stream-name "2024/01/15/[\$LATEST]abcdef123456" ``` Logs are forwarded to your observability sink for centralized monitoring. ### CloudWatch Metrics Infrastructure metrics are automatically collected: * **RDS**: Database connections, query latency, storage usage * **Lambda**: Invocations, duration, errors, throttles * **EKS**: Node CPU/memory, pod counts, API server metrics * **ALB**: Request counts, latency, HTTP status codes Custom application metrics can be published: ```python theme={null} import os import boto3 cloudwatch = boto3.client('cloudwatch') namespace = os.environ['NAMESPACE'] cloudwatch.put_metric_data( Namespace='MyApp', MetricData=[ { 'MetricName': 'OrdersProcessed', 'Value': 42, 'Unit': 'Count', 'Dimensions': [ { 'Name': 'Namespace', 'Value': namespace } ] } ] ) ``` ### AWS X-Ray Enable distributed tracing for Lambda and containerized applications: ```terraform theme={null} resource "aws_lambda_function" "api" { function_name = "${var.namespace}myapp-api" runtime = "nodejs18.x" handler = "index.handler" role = aws_iam_role.lambda.arn tracing_config { mode = "Active" } environment { variables = { AWS_XRAY_TRACING_NAME = "${var.namespace}myapp-api" } } } ``` X-Ray traces are accessible through the Steady-state role and forwarded to your observability sink. ### CloudTrail auditing All API calls within the customer's AWS account are logged to CloudTrail, providing a complete audit trail of what your control plane does: * Role assumptions (when Deploy or Operate roles are assumed) * Resource creation, modification, deletion * Permission denials * Configuration changes Customers have full visibility into your control plane's actions through their CloudTrail logs. ## Artifacts AWS appliances automatically provision private artifact repositories to store container images and application files deployed by your deployment stacks. ### Container images (Amazon ECR) When you deploy an appliance, Tensor9 automatically provisions a private ECR repository in the customer's AWS account to store your container images. **Example: Origin stack with ECS service** Your origin stack references container images from your vendor ECR repository: ```terraform theme={null} # ECS Task Definition in your origin stack resource "aws_ecs_task_definition" "app" { family = "${var.namespace}myapp" network_mode = "awsvpc" requires_compatibilities = ["FARGATE"] cpu = "256" memory = "512" container_definitions = jsonencode([ { name = "api" # Reference to your vendor ECR repository image = "123456789012.dkr.ecr.us-west-2.amazonaws.com/myapp-api:1.0.0" portMappings = [ { containerPort = 8080 protocol = "tcp" } ] environment = [ { name = "NAMESPACE" value = var.namespace } ] } ]) } # ECS Service resource "aws_ecs_service" "app" { name = "${var.namespace}myapp-service" cluster = aws_ecs_cluster.main.id task_definition = aws_ecs_task_definition.app.arn desired_count = 2 launch_type = "FARGATE" network_configuration { subnets = aws_subnet.private[*].id security_groups = [aws_security_group.ecs_tasks.id] assign_public_ip = false } } ``` **Container copy during deployment** When you deploy the deployment stack, Tensor9 automatically: 1. **Detects the container image reference** in your ECS task definition 2. **Provisions a private ECR repository** in the appliance (e.g., `987654321098.dkr.ecr.us-east-1.amazonaws.com/myapp-api-000000007e`) 3. **Copies the container image** from your vendor ECR (`123456789012.dkr.ecr.us-west-2.amazonaws.com/myapp-api:1.0.0`) to the appliance's private ECR 4. **Rewrites the deployment stack** to reference the appliance-local ECR repository The compiled deployment stack will contain: ```terraform theme={null} container_definitions = jsonencode([ { name = "api" # Rewritten to reference appliance's private ECR image = "987654321098.dkr.ecr.us-east-1.amazonaws.com/myapp-api-000000007e:1.0.0" # ... rest of configuration } ]) ``` This ensures the container image is stored locally in the customer's account and the application doesn't depend on cross-account access to your vendor ECR. **Artifact lifecycle** Container artifacts are tied to the deployment stack lifecycle: * **Deploy (tofu apply)**: Tensor9 copies the container image from your vendor ECR to the appliance's private ECR * **Destroy (tofu destroy)**: Deleting the deployment stack also deletes the copied container artifact from the appliance's private ECR This ensures that artifacts are cleaned up when deployments are removed, preventing orphaned resources. ### Lambda deployment packages (S3) For Lambda functions, Tensor9 supports copying Lambda deployment packages (zip files) from S3. This follows the same copy pattern as container images: ```terraform theme={null} # Lambda function referencing S3 deployment package in your origin stack resource "aws_lambda_function" "processor" { function_name = "${var.namespace}myapp-processor" role = aws_iam_role.lambda.arn handler = "index.handler" runtime = "python3.11" # Reference to Lambda zip in your vendor S3 bucket s3_bucket = "my-vendor-lambda-artifacts" s3_key = "functions/processor-v1.0.0.zip" environment { variables = { NAMESPACE = var.namespace } } } ``` During deployment, Tensor9: 1. Provisions a private S3 bucket in the appliance for Lambda artifacts 2. Copies the Lambda zip file from your vendor S3 bucket to the appliance's S3 bucket 3. Rewrites the Lambda function definition to reference the appliance-local S3 bucket Like container images, destroying the deployment stack (tofu destroy) removes the copied Lambda deployment packages. See [Artifacts](/fundamentals/artifacts) for documentation on artifact management, including immutability requirements and supported artifact types. ## Secrets management Store secrets in AWS Secrets Manager or AWS Systems Manager Parameter Store, then pass them to your application as environment variables. ### Secret naming and injection Always use parameterized secret names and inject them as environment variables: ```terraform theme={null} # AWS Secrets Manager secret resource "aws_secretsmanager_secret" "db_password" { name = "prod/db/password" } resource "aws_secretsmanager_secret_version" "db_password" { secret_id = aws_secretsmanager_secret.db_password.id secret_string = var.db_password } # ECS Fargate task - inject secret as environment variable resource "aws_ecs_task_definition" "app" { family = "${var.namespace}myapp" container_definitions = jsonencode([ { name = "app" image = "myapp:latest" # Inject secret as environment variable secrets = [ { name = "DB_PASSWORD" valueFrom = aws_secretsmanager_secret.db_password.arn } ] } ]) } ``` Your application reads secrets from environment variables: ```python theme={null} import os # Read secret from environment variable db_password = os.environ['DB_PASSWORD'] ``` AWS applications can read Secrets Manager directly with the AWS SDK or receive secret values through environment variables. Cross-cloud runtime reads use the configured [Secrets Manager service adapter](/service-adapters/aws/security-identity/secrets-manager); check its operation coverage and target-specific limits. See [Secrets](/fundamentals/secrets) for detailed secret management patterns. ## Operations Perform remote operations on AWS appliances using the Operate role. ### kubectl on EKS Execute kubectl commands against EKS clusters: ```bash theme={null} tensor9 ops kubectl \ -appName my-app \ -customerName acme-corp \ -originResourceId "aws_eks_cluster.main_cluster" \ -command "kubectl get pods -n my-app-namespace" ``` Output: ``` NAME READY STATUS RESTARTS AGE api-7d9f8b5c6d-9k2lm 1/1 Running 0 2h worker-5c8d7b4f3-8h4km 1/1 Running 0 2h ``` ### AWS CLI operations Execute AWS CLI commands: ```bash theme={null} # List S3 bucket contents tensor9 ops aws \ -appName my-app \ -customerName acme-corp \ -originResourceId "aws_s3_bucket.data" \ -command "aws s3 ls s3://myapp-data-000000007e/" # Invoke Lambda function tensor9 ops aws \ -appName my-app \ -customerName acme-corp \ -originResourceId "aws_lambda_function.api" \ -command "aws lambda invoke --function-name myapp-api-000000007e output.json" # View RDS status tensor9 ops aws \ -appName my-app \ -customerName acme-corp \ -originResourceId "aws_db_instance.postgres" \ -command "aws rds describe-db-instances --db-instance-identifier myapp-db-000000007e" ``` ### Database queries Execute SQL queries against RDS databases: ```bash theme={null} tensor9 ops db \ -appName my-app \ -customerName acme-corp \ -originResourceId "aws_db_instance.postgres" \ -command "SELECT count(*) FROM users WHERE created_at > NOW() - INTERVAL '24 hours'" ``` ### Operations endpoints Create temporary operations endpoints for interactive access: ```bash theme={null} # Create kubectl endpoint tensor9 ops endpoint create \ -appName my-app \ -customerName acme-corp \ -originResourceId "aws_eks_cluster.main_cluster" \ -endpointType kubectl \ -ttl 3600 # Output: # Endpoint created: https://ops.tensor9.io/kubectl/abc123 # Expires in: 1 hour # Use: kubectl --server=https://ops.tensor9.io/kubectl/abc123 get pods ``` See [Operations](/fundamentals/operations) for the full operations documentation. ## Example: Complete AWS appliance Here's a complete example of a Terraform origin stack for an AWS appliance: ### main.tf ```terraform theme={null} # VPC resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true tags = { Name = "${var.namespace}myapp-vpc" } } # Subnets resource "aws_subnet" "private" { count = 2 vpc_id = aws_vpc.main.id cidr_block = "10.0.${count.index}.0/24" availability_zone = data.aws_availability_zones.available.names[count.index] tags = { Name = "${var.namespace}myapp-private-${count.index}" } } # EKS Cluster resource "aws_eks_cluster" "main" { name = "${var.namespace}myapp-cluster" role_arn = aws_iam_role.cluster.arn version = "1.28" vpc_config { subnet_ids = aws_subnet.private[*].id } enabled_cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"] } # RDS PostgreSQL resource "aws_db_instance" "postgres" { identifier = "${var.namespace}myapp-db" engine = "postgres" engine_version = "15.3" instance_class = "db.t3.micro" allocated_storage = 20 db_name = "myapp" username = "admin" password = var.db_password db_subnet_group_name = aws_db_subnet_group.main.name vpc_security_group_ids = [aws_security_group.db.id] skip_final_snapshot = false final_snapshot_identifier = "${var.namespace}myapp-db-final" } # S3 bucket resource "aws_s3_bucket" "data" { bucket = "${var.namespace}myapp-data" } resource "aws_s3_bucket_versioning" "data" { bucket = aws_s3_bucket.data.id versioning_configuration { status = "Enabled" } } # ElastiCache Redis resource "aws_elasticache_cluster" "redis" { cluster_id = "${var.namespace}myapp-redis" engine = "redis" node_type = "cache.t3.micro" num_cache_nodes = 1 parameter_group_name = "default.redis7" engine_version = "7.0" port = 6379 subnet_group_name = aws_elasticache_subnet_group.main.name security_group_ids = [aws_security_group.redis.id] } # CloudWatch Log Groups resource "aws_cloudwatch_log_group" "eks" { name = "/aws/eks/${var.namespace}myapp-cluster/cluster" retention_in_days = 7 } # Secrets resource "aws_secretsmanager_secret" "db_password" { name = "prod/db/password" } resource "aws_secretsmanager_secret_version" "db_password" { secret_id = aws_secretsmanager_secret.db_password.id secret_string = var.db_password } ``` ### variables.tf ```terraform theme={null} #@namespace(max_size=32, delimiter='-') variable "namespace" { type = string description = "Prefix that keeps globally-unique resource names distinct per install" default = "" } variable "db_password" { type = string description = "Database master password" sensitive = true } variable "region" { type = string description = "AWS region" default = "us-west-2" } ``` ### outputs.tf ```terraform theme={null} output "eks_cluster_endpoint" { description = "EKS cluster API endpoint" value = aws_eks_cluster.main.endpoint } output "database_endpoint" { description = "RDS database endpoint" value = aws_db_instance.postgres.endpoint sensitive = true } output "redis_endpoint" { description = "Redis cache endpoint" value = aws_elasticache_cluster.redis.cache_nodes[0].address } output "data_bucket" { description = "S3 data bucket name" value = aws_s3_bucket.data.id } ``` ## Best practices Every AWS resource with a name or identifier should be prefixed with `${var.namespace}` to prevent conflicts across customer appliances. Don't skip account-scoped or region-scoped names: nothing stops two installs sharing one account: ```terraform theme={null} # ✓ CORRECT resource "aws_s3_bucket" "data" { bucket = "${var.namespace}myapp-data" } resource "aws_iam_role" "lambda" { name = "${var.namespace}myapp-lambda" } # ✗ INCORRECT - Will cause collisions resource "aws_s3_bucket" "data" { bucket = "myapp-data" } ``` You don't need to add appliance-identifying tags yourself. Tensor9 stamps `t9-appliance-id`, `t9-buyer-name`, `t9-app-name`, and related tags onto every resource whose provider schema supports tags, merging them with your own. This enables: * IAM permission scoping * CloudWatch filtering * Cost tracking * Resource discovery Configure logging for Lambda, EKS, RDS, and other services: ```terraform theme={null} resource "aws_cloudwatch_log_group" "service_logs" { name = "/aws/service/${var.namespace}myapp" retention_in_days = 14 } ``` This ensures observability data flows to your control plane. Never hardcode secrets. Use Secrets Manager with parameterized names and pass them to your application as environment variables: ```terraform theme={null} # Define secret resource "aws_secretsmanager_secret" "api_key" { name = "prod/api/key" } # Inject into ECS task as environment variable resource "aws_ecs_task_definition" "app" { family = "${var.namespace}myapp" container_definitions = jsonencode([ { name = "app" image = "myapp:latest" secrets = [ { name = "API_KEY" valueFrom = aws_secretsmanager_secret.api_key.arn } ] } ]) } ``` Use environment variables for values needed at startup, or the [Secrets Manager service adapter](/service-adapters/aws/security-identity/secrets-manager) when the application needs to fetch values while running. ## Troubleshooting **Symptom**: Terraform apply fails with "AccessDenied" or "UnauthorizedOperation" errors. **Solutions**: * Verify the Tensor9 controller has successfully assumed the Deploy role * Check the Deploy role's IAM policy includes necessary permissions for the resources being created * Ensure the trust policy allows the Tensor9 controller to assume it * Verify the `DeployAccess` tag is set and the time window hasn't expired * Review CloudTrail logs in the customer account to see which specific API call was denied **Symptom**: "ResourceAlreadyExists" or "BucketAlreadyExists" errors during deployment. **Solutions**: * Ensure all resource names are prefixed with `${var.namespace}` * Verify the `@namespace` annotated variable is being passed correctly * Check that no hardcoded resource names exist in your origin stack * For S3 buckets, remember they must be globally unique - prefix them with `${var.namespace}` **Symptom**: CloudWatch logs and metrics aren't appearing in your observability sink. **Solutions**: * Verify the Steady-state role has permissions to read CloudWatch logs and metrics * Check that all log groups and resources are tagged with Tensor9's `t9-appliance-id` * Ensure log group names are parameterized and follow the expected pattern * Verify CloudWatch log retention is set (logs may be deleted if retention is too short) * Check that the control plane is successfully assuming the Steady-state role **Symptom**: "VpcLimitExceeded" error when creating VPCs. **Solutions**: * Ask the customer to request a VPC quota increase from AWS (default is 5 per region) * Consider deploying appliances in separate AWS regions * Use existing customer VPCs with dedicated subnets instead of creating new VPCs * Ask the customer to clean up unused VPCs in their account **Symptom**: "InvalidParameterCombination" when enabling encryption on RDS instances. **Solutions**: * Ensure `storage_encrypted = true` is set when creating the instance * Use a customer-managed KMS key if required by customer policy * Note that encryption cannot be enabled on existing unencrypted instances - must create new instance * Verify the Deploy role has KMS permissions if using customer-managed keys If you're experiencing issues not covered here or need additional assistance with AWS deployments, we're here to help: * **Slack**: Join our community Slack workspace for real-time support * **Email**: Contact us at [support@tensor9.com](mailto:support@tensor9.com) Our team can help with deployment troubleshooting, IAM configuration, service equivalents, and best practices for AWS environments. ## Next steps Now that you understand deploying to AWS customer environments, explore these related topics: * [**Permissions Model**](/fundamentals/permissions-model): Understand the four-phase IAM permissions model in detail * [**Deployments**](/fundamentals/deployments): Learn how to create releases and deploy to customer appliances * [**Operations**](/fundamentals/operations): Execute remote operations on AWS appliances * [**Observability**](/fundamentals/observability): Set up monitoring and logging * [**Terraform Origin Stacks**](/origin-stack/terraform): Write Terraform origin stacks optimized for AWS # Microsoft Azure Source: https://docs.tensor9.com/form-factor/azure Microsoft Azure is a fully supported deployment platform for Tensor9 appliances. Deploying to Azure customer environments provides access to Microsoft's global cloud infrastructure, its security and compliance controls, and integration with customers' existing Azure resources. ## Overview When you deploy an application to Azure customer environments using Tensor9: * **Customer appliances** run entirely within the customer's Azure subscription * **Your control plane** orchestrates deployments from your dedicated Tensor9 AWS account * **Managed identities and RBAC** enable your control plane to manage customer appliances with customer-approved permissions * **Service equivalents** compile your origin stack into Azure-native resources Azure appliances use Azure services for compute, storage, networking, and observability, so they fit into the Azure environments your customers already run. ## Prerequisites Before deploying appliances to Azure customer environments, ensure: ### Your control plane * **Dedicated AWS account** for your Tensor9 control plane * **Control plane installed** - See [Installing Tensor9](/fundamentals/install) * **Origin stack published** - Your application infrastructure defined and uploaded ### Customer Azure subscription Your customers must provide: * **Azure subscription** where the appliance will be deployed * **Managed identities configured** for the four-phase permissions model (Install, Steady-state, Deploy, Operate) * **Virtual network and networking** configured according to their requirements * **Sufficient subscription quotas** for your application's resource needs * **Azure region** where they want the appliance deployed ### Your development environment * **Azure CLI** installed and configured * **kubectl** for Kubernetes operations * **Terraform or OpenTofu** (if using Terraform origin stacks) * **Docker** (if deploying container-based applications) ## How Azure appliances work Azure appliances are deployed using Azure-native services orchestrated by your Tensor9 control plane. Your customer creates four managed identities in their Azure subscription, each corresponding to a permission phase: Install, Steady-state, Deploy, and Operate. These identities define what the Tensor9 controller can do within their environment. The customer configures RBAC role assignments that allow your control plane to impersonate these managed identities with appropriate conditions (time windows, approval tags, etc.). You create a release targeting the customer's appliance: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -customerName acme-corp \ -vendorVersion "1.0.0" \ -description "Initial production deployment" ``` Your control plane compiles your origin stack into a deployment stack tailored for Azure, compiling any non-Azure resources to their Azure service equivalents. The deployment stack downloads to your local environment. The customer approves the deployment by granting temporary deploy access. This can be manual (updating RBAC role assignments) or automated (scheduled maintenance windows). Once approved, the Tensor9 controller in the appliance can use the Deploy managed identity in the customer's subscription. You run the deployment locally against the downloaded deployment stack: ```bash theme={null} cd acme-corp-production tofu init tofu apply ``` The deployment stack is configured to route resource creation through the Tensor9 controller inside the customer's appliance. The controller uses the Deploy managed identity and creates all infrastructure resources in the customer's Azure subscription: * Virtual networks, subnets, network security groups * AKS clusters, Container Instances, Azure Functions * Azure Database for PostgreSQL/MySQL, Azure Blob Storage, Azure Cache for Redis * Azure Monitor workspaces, Log Analytics, managed identities, Azure DNS zones * Any other Azure resources defined in your origin stack After deployment, your control plane uses the Steady-state managed identity to continuously collect observability data (logs, metrics, traces) from the customer's appliance without requiring additional approvals. This data flows to your observability sink, giving you visibility into appliance health and performance. ## Service adapters When you deploy an origin stack to Azure customer environments, Tensor9 automatically compiles resources from other cloud providers to their Azure equivalents. ### How service equivalents work When compiling a deployment stack for Azure: 1. **AWS resources are compiled** - AWS resources are converted to their Azure equivalents 2. **Generic resources are adapted** - Cloud-agnostic resources (like Kubernetes manifests) are adapted for Azure 3. **Configuration is adjusted** - Resource configurations are modified to match Azure conventions and best practices ### Common service equivalents Which AWS services reach Azure, what each becomes, and the tier it lands at are maintained in one place rather than repeated per form factor: see [service adapters](/service-adapters/overview) and the generated [Service Catalog](/service-adapters/catalog). EC2, DynamoDB, and EFS are all offered. Some services are not adapted yet, including Step Functions, API Gateway, Cognito, AppSync, and Redshift. See [Service Catalog](/service-adapters/catalog#services-we-do-not-adapt-yet). ### Example: Compiling an AWS origin stack If your origin stack defines a Lambda function: ```terraform theme={null} # Origin stack (AWS) resource "aws_lambda_function" "api" { function_name = "${var.namespace}myapp-api" handler = "index.handler" runtime = "nodejs18.x" role = aws_iam_role.api_role.arn environment { variables = { NAMESPACE = var.namespace } } } ``` Tensor9 compiles it to an Azure Function: ```terraform theme={null} # Deployment stack (Azure) resource "azurerm_linux_function_app" "api" { name = "${var.namespace}myapp-api" location = var.location resource_group_name = var.resource_group_name service_plan_id = azurerm_service_plan.main.id site_config { application_stack { node_version = "18" } } app_settings = { NAMESPACE = var.namespace } identity { type = "SystemAssigned" } } ``` ## Permissions model Azure appliances use a four-phase managed identity permissions model that balances operational capability with customer control. ### The four permission phases | Phase | Managed Identity | Purpose | Access Pattern | | ---------------- | ------------------------------------ | ----------------------------------------------- | ------------------------------- | | **Install** | `tensor9-install-` | Initial setup, major infrastructure changes | Customer-approved, rare | | **Steady-state** | `tensor9-steadystate-` | Continuous observability collection (read-only) | Active by default | | **Deploy** | `tensor9-deploy-` | Deployments, updates, configuration changes | Customer-approved, time-bounded | | **Operate** | `tensor9-operate-` | Remote operations, troubleshooting, debugging | Customer-approved, time-bounded | ### Managed identity structure Each managed identity is created in the customer's Azure subscription with RBAC role assignments that allow your control plane to use it. **Example: Deploy managed identity with conditional access** ```hcl theme={null} # Deploy managed identity resource "azurerm_user_assigned_identity" "deploy" { name = "tensor9-deploy-000000007e" resource_group_name = var.resource_group_name location = var.location tags = { phase = "deploy" } } # Grant Deploy identity permissions in customer subscription resource "azurerm_role_assignment" "deploy_contributor" { scope = data.azurerm_subscription.current.id role_definition_name = "Contributor" principal_id = azurerm_user_assigned_identity.deploy.principal_id condition_version = "2.0" condition = <<-EOT ( @Resource[Microsoft.Resources/tags:t9-appliance-id] StringEquals '' ) EOT } # Allow vendor control plane to use this identity resource "azurerm_role_assignment" "deploy_identity_operator" { scope = azurerm_user_assigned_identity.deploy.id role_definition_name = "Managed Identity Operator" principal_id = var.vendor_control_plane_identity_id } ``` Your control plane can only use the Deploy managed identity when: * The customer has granted the Managed Identity Operator role * Resources being created are tagged with Tensor9's `t9-appliance-id` * The time window hasn't expired (enforced via conditional access policies) Customers control when and how long deploy access is granted. **Example: Steady-state managed identity (read-only observability)** ```hcl theme={null} # Steady-state managed identity resource "azurerm_user_assigned_identity" "steadystate" { name = "tensor9-steadystate-000000007e" resource_group_name = var.resource_group_name location = var.location tags = { phase = "steadystate" } } # Grant read-only permissions scoped to appliance resources resource "azurerm_role_assignment" "steadystate_monitoring_reader" { scope = data.azurerm_subscription.current.id role_definition_name = "Monitoring Reader" principal_id = azurerm_user_assigned_identity.steadystate.principal_id condition_version = "2.0" condition = <<-EOT ( @Resource[Microsoft.Resources/tags:t9-appliance-id] StringEquals '' ) EOT } resource "azurerm_role_assignment" "steadystate_log_reader" { scope = data.azurerm_subscription.current.id role_definition_name = "Log Analytics Reader" principal_id = azurerm_user_assigned_identity.steadystate.principal_id condition_version = "2.0" condition = <<-EOT ( @Resource[Microsoft.Resources/tags:t9-appliance-id] StringEquals '' ) EOT } # Allow vendor control plane to use this identity (no time restriction) resource "azurerm_role_assignment" "steadystate_identity_operator" { scope = azurerm_user_assigned_identity.steadystate.id role_definition_name = "Managed Identity Operator" principal_id = var.vendor_control_plane_identity_id } ``` The Steady-state managed identity: * Can read observability data from resources tagged with the appliance's `t9-appliance-id` * Cannot modify, delete, or terminate any resources * Cannot change RBAC role assignments ### Deployment workflow with managed identities Customer approves a deployment by granting the Managed Identity Operator role and setting up conditional access policies. This can be done manually or through automated approval workflows. You run the deployment locally against the downloaded deployment stack: ```bash theme={null} cd acme-corp-production tofu init tofu apply ``` The deployment stack is configured to route resource creation through the Tensor9 controller in the appliance. For each resource Terraform attempts to create, the Tensor9 controller inside the appliance uses the Deploy managed identity and creates the resource in the customer's subscription. All infrastructure changes occur within the customer's subscription using their Deploy managed identity permissions. After the time window expires or the role assignment is removed, the Deploy identity can no longer be used. Your control plane automatically reverts to using only the Steady-state identity for observability. See [Permissions Model](/fundamentals/permissions-model) for detailed information on all four phases. ## Networking Azure appliances use an isolated networking architecture with a Tensor9 controller that manages communication with your control plane. ### Tensor9 controller VNet When an appliance is deployed, Tensor9 creates an isolated VNet containing the Tensor9 controller. This VNet is configured with: * **Azure NAT Gateway**: Provides outbound internet connectivity * **Route to control plane**: Establishes a secure channel to your Tensor9 control plane * **No inbound NSG rules**: The controller VNet does not accept inbound connections - all communication is outbound-only The Tensor9 controller uses this secure channel to: * **Receive deployments**: Deployment stacks are pushed from your control plane to the appliance * **Configure observability pipeline**: Set up log, metric, and trace forwarding to your observability sink * **Receive operational commands**: Execute remote operations initiated from your control plane ### Outbound-only security model The Tensor9 controller in your customer's appliance is designed to only make outbound connections and not require ingress ports to be opened in your customer's network perimeter: ```terraform theme={null} # Example: Controller VNet configuration (managed by Tensor9) resource "azurerm_virtual_network" "tensor9_controller" { name = "tensor9-controller-000000007e" location = var.location resource_group_name = var.resource_group_name address_space = ["10.0.0.0/24"] tags = { managed-by = "tensor9" } } resource "azurerm_subnet" "controller" { name = "controller-subnet" resource_group_name = var.resource_group_name virtual_network_name = azurerm_virtual_network.tensor9_controller.name address_prefixes = ["10.0.0.0/26"] } # NAT Gateway for outbound connectivity resource "azurerm_public_ip" "nat" { name = "tensor9-nat-ip-000000007e" location = var.location resource_group_name = var.resource_group_name allocation_method = "Static" sku = "Standard" tags = { managed-by = "tensor9" } } resource "azurerm_nat_gateway" "controller" { name = "tensor9-nat-000000007e" location = var.location resource_group_name = var.resource_group_name sku_name = "Standard" tags = { managed-by = "tensor9" } } resource "azurerm_nat_gateway_public_ip_association" "controller" { nat_gateway_id = azurerm_nat_gateway.controller.id public_ip_address_id = azurerm_public_ip.nat.id } resource "azurerm_subnet_nat_gateway_association" "controller" { subnet_id = azurerm_subnet.controller.id nat_gateway_id = azurerm_nat_gateway.controller.id } # NSG: egress only, no ingress resource "azurerm_network_security_group" "controller" { name = "tensor9-controller-nsg-000000007e" location = var.location resource_group_name = var.resource_group_name # Allow outbound HTTPS security_rule { name = "AllowHTTPSOutbound" priority = 100 direction = "Outbound" access = "Allow" protocol = "Tcp" source_port_range = "*" destination_port_range = "443" source_address_prefix = "*" destination_address_prefix = "*" } # No inbound rules - controller never accepts inbound connections tags = { managed-by = "tensor9" } } resource "azurerm_subnet_network_security_group_association" "controller" { subnet_id = azurerm_subnet.controller.id network_security_group_id = azurerm_network_security_group.controller.id } ``` This architecture ensures that the customer's appliance cannot be compromised via inbound network attacks on the controller. ### Application VNet topology Your application resources run in their own VNet(s), completely separate from the Tensor9 controller VNet. The application VNet topology is defined entirely by your origin stack - whatever VPC resources you define in your origin stack will be compiled to Azure VNet resources in the appliance. **Example: Application VNet with internet-facing load balancer** If your origin stack defines an AWS VPC with public subnets and a load balancer, that topology will compile to Azure VNet resources in the customer's appliance: ```terraform theme={null} # AWS origin stack - Application VPC resource "aws_vpc" "application" { cidr_block = "10.1.0.0/16" enable_dns_hostnames = true enable_dns_support = true tags = { Name = "${var.namespace}myapp-vpc" } } # Public subnet for load balancer resource "aws_subnet" "public" { vpc_id = aws_vpc.application.id cidr_block = "10.1.0.0/24" availability_zone = data.aws_availability_zones.available.names[0] map_public_ip_on_launch = true tags = { Name = "${var.namespace}myapp-public" } } # Private subnet for application servers resource "aws_subnet" "private" { vpc_id = aws_vpc.application.id cidr_block = "10.1.1.0/24" availability_zone = data.aws_availability_zones.available.names[0] tags = { Name = "${var.namespace}myapp-private" } } # Application Load Balancer resource "aws_lb" "application" { name = "${var.namespace}myapp-lb" internal = false load_balancer_type = "application" security_groups = [aws_security_group.lb.id] subnets = [aws_subnet.public.id] } ``` This application VPC topology is deployed alongside the Tensor9 controller VNet, but they remain completely separate. The controller VNet manages the control plane connection, while the application VNet handles your application's traffic and resources. ## Resource naming and tagging All Azure resources should incorporate the `@namespace` annotated variable to ensure uniqueness across multiple customer appliances. ### Parameterization pattern This page uses `max_size=16` because storage account names cap at 24 characters and allow only lowercase letters and digits, so they take the namespace with its hyphens stripped. ```terraform theme={null} #@namespace(max_size=16, delimiter='-') variable "namespace" { type = string description = "Prefix that keeps globally-unique resource names distinct per install" default = "" } # Storage accounts resource "azurerm_storage_account" "data" { name = "${replace(var.namespace, "-", "")}data" resource_group_name = var.resource_group_name location = var.location account_tier = "Standard" account_replication_type = "LRS" } # Azure Database for PostgreSQL resource "azurerm_postgresql_flexible_server" "main" { name = "${var.namespace}myapp-db" resource_group_name = var.resource_group_name location = var.location version = "15" sku_name = "B_Standard_B1ms" } # Azure Functions resource "azurerm_linux_function_app" "api" { name = "${var.namespace}myapp-api" resource_group_name = var.resource_group_name location = var.location service_plan_id = azurerm_service_plan.main.id } # Managed identities resource "azurerm_user_assigned_identity" "app" { name = "${var.namespace}myapp" resource_group_name = var.resource_group_name location = var.location } ``` ### Tags applied by Tensor9 You don't need to tag resources to identify the appliance. When Tensor9 compiles your origin stack it stamps `t9-app-name`, `t9-app-id`, `t9-buyer-name`, `t9-appliance-id`, `t9-projection-id`, `t9-release-id`, and `t9-release-version` onto every resource whose provider schema supports tags, merging them with any tags you set yourself: ```terraform theme={null} resource "azurerm_storage_account" "data" { name = "${replace(var.namespace, "-", "")}data" resource_group_name = var.resource_group_name location = var.location account_tier = "Standard" account_replication_type = "LRS" tags = { application = "my-app" managed-by = "tensor9" } } resource "azurerm_kubernetes_cluster" "main" { name = "${var.namespace}myapp-aks" location = var.location resource_group_name = var.resource_group_name dns_prefix = "${var.namespace}myapp" default_node_pool { name = "default" node_count = 2 vm_size = "Standard_D2_v2" } tags = { application = "my-app" managed-by = "tensor9" } } ``` The `t9-appliance-id` tag: * Enables RBAC condition expressions to scope permissions to specific appliances * Allows Azure Monitor filters to isolate telemetry by appliance * Helps customers track costs per appliance * Facilitates resource discovery by Tensor9 controllers ## Observability Azure appliances provide observability through Azure Monitor, Log Analytics, and Application Insights. ### Azure Monitor Logs Application and infrastructure logs flow to Log Analytics workspaces: ```terraform theme={null} # Log Analytics workspace resource "azurerm_log_analytics_workspace" "main" { name = "${var.namespace}myapp-logs" location = var.location resource_group_name = var.resource_group_name sku = "PerGB2018" retention_in_days = 30 } # Enable diagnostics for AKS resource "azurerm_monitor_diagnostic_setting" "aks" { name = "aks-diagnostics" target_resource_id = azurerm_kubernetes_cluster.main.id log_analytics_workspace_id = azurerm_log_analytics_workspace.main.id enabled_log { category = "kube-apiserver" } enabled_log { category = "kube-controller-manager" } enabled_log { category = "kube-scheduler" } metric { category = "AllMetrics" enabled = true } } ``` Your control plane uses the Steady-state managed identity to continuously fetch logs: ```bash theme={null} az monitor log-analytics query \ --workspace customer-workspace-id \ --analytics-query "AzureDiagnostics | where tags_s contains 't9-appliance-id=' | take 100" \ --identity tensor9-steadystate-000000007e ``` Logs are forwarded to your observability sink for centralized monitoring. ### Azure Monitor Metrics Infrastructure metrics are automatically collected: * **Virtual Machines**: CPU percentage, network in/out, disk operations * **Azure Database for PostgreSQL**: Database connections, CPU percent, storage used * **Azure Functions**: Execution count, execution units, errors * **AKS**: Node CPU/memory, pod counts, API server metrics * **Azure Load Balancer**: Data path availability, health probe status, packet count ### Application Insights Enable distributed tracing for Azure Functions and containerized applications: ```terraform theme={null} resource "azurerm_application_insights" "main" { name = "${var.namespace}myapp-insights" location = var.location resource_group_name = var.resource_group_name application_type = "web" workspace_id = azurerm_log_analytics_workspace.main.id } resource "azurerm_linux_function_app" "api" { name = "${var.namespace}myapp-api" location = var.location resource_group_name = var.resource_group_name service_plan_id = azurerm_service_plan.main.id app_settings = { APPINSIGHTS_INSTRUMENTATIONKEY = azurerm_application_insights.main.instrumentation_key APPLICATIONINSIGHTS_CONNECTION_STRING = azurerm_application_insights.main.connection_string ApplicationInsightsAgent_EXTENSION_VERSION = "~3" NAMESPACE = var.namespace } } ``` Application Insights traces are accessible through the Steady-state identity and forwarded to your observability sink. ### Azure Activity Log All API calls within the customer's Azure subscription are logged to Activity Log, providing a complete audit trail of what your control plane does: * Managed identity usage * Resource creation, modification, deletion * Permission denials * Role assignment changes Customers have full visibility into your control plane's actions through their Activity Log. ## Artifacts Azure appliances automatically provision private artifact repositories to store container images and application files deployed by your deployment stacks. ### Container images (Azure Container Registry) When you deploy an appliance, Tensor9 automatically provisions a private Azure Container Registry in the customer's Azure subscription to store your container images. **Example: Origin stack with container service** Your AWS origin stack references container images from your vendor's Amazon ECR: ```terraform theme={null} # ECS Fargate service in your origin stack resource "aws_ecs_task_definition" "api" { family = "${var.namespace}myapp-api" requires_compatibilities = ["FARGATE"] network_mode = "awsvpc" cpu = "256" memory = "512" container_definitions = jsonencode([ { name = "api" # Reference to your vendor ECR registry image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp/api:1.0.0" portMappings = [ { containerPort = 8080 protocol = "tcp" } ] environment = [ { name = "NAMESPACE" value = var.namespace } ] } ]) } ``` **Container copy during deployment** When you deploy the deployment stack, Tensor9 automatically: 1. **Detects the container image reference** in your ECS task definition 2. **Provisions a private Azure Container Registry** in the appliance (e.g., `myappacr000000007e.azurecr.io`) 3. **Copies the container image** from your vendor ECR registry to the appliance's private ACR 4. **Rewrites the deployment stack** to reference the appliance-local registry The compiled deployment stack will contain an Azure Container Instances or AKS deployment with the rewritten image reference: ```terraform theme={null} # Azure Container Instances image = "myappacr000000007e.azurecr.io/api:1.0.0" ``` This ensures the container image is stored locally in the customer's subscription and the application doesn't depend on cross-subscription access to your vendor registry. **Artifact lifecycle** Container artifacts are tied to the deployment stack lifecycle: * **Deploy (tofu apply)**: Tensor9 copies the container image from your vendor registry to the appliance's private registry * **Destroy (tofu destroy)**: Deleting the deployment stack also deletes the copied container artifact from the appliance's private registry This ensures that artifacts are cleaned up when deployments are removed, preventing orphaned resources. ### Function source code For Lambda functions in your AWS origin stack, Tensor9 automatically handles copying function source code to the customer's Azure environment: ```terraform theme={null} # Lambda function in your AWS origin stack resource "aws_lambda_function" "processor" { function_name = "${var.namespace}myapp-processor" handler = "processor.process_event" runtime = "python3.11" role = aws_iam_role.processor.arn # Reference to function code in your vendor S3 bucket s3_bucket = "vendor-lambda-sources" s3_key = "processor-v1.0.0.zip" environment { variables = { NAMESPACE = var.namespace } } } ``` During deployment, Tensor9: 1. Provisions a private Azure Storage Account in the appliance for function sources 2. Copies the Lambda source archive from your vendor S3 bucket to the appliance's Storage Account 3. Compiles the Lambda function to a Container Apps service with the appliance-local source reference Like container images, destroying the deployment stack (tofu destroy) removes the copied function source archives. See [Artifacts](/fundamentals/artifacts) for documentation on artifact management, including immutability requirements and supported artifact types. ## Secrets management Store secrets in AWS Secrets Manager or AWS Systems Manager Parameter Store in your AWS origin stack, then pass them to your application as environment variables. ### Secret naming and injection Always use parameterized secret names and inject them as environment variables: ```terraform theme={null} # AWS Secrets Manager secret resource "aws_secretsmanager_secret" "db_password" { name = "prod/db/password" } resource "aws_secretsmanager_secret_version" "db_password" { secret_id = aws_secretsmanager_secret.db_password.id secret_string = var.db_password } # ECS Fargate task - inject secret as environment variable resource "aws_ecs_task_definition" "app" { family = "${var.namespace}myapp" container_definitions = jsonencode([ { name = "app" image = "myapp:latest" # Inject secret as environment variable secrets = [ { name = "DB_PASSWORD" valueFrom = aws_secretsmanager_secret.db_password.arn } ] } ]) } ``` Your application reads secrets from environment variables: ```python theme={null} import os # Read secret from environment variable db_password = os.environ['DB_PASSWORD'] ``` Your application can keep using AWS Secrets Manager SDK calls on Azure through the configured [Secrets Manager service adapter](/service-adapters/aws/security-identity/secrets-manager). The adapter serves supported requests and maintains secret values and versions. Review the target-specific limits before deployment; environment variables remain an option for values needed at startup. See [Secrets](/fundamentals/secrets) for detailed secret management patterns. ## Operations Perform remote operations on Azure appliances using the Operate managed identity. ### kubectl on AKS Execute kubectl commands against AKS clusters: ```bash theme={null} tensor9 ops kubectl \ -appName my-app \ -customerName acme-corp \ -originResourceId "azurerm_kubernetes_cluster.main" \ -command "kubectl get pods -n my-app-namespace" ``` Output: ``` NAME READY STATUS RESTARTS AGE api-7d9f8b5c6d-9k2lm 1/1 Running 0 2h worker-5c8d7b4f3-8h4km 1/1 Running 0 2h ``` ### Azure CLI operations Execute Azure CLI commands: ```bash theme={null} # List storage container contents tensor9 ops az \ -appName my-app \ -customerName acme-corp \ -originResourceId "azurerm_storage_account.data" \ -command "az storage blob list --account-name myappdata000000007e --container-name files" # Invoke Azure Function tensor9 ops az \ -appName my-app \ -customerName acme-corp \ -originResourceId "azurerm_linux_function_app.api" \ -command "az functionapp function invoke --name myapp-api-000000007e --function-name processor" # View PostgreSQL status tensor9 ops az \ -appName my-app \ -customerName acme-corp \ -originResourceId "azurerm_postgresql_flexible_server.main" \ -command "az postgres flexible-server show --name myapp-db-000000007e --resource-group myapp-rg" ``` ### Database queries Execute SQL queries against Azure Database for PostgreSQL: ```bash theme={null} tensor9 ops db \ -appName my-app \ -customerName acme-corp \ -originResourceId "azurerm_postgresql_flexible_server.main" \ -command "SELECT count(*) FROM users WHERE created_at > NOW() - INTERVAL '24 hours'" ``` ### Operations endpoints Create temporary operations endpoints for interactive access: ```bash theme={null} # Create kubectl endpoint tensor9 ops endpoint create \ -appName my-app \ -customerName acme-corp \ -originResourceId "azurerm_kubernetes_cluster.main" \ -endpointType kubectl \ -ttl 3600 # Output: # Endpoint created: https://ops.tensor9.io/kubectl/abc123 # Expires in: 1 hour # Use: kubectl --server=https://ops.tensor9.io/kubectl/abc123 get pods ``` See [Operations](/fundamentals/operations) for the full operations documentation. ## Example: Complete Azure appliance Here's a complete example of a deployment stack for an Azure appliance, compiled from an AWS origin stack: ### main.tf ```terraform theme={null} # Virtual Network resource "azurerm_virtual_network" "main" { name = "${var.namespace}myapp-vnet" location = var.location resource_group_name = var.resource_group_name address_space = ["10.0.0.0/16"] } # Subnets resource "azurerm_subnet" "private" { name = "private-subnet" resource_group_name = var.resource_group_name virtual_network_name = azurerm_virtual_network.main.name address_prefixes = ["10.0.1.0/24"] } # AKS Cluster resource "azurerm_kubernetes_cluster" "main" { name = "${var.namespace}myapp-aks" location = var.location resource_group_name = var.resource_group_name dns_prefix = "${var.namespace}myapp" kubernetes_version = "1.28.3" default_node_pool { name = "default" node_count = 2 vm_size = "Standard_D2_v2" vnet_subnet_id = azurerm_subnet.private.id } identity { type = "SystemAssigned" } oms_agent { log_analytics_workspace_id = azurerm_log_analytics_workspace.main.id } } # Azure Database for PostgreSQL resource "azurerm_postgresql_flexible_server" "main" { name = "${var.namespace}myapp-db" resource_group_name = var.resource_group_name location = var.location version = "15" sku_name = "B_Standard_B1ms" storage_mb = 32768 administrator_login = "adminuser" administrator_password = var.db_password zone = "1" } resource "azurerm_postgresql_flexible_server_database" "main" { name = "myapp" server_id = azurerm_postgresql_flexible_server.main.id charset = "UTF8" collation = "en_US.utf8" } # Storage Account resource "azurerm_storage_account" "data" { name = "${replace(var.namespace, "-", "")}data" resource_group_name = var.resource_group_name location = var.location account_tier = "Standard" account_replication_type = "LRS" blob_properties { versioning_enabled = true } } resource "azurerm_storage_container" "data" { name = "application-data" storage_account_name = azurerm_storage_account.data.name container_access_type = "private" } # Azure Cache for Redis resource "azurerm_redis_cache" "main" { name = "${var.namespace}myapp-redis" location = var.location resource_group_name = var.resource_group_name capacity = 0 family = "C" sku_name = "Basic" redis_version = "6" } # Log Analytics Workspace resource "azurerm_log_analytics_workspace" "main" { name = "${var.namespace}myapp-logs" location = var.location resource_group_name = var.resource_group_name sku = "PerGB2018" retention_in_days = 30 } # Application Insights resource "azurerm_application_insights" "main" { name = "${var.namespace}myapp-insights" location = var.location resource_group_name = var.resource_group_name workspace_id = azurerm_log_analytics_workspace.main.id application_type = "web" } ``` ### variables.tf ```terraform theme={null} #@namespace(max_size=16, delimiter='-') variable "namespace" { type = string description = "Prefix that keeps globally-unique resource names distinct per install" default = "" } variable "resource_group_name" { type = string description = "Azure resource group name" } variable "location" { type = string description = "Azure region" default = "eastus" } variable "db_password" { type = string description = "Database administrator password" sensitive = true } ``` ### outputs.tf ```terraform theme={null} output "aks_cluster_endpoint" { description = "AKS cluster endpoint" value = azurerm_kubernetes_cluster.main.kube_config[0].host sensitive = true } output "database_fqdn" { description = "PostgreSQL database FQDN" value = azurerm_postgresql_flexible_server.main.fqdn sensitive = true } output "redis_hostname" { description = "Redis cache hostname" value = azurerm_redis_cache.main.hostname } output "storage_account_name" { description = "Storage account name" value = azurerm_storage_account.data.name } output "application_insights_key" { description = "Application Insights instrumentation key" value = azurerm_application_insights.main.instrumentation_key sensitive = true } ``` ## Best practices Every Azure resource with a name should be prefixed with `${var.namespace}` to prevent conflicts across customer appliances. Don't skip resource-group-scoped names: two installs commonly share one resource group: ```terraform theme={null} # ✓ CORRECT resource "azurerm_storage_account" "data" { name = "${replace(var.namespace, "-", "")}data" } resource "azurerm_kubernetes_cluster" "main" { name = "${var.namespace}myapp-aks" } # ✗ INCORRECT - Will cause collisions resource "azurerm_storage_account" "data" { name = "myappdata" } ``` Note: Storage account names must be globally unique and can only contain lowercase letters and numbers (no hyphens). You don't need to add appliance-identifying tags yourself. Tensor9 stamps `t9-appliance-id`, `t9-buyer-name`, `t9-app-name`, and related tags onto every resource whose provider schema supports tags, merging them with your own: ```terraform theme={null} tags = { application = "my-app" managed-by = "tensor9" } ``` This enables: * RBAC condition expressions for permission scoping * Azure Monitor filtering * Cost tracking * Resource discovery Configure diagnostics for AKS, Azure Functions, databases, and other services: ```terraform theme={null} resource "azurerm_monitor_diagnostic_setting" "aks" { name = "aks-diagnostics" target_resource_id = azurerm_kubernetes_cluster.main.id log_analytics_workspace_id = azurerm_log_analytics_workspace.main.id enabled_log { category = "kube-apiserver" } metric { category = "AllMetrics" enabled = true } } ``` This ensures observability data flows to your control plane. Never hardcode secrets. Use AWS Secrets Manager or SSM Parameter Store with parameterized names in your AWS origin stack: ```terraform theme={null} # AWS Secrets Manager (recommended) resource "aws_secretsmanager_secret" "api_key" { name = "prod/api/key" } # Or AWS Systems Manager Parameter Store resource "aws_ssm_parameter" "db_password" { name = "/prod/db/password" type = "SecureString" value = var.db_password } ``` Environment variables supply values at application startup. For runtime AWS Secrets Manager reads, use the configured [Secrets Manager service adapter](/service-adapters/aws/security-identity/secrets-manager) and check the operations your application needs. ## Troubleshooting **Symptom**: Terraform apply fails with "AuthorizationFailed" or "Forbidden" errors. **Solutions**: * Verify the Tensor9 controller has successfully authenticated with the Deploy managed identity * Check the Deploy identity's role assignments include necessary permissions for the resources being created * Ensure the RBAC conditional access policies allow the operation * Verify resources are tagged with Tensor9's `t9-appliance-id` * Review Azure Activity Log in the customer subscription to see which specific API call was denied **Symptom**: "ResourceExists" or "NameNotAvailable" errors during deployment. **Solutions**: * Ensure all resource names are prefixed with `${var.namespace}` * Verify the `@namespace` annotated variable is being passed correctly * Check that no hardcoded resource names exist in your origin stack * For storage accounts, remember names must be globally unique and only contain lowercase letters and numbers * For storage accounts, ensure the name is between 3-24 characters **Symptom**: Azure Monitor logs and metrics aren't appearing in your observability sink. **Solutions**: * Verify the Steady-state identity has Monitoring Reader and Log Analytics Reader permissions * Check that all resources are tagged with Tensor9's `t9-appliance-id` * Ensure diagnostic settings are configured for all resources * Verify Log Analytics workspace retention is set appropriately * Check that the control plane is successfully using the Steady-state identity **Symptom**: "QuotaExceeded" or "OperationNotAllowed" errors when creating resources. **Solutions**: * Ask the customer to request quota increases from Azure Portal * Consider deploying appliances in separate Azure regions * Review and clean up unused resources in the customer's subscription * For virtual machine quotas, consider using different VM sizes **Symptom**: Kubernetes cluster creation times out or fails. **Solutions**: * Verify the region supports AKS * Check that the Kubernetes version is supported in the region * Ensure the VM SKU is available in the region * Verify VNet and subnet configuration is correct * Check that service principal or managed identity has necessary permissions * Review Azure Service Health for service incidents **Symptom**: "StorageAccountNameInvalid" errors. **Solutions**: * Ensure storage account names only contain lowercase letters and numbers (no hyphens) * Verify the name is between 3-24 characters * Check that `${var.namespace}` doesn't contain characters the target namespace rejects * Consider shortening the app name prefix if the full name is too long If you're experiencing issues not covered here or need additional assistance with Azure deployments, we're here to help: * **Slack**: Join our community Slack workspace for real-time support * **Email**: Contact us at [support@tensor9.com](mailto:support@tensor9.com) Our team can help with deployment troubleshooting, managed identity configuration, service equivalents, and best practices for Azure environments. ## Next steps Now that you understand deploying to Azure customer environments, explore these related topics: * [**Permissions Model**](/fundamentals/permissions-model): Understand the four-phase permissions model in detail * [**Deployments**](/fundamentals/deployments): Learn how to create releases and deploy to customer appliances * [**Operations**](/fundamentals/operations): Execute remote operations on Azure appliances * [**Observability**](/fundamentals/observability): Set up monitoring and logging * [**Terraform Origin Stacks**](/origin-stack/terraform): Write Terraform origin stacks optimized for Azure # Google Cloud Source: https://docs.tensor9.com/form-factor/gcp Google Cloud is a fully supported deployment platform for Tensor9 appliances. Deploying to Google Cloud customer environments provides access to Google's global infrastructure, its security and scaling controls, and integration with customers' existing Google Cloud resources. ## Overview When you deploy an application to Google Cloud customer environments using Tensor9: * **Customer appliances** run entirely within the customer's Google Cloud project * **Your control plane** orchestrates deployments from your dedicated Tensor9 AWS account * **Service account impersonation** enables your control plane to manage customer appliances with customer-approved permissions * **Service equivalents** compile your origin stack into Google Cloud-native resources Google Cloud appliances use Google Cloud services for compute, storage, networking, and observability, so they fit into the Google Cloud environments your customers already run. ## Prerequisites Before deploying appliances to Google Cloud customer environments, ensure: ### Your control plane * **Dedicated AWS account** for your Tensor9 control plane * **Control plane installed** - See [Installing Tensor9](/fundamentals/install) * **Origin stack published** - Your application infrastructure defined and uploaded ### Customer Google Cloud project Your customers must provide: * **Google Cloud project** where the appliance will be deployed * **Service accounts configured** for the four-phase permissions model (Install, Steady-state, Deploy, Operate) * **VPC and networking** configured according to their requirements * **Sufficient API quotas** for your application's resource needs * **Google Cloud region** where they want the appliance deployed ### Your development environment * **gcloud CLI** installed and configured * **Terraform or OpenTofu** (if using Terraform origin stacks) * **Docker** (if deploying container-based applications) ## How Google Cloud appliances work Google Cloud appliances are deployed using Google Cloud-native services orchestrated by your Tensor9 control plane. Your customer creates four service accounts in their Google Cloud project, each corresponding to a permission phase: Install, Steady-state, Deploy, and Operate. These service accounts define what your control plane can do within their environment. The customer configures IAM policies that allow your control plane's service account to impersonate these service accounts with appropriate conditions (time windows, approval labels, etc.). You create a release targeting the customer's appliance: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -customerName acme-corp \ -vendorVersion "1.0.0" \ -description "Initial production deployment" ``` Your control plane compiles your origin stack into a deployment stack tailored for Google Cloud, compiling any non-Google Cloud resources to their Google Cloud service equivalents. The deployment stack downloads to your local environment. The customer approves the deployment by granting temporary deploy access. This can be manual (updating IAM policy conditions) or automated (scheduled maintenance windows). Once approved, the Tensor9 controller in the appliance can impersonate the Deploy service account in the customer's project. You run the deployment locally against the downloaded deployment stack: ```bash theme={null} cd acme-corp-production tofu init tofu apply ``` The deployment stack is configured to route resource creation through the Tensor9 controller inside the customer's appliance. The controller impersonates the Deploy service account and creates all infrastructure resources in the customer's Google Cloud project: * VPCs, subnets, firewall rules * Compute Engine instances, GKE clusters, Cloud Functions * Cloud SQL databases, Cloud Storage buckets, Memorystore clusters * Cloud Logging log sinks, service accounts, Cloud DNS records * Any other Google Cloud resources defined in your origin stack After deployment, your control plane uses the Steady-state service account to continuously collect observability data (logs, metrics, traces) from the customer's appliance without requiring additional approvals. This data flows to your observability sink, giving you visibility into appliance health and performance. ## Service adapters When you deploy an origin stack to Google Cloud customer environments, Tensor9 automatically compiles resources from other cloud providers to their Google Cloud equivalents. This allows you to maintain a single origin stack and deploy it across different customer environments. ### How service equivalents work When compiling a deployment stack for Google Cloud: 1. **AWS resources are compiled** - AWS resources are converted to their Google Cloud equivalents 2. **Generic resources are adapted** - Cloud-agnostic resources (like Kubernetes manifests) are adapted for Google Cloud 3. **Configuration is adjusted** - Resource configurations are modified to match Google Cloud conventions and best practices ### Common service equivalents Which AWS services reach Google Cloud, what each becomes, and the tier it lands at are maintained in one place rather than repeated per form factor: see [service adapters](/service-adapters/overview) and the generated [Service Catalog](/service-adapters/catalog). EC2, DynamoDB, and EFS are all offered. Some services are not adapted yet, including Step Functions, API Gateway, Cognito, AppSync, and Redshift. See [Service Catalog](/service-adapters/catalog#services-we-do-not-adapt-yet). ### Example: Compiling an AWS origin stack If your origin stack defines a Lambda function: ```terraform theme={null} # Origin stack (AWS) resource "aws_lambda_function" "api" { function_name = "${var.namespace}myapp-api" handler = "index.handler" runtime = "nodejs18.x" role = aws_iam_role.api_role.arn environment { variables = { NAMESPACE = var.namespace } } } ``` Tensor9 compiles it to a Cloud Function for Google Cloud: ```terraform theme={null} # Deployment stack (Google Cloud) resource "google_cloudfunctions2_function" "api" { name = "${var.namespace}myapp-api" location = var.region project = var.project_id build_config { runtime = "nodejs18" entry_point = "handler" } service_config { environment_variables = { NAMESPACE = var.namespace } service_account_email = google_service_account.api.email } } ``` ## Permissions model Google Cloud appliances use a four-phase service account permissions model that balances operational capability with customer control. ### The four permission phases | Phase | Service Account | Purpose | Access Pattern | | ---------------- | ------------------------------------------------------ | ----------------------------------------------- | ------------------------------- | | **Install** | `install@customer-project.iam.gserviceaccount.com` | Initial setup, major infrastructure changes | Customer-approved, rare | | **Steady-state** | `steadystate@customer-project.iam.gserviceaccount.com` | Continuous observability collection (read-only) | Active by default | | **Deploy** | `deploy@customer-project.iam.gserviceaccount.com` | Deployments, updates, configuration changes | Customer-approved, time-bounded | | **Operate** | `operate@customer-project.iam.gserviceaccount.com` | Remote operations, troubleshooting, debugging | Customer-approved, time-bounded | ### Service account structure Each service account is created in the customer's Google Cloud project with IAM policies that allow your control plane to impersonate it. **Example: Deploy service account with conditional access** ```hcl theme={null} # Deploy service account resource "google_service_account" "deploy" { account_id = "tensor9-deploy-000000007e" display_name = "Tensor9 Deploy Service Account" project = var.customer_project_id } # IAM binding allowing vendor control plane to impersonate resource "google_service_account_iam_binding" "deploy_impersonation" { service_account_id = google_service_account.deploy.name role = "roles/iam.serviceAccountTokenCreator" members = [ "serviceAccount:controlplane@vendor-project.iam.gserviceaccount.com" ] condition { title = "Deploy access time window" description = "Allow impersonation during approved time window" expression = <<-EOT request.time >= timestamp("2024-01-01T00:00:00Z") && request.time <= timestamp("2024-12-31T23:59:59Z") && resource.labels.deploy_access == "enabled" EOT } } # Grant Deploy service account permissions in customer project resource "google_project_iam_member" "deploy_compute" { project = var.customer_project_id role = "roles/compute.instanceAdmin.v1" member = "serviceAccount:${google_service_account.deploy.email}" } resource "google_project_iam_member" "deploy_storage" { project = var.customer_project_id role = "roles/storage.admin" member = "serviceAccount:${google_service_account.deploy.email}" } ``` Your control plane can only impersonate the Deploy service account when: * The `deploy_access` label is set to "enabled" * The current time is within the allowed window Customers control when and how long deploy access is granted. **Example: Steady-state service account (read-only observability)** ```hcl theme={null} # Steady-state service account resource "google_service_account" "steadystate" { account_id = "tensor9-steadystate-000000007e" display_name = "Tensor9 Steady-State Service Account" project = var.customer_project_id } # Allow vendor control plane to impersonate (no time restriction) resource "google_service_account_iam_binding" "steadystate_impersonation" { service_account_id = google_service_account.steadystate.name role = "roles/iam.serviceAccountTokenCreator" members = [ "serviceAccount:controlplane@vendor-project.iam.gserviceaccount.com" ] } # Grant read-only permissions, scoped to the appliance's own project resource "google_project_iam_member" "steadystate_logging_viewer" { project = var.customer_project_id role = "roles/logging.viewer" member = "serviceAccount:${google_service_account.steadystate.email}" } resource "google_project_iam_member" "steadystate_monitoring_viewer" { project = var.customer_project_id role = "roles/monitoring.viewer" member = "serviceAccount:${google_service_account.steadystate.email}" } ``` The Steady-state service account: * Can read observability data from the appliance's project, which holds only that appliance's resources * Cannot modify, delete, or terminate any resources * Cannot change IAM policies ### Deployment workflow with service accounts Customer approves a deployment by setting the `deploy_access` label to "enabled" and defining a time window. This can be done manually or through automated approval workflows. You run the deployment locally against the downloaded deployment stack: ```bash theme={null} cd acme-corp-production tofu init tofu apply ``` The deployment stack is configured to route resource creation through the Tensor9 controller in the appliance. For each resource Terraform attempts to create, the Tensor9 controller inside the appliance impersonates the Deploy service account and creates the resource in the customer's project. All infrastructure changes occur within the customer's project using their Deploy service account permissions. After the time window expires, the Deploy service account can no longer be impersonated. Your control plane automatically reverts to using only the Steady-state service account for observability. See [Permissions Model](/fundamentals/permissions-model) for detailed information on all four phases. ## Networking Google Cloud appliances use an isolated networking architecture with a Tensor9 controller that manages communication with your control plane. ### Tensor9 controller VPC When an appliance is deployed, Tensor9 creates an isolated VPC containing the Tensor9 controller. This VPC is configured with: * **Cloud NAT**: Provides outbound internet connectivity * **Route to control plane**: Establishes a secure channel to your Tensor9 control plane * **No ingress firewall rules**: The controller VPC does not accept inbound connections - all communication is outbound-only The Tensor9 controller uses this secure channel to: * **Receive deployments**: Deployment stacks are pushed from your control plane to the appliance * **Configure observability pipeline**: Set up log, metric, and trace forwarding to your observability sink * **Receive operational commands**: Execute remote operations initiated from your control plane ### Outbound-only security model The Tensor9 controller in your customer's appliance is designed to only make outbound connections and not require ingress ports to be opened in your customer's network perimeter: ```terraform theme={null} # Example: Controller VPC configuration (managed by Tensor9) resource "google_compute_network" "tensor9_controller" { name = "tensor9-controller-000000007e" auto_create_subnetworks = false project = var.customer_project_id } resource "google_compute_subnetwork" "controller" { name = "tensor9-controller-subnet-000000007e" ip_cidr_range = "10.0.0.0/24" region = var.region network = google_compute_network.tensor9_controller.id project = var.customer_project_id } # Cloud Router for NAT resource "google_compute_router" "controller" { name = "tensor9-controller-router-000000007e" region = var.region network = google_compute_network.tensor9_controller.id project = var.customer_project_id } # Cloud NAT for outbound connectivity resource "google_compute_router_nat" "controller" { name = "tensor9-controller-nat-000000007e" router = google_compute_router.controller.name region = var.region nat_ip_allocate_option = "AUTO_ONLY" source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES" project = var.customer_project_id } # Firewall: egress only, no ingress resource "google_compute_firewall" "controller_egress" { name = "tensor9-controller-egress-000000007e" network = google_compute_network.tensor9_controller.name project = var.customer_project_id allow { protocol = "tcp" ports = ["443"] } direction = "EGRESS" destination_ranges = ["0.0.0.0/0"] } # No ingress firewall rules - controller never accepts inbound connections ``` This architecture ensures that the customer's appliance cannot be compromised via inbound network attacks on the controller. ### Application VPC topology Your application resources run in their own VPC(s), completely separate from the Tensor9 controller VPC. The application VPC topology is defined entirely by your origin stack - whatever VPC resources you define in your origin stack will be deployed into the appliance. **Example: Application VPC with internet-facing load balancer** If your origin stack defines an AWS VPC with public subnets and a load balancer, that topology will compile to Google Cloud VPC resources in the customer's appliance: ```terraform theme={null} # AWS origin stack - Application VPC resource "aws_vpc" "application" { cidr_block = "10.1.0.0/16" enable_dns_hostnames = true enable_dns_support = true tags = { Name = "${var.namespace}myapp-vpc" } } # Public subnet for load balancer resource "aws_subnet" "public" { vpc_id = aws_vpc.application.id cidr_block = "10.1.0.0/24" availability_zone = data.aws_availability_zones.available.names[0] map_public_ip_on_launch = true tags = { Name = "${var.namespace}myapp-public" } } # Private subnet for application servers resource "aws_subnet" "private" { vpc_id = aws_vpc.application.id cidr_block = "10.1.1.0/24" availability_zone = data.aws_availability_zones.available.names[0] tags = { Name = "${var.namespace}myapp-private" } } # NAT Gateway for private subnet outbound access resource "aws_eip" "nat" { domain = "vpc" tags = { Name = "${var.namespace}myapp-nat" } } resource "aws_nat_gateway" "application" { allocation_id = aws_eip.nat.id subnet_id = aws_subnet.public.id tags = { Name = "${var.namespace}myapp-nat" } } # Application Load Balancer resource "aws_lb" "application" { name = "${var.namespace}myapp-lb" internal = false load_balancer_type = "application" security_groups = [aws_security_group.lb.id] subnets = [aws_subnet.public.id] } ``` This application VPC topology is deployed alongside the Tensor9 controller VPC, but they remain completely separate. The controller VPC manages the control plane connection, while the application VPC handles your application's traffic and resources. ## Resource naming and labeling All Google Cloud resources should incorporate the `@namespace` annotated variable to ensure uniqueness across multiple customer appliances. ### Parameterization pattern This page uses `max_size=16` because service account IDs cap at 30 characters, which a longer namespace would exhaust. ```terraform theme={null} #@namespace(max_size=16, delimiter='-') variable "namespace" { type = string description = "Prefix that keeps globally-unique resource names distinct per install" default = "" } # Cloud Storage buckets resource "google_storage_bucket" "data" { name = "${var.namespace}myapp-data" location = var.region project = var.project_id } # Cloud SQL databases resource "google_sql_database_instance" "postgres" { name = "${var.namespace}myapp-db" database_version = "POSTGRES_15" region = var.region project = var.project_id settings { tier = "db-f1-micro" } } # Cloud Functions resource "google_cloudfunctions2_function" "api" { name = "${var.namespace}myapp-api" location = var.region project = var.project_id build_config { runtime = "nodejs20" entry_point = "handleRequest" } } # Service accounts resource "google_service_account" "app" { account_id = "${var.namespace}myapp" display_name = "Application Service Account" project = var.project_id } ``` ### Labeling resources Label resources with whatever your own tooling needs. Tensor9 does not require any particular label: each appliance gets its own Google Cloud project, so telemetry and permissions are already scoped to it. ```terraform theme={null} resource "google_storage_bucket" "data" { name = "${var.namespace}myapp-data" location = var.region project = var.project_id labels = { application = "my-app" managed-by = "tensor9" } } resource "google_compute_instance" "app" { name = "${var.namespace}myapp-instance" machine_type = "e2-medium" zone = "${var.region}-a" project = var.project_id labels = { application = "my-app" managed-by = "tensor9" } } ``` The appliance's own project: * Scopes IAM role bindings to a single appliance * Isolates Cloud Logging and Cloud Monitoring telemetry by appliance * Helps customers track costs per appliance * Lets Tensor9 controllers discover the appliance's resources ## Observability Google Cloud appliances provide observability through Cloud Logging, Cloud Monitoring, and Cloud Trace. ### Cloud Logging Application and infrastructure logs flow to Cloud Logging: ```terraform theme={null} # Log sink for application logs resource "google_logging_project_sink" "app_logs" { name = "${var.namespace}myapp-logs" destination = "storage.googleapis.com/${google_storage_bucket.logs.name}" project = var.project_id unique_writer_identity = true } # Grant sink service account permissions resource "google_storage_bucket_iam_member" "logs_writer" { bucket = google_storage_bucket.logs.name role = "roles/storage.objectCreator" member = google_logging_project_sink.app_logs.writer_identity } ``` Your control plane uses the Steady-state service account to continuously fetch logs: ```bash theme={null} gcloud logging read \ --limit 100 \ --format json \ --project customer-project-id \ --impersonate-service-account steadystate@customer-project.iam.gserviceaccount.com ``` Logs are forwarded to your observability sink for centralized monitoring. ### Cloud Monitoring Infrastructure metrics are automatically collected: * **Compute Engine**: CPU utilization, network I/O, disk I/O * **Cloud SQL**: Database connections, query latency, storage usage * **Cloud Functions**: Invocations, execution time, errors * **GKE**: Node CPU/memory, pod counts, API server metrics * **Cloud Load Balancing**: Request counts, latency, HTTP status codes ### Cloud Trace Enable distributed tracing for Cloud Functions and containerized applications: ```terraform theme={null} resource "google_cloudfunctions2_function" "api" { name = "${var.namespace}myapp-api" location = var.region project = var.project_id build_config { runtime = "nodejs20" entry_point = "handleRequest" } service_config { environment_variables = { NAMESPACE = var.namespace GOOGLE_CLOUD_TRACE_ENABLED = "true" GOOGLE_CLOUD_TRACE_NEW_CONTEXT = "true" } } } ``` Cloud Trace data is accessible through the Steady-state service account and forwarded to your observability sink. ### Cloud Audit Logs All API calls within the customer's Google Cloud project are logged to Cloud Audit Logs, providing a complete audit trail of what your control plane does: * Service account impersonations * Resource creation, modification, deletion * Permission denials * Configuration changes Customers have full visibility into your control plane's actions through their Cloud Audit Logs. ## Artifacts Google Cloud appliances automatically provision private artifact repositories to store container images and application files deployed by your deployment stacks. ### Container images (Artifact Registry) When you deploy an appliance, Tensor9 automatically provisions a private Artifact Registry repository in the customer's Google Cloud project to store your container images. **Example: Origin stack with container service** Your AWS origin stack references container images from your vendor's Amazon ECR: ```terraform theme={null} # ECS Fargate service in your origin stack resource "aws_ecs_task_definition" "api" { family = "${var.namespace}myapp-api" requires_compatibilities = ["FARGATE"] network_mode = "awsvpc" cpu = "256" memory = "512" container_definitions = jsonencode([ { name = "api" # Reference to your vendor ECR registry image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp/api:1.0.0" portMappings = [ { containerPort = 8080 protocol = "tcp" } ] environment = [ { name = "NAMESPACE" value = var.namespace } ] } ]) } ``` **Container copy during deployment** When you deploy the deployment stack, Tensor9 automatically: 1. **Detects the container image reference** in your ECS task definition 2. **Provisions a private Artifact Registry repository** in the appliance (e.g., `us-docker.pkg.dev/customer-project/myapp-000000007e/api`) 3. **Copies the container image** from your vendor ECR registry to the appliance's private Artifact Registry 4. **Rewrites the deployment stack** to reference the appliance-local registry The compiled deployment stack will contain a Cloud Run service with the rewritten image reference: ```terraform theme={null} template { containers { # Rewritten to reference appliance's private Artifact Registry image = "us-docker.pkg.dev/customer-project/myapp-000000007e/api:1.0.0" # ... rest of configuration compiled from ECS task definition } } ``` This ensures the container image is stored locally in the customer's project and the application doesn't depend on cross-project access to your vendor registry. **Artifact lifecycle** Container artifacts are tied to the deployment stack lifecycle: * **Deploy (tofu apply)**: Tensor9 copies the container image from your vendor registry to the appliance's private registry * **Destroy (tofu destroy)**: Deleting the deployment stack also deletes the copied container artifact from the appliance's private registry This ensures that artifacts are cleaned up when deployments are removed, preventing orphaned resources. ### Function source code For Lambda functions in your AWS origin stack, Tensor9 automatically handles copying function source code to the customer's Google Cloud environment: ```terraform theme={null} # Lambda function in your AWS origin stack resource "aws_lambda_function" "processor" { function_name = "${var.namespace}myapp-processor" handler = "processor.process_event" runtime = "python3.11" role = aws_iam_role.processor.arn # Reference to function code in your vendor S3 bucket s3_bucket = "vendor-lambda-sources" s3_key = "processor-v1.0.0.zip" environment { variables = { NAMESPACE = var.namespace } } } ``` During deployment, Tensor9: 1. Provisions a private Cloud Storage bucket in the appliance for function sources 2. Copies the Lambda source archive from your vendor S3 bucket to the appliance's Cloud Storage bucket 3. Compiles the Lambda function to a Cloud Run service with the appliance-local source reference Like container images, destroying the deployment stack (tofu destroy) removes the copied function source archives. See [Artifacts](/fundamentals/artifacts) for documentation on artifact management, including immutability requirements and supported artifact types. ## Secrets management Store secrets in AWS Secrets Manager or AWS Systems Manager Parameter Store in your AWS origin stack, then pass them to your application as environment variables. ### Secret naming and injection Always use parameterized secret names and inject them as environment variables: ```terraform theme={null} # AWS Secrets Manager secret resource "aws_secretsmanager_secret" "db_password" { name = "prod/db/password" } resource "aws_secretsmanager_secret_version" "db_password" { secret_id = aws_secretsmanager_secret.db_password.id secret_string = var.db_password } # ECS Fargate task - inject secret as environment variable resource "aws_ecs_task_definition" "app" { family = "${var.namespace}myapp" container_definitions = jsonencode([ { name = "app" image = "myapp:latest" # Inject secret as environment variable secrets = [ { name = "DB_PASSWORD" valueFrom = aws_secretsmanager_secret.db_password.arn } ] } ]) } ``` Your application reads secrets from environment variables: ```python theme={null} import os # Read secret from environment variable db_password = os.environ['DB_PASSWORD'] ``` Your application can keep using AWS Secrets Manager SDK calls on Google Cloud through the configured [Secrets Manager service adapter](/service-adapters/aws/security-identity/secrets-manager). The adapter serves supported requests and maintains secret values and versions. Review the target-specific limits before deployment; environment variables remain an option for values needed at startup. See [Secrets](/fundamentals/secrets) for detailed secret management patterns. ## Operations Perform remote operations on Google Cloud appliances using the Operate service account. ### kubectl on GKE Execute kubectl commands against GKE clusters: ```bash theme={null} tensor9 ops kubectl \ -appName my-app \ -customerName acme-corp \ -originResourceId "google_container_cluster.main" \ -command "kubectl get pods -n my-app-namespace" ``` Output: ``` NAME READY STATUS RESTARTS AGE api-7d9f8b5c6d-9k2lm 1/1 Running 0 2h worker-5c8d7b4f3-8h4km 1/1 Running 0 2h ``` ### gcloud CLI operations Execute gcloud commands: ```bash theme={null} # List Cloud Storage bucket contents tensor9 ops gcloud \ -appName my-app \ -customerName acme-corp \ -originResourceId "google_storage_bucket.data" \ -command "gcloud storage ls gs://myapp-data-000000007e/" # Invoke Cloud Function tensor9 ops gcloud \ -appName my-app \ -customerName acme-corp \ -originResourceId "google_cloudfunctions2_function.api" \ -command "gcloud functions call myapp-api-000000007e --data '{\"test\":true}'" # View Cloud SQL status tensor9 ops gcloud \ -appName my-app \ -customerName acme-corp \ -originResourceId "google_sql_database_instance.postgres" \ -command "gcloud sql instances describe myapp-db-000000007e" ``` ### Database queries Execute SQL queries against Cloud SQL databases: ```bash theme={null} tensor9 ops db \ -appName my-app \ -customerName acme-corp \ -originResourceId "google_sql_database_instance.postgres" \ -command "SELECT count(*) FROM users WHERE created_at > NOW() - INTERVAL '24 hours'" ``` ### Operations endpoints Create temporary operations endpoints for interactive access: ```bash theme={null} # Create kubectl endpoint tensor9 ops endpoint create \ -appName my-app \ -customerName acme-corp \ -originResourceId "google_container_cluster.main" \ -endpointType kubectl \ -ttl 3600 # Output: # Endpoint created: https://ops.tensor9.io/kubectl/abc123 # Expires in: 1 hour # Use: kubectl --server=https://ops.tensor9.io/kubectl/abc123 get pods ``` See [Operations](/fundamentals/operations) for the full operations documentation. ## Example: Complete Google Cloud appliance Here's a complete example of a deployment stack for a Google Cloud appliance, compiled from an AWS origin stack: ### main.tf ```terraform theme={null} # VPC resource "google_compute_network" "main" { name = "${var.namespace}myapp-vpc" auto_create_subnetworks = false project = var.project_id } # Subnets resource "google_compute_subnetwork" "private" { name = "${var.namespace}myapp-private" ip_cidr_range = "10.0.1.0/24" region = var.region network = google_compute_network.main.id project = var.project_id secondary_ip_range { range_name = "pods" ip_cidr_range = "10.1.0.0/16" } secondary_ip_range { range_name = "services" ip_cidr_range = "10.2.0.0/16" } } # GKE Cluster resource "google_container_cluster" "main" { name = "${var.namespace}myapp-cluster" location = var.region project = var.project_id network = google_compute_network.main.name subnetwork = google_compute_subnetwork.private.name ip_allocation_policy { cluster_secondary_range_name = "pods" services_secondary_range_name = "services" } logging_config { enable_components = ["SYSTEM_COMPONENTS", "WORKLOADS"] } monitoring_config { enable_components = ["SYSTEM_COMPONENTS"] managed_prometheus { enabled = true } } resource_labels = { } } # Cloud SQL PostgreSQL resource "google_sql_database_instance" "postgres" { name = "${var.namespace}myapp-db" database_version = "POSTGRES_15" region = var.region project = var.project_id settings { tier = "db-f1-micro" availability_type = "REGIONAL" disk_size = 20 backup_configuration { enabled = true start_time = "03:00" } ip_configuration { ipv4_enabled = false private_network = google_compute_network.main.id } user_labels = { } } deletion_protection = false } resource "google_sql_database" "main" { name = "myapp" instance = google_sql_database_instance.postgres.name project = var.project_id } resource "google_sql_user" "main" { name = "admin" instance = google_sql_database_instance.postgres.name password = var.db_password project = var.project_id } # Cloud Storage bucket resource "google_storage_bucket" "data" { name = "${var.namespace}myapp-data" location = var.region project = var.project_id versioning { enabled = true } } # Memorystore Redis resource "google_redis_instance" "cache" { name = "${var.namespace}myapp-redis" tier = "BASIC" memory_size_gb = 1 region = var.region project = var.project_id redis_version = "REDIS_7_0" authorized_network = google_compute_network.main.id } # Secrets (AWS Secrets Manager - proxied by Tensor9) resource "aws_secretsmanager_secret" "db_password" { name = "prod/db/password" } resource "aws_secretsmanager_secret_version" "db_password" { secret_id = aws_secretsmanager_secret.db_password.id secret_string = var.db_password } ``` ### variables.tf ```terraform theme={null} #@namespace(max_size=16, delimiter='-') variable "namespace" { type = string description = "Prefix that keeps globally-unique resource names distinct per install" default = "" } variable "project_id" { type = string description = "Google Cloud project ID" } variable "db_password" { type = string description = "Database master password" sensitive = true } variable "region" { type = string description = "Google Cloud region" default = "us-central1" } ``` ### outputs.tf ```terraform theme={null} output "gke_cluster_endpoint" { description = "GKE cluster endpoint" value = google_container_cluster.main.endpoint sensitive = true } output "database_connection" { description = "Cloud SQL connection name" value = google_sql_database_instance.postgres.connection_name sensitive = true } output "redis_host" { description = "Redis instance host" value = google_redis_instance.cache.host } output "data_bucket" { description = "Cloud Storage bucket name" value = google_storage_bucket.data.name } ``` ## Best practices Every Google Cloud resource with a name or identifier should be prefixed with `${var.namespace}` to prevent conflicts across customer appliances. Don't skip project-scoped names: nothing stops two installs sharing one project: ```terraform theme={null} # ✓ CORRECT resource "google_storage_bucket" "data" { name = "${var.namespace}myapp-data" } resource "google_service_account" "app" { account_id = "${var.namespace}myapp" } # ✗ INCORRECT - Will cause collisions resource "google_storage_bucket" "data" { name = "myapp-data" } ``` Each appliance gets its own Google Cloud project, so you don't need an appliance-identifying label. Label resources only for your own cost attribution and inventory. The project boundary provides: * IAM role bindings scoped to one appliance * Cloud Logging and Monitoring isolation * Cost tracking * Resource discovery Configure logging for Cloud Functions, GKE, Cloud SQL, and other services: ```terraform theme={null} # GKE logging logging_config { enable_components = ["SYSTEM_COMPONENTS", "WORKLOADS"] } # Cloud Function logging (enabled by default) # Cloud SQL logging settings { database_flags { name = "log_statement" value = "all" } } ``` This ensures observability data flows to your control plane. Never hardcode secrets. Use AWS Secrets Manager or SSM Parameter Store with parameterized names in your AWS origin stack: ```terraform theme={null} # AWS Secrets Manager (recommended) resource "aws_secretsmanager_secret" "api_key" { name = "prod/api/key" } # Or AWS Systems Manager Parameter Store resource "aws_ssm_parameter" "db_password" { name = "/prod/db/password" type = "SecureString" value = var.db_password } ``` Environment variables supply values at application startup. For runtime AWS Secrets Manager reads, use the configured [Secrets Manager service adapter](/service-adapters/aws/security-identity/secrets-manager) and check the operations your application needs. ## Troubleshooting **Symptom**: Terraform apply fails with "Permission denied" or "403 Forbidden" errors. **Solutions**: * Verify your control plane has successfully impersonated the Deploy service account * Check the Deploy service account's IAM roles include necessary permissions for the resources being created * Ensure the impersonation policy allows your control plane's service account * Verify the `deploy_access` label is set and the time window hasn't expired * Review Cloud Audit Logs in the customer project to see which specific API call was denied **Symptom**: "Resource already exists" or "Name is already in use" errors during deployment. **Solutions**: * Ensure all resource names are prefixed with `${var.namespace}` * Verify the `@namespace` annotated variable is being passed correctly * Check that no hardcoded resource names exist in your origin stack * For Cloud Storage buckets, remember they must be globally unique - prefix them with `${var.namespace}` **Symptom**: Cloud Logging and Monitoring data aren't appearing in your observability sink. **Solutions**: * Verify the Steady-state service account has permissions to read logs and metrics * Check that the Steady-state service account is bound in the appliance's project * Ensure log sinks are configured correctly * Verify resource names are parameterized and follow the expected pattern * Check that the control plane is successfully impersonating the Steady-state service account **Symptom**: "Quota exceeded" errors when creating resources. **Solutions**: * Ask the customer to request quota increases from Google Cloud Console * Consider deploying appliances in separate Google Cloud regions * Ask the customer to review their current quota usage * Ask the customer to clean up unused resources in their project **Symptom**: Cloud SQL instance creation fails with VPC peering errors. **Solutions**: * Ensure VPC has a private service connection allocated * Verify the IP address range doesn't conflict with existing ranges * Check that `servicenetworking.googleapis.com` API is enabled * Ensure the Deploy service account has `compute.networks.updatePolicy` permission If you're experiencing issues not covered here or need additional assistance with Google Cloud deployments, we're here to help: * **Slack**: Join our community Slack workspace for real-time support * **Email**: Contact us at [support@tensor9.com](mailto:support@tensor9.com) Our team can help with deployment troubleshooting, service account configuration, service equivalents, and best practices for Google Cloud environments. ## Next steps Now that you understand deploying to Google Cloud customer environments, explore these related topics: * [**Permissions Model**](/fundamentals/permissions-model): Understand the four-phase permissions model in detail * [**Deployments**](/fundamentals/deployments): Learn how to create releases and deploy to customer appliances * [**Operations**](/fundamentals/operations): Execute remote operations on Google Cloud appliances * [**Observability**](/fundamentals/observability): Set up monitoring and logging * [**Terraform Origin Stacks**](/origin-stack/terraform): Write Terraform origin stacks optimized for Google Cloud # Private Kubernetes Source: https://docs.tensor9.com/form-factor/kubernetes Private Kubernetes is a fully supported deployment platform for Tensor9 appliances. Deploying to customer-managed Kubernetes clusters provides flexibility for customers who want to run appliances in their own Kubernetes infrastructure, whether on-premises, in private data centers, or on self-managed cloud Kubernetes. ## Overview When you deploy an application to Private Kubernetes environments using Tensor9: * **Customer appliances** run entirely within the customer's Kubernetes cluster * **Your control plane** orchestrates deployments from your dedicated Tensor9 AWS account * **Kubernetes RBAC** enables your control plane to manage customer appliances with customer-approved permissions * **Kubernetes-native resources** define your application infrastructure Private Kubernetes appliances are built from Kubernetes primitives (Deployments, Services, Ingress, ConfigMaps, Secrets) for compute, storage, networking, and configuration, which gives you one cloud-agnostic deployment model that runs on any Kubernetes distribution. ## Prerequisites Before deploying appliances to Private Kubernetes environments, ensure: ### Your control plane * **Dedicated AWS account** for your Tensor9 control plane * **Control plane installed** - See [Installing Tensor9](/fundamentals/install) * **Origin stack published** - Your application infrastructure defined and uploaded ### Customer Kubernetes cluster Your customers must provide: * **Kubernetes cluster** (version 1.24+) where the appliance will be deployed * **Cluster access credentials** (kubeconfig) for the four-phase permissions model * **ServiceAccounts configured** for the four-phase permissions model (Install, Steady-state, Deploy, Operate) * **Sufficient cluster resources** (CPU, memory, storage) for your application's needs * **Two namespaces**: * One for the Tensor9 controller (e.g., `tensor9-system`) * One for your application (e.g., `acme-corp-prod`) * **Ingress controller** (optional, for external traffic) ### Your development environment * **kubectl** installed and configured * **Helm** installed (required for customer controller installation) * **Terraform or OpenTofu** (if using Terraform origin stacks with Kubernetes provider) ## How Private Kubernetes appliances work Private Kubernetes appliances are deployed using Kubernetes-native resources orchestrated by your Tensor9 control plane. You provide your customer with a signup link (hosted on your vanity domain, e.g., `https://tensor9.vendor.co`) that walks them through the setup process. The signup flow provides them with: * Customized namespace names for their appliance * A Helm chart download link (hosted from your vanity domain) * RBAC configuration templates for their specific deployment Your customer completes the setup by: 1. **Creating two namespaces** in their Kubernetes cluster: ```bash theme={null} kubectl create namespace tensor9-system kubectl create namespace acme-corp-prod ``` 2. **Downloading and installing the Tensor9 controller** via the Helm chart provided in the signup flow: ```bash theme={null} # Download the Helm chart from your signup link curl -O https://tensor9.vendor.co/helm/controller-000000007e.tgz # Install the controller in the controller namespace helm install tensor9-controller ./controller-000000007e.tgz \ --namespace tensor9-system \ --set appNamespace=acme-corp-prod ``` 3. **Creating four ServiceAccounts** with RBAC permissions using the templates from the signup flow. Each ServiceAccount corresponds to a permission phase: Install, Steady-state, Deploy, and Operate. These ServiceAccounts define what the Tensor9 controller can do within their cluster. The customer configures RBAC Roles and RoleBindings (or ClusterRoles and ClusterRoleBindings) that grant appropriate permissions to each ServiceAccount in both namespaces. You create a release targeting the customer's appliance: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -customerName acme-corp \ -vendorVersion "1.0.0" \ -description "Initial production deployment" ``` Your control plane compiles your origin stack into a deployment stack tailored for Kubernetes. The deployment stack downloads to your local environment. The customer approves the deployment by providing kubeconfig credentials for the Deploy ServiceAccount or updating RBAC to allow the Tensor9 controller to use the Deploy ServiceAccount. Once approved, the Tensor9 controller in the appliance can use the Deploy ServiceAccount to create resources in the customer's cluster. You run the deployment locally against the downloaded deployment stack: ```bash theme={null} cd acme-corp-production tofu init tofu apply ``` The deployment stack uses the Terraform Kubernetes provider to create your application resources in the customer's cluster: * Deployments, StatefulSets, DaemonSets (in the application namespace) * Services (ClusterIP, LoadBalancer) * Ingress resources * ConfigMaps and Secrets * PersistentVolumeClaims * Any other Kubernetes resources defined in your origin stack The Terraform provider connects to the customer's cluster using the Deploy ServiceAccount credentials (provided via kubeconfig), and the Tensor9 controller (already installed in the controller namespace) monitors the deployment. After deployment, your control plane uses the Steady-state ServiceAccount to continuously collect observability data (logs, metrics) from the customer's appliance without requiring additional approvals. This data flows to your observability sink, giving you visibility into appliance health and performance. ## Service adapters When you deploy an origin stack to Private Kubernetes environments, Tensor9 automatically compiles resources from your AWS origin stack to their Kubernetes equivalents. ### How service equivalents work When compiling a deployment stack for Private Kubernetes: 1. **AWS resources are compiled** - AWS resources are converted to their Kubernetes equivalents 2. **Container resources are adapted** - Container-based resources (ECS, Lambda) are converted to Kubernetes Deployments, StatefulSets, or Jobs 3. **Configuration is adjusted** - Resource configurations are modified to match Kubernetes conventions and best practices ### Common service equivalents | Service Category | AWS | Private Kubernetes Equivalent | | ---------------------------------- | ------------------------------------- | ----------------------------------------- | | **Containers** | EKS, ECS | Kubernetes | | **Functions** | Lambda | Knative (unmanaged) | | **Networking** | VPC | - | | **Load balancing** | Load Balancer | Cloudflare (optional) | | **DNS** | Route 53 | Cloudflare (optional) | | **Identity and access management** | IAM | - | | **Object storage** | S3 | Backblaze B2, MinIO (unmanaged) | | **Databases (PostgreSQL)** | RDS Aurora PostgreSQL, RDS PostgreSQL | Neon, CloudNative PostgreSQL (unmanaged) | | **Databases (MySQL)** | RDS Aurora MySQL, RDS MySQL | PlanetScale, MySQL (unmanaged) | | **Databases (MongoDB)** | DocumentDB | MongoDB Atlas, MongoDB (unmanaged) | | **Caching** | ElastiCache | Redis Enterprise Cloud, Redis (unmanaged) | | **Message streaming** | MSK (Managed Streaming for Kafka) | Confluent Cloud, Kafka (unmanaged) | | **Search** | OpenSearch Service | OpenSearch (unmanaged) | | **Workflow** | MWAA (Managed Airflow) | Astronomer, Airflow (unmanaged) | | **Analytics** | Amazon Athena | Presto (unmanaged) | Third-party managed equivalents (Backblaze B2, Neon, PlanetScale, MongoDB Atlas, Redis Enterprise Cloud, Confluent Cloud, Astronomer) require your customers to bring their own credentials and accounts with these services. EC2, DynamoDB, and EFS are all offered. Some services are not adapted yet, including Step Functions, API Gateway, Cognito, AppSync, and Redshift. See [Service Catalog](/service-adapters/catalog#services-we-do-not-adapt-yet). ### Service dependencies When Tensor9 compiles your origin stack to use unmanaged service equivalents, those equivalents often require **dependencies** - infrastructure components like Kubernetes operators or Helm charts that must be installed in the customer's environment. For example, when Tensor9 maps AWS RDS PostgreSQL to CloudNative PostgreSQL (CNPG) for a Kubernetes environment, the CNPG operator must be installed in the cluster before the PostgreSQL instances can be created. #### How dependencies work Dependencies are cluster-wide components that may be shared across multiple services. For example, if your app has two PostgreSQL databases, both use the same CNPG operator rather than installing it twice. You specify which dependency versions are acceptable using semver constraints (e.g., `>=1.24.0`), and Tensor9 installs the latest version that satisfies the constraint. #### Reference counting Tensor9 manages dependencies using reference counting to ensure they're installed exactly once and cleaned up properly: * **First reference (0→1)**: When the first service needs a dependency, Tensor9 installs it * **Additional references**: Subsequent services increment the reference count without reinstalling * **Removing references**: When services are removed, the reference count decrements * **Last reference removed (1→0)**: When no services need the dependency, Tensor9 uninstalls it This ensures cluster-wide dependencies don't accumulate over time and are properly cleaned up when no longer needed. #### Install methods Dependencies can be installed in two ways: * **Managed**: Tensor9 installs and manages the dependency automatically * **PreInstalled**: The customer is expected to have the dependency already installed in their environment PreInstalled is useful when customers have standardized on specific operator versions or have organizational policies about which components can be installed in their clusters. For Private Kubernetes environments, dependencies can only use the PreInstalled method. Customers must have the required operators and Helm charts installed in their clusters before deployment. ### Example: Compiling an AWS origin stack If your origin stack defines an ECS Fargate service: ```terraform theme={null} # Origin stack (AWS) resource "aws_ecs_service" "api" { name = "${var.namespace}myapp-api" cluster = aws_ecs_cluster.main.id task_definition = aws_ecs_task_definition.api.arn desired_count = 3 launch_type = "FARGATE" network_configuration { subnets = var.subnet_ids security_groups = [aws_security_group.api.id] } } resource "aws_ecs_task_definition" "api" { family = "${var.namespace}myapp-api" requires_compatibilities = ["FARGATE"] network_mode = "awsvpc" cpu = "512" memory = "1024" container_definitions = jsonencode([{ name = "api" image = "myapp/api:1.0.0" portMappings = [{ containerPort = 8080 protocol = "tcp" }] environment = [ { name = "NAMESPACE", value = var.namespace } ] }]) } ``` Tensor9 compiles it to a Kubernetes Deployment: ```yaml theme={null} # Deployment stack (Kubernetes) apiVersion: apps/v1 kind: Deployment metadata: name: myapp-api namespace: acme-corp-prod labels: app: myapp-api instance-id: "000000007e" spec: replicas: 3 selector: matchLabels: app: myapp-api template: metadata: labels: app: myapp-api instance-id: "000000007e" spec: containers: - name: api image: myapp/api:1.0.0 ports: - containerPort: 8080 protocol: TCP env: - name: NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace resources: requests: cpu: "512m" memory: "1024Mi" limits: cpu: "512m" memory: "1024Mi" ``` ## Supported Kubernetes distributions Tensor9 supports deploying to any standard Kubernetes cluster that conforms to the Kubernetes API specification (version 1.24+): | Distribution | Environment | Notes | | ---------------------------- | -------------------------------- | -------------------------------------------- | | **Vanilla Kubernetes** | On-premises, bare metal | Self-managed Kubernetes clusters | | **K3s** | Edge, IoT, resource-constrained | Lightweight Kubernetes distribution | | **MicroK8s** | Developer workstations, edge | Canonical's minimal Kubernetes | | **RKE/RKE2** | On-premises, enterprise | Rancher Kubernetes distributions | | **OpenShift** | On-premises, hybrid cloud | Red Hat's Kubernetes platform | | **Tanzu Kubernetes Grid** | On-premises, VMware environments | VMware's enterprise Kubernetes | | **Self-managed EKS/GKE/AKS** | Cloud (self-managed) | Customer-managed clusters in cloud providers | ## Permissions model Private Kubernetes appliances use a four-phase ServiceAccount permissions model that balances operational capability with customer control. ### The four permission phases | Phase | ServiceAccount | Purpose | Access Pattern | | ---------------- | --------------------- | -------------------------------------------------------------- | ------------------------------- | | **Install** | `tensor9-install` | Initial setup, major infrastructure changes (CRDs, namespaces) | Customer-approved, rare | | **Steady-state** | `tensor9-steadystate` | Continuous observability collection (read-only) | Active by default | | **Deploy** | `tensor9-deploy` | Deployments, updates, configuration changes | Customer-approved, time-bounded | | **Operate** | `tensor9-operate` | Remote operations, troubleshooting, debugging | Customer-approved, time-bounded | ### ServiceAccount and RBAC structure Each ServiceAccount is created in the customer's Kubernetes cluster with RBAC policies that grant appropriate permissions to both the controller namespace and the application namespace. **Example: Deploy ServiceAccount with scoped permissions** ```yaml theme={null} # Deploy ServiceAccount (in controller namespace) apiVersion: v1 kind: ServiceAccount metadata: name: tensor9-deploy namespace: tensor9-system labels: instance-id: "000000007e" phase: deploy --- # Role for controller namespace apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: tensor9-deploy-controller-role namespace: tensor9-system rules: # Allow managing controller deployment - apiGroups: ["apps"] resources: ["deployments"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # Allow managing controller configmaps - apiGroups: [""] resources: ["configmaps", "secrets"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] --- # Role for application namespace apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: tensor9-deploy-app-role namespace: acme-corp-prod rules: # Allow creating and managing deployments - apiGroups: ["apps"] resources: ["deployments", "statefulsets", "daemonsets", "replicasets"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # Allow managing services - apiGroups: [""] resources: ["services"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # Allow managing configmaps and secrets - apiGroups: [""] resources: ["configmaps", "secrets"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # Allow managing ingress - apiGroups: ["networking.k8s.io"] resources: ["ingresses"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # Allow managing persistent volume claims - apiGroups: [""] resources: ["persistentvolumeclaims"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] # Allow reading pods for status - apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"] --- # RoleBinding for controller namespace apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: tensor9-deploy-controller-binding namespace: tensor9-system subjects: - kind: ServiceAccount name: tensor9-deploy namespace: tensor9-system roleRef: kind: Role name: tensor9-deploy-controller-role apiGroup: rbac.authorization.k8s.io --- # RoleBinding for application namespace apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: tensor9-deploy-app-binding namespace: acme-corp-prod subjects: - kind: ServiceAccount name: tensor9-deploy namespace: tensor9-system roleRef: kind: Role name: tensor9-deploy-app-role apiGroup: rbac.authorization.k8s.io ``` The Deploy ServiceAccount can: * Create and manage the Tensor9 controller in the controller namespace * Create and manage application resources in the application namespace * Perform operations allowed by the Roles * Access resources labeled with the appliance's `t9-appliance-id` Customers control when and how long deploy access is granted by providing or revoking the kubeconfig for the ServiceAccount. **Example: Steady-state ServiceAccount (read-only observability)** ```yaml theme={null} # Steady-state ServiceAccount (in controller namespace) apiVersion: v1 kind: ServiceAccount metadata: name: tensor9-steadystate namespace: tensor9-system labels: instance-id: "000000007e" phase: steadystate --- # Role for controller namespace (read-only) apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: tensor9-steadystate-controller-role namespace: tensor9-system rules: # Read-only access to controller pods and logs - apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"] # Read-only access to controller deployment - apiGroups: ["apps"] resources: ["deployments"] verbs: ["get", "list", "watch"] # Read-only access to events - apiGroups: [""] resources: ["events"] verbs: ["get", "list", "watch"] --- # Role for application namespace (read-only) apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: tensor9-steadystate-app-role namespace: acme-corp-prod rules: # Read-only access to pods and logs - apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"] # Read-only access to deployments and statefulsets - apiGroups: ["apps"] resources: ["deployments", "statefulsets", "daemonsets", "replicasets"] verbs: ["get", "list", "watch"] # Read-only access to services and ingress - apiGroups: [""] resources: ["services"] verbs: ["get", "list", "watch"] - apiGroups: ["networking.k8s.io"] resources: ["ingresses"] verbs: ["get", "list", "watch"] # Read-only access to events - apiGroups: [""] resources: ["events"] verbs: ["get", "list", "watch"] --- # RoleBinding for controller namespace apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: tensor9-steadystate-controller-binding namespace: tensor9-system subjects: - kind: ServiceAccount name: tensor9-steadystate namespace: tensor9-system roleRef: kind: Role name: tensor9-steadystate-controller-role apiGroup: rbac.authorization.k8s.io --- # RoleBinding for application namespace apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: tensor9-steadystate-app-binding namespace: acme-corp-prod subjects: - kind: ServiceAccount name: tensor9-steadystate namespace: tensor9-system roleRef: kind: Role name: tensor9-steadystate-app-role apiGroup: rbac.authorization.k8s.io ``` The Steady-state ServiceAccount: * Can only read resources in both the controller and application namespaces * Cannot modify, delete, or create any resources * Cannot access secrets or configmaps (unless explicitly granted) * Allows continuous monitoring without customer intervention ### Deployment workflow with ServiceAccounts Customer approves a deployment by providing kubeconfig credentials for the Deploy ServiceAccount or updating the RoleBinding to allow the Tensor9 controller to assume the Deploy ServiceAccount. You run the deployment locally against the downloaded deployment stack: ```bash theme={null} cd acme-corp-production tofu init tofu apply ``` The deployment creates all Kubernetes resources in the customer's cluster. The Terraform Kubernetes provider uses the Deploy ServiceAccount credentials (provided via kubeconfig) to create resources in the customer's cluster. All infrastructure changes occur within the customer's namespaces (controller and application) using the Deploy ServiceAccount permissions. After the deployment window expires or the customer revokes access, the Deploy ServiceAccount credentials can no longer be used. Your control plane automatically reverts to using only the Steady-state ServiceAccount for observability. See [Permissions Model](/fundamentals/permissions-model) for detailed information on all four phases. ## Networking Private Kubernetes appliances use standard Kubernetes networking primitives for both internal and external connectivity. ### Tensor9 controller Deployment When an appliance is deployed, Tensor9 creates a dedicated Deployment for the Tensor9 controller in the customer's controller namespace (e.g., `tensor9-system`). The controller: * **Communicates outbound** to your Tensor9 control plane over HTTPS * **Manages appliance resources** using the customer's ServiceAccount credentials * **Forwards observability data** to your observability sink * **Does not accept inbound connections** - all communication is outbound-only ```yaml theme={null} # Example: Controller Deployment (managed by Tensor9) apiVersion: apps/v1 kind: Deployment metadata: name: tensor9-controller namespace: tensor9-system labels: app: tensor9-controller instance-id: "000000007e" spec: replicas: 2 selector: matchLabels: app: tensor9-controller template: metadata: labels: app: tensor9-controller instance-id: "000000007e" spec: serviceAccountName: tensor9-controller containers: - name: controller image: tensor9/controller:v1.0.0 env: - name: NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: CONTROL_PLANE_URL value: "https://control-plane.tensor9.io" - name: APP_NAMESPACE value: "acme-corp-prod" resources: requests: cpu: "100m" memory: "128Mi" limits: cpu: "500m" memory: "512Mi" --- # Controller Service (ClusterIP - internal only) apiVersion: v1 kind: Service metadata: name: tensor9-controller namespace: tensor9-system spec: type: ClusterIP selector: app: tensor9-controller ports: - port: 8080 targetPort: 8080 protocol: TCP ``` The Tensor9 controller only makes outbound HTTPS connections and does not expose any inbound ports, ensuring the customer's cluster cannot be compromised via inbound network attacks. ### Application networking Your application resources use standard Kubernetes Services and Ingress for networking: **Internal communication (ClusterIP Services)** ```yaml theme={null} # Internal API service apiVersion: v1 kind: Service metadata: name: myapp-api namespace: acme-corp-prod labels: instance-id: "000000007e" spec: type: ClusterIP selector: app: myapp-api ports: - port: 8080 targetPort: 8080 protocol: TCP ``` **External access (LoadBalancer or Ingress)** For external access, use either a LoadBalancer Service (if supported by the cluster) or an Ingress resource: ```yaml theme={null} # LoadBalancer Service (if cluster supports it) apiVersion: v1 kind: Service metadata: name: myapp-external namespace: acme-corp-prod labels: instance-id: "000000007e" spec: type: LoadBalancer selector: app: myapp-api ports: - port: 443 targetPort: 8080 protocol: TCP --- # Or use Ingress apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: myapp-ingress namespace: acme-corp-prod labels: instance-id: "000000007e" annotations: cert-manager.io/cluster-issuer: "letsencrypt-prod" spec: ingressClassName: nginx tls: - hosts: - myapp-000000007e.customer.com secretName: myapp-tls rules: - host: myapp-000000007e.customer.com http: paths: - path: / pathType: Prefix backend: service: name: myapp-api port: number: 8080 ``` ## Resource naming and labeling Since each appliance runs in its own dedicated namespace, resource names don't need a namespace prefix for uniqueness. Labeling resources with the appliance's ID is still useful for observability and tracking. ### Resource naming Use descriptive names for your Kubernetes resources: ```yaml theme={null} apiVersion: apps/v1 kind: Deployment metadata: name: myapp-api namespace: acme-corp-prod labels: app: myapp-api instance-id: "000000007e" spec: replicas: 3 selector: matchLabels: app: myapp-api template: metadata: labels: app: myapp-api instance-id: "000000007e" spec: containers: - name: api image: myapp/api:1.0.0 env: - name: NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace ``` ### Labeling resources Labeling resources with the appliance's ID makes observability and tracking easier: ```yaml theme={null} labels: instance-id: "000000007e" application: "my-app" managed-by: "tensor9" ``` The `instance-id` label: * Allows filtering of observability data by appliance * Helps track resource usage and costs per appliance * Facilitates resource discovery by Tensor9 controllers * Enables correlation of resources across namespaces (controller + application) ### Ingress hostnames For Ingress resources, use a hostname that includes the appliance's ID to ensure uniqueness across appliances: ```yaml theme={null} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: myapp-ingress namespace: acme-corp-prod labels: instance-id: "000000007e" spec: rules: - host: myapp-000000007e.customer.com # Include the appliance ID in the hostname http: paths: - path: / pathType: Prefix backend: service: name: myapp-api port: number: 8080 ``` ## Observability Private Kubernetes appliances provide observability through standard Kubernetes logging and metrics. ### Container logs Application logs from containers are collected via kubectl: ```bash theme={null} # View logs for all pods in the appliance kubectl logs -n acme-corp-prod -l instance-id=000000007e --tail=100 # Stream logs for a specific deployment kubectl logs -n acme-corp-prod -l app=myapp-api,instance-id=000000007e -f ``` Your control plane uses the Steady-state ServiceAccount to continuously fetch logs and forward them to your observability sink. ### Metrics **Kubernetes metrics (via Metrics Server)** Basic resource metrics are available if the cluster has Metrics Server installed: ```bash theme={null} # View pod resource usage kubectl top pods -n acme-corp-prod -l instance-id=000000007e # View node resource usage kubectl top nodes ``` **Prometheus metrics (recommended)** For a full metrics pipeline, recommend that customers install Prometheus: ```yaml theme={null} # ServiceMonitor for Prometheus scraping apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: myapp-metrics namespace: acme-corp-prod labels: instance-id: "000000007e" spec: selector: matchLabels: app: myapp-api instance-id: "000000007e" endpoints: - port: metrics interval: 30s path: /metrics ``` ### Events Kubernetes Events provide insight into cluster operations: ```bash theme={null} # View recent events kubectl get events -n acme-corp-prod \ --field-selector involvedObject.labels.instance-id=000000007e \ --sort-by='.lastTimestamp' ``` Your control plane's Steady-state ServiceAccount can read Events to track deployments, failures, and scaling operations. ### Distributed tracing (optional) For distributed tracing, recommend that customers install Jaeger or other OpenTelemetry-compatible collectors. Configure your application to send traces to the collector endpoint: ```yaml theme={null} apiVersion: apps/v1 kind: Deployment metadata: name: myapp-api namespace: acme-corp-prod labels: instance-id: "000000007e" spec: template: spec: containers: - name: api env: - name: OTEL_EXPORTER_OTLP_ENDPOINT value: "http://jaeger-collector:4318" - name: OTEL_SERVICE_NAME value: "myapp-api" ``` ## Artifacts Private Kubernetes appliances use container registries to store container images deployed by your deployment stacks. ### Container images **Customer-managed registry** Customers can configure their own container registry (Harbor, Nexus, JFrog Artifactory, etc.): ```yaml theme={null} # Pull from customer's private registry apiVersion: apps/v1 kind: Deployment metadata: name: myapp-api namespace: acme-corp-prod labels: instance-id: "000000007e" spec: template: spec: # Image pull secret for customer's registry imagePullSecrets: - name: registry-credentials containers: - name: api image: registry.customer.com/myapp/api:1.0.0 ``` **Tensor9-managed registry copy** Alternatively, Tensor9 can automatically copy images to the customer's cluster-local registry: 1. **Detects the container image reference** in your Kubernetes manifests 2. **Provisions image pull configuration** for the customer's registry 3. **Copies the container image** from your vendor registry to the customer's registry 4. **Rewrites the deployment stack** to reference the customer-local registry ### Artifact lifecycle Container artifacts are tied to the deployment lifecycle: * **Deploy (tofu apply)**: Images are pulled from the configured registry * **Destroy (tofu destroy)**: Deleting the deployment stops using the images (cleanup depends on registry retention policies) See [Artifacts](/fundamentals/artifacts) for documentation on artifact management. ## Secrets management Store secrets in AWS Secrets Manager or AWS Systems Manager Parameter Store in your AWS origin stack. Tensor9 will copy the secret values and inject them as Kubernetes Secrets that get mounted as environment variables. ### Secret injection pattern Define secrets in your origin stack: ```terraform theme={null} # AWS Secrets Manager secret resource "aws_secretsmanager_secret" "db_password" { name = "prod/db/password" } resource "aws_secretsmanager_secret_version" "db_password" { secret_id = aws_secretsmanager_secret.db_password.id secret_string = var.db_password } ``` During deployment, Tensor9 copies the secret value from AWS Secrets Manager and creates a Kubernetes Secret in the customer's cluster: ```yaml theme={null} apiVersion: v1 kind: Secret metadata: name: myapp-db-password namespace: acme-corp-prod labels: instance-id: "000000007e" type: Opaque stringData: DB_PASSWORD: ``` Then inject into your pods: ```yaml theme={null} apiVersion: apps/v1 kind: Deployment metadata: name: myapp-api namespace: acme-corp-prod labels: instance-id: "000000007e" spec: template: spec: containers: - name: api image: myapp/api:1.0.0 env: # Inject secret as environment variable - name: DB_PASSWORD valueFrom: secretKeyRef: name: myapp-db-password key: DB_PASSWORD ``` Your application reads secrets from environment variables: ```python theme={null} import os # Read secret from environment variable db_password = os.environ['DB_PASSWORD'] ``` If your application dynamically fetches secrets using AWS SDK calls (e.g., `boto3.client('secretsmanager').get_secret_value()`), those calls will NOT work in Kubernetes environments. Always pass secrets as environment variables via Kubernetes Secrets. See [Secrets](/fundamentals/secrets) for detailed secret management patterns. ## Operations Perform remote operations on Private Kubernetes appliances using the Operate ServiceAccount. ### kubectl operations Execute kubectl commands against the customer's cluster: ```bash theme={null} # Get pods tensor9 ops kubectl \ -appName my-app \ -customerName acme-corp \ -command "kubectl get pods -n acme-corp-prod -l instance-id=000000007e" # View logs tensor9 ops kubectl \ -appName my-app \ -customerName acme-corp \ -command "kubectl logs -n acme-corp-prod -l app=myapp-api --tail=100" # Describe deployment tensor9 ops kubectl \ -appName my-app \ -customerName acme-corp \ -command "kubectl describe deployment myapp-api -n acme-corp-prod" ``` ### Database operations For databases running in Kubernetes, execute SQL queries: ```bash theme={null} tensor9 ops db \ -appName my-app \ -customerName acme-corp \ -originResourceId "kubernetes_stateful_set.postgres" \ -command "SELECT count(*) FROM users WHERE created_at > NOW() - INTERVAL '24 hours'" ``` ### Pod exec operations Execute commands inside running pods: ```bash theme={null} tensor9 ops kubectl \ -appName my-app \ -customerName acme-corp \ -command "kubectl exec -n acme-corp-prod myapp-api-abc123 -- /bin/sh -c 'env | grep INSTANCE_ID'" ``` ### Port forwarding Create temporary port forwards for debugging: ```bash theme={null} tensor9 ops kubectl \ -appName my-app \ -customerName acme-corp \ -command "kubectl port-forward -n acme-corp-prod svc/myapp-api 8080:8080" ``` See [Operations](/fundamentals/operations) for the full operations documentation. ## Example: Complete Private Kubernetes appliance Here's a complete example of an AWS origin stack using EKS and the Kubernetes provider. This will compile to a deployment stack for the customer's Private Kubernetes cluster: ### main.tf ```terraform theme={null} # EKS cluster for the origin stack (runs in vendor's AWS account) resource "aws_eks_cluster" "main" { name = "${var.namespace}myapp-origin" role_arn = aws_iam_role.eks_cluster.arn vpc_config { subnet_ids = var.subnet_ids } } # Kubernetes provider configured for EKS provider "kubernetes" { host = aws_eks_cluster.main.endpoint cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data) exec { api_version = "client.authentication.k8s.io/v1beta1" command = "aws" args = ["eks", "get-token", "--cluster-name", aws_eks_cluster.main.name] } } # Namespaces resource "kubernetes_namespace" "controller" { metadata { name = "tensor9-system" labels = { managed-by = "tensor9" purpose = "controller" } } } resource "kubernetes_namespace" "app" { metadata { name = "acme-corp-prod" labels = { managed-by = "tensor9" purpose = "application" } } } # Secrets from AWS Secrets Manager resource "aws_secretsmanager_secret" "db_password" { name = "prod/db/password" } data "aws_secretsmanager_secret_version" "db_password" { secret_id = aws_secretsmanager_secret.db_password.id } resource "kubernetes_secret" "db_password" { metadata { name = "myapp-db-password" namespace = kubernetes_namespace.app.metadata[0].name } data = { DB_PASSWORD = data.aws_secretsmanager_secret_version.db_password.secret_string } } # API Deployment resource "kubernetes_deployment" "api" { metadata { name = "myapp-api" namespace = kubernetes_namespace.app.metadata[0].name labels = { app = "myapp-api" } } spec { replicas = 3 selector { match_labels = { app = "myapp-api" } } template { metadata { labels = { app = "myapp-api" } } spec { container { name = "api" image = "myapp/api:1.0.0" port { container_port = 8080 name = "http" } env { name = "NAMESPACE" value = var.namespace } env { name = "DB_HOST" value = "myapp-postgres" } env { name = "DB_PASSWORD" value_from { secret_key_ref { name = kubernetes_secret.db_password.metadata[0].name key = "DB_PASSWORD" } } } resources { requests = { cpu = "200m" memory = "256Mi" } limits = { cpu = "1000m" memory = "512Mi" } } liveness_probe { http_get { path = "/health" port = 8080 } initial_delay_seconds = 30 period_seconds = 10 } readiness_probe { http_get { path = "/ready" port = 8080 } initial_delay_seconds = 5 period_seconds = 5 } } } } } } # PostgreSQL StatefulSet resource "kubernetes_stateful_set" "postgres" { metadata { name = "myapp-postgres" namespace = kubernetes_namespace.app.metadata[0].name labels = { app = "myapp-postgres" } } spec { service_name = "myapp-postgres" replicas = 1 selector { match_labels = { app = "myapp-postgres" } } template { metadata { labels = { app = "myapp-postgres" } } spec { container { name = "postgres" image = "postgres:15" port { container_port = 5432 name = "postgres" } env { name = "POSTGRES_DB" value = "myapp" } env { name = "POSTGRES_USER" value = "myapp" } env { name = "POSTGRES_PASSWORD" value_from { secret_key_ref { name = kubernetes_secret.db_password.metadata[0].name key = "DB_PASSWORD" } } } volume_mount { name = "postgres-data" mount_path = "/var/lib/postgresql/data" } resources { requests = { cpu = "500m" memory = "1Gi" } limits = { cpu = "2000m" memory = "2Gi" } } } } } volume_claim_template { metadata { name = "postgres-data" } spec { access_modes = ["ReadWriteOnce"] resources { requests = { storage = "20Gi" } } } } } } # Services resource "kubernetes_service" "api" { metadata { name = "myapp-api" namespace = kubernetes_namespace.app.metadata[0].name } spec { type = "ClusterIP" selector = { app = "myapp-api" } port { port = 8080 target_port = 8080 protocol = "TCP" name = "http" } } } resource "kubernetes_service" "postgres" { metadata { name = "myapp-postgres" namespace = kubernetes_namespace.app.metadata[0].name } spec { type = "ClusterIP" selector = { app = "myapp-postgres" } port { port = 5432 target_port = 5432 protocol = "TCP" name = "postgres" } } } # Ingress resource "kubernetes_ingress_v1" "main" { metadata { name = "myapp-ingress" namespace = kubernetes_namespace.app.metadata[0].name annotations = { "cert-manager.io/cluster-issuer" = "letsencrypt-prod" "nginx.ingress.kubernetes.io/ssl-redirect" = "true" } } spec { ingress_class_name = "nginx" tls { hosts = ["myapp.${var.domain_root}"] secret_name = "myapp-tls" } rule { host = "myapp.${var.domain_root}" http { path { path = "/" path_type = "Prefix" backend { service { name = kubernetes_service.api.metadata[0].name port { number = 8080 } } } } } } } } ``` ### variables.tf ```terraform theme={null} #@namespace(max_size=32, delimiter='-') variable "namespace" { type = string description = "Prefix that keeps globally-unique resource names distinct per install" default = "" } # @vanity_domain_root() variable "domain_root" { type = string description = "Root domain assigned to this install" default = "local.example.com" } variable "subnet_ids" { type = list(string) description = "VPC subnet IDs for EKS cluster" } variable "db_password" { type = string description = "Database password" sensitive = true } ``` ### outputs.tf ```terraform theme={null} output "eks_cluster_endpoint" { description = "EKS cluster endpoint" value = aws_eks_cluster.main.endpoint sensitive = true } output "api_service_name" { description = "API service name" value = kubernetes_service.api.metadata[0].name } output "ingress_hostname" { description = "Ingress hostname" value = "myapp.${var.domain_root}" } ``` ## Best practices Always specify resource requests and limits for containers: ```yaml theme={null} resources: requests: cpu: "200m" memory: "256Mi" limits: cpu: "1000m" memory: "512Mi" ``` This ensures: * Proper pod scheduling * Protection against resource exhaustion * Predictable performance Always configure liveness and readiness probes: ```yaml theme={null} livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5 ``` This enables Kubernetes to: * Restart unhealthy pods * Route traffic only to ready pods * Ensure high availability Use Kubernetes Secrets and inject them as environment variables: ```yaml theme={null} env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: myapp-db-password key: DB_PASSWORD ``` Never hardcode secrets in container images or ConfigMaps. ## Troubleshooting **Symptom**: Deployment fails with "forbidden" or "unauthorized" errors during `tofu apply`. **Solutions**: * Verify the ServiceAccount has the necessary RBAC permissions * Check that RoleBindings or ClusterRoleBindings are correctly configured * Ensure the kubeconfig is using the correct ServiceAccount * Verify the ServiceAccount has access to both the controller and application namespaces * Check for typos in resource names or apiGroups in RBAC rules **Symptom**: Logs and metrics aren't appearing in observability sink. **Solutions**: * Verify Steady-state ServiceAccount has read permissions for both namespaces * Check Tensor9 controller is running: `kubectl get pods -n tensor9-system -l app=tensor9-controller` * Ensure controller can reach control plane (check network connectivity) * Verify all resources are labeled with `instance-id` * Check controller logs for errors: `kubectl logs -n tensor9-system -l app=tensor9-controller` If you're experiencing issues not covered here or need additional assistance with Private Kubernetes deployments, we're here to help: * **Slack**: Join our community Slack workspace for real-time support * **Email**: Contact us at [support@tensor9.com](mailto:support@tensor9.com) Our team can help with deployment troubleshooting, RBAC configuration, service equivalents, and best practices for Private Kubernetes environments. ## Next steps Now that you understand deploying to Private Kubernetes environments, explore these related topics: * [**Permissions Model**](/fundamentals/permissions-model): Understand the four-phase permissions model in detail * [**Deployments**](/fundamentals/deployments): Learn how to create releases and deploy to customer appliances * [**Operations**](/fundamentals/operations): Execute remote operations on Kubernetes appliances * [**Observability**](/fundamentals/observability): Set up monitoring and logging * [**Kubernetes Origin Stacks**](/origin-stack/kubernetes): Write Kubernetes manifests optimized for Tensor9 # On-Prem & Bare Metal Source: https://docs.tensor9.com/form-factor/on-prem **On-premises and bare metal** deployments allow your customers to run your application on physical servers in their own data centers or co-location facilities. These environments provide maximum control over hardware, networking, and security, making them ideal for customers with strict data residency, compliance, or performance requirements. ## Overview On-premises and bare metal deployments use Kubernetes as the underlying orchestration platform. Your customer installs and manages their own Kubernetes cluster on their infrastructure, and then Tensor9 deploys your application into that cluster following the same pattern as [Private Kubernetes](/form-factor/kubernetes) deployments. This approach gives customers full control over: * **Hardware**: Physical servers, storage, and networking equipment * **Infrastructure**: Data center location, network topology, and security perimeter * **Kubernetes distribution**: Choice of Kubernetes implementation and version * **Operational practices**: Backup, disaster recovery, and maintenance schedules ## Prerequisites Before deploying to on-premises or bare metal environments, your customer must: ### Customer's infrastructure * **Kubernetes cluster**: A working Kubernetes cluster installed and configured on their infrastructure (see [Kubernetes distributions](#kubernetes-distributions) below) * **Two namespaces**: One for the Tensor9 controller, one for your application * **Network connectivity**: Outbound HTTPS access for the Tensor9 controller to communicate with your control plane * **Container registry**: Access to a container registry (public or private) for pulling container images * **Storage**: Persistent storage solution compatible with Kubernetes (local storage, NFS, SAN, etc.) ### Your development environment * **kubectl** installed and configured * **Helm** installed (required for customer controller installation) * **Terraform or OpenTofu** (if using Terraform origin stacks with Kubernetes provider) ## Kubernetes distributions Customers can choose from various Kubernetes distributions for their on-premises or bare metal infrastructure: | Distribution | Best For | Notes | | ------------------------- | ------------------------------------------------- | --------------------------------------------------- | | **Vanilla Kubernetes** | Maximum flexibility and control | Requires manual setup and management | | **K3s** | Edge computing, resource-constrained environments | Lightweight, single binary, simplified architecture | | **MicroK8s** | Developer workstations, small clusters | Ubuntu-optimized, snap-based installation | | **RKE/RKE2** | Rancher users, enterprise environments | Integrated with Rancher management platform | | **OpenShift** | Red Hat environments, enterprise support | Enterprise Kubernetes with additional tooling | | **Tanzu Kubernetes Grid** | VMware environments | VMware's enterprise Kubernetes platform | The customer is responsible for installing, configuring, and maintaining their chosen Kubernetes distribution. ## Service adapters When you deploy an origin stack to on-premises environments, Tensor9 automatically compiles resources from your AWS origin stack to their Kubernetes equivalents. Since on-premises deployments use Kubernetes as the underlying platform, the service equivalents are identical to Private Kubernetes environments. ### Common service equivalents Which AWS services reach on-prem and bare-metal targets, what each becomes, and the tier it lands at are maintained in one place rather than repeated per form factor: see [service adapters](/service-adapters/overview) and the generated [Service Catalog](/service-adapters/catalog). Third-party managed equivalents (Backblaze B2, Neon, PlanetScale, MongoDB Atlas, Redis Enterprise Cloud, Confluent Cloud, Astronomer) require your customers to bring their own credentials and accounts with these services. EC2, DynamoDB, and EFS are all offered. Some services are not adapted yet, including Step Functions, API Gateway, Cognito, AppSync, and Redshift. See [Service Catalog](/service-adapters/catalog#services-we-do-not-adapt-yet). For detailed service adapter mappings and examples, see [**Service adapters**](/service-adapters/overview) or the [**Private Kubernetes service adapters section**](/form-factor/kubernetes#service-adapters). ## How it works Once your customer has a Kubernetes cluster running on their infrastructure, the deployment process follows the same workflow as Private Kubernetes environments: Your customer provisions physical servers and installs their chosen Kubernetes distribution following the vendor's installation guide. This includes: * Setting up control plane nodes * Joining worker nodes to the cluster * Configuring networking (CNI plugin) * Setting up storage classes for persistent volumes * Configuring ingress for external traffic After the Kubernetes cluster is running, your customer follows the standard Private Kubernetes setup: * Creates two namespaces (one for controller, one for application) * Installs the Tensor9 controller via Helm chart * Creates ServiceAccounts with appropriate RBAC permissions * Configures network access for the controller to reach your control plane You create releases and deploy your application following the standard deployment workflow, identical to Private Kubernetes deployments. ## Deployment workflow After the Kubernetes cluster is set up, **all deployment steps are identical to Private Kubernetes environments**. See the [**Private Kubernetes**](/form-factor/kubernetes) documentation for complete details on: * [How appliances work](/form-factor/kubernetes#how-private-kubernetes-appliances-work) * [Service adapters](/form-factor/kubernetes#service-adapters) * [Permissions model](/form-factor/kubernetes#permissions-model) * [Networking](/form-factor/kubernetes#networking) * [Observability](/form-factor/kubernetes#observability) * [Secrets management](/form-factor/kubernetes#secrets-management) * [Operations](/form-factor/kubernetes#operations) * [Complete deployment example](/form-factor/kubernetes#example-complete-private-kubernetes-appliance) ## Considerations for on-prem deployments While the Tensor9 deployment process is identical to Private Kubernetes, on-premises environments have unique infrastructure considerations: ### Hardware and capacity planning Customers need to provision sufficient hardware resources: * **Compute**: CPU and memory for application workloads * **Storage**: Persistent volumes for databases and stateful services * **Network**: Bandwidth for application traffic and data transfer * **High availability**: Multiple nodes for redundancy and fault tolerance ### Networking On-premises networking often requires additional configuration: * **Load balancing**: External load balancer or MetalLB for Kubernetes Services * **DNS**: Internal DNS records or external DNS management * **Firewall rules**: Outbound HTTPS access for Tensor9 controller * **TLS certificates**: SSL/TLS certificates for ingress endpoints ### Storage Persistent storage options for on-premises Kubernetes: * **Local storage**: Direct-attached storage on worker nodes (fast but not highly available) * **NFS**: Network File System for shared storage across nodes * **SAN**: Storage Area Network for enterprise environments * **Ceph/Rook**: Software-defined storage for cloud-native storage management * **Longhorn**: Cloud-native distributed block storage ### Maintenance and operations Customers are responsible for: * **Kubernetes upgrades**: Planning and executing cluster upgrades * **Node maintenance**: Patching OS, replacing failed hardware * **Backup and disaster recovery**: Protecting cluster state and application data * **Monitoring**: Infrastructure monitoring (server health, disk space, network) * **Security**: Physical security, network security, access control ## Best practices If possible, create a test appliance using the same Kubernetes distribution your customer will use in production. Different distributions may have subtle differences in behavior or available features. Provide clear hardware requirements and capacity planning guidance for your application. Include minimum and recommended specifications for CPU, memory, storage, and network bandwidth. Some on-premises environments have restricted internet access. Ensure your deployment process accounts for: * Air-gapped container image distribution * Limited or scheduled connectivity windows * On-premises artifact mirrors Create detailed operational documentation for customers managing their own infrastructure: * Troubleshooting common issues * Performance tuning guidelines * Backup and restore procedures * Scaling recommendations ## Troubleshooting If you're experiencing issues or need assistance with on-premises and bare metal deployments, we're here to help: * **Slack**: Join our community Slack workspace for real-time support * **Email**: Contact us at [support@tensor9.com](mailto:support@tensor9.com) Our team can help with deployment troubleshooting, configuration, and best practices for on-premises and bare metal environments. # Customer Appliances Source: https://docs.tensor9.com/fundamentals/appliances An **appliance** is a secure, self-contained system that you deploy into a customer's cloud infrastructure or private environment. Tensor9 appliances package all the components needed to run your app - compute, storage, networking, and the Tensor9 controller - into a single deployable unit. ## What is an appliance? Each appliance is a complete, isolated system that runs in a customer's environment. Unlike traditional cloud deployments where all customers share common infrastructure, each Tensor9 appliance is: * **Customer-hosted**: The appliance runs entirely within the customer's cloud account or private infrastructure - their "environment" * **Isolated**: Each customer's appliance is completely separated from other customers' appliances * **Self-contained**: Application data and components stay within the appliance and do not leave the customer's environment. The exception is a third-party managed equivalent your customer chooses, such as MongoDB Atlas or Confluent Cloud, which is hosted externally by design; [service adapters](/service-adapters/overview) marks which equivalents those are * **Abstracted**: Customers interact with the appliance as a cohesive unit, not individual infrastructure components * **Mirrored**: The appliance mirrors your [origin stack](/fundamentals/origin-stacks) configuration, adapted to the customer's specific [form factor](/fundamentals/key-concepts#form-factor) All application data stays in the customer's environment and never leaves their infrastructure. This provides complete data sovereignty and meets strict compliance requirements. ## Environment vs appliance It's important to understand the distinction: * **Environment**: The customer's infrastructure - their cloud account (AWS, Azure, GCP), Kubernetes cluster, or data center. This is what the customer owns and controls. * **Appliance**: The Tensor9-provided system that runs **within** that environment. This is what you (the vendor) deploy into the customer's infrastructure. * **App**: The software that runs **on** the appliance. This is what gets installed. Today, Tensor9 appliances come bundled with your app pre-installed. In the future, customers will be able to provision empty appliances and choose which apps to install - similar to an app store model. The appliance abstraction is designed to support both models. ## Why appliances? Appliances solve a critical challenge for software vendors: how to deliver modern, cloud-native applications to customers who require data sovereignty, regulatory compliance, or disconnected environments. With Tensor9 appliances, you can: * **Meet compliance requirements**: Deploy into regulated industries (healthcare, finance, government) that mandate data residency * **Support multi-cloud**: Deliver your application consistently across AWS, Google Cloud, Azure, or private Kubernetes * **Maintain control**: Observe and operate customer appliances remotely through secure, audited channels * **Scale efficiently**: Avoid the engineering overhead of maintaining multiple product versions for different customer environments ## Appliance types Tensor9 supports two types of appliances: **customer appliances** and **test appliances**. ### Customer appliances **Customer appliances** are production environments that run your application for end customers. These appliances are: * Owned and controlled by the customer * Managed through your [control plane](/fundamentals/control-plane) with appropriate customer approval workflows * Long-lived and follow a defined lifecycle (Setup → Live → Retiring → Retired) * Configured with customer-specific form factors, regions, and security requirements #### Lifecycle Every customer appliance progresses through four lifecycle states: | Lifecycle State | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Setup** | The appliance is being provisioned and configured. Infrastructure is being created, networking is being established, and the Tensor9 controller is being deployed. | | **Live** | The appliance is fully operational and serving traffic. Your app is deployed and running in the customer's environment. | | **Retiring** | The appliance is being decommissioned. Existing workloads continue to run, but no new deployments are accepted. | | **Retired** | The appliance has been fully decommissioned. All resources have been cleaned up and the appliance is no longer accessible. | ### Test appliances **Test appliances** are automatically managed environments used for testing and development. These appliances allow you to: * Quickly spin up a temporary environment to test new releases * Validate deployments before rolling out to production customer appliances * Experiment with different form factors and configurations Test appliances have a simplified lifecycle and can be retired when no longer needed. ## Appliance architecture Each appliance has three components: a **controller**, a **runtime environment**, and **artifact storage**. For any service at the Max Adaptation Tier it also runs the Tensor9 adapter, which answers the AWS API your application calls; see [service adapters](/service-adapters/overview) for which services need it. ### Controller The **controller** is Tensor9-provided software that runs inside the appliance and acts as the bridge between your control plane and the customer's infrastructure. The controller: * Receives deployment instructions from your control plane * Executes infrastructure-as-code deployments (Terraform, Kubernetes, etc.) * Collects telemetry (logs, metrics, traces) and forwards it to your control plane * Handles operations commands securely with proper authentication and audit logging ### Runtime environment The **runtime environment** is where your application code actually runs. This includes: * Compute resources (containers, virtual machines, serverless functions) * Data stores (databases, caches, object storage) * Networking (load balancers, service meshes, DNS) * Any managed services specified by the form factor The exact composition depends on your origin stack and the appliance's form factor. ### Artifact storage Each appliance includes dedicated storage for: * Container images and other deployment artifacts * Infrastructure-as-code state files * Configuration and secrets specific to this appliance * Backup and disaster recovery data ## Connectivity modes Appliances support different connectivity modes to accommodate various customer requirements: | Connectivity Mode | Description | Use Cases | | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | **Connected** | The appliance establishes an outbound connection to your control plane. This connection is used to receive deployments, send telemetry back to your control plane, and receive operational commands. The appliance **never opens inbound network ports** - all communication is outbound from the appliance to your control plane. | Most common deployment model. Works in cloud environments and on-premises environments where outbound HTTPS is allowed. | | **Disconnected**

(private beta) | The appliance cannot establish a connection to your control plane. Deployments, observability, and operations must be manually proxied by your customer through offline processes. | Highly restricted networks with no outbound internet access. | Even connected appliances never open ingress ports to allow inbound requests from your control plane. The communication direction is always outbound from an appliance to your control plane. This means you don't have to ask your customers to open ingress ports, which is often a logistically challenging requirement. The connectivity mode is specified as part of the appliance's [form factor](/fundamentals/key-concepts#form-factor). Within Connected mode, the outbound channel from the appliance's controller to your control plane is established over the public internet by default. AWS PrivateLink is optionally available as an additional path when both your control plane and the customer's controller are on AWS, and a vendor-provided Tailscale tailnet is optionally available as a path on any cloud. See the [security model](/customer/security/security-model#no-inbound-network-access) for the underlying handshake, the [AWS form factor](/form-factor/aws#controller-connectivity) page for how the choice is exposed when authoring an AWS form factor, and the [Connectivity](/fundamentals/connectivity) page for a side-by-side comparison of all available network paths. ## Creating a customer appliance Customer appliances are created through a self-service signup flow. As a vendor, you generate a signup link and send it to your customer: ```bash theme={null} tensor9 app signup-link -appName ``` This command generates a signup URL that you can send to your customer. You can also generate signup links from the **Vendor Portal** under your app's detail page. ### Example: Generating a signup link ```bash theme={null} # Generate a signup link for your app tensor9 app signup-link -appName my-app # Output: # https://portal.tensor9.com/buyerSignup?appId=0000000000000213 ``` ### Customer signup flow When a customer appliance is created, Tensor9 orchestrates the following process: You generate a signup link using the `tensor9 app signup-link` command and send it to your customer. The customer uses this link to sign up and create their appliance through the Tensor9 portal. Through the signup portal, your customer selects: * The [form factor](/fundamentals/key-concepts#form-factor) (cloud provider, connectivity, managed services) * The cloud region where the appliance will run * Any specific configuration options The customer then runs the provided setup script in their environment. The setup script provisions the foundational infrastructure for the appliance in the customer's environment, including: * A dedicated VPC or namespace (depending on the form factor) * Networking components (load balancers, DNS, routing) * Storage for runtime artifacts and configuration * The Tensor9 controller that manages the appliance The appliance establishes a secure, outbound-only connection to your [control plane](/fundamentals/control-plane). This connection: * Uses mutual TLS authentication (see [Connection Security](/fundamentals/connection-security)) * Allows your control plane to send deployment instructions * Enables telemetry to flow from the appliance to your [observability sink](/fundamentals/key-concepts#observability-sink) * Provides the foundation for [operations endpoints](/fundamentals/operations) Once the appliance is ready, you [compile](/fundamentals/how-tensor9-works) your origin stack for the appliance's form factor and [deploy](/fundamentals/deployments) it using standard tooling (e.g., `terraform apply`, `tofu apply`). Tensor9 architecture showing control plane and customer appliances Tensor9 architecture showing control plane and customer appliances The setup process typically takes 5-15 minutes, depending on the cloud provider and form factor complexity. ### Monitoring appliance setup and status You can view the status of all your appliances using the `tensor9 report` command or the **Vendor Portal**. The Vendor Portal shows your appliances on the main dashboard with real-time health status. ```bash theme={null} tensor9 report ``` This generates a report showing all your apps, form factors, customer appliances, and test appliances. Here's an example of what the output looks like: ``` Vendor: Acme Software [id: 000000000000003b]: Name: Acme Software Apps: (1) my-app [id: 0000000000000213]: Name: my-app Domain: my-app.customers.alpha.tensor9.com Stacks: (1) Terraform: (1) my-stack | s3://t9-ctrl-000001/my-stack.tf.tgz Form Factors: (2) aws-connected [id: 0000000000000213:6d86e442e68ae6b8]: Name: aws-connected Description: AWS + Connected Env: Aws Connectivity: Connected azure-connected [id: 0000000000000213:7e97f553f79bf7c9]: Name: azure-connected Description: Azure + Connected Env: Azure Connectivity: Connected Customer Appliances: (2) Customer Appliance: acme-corp-production [id: 000000000000007e]: Status: Live Name: acme-corp-production Customer: Acme Corp [id: 000000000000004a] Cloud Details: Aws(us-west-2) Form Factor: aws-connected Appliance Id: 000000000000007e Installs: Acme Software/my-app → Acme Corp [id: 0000000000000213:000000000000007e:0000000000000159] Vendor: Acme Software [id: 000000000000003b] App: my-app [id: 0000000000000213] Customer: Acme Corp [id: 000000000000004a] Release: Acme Software/my-app → Acme Corp [version: 1.2.0] Version: 1.2.0 Outputs: api_endpoint: https://api.acme-corp-production.my-app.customers.alpha.tensor9.com web_endpoint: https://web.acme-corp-production.my-app.customers.alpha.tensor9.com Releases: Effective Releases: (1) Acme Software/my-app → Acme Corp [version: 1.2.0] Version: 1.2.0 Lifecycle: Submitted Deployment: Deployed Created: 2 days ago Updated: 2 days ago Description: Production release with new features Origin: s3://t9-ctrl-000001/my-stack.tf.tgz Id: s3://t9-ctrl-000001/my-stack.tf.tgz:4de31b51:0000000000000754 Notes: Deployed successfully Prepped Releases: (0) Hardware: (updated 30 seconds ago) Uptime: 3 days 4 hours Capacity Machines: 3 Customer Appliance: bigco-staging [id: 000000000000008f]: Status: Setup Name: bigco-staging Customer: BigCo Inc [id: 000000000000005b] Cloud Details: Azure(eastus) Form Factor: azure-connected Appliance Id: 000000000000008f Installs: Releases: Effective Releases: (0) Prepped Releases: (0) Test Appliances: (1) Test Appliance: test-aws-us-west-2 [id: 000000000000009a]: Status: Live Name: test-aws-us-west-2 Customer: Acme Software Test [id: 000000000000003c] Cloud Details: Aws(us-west-2) Form Factor: aws-connected Appliance Id: 000000000000009a Test Appliance Name: test-aws-connected Test Appliance Id: 000000000000003b:0000000000000006 Installs: Acme Software/my-app → Acme Software Test [id: 0000000000000213:000000000000009a:000000000000016a] Vendor: Acme Software [id: 000000000000003b] App: my-app [id: 0000000000000213] Customer: Acme Software Test [id: 000000000000003c] Release: Acme Software/my-app → Acme Software Test [version: 1.3.0-rc1] Version: 1.3.0-rc1 Outputs: api_endpoint: https://api.test-aws-us-west-2.my-app.customers.alpha.tensor9.com Releases: Effective Releases: (1) Acme Software/my-app → Acme Software Test [version: 1.3.0-rc1] Version: 1.3.0-rc1 Lifecycle: Submitted Deployment: Deployed Created: 4 hours ago Updated: 4 hours ago Description: Release candidate for testing Origin: s3://t9-ctrl-000001/my-stack.tf.tgz Id: s3://t9-ctrl-000001/my-stack.tf.tgz:5ef42c62:0000000000000812 Notes: Testing new features Prepped Releases: (0) Hardware: (updated 45 seconds ago) Uptime: 6 hours 23 minutes Capacity Machines: 1 ``` You can also list all appliances individually using: ```bash theme={null} tensor9 appliance list ``` This outputs detailed reports for each appliance, one at a time. ## Creating test appliances Test appliances can be created directly using the CLI: ```bash theme={null} tensor9 test appliance create \ -appName \ -formFactorName \ -region ``` ### Example: Creating a test appliance ```bash theme={null} # Create a test appliance for testing in AWS us-west-2 tensor9 test appliance create \ -appName my-app \ -formFactorName aws-connected \ -region aws:us-west-2 # The test appliance will be automatically provisioned and show up in tensor9 report ``` Test appliances are provisioned automatically by Tensor9 and typically become available within 10-15 minutes. ## Retiring test appliances When you no longer need a test appliance, you can retire it: ```bash theme={null} tensor9 test appliance retire \ -testApplianceName ``` This will decommission the test appliance and clean up all associated resources. **Note**: Customer appliances cannot be retired through the CLI for safety reasons. Contact Tensor9 support if you need to retire a customer appliance. ## Appliances and installs An **appliance** is the system, while an [**install**](/fundamentals/key-concepts#install) is a specific app running on that appliance. The relationship is: * **One appliance** hosts exactly **one install** (current limitation) * **One install** belongs to exactly **one appliance** * Each install represents a specific app deployed onto a specific appliance For example, if you have two apps ("Analytics" and "Dashboard") and customer "Acme Corp" needs both, you would currently create two separate appliances - one for each app. Multi-app support (multiple installs per appliance) is planned for a future release. When available, a single appliance will be able to host multiple apps, similar to how a single device can run multiple applications. ## Appliances and form factors The [**form factor**](/fundamentals/key-concepts#form-factor) defines the characteristics of the customer's environment where the appliance will be deployed. When you create an appliance, you specify its form factor, which determines: * **Cloud provider**: AWS, Google Cloud, Azure, or private Kubernetes * **Connectivity**: Connected or disconnected * **Managed services**: Which managed services are available (e.g., AWS RDS, Google Cloud SQL) * **Security requirements**: FIPS compliance, CMMC compliance, etc. * **Compute requirements**: Minimum CPU, memory, storage Your [control plane](/fundamentals/control-plane) uses the form factor to compile your origin stack into a deployment stack that's optimized for deploying the appliance into that specific environment. ## Security and isolation Appliances are designed with security and isolation as core principles: ### Network isolation * The appliance's Tensor9 controller runs in a dedicated VPC, Kubernetes namespace, or set of virtual machines (depending on the appliance's environment) * The software defined network mirrors the origin stack's network topology as closely as possible * Appliances communicate with your control plane through secure, authenticated channels ### Data isolation * All application data remains within the appliance (and thus within the customer's control) * Telemetry sent to your control plane is limited to logs, metrics, and traces - no customer business data * Secrets and credentials are stored encrypted and scoped to the specific appliance ### Access control * All operations commands require customer approval (configurable by the customer) * Every action is logged and auditable * Your control plane and each appliance authenticate each other with mutual TLS (see [Connection Security](/fundamentals/connection-security)) * Temporary access (e.g., JIT IAM roles) is granted only with explicit customer approval ## Observability Once your appliance is live and your app is deployed, you can [observe](/fundamentals/observability) its state, performance, and usage through telemetry synchronized back to your control plane. Tensor9 automatically configures the appliance to forward: * **Logs**: Application logs from containers, VMs, and serverless functions * **Metrics**: System metrics (CPU, memory, disk) and custom application metrics * **Traces**: Distributed traces for request flow analysis All telemetry is forwarded from the appliance to your control plane, which then routes it to your configured [observability sink](/fundamentals/key-concepts#observability-sink) (Datadog, New Relic, CloudWatch, etc.). ## Operations Your control plane provides [operational endpoints](/fundamentals/operations) that allow you to perform day-2 operations on appliances: * **Remote shell access**: Execute commands inside the appliance for debugging * **Configuration management**: Update secrets, environment variables, and configuration * **Service restarts**: Restart services or trigger state changes * **Database migrations**: Run one-off scripts or database migrations All operations are authenticated, authorized, and logged, with configurable customer approval workflows. ## Best practices Use clear, descriptive names for appliances that include: * Customer identifier * Environment type (production, staging, etc.) * Optional region or purpose Examples: * `acme-corp-production` * `bigco-staging` * `megacorp-emea-compliance` Always create and test releases in test appliances before deploying to customer appliances: 1. Create a test appliance for your target form factor 2. Deploy and validate your release in the test appliance 3. Once validated, deploy the same release to customer appliances Regularly monitor the health of your appliances using `tensor9 report` or the **Vendor Portal** dashboard. Pay attention to: * **Status**: Ensure appliances are "Live" and not stuck in "Setup" or "Starting up" * **Deployment**: Check that releases show "Deployed" status * **Hardware**: Monitor uptime and capacity machines * **Blocking Issues**: Address any blocking issues in prepped releases before deployment ## Next steps Now that you understand appliances, explore these related topics: * [**Deployments**](/fundamentals/deployments): Learn how to deploy to appliances * [**Observability**](/fundamentals/observability): Monitor your appliances * [**Operations**](/fundamentals/operations): Operate your appliances remotely # Delivering Artifacts to Git Source: https://docs.tensor9.com/fundamentals/artifact-git-delivery Most artifacts are delivered to a place your application reads from: a file in an object store, an image in a container registry. Some destinations cannot be read from at all. A hosted build service that builds your application from a git repository has to be **pushed to**, and there is nobody inside your customer's account to do the push. The `# @artifact_commit()` annotation closes that gap. It declares that the contents of one of your artifacts become a commit on a branch of a git repository in your customer's account. The appliance controller performs the push itself, using its own identity in that account, so no git credential, access token, or console authorization is involved anywhere. The artifact is still an ordinary object-store file. What is new is where it lands and who moves it. ## When you need this You need this when something inside your customer's account builds your application from a git repository rather than running a bundle you built yourself. That happens when two things hold at once. The build has to run **per install**, because some of what goes into it is created by the install's own apply. A framework that compiles public environment values into its browser bundle cannot be served by one bundle you built in advance. And the thing that builds has to be **pushed to** rather than read from. If it accepts an artifact you hand it, [ordinary artifact delivery](/fundamentals/artifacts) already covers you. AWS Amplify is an example that shows both halves at once: * **Build-time values are compiled into the bundle.** A framework like Next.js bakes its public environment values into the browser bundle during the build, and some of those values (an identity provider client ID, for example) are created by the install's own apply. One prebuilt bundle therefore cannot serve every install. * **Amplify has no manual deploy path for server-rendered apps.** For an SSR app, a git repository is the only input it accepts. * **Of the git sources Amplify accepts, only CodeCommit authenticates without a person.** GitHub, GitLab, and Bitbucket each require somebody to complete an interactive authorization flow and hand over a token. Nobody is present inside your customer's account to do that. Nothing in the mechanism is specific to Amplify. Any service inside your customer's account that builds from a branch it watches has the same shape. ## How a delivery works Your build packages the tree you want committed as a tar archive and uploads it to a bucket in your own account. A data source for the object, an `aws_codecommit_repository` resource for the destination, and a local that names both, annotated with `# @artifact_commit()`. During deployment, Tensor9 fetches the object from your bucket and streams it to the appliance controller. The appliance controller unpacks the archive and pushes the tree onto the branch as a single commit, authenticating with its own identity in the customer's account. Whatever watches that branch sees the push and builds from it. Deliveries are idempotent. Before pushing, the appliance controller compares the tree it is about to write against what is already at the branch tip and writes nothing when they match, so redeploying unchanged contents does not trigger a rebuild. When it does write, the delivery replaces the branch with a single commit. The branch belongs to the delivery, and hand edits to it are overwritten. ### Packaging the artifact * The archive is `tar`, optionally gzipped. The format is detected from the file's contents, so the object key can be named anything. * Entry paths are preserved exactly and no leading directory is stripped. Create the archive from inside the directory you want at the repository root. * A `.git` directory inside the archive is ignored, so an archive made from a working tree does not carry its history in. ## Declaring a delivery ```hcl theme={null} data "aws_s3_object" "webapp_src" { count = local.webapp_seeded ? 1 : 0 bucket = var.webapp_artifact_bucket key = var.webapp_artifact_key } locals { # @artifact_commit() webapp_delivery = { src = one(data.aws_s3_object.webapp_src) dest = one(aws_codecommit_repository.webapp) branch = var.webapp_branch after = [aws_amplify_branch.web] } } ``` All four keys are required. | Key | Value | | -------- | ----------------------------------------------------------------------------------------------------------------- | | `src` | A reference to the `data "aws_s3_object"` block holding the artifact. | | `dest` | A reference to the `aws_codecommit_repository` resource the commit lands on. | | `branch` | The branch the commit lands on. | | `after` | The resources that watch the repository for pushes and must therefore exist before the artifact lands. See below. | The annotation itself takes no arguments. Every value is a real Terraform reference inside the object, so Terraform validates it: a rename breaks loudly, your editor completes it, and your linter sees it. ### `after` decides whether anything builds `after` names the resources that must exist before the artifact lands, written as addresses, the way `depends_on` entries are written. Tensor9 already orders the delivery after every resource it can see **consuming** the destination repository. `after` is for a resource that **watches** the repository without consuming it, which no reading of your Terraform can find. `aws_amplify_branch` is the example: it references the Amplify app's ID and names the repository nowhere, so nothing connects it to the delivery. **Getting `after` wrong fails silently.** A build service that watches a branch discards a push notification for a branch it holds no record of, and it never re-scans. If the artifact lands before the watcher exists, the apply succeeds, no build runs, no build fails, and the install looks healthy while serving nothing. Creating the watcher afterwards does not back-fill a build for the commit already sitting at the branch tip. One caution, and it applies only when a resource you name in `after` is itself gated by `count` or `for_each`. Terraform accepts a `depends_on` entry naming a resource whose count is zero, and orders the delivery against nothing at all. Tensor9 does not compare that gate against the delivery's own. So if the watcher is switched off on an apply where the delivery still runs, the delivery lands unordered, and you are back in the silent case above: the apply succeeds and nothing builds. Gate the delivery so it can never be on while the watcher is off. The usual way is to build the delivery's gate out of the watcher's own: ```hcl theme={null} locals { # var.enable_web_app is also what gates aws_amplify_branch.web webapp_seeded = var.enable_web_app && var.webapp_artifact_key != "" } ``` A delivery gated this way always has a branch to be ordered against. Write `after = []` only if nothing watches the repository. `after = null` is an error, to prevent silent misconfigurations. ### The version and the etag are derived, not declared Tensor9 reads four values off `src`: the source object's bucket, key, `version_id`, and `etag`. Declaring a `version` key or an `etag` key is refused. Both are functions of `src`, so stating either separately could only ever disagree with it. The two do different jobs, and they are not interchangeable: * The **version is the pin**. It is what Tensor9 hands S3 when it fetches the object, so it decides which bytes are delivered. * The **etag is only a change token**. It is never read. It exists so that a republished artifact plans a difference. This is also why `src` has to be a data source rather than a bucket and key you write out yourself. Both `version_id` and `etag` come from an S3 `HeadObject` performed at plan time, and your own AWS provider is the only thing positioned to make that call. The bucket and the key are just the block's own arguments read back. ### How Tensor9 notices a republished artifact A delivery re-runs when any of those four values changes, so republishing your artifact plans a difference however you choose to publish it. The etag is what covers the awkward case: an unversioned bucket reports an empty version, so an object overwritten at a stable key moves nothing else Terraform could compare. Versioning the bucket, or writing a new object key per build, is still worth doing, for a different reason. It gives the delivery a pin: * A **versioned bucket** produces a new version per publish, and Tensor9 fetches that exact version. * **Immutable keys** (a build ID or commit SHA in the key, never overwritten) need no pin, because the current version of that key is the only version there will ever be. * An **unversioned bucket written at a stable key** has neither. Tensor9 fetches whatever the key holds at the moment the appliance controller reads it, which is not necessarily the object the plan compared. On an unversioned bucket with stable keys, republishing between plan and apply delivers the newer bytes rather than the ones the plan saw. The delivery still lands and the build still runs; it is just not the artifact you were looking at. Either of the other two arrangements closes that window. ## What ships today | Requirement | Detail | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Source | `src` must reference a `data "aws_s3_object"` block. | | Destination | `dest` must reference an `aws_codecommit_repository` resource. | | Module | Both blocks must be declared in the same module as the annotated local. | | Appliance | The appliance must be on AWS, with a [connected](/fundamentals/connectivity) link. | | Counted blocks | If `src` and `dest` are `count`-gated, both counts must agree, and each reference must select a single instance with `one(...)` or `[0]`. `for_each` is not supported. | | Source use | Tensor9 evaluates the `data "aws_s3_object"` block with your own credentials against your own bucket, so no resource that runs inside the appliance may also read it. | Declaring a delivery that Tensor9 cannot perform is a **compile error, not a silent skip**. A declaration compiled for an appliance that has nowhere to put the commit fails the build rather than quietly producing a deployment that reads as healthy and serves nothing. ## It stays inert in your own deployment The annotation is a comment, and the declaration is an ordinary Terraform local. Your own `terraform plan` and `terraform apply` see a local that nobody reads, and behave exactly as they did before you added it. That is deliberate: an integration that stopped your ordinary deployments from planning would not be one you could adopt. Gate the delivery the same way you gate the rest of the feature. In the example above, `count = local.webapp_seeded ? 1 : 0` on the source leaves the whole declaration inert when the feature is off. One consequence is worth knowing. During a Tensor9 deployment, the annotated local is replaced by the delivery itself, so the keys you wrote are no longer readable from it, and referencing `local.webapp_delivery.src` elsewhere in your stack is refused at compile time. The delivery does expose `repository_name`, `branch_name`, `commit_id`, and `tree_digest`, but none of those exist in a deployment without Tensor9, so a stack you also deploy yourself should leave the local unread. A resource that reads one of those values must not also consume the destination repository, and must not appear in `after`. Either arrangement asks for the artifact to land both before and after the same resource, so Tensor9 refuses the declaration at compile time rather than emitting a graph Terraform would report as a cycle far from its cause. ## Related Topics * [**Artifacts**](/fundamentals/artifacts): How artifacts move from your environment into an appliance * [**Origin Stacks**](/fundamentals/origin-stacks): How to define portable infrastructure code * [**Deployments**](/fundamentals/deployments): How compilation and deployment works # Artifacts Source: https://docs.tensor9.com/fundamentals/artifacts Tensor9 enables the secure and reliable deployment of vendor application artifacts (such as container images and models) into customer-connected appliances using Infrastructure as Code (IaC). Artifacts are the essential files needed for your application to run. Tensor9 focuses on two primary artifact kinds: 1. Files in an object store (e.g., S3 objects) ```hcl {3-4} theme={null} resource "aws_lambda_function" "example_lambda" { function_name = "example-function" s3_bucket = "example-bucket" s3_key = "example-key" handler = "index.handler" runtime = "nodejs24.x" role = aws_iam_role.example_lambda_role.arn } ``` 2. Container images (stored in ECR) ```hcl {18} theme={null} resource "kubernetes_manifest" "example_deployment" { manifest = { "apiVersion" = "apps/v1" "kind" = "Deployment" "metadata" = { "name" = "example-deployment" } "spec" = { "replicas" = 2 "selector" = { "matchLabels" = { "app" = "example-app" } } "spec" = { "containers" = [{ "name" = "example-container" "image" = "123456789012.dkr.ecr.us-west-2.amazonaws.com/example-image:example-tag" "ports" = [{ "containerPort" = 80 }] }] } } } } ``` ## Artifact deployment strategies Tensor9 supports two primary strategies for moving artifacts from the vendor's source environment to the customer's appliance environment: Copy during deployment or direct reference. ### 1. Copy during deployment model: Default and recommended In the Copy model, the Vendor Controller copies the artifact from the vendor's source location to a dedicated artifact repository within the customer's appliance account during the deployment process. In this model, only the Vendor Controller requires read access to the artifacts and the vendor does not have to grant read permissions for each artifact to every appliance. | Scenario | Description | Tensor9 Action | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Deploy-Time Artifacts | Artifacts needed for the infrastructure resource definition (e.g., a Container Image referenced in an ECS Task Definition or a Lambda function zip archive in S3). | Tensor9 automatically detects the artifact in your Terraform, copies it to the appliance account, and updates your compiled code to point to the new, local location. | | Run-Time Artifacts | Artifacts referenced at runtime (e.g., an LLM Model in S3) that need to be available in the appliance. | You declare the artifact with an S3 data source rather than relying on Tensor9 to find it inside a resource, and Tensor9 copies it as part of the apply step. | #### Vendor workflow (container image copy): 1. Your CI/CD builds the container image and publishes it to your origin ECR repository. 2. Your Terraform references the image using its full path/tag in your compute resource definition (e.g., `resource.aws_ecs_task_definition.container_definitions[*].image`). 3. When you run the Tensor9 build and apply steps, the compiler: * Identifies the artifact reference. * Creates a process to copy the artifact from your ECR to the appliance's ECR. * Rewrites your compiled infrastructure resource to reference the new, appliance-local ECR path. ### 2. Direct reference model: Override In the Direct Reference model the artifact is not copied. The appliance is configured to reach out and pull the artifact directly from the vendor's source location. This model is generally reserved for special cases, such as: * Publicly accessible artifacts from sources trusted by both the vendor and the customer. * Extremely large artifacts where minimizing data transfer/copy costs through the Vendor Controller account is critical. * Artifacts that the vendor only wants to access dynamically at runtime. | Action | Vendor Requirement | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Override Default | You must provide an annotation in your IaC to explicitly tell the Tensor9 compiler to skip copying a specific artifact reference. | | Permissions | You are responsible for managing cross-account permissions (e.g., setting up appropriate IAM roles/policies) to ensure the customer's appliance can read the artifact directly from your vendor source account. | ## Copy during deployment details ### How Tensor9 finds your artifacts Tensor9 finds artifacts two ways, and the difference decides whether you have to write anything at all. **By consumer.** This is the default, and it asks nothing of you. The compiler walks resource shapes it already understands, and a field's position in one of those shapes is enough to say what the field holds: an ECS task definition's `image`, a Kubernetes pod spec's `containers[].image`, a Lambda function's `s3_bucket` / `s3_key` / `s3_object_version`. Both examples at the top of this page are found this way. **By declaration.** Where position cannot tell Tensor9 anything, you name the artifact with a data source. The case that forces this is an image reference the compiler has no way to rewrite, such as one inside a Helm chart's `values`, which is a YAML string rendered at plan time and carries no reference to rewrite. Name the image with the Terraform block that already exists for it: ```hcl theme={null} data "aws_ecr_image" "worker" { registry_id = var.vendor_account_id region = var.vendor_region repository_name = "worker" image_tag = var.worker_tag } ``` Tensor9 replaces that block with the appliance's copy, and every reader of `data.aws_ecr_image.worker.image_uri` reads the copy instead. Your arguments are moved into the copy as expressions rather than resolved during compilation, so a tag supplied at deploy time copies the image you actually deploy rather than the variable's default. Two data sources in the same module that name the same image with the same expressions share a single copy, so spelling one image twice costs nothing. #### What a declared image requires | Requirement | Why | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `registry_id`, `region`, and `repository_name` all written out | Terraform treats the first two as optional and falls back to your provider's account and region. The copy is fetched from outside that account and region, so the fallback would name the wrong registry, and there is no second place to read the right coordinates from. | | `image_tag` or `image_digest` | The copy is addressed by a single image reference, which needs a tag or a digest. | | No `most_recent` | `most_recent` is a query over the repository rather than a name for one image. | | Every reference read inside the appliance | The copy is made during the deployment, so its attributes are not available to a `count` or `for_each` expression (Terraform resolves those while planning), nor to an output, a module call, or a resource that stays in your own account. | | Only the attributes the copy records | `image_uri`, `image_digest`, `image_tag`, and `repository_name` are rewritten onto the copy everywhere, and `registry_id` and `region` wherever the appliance copies into ECR. `image_tags`, `image_pushed_at`, `image_size_in_bytes`, and `id` are not recorded at all, so reading one is a compile error rather than a reference left dangling. | #### Data sources also carry what only a plan-time read can produce A data source gives Tensor9 something a bucket and key written out as literals cannot: attributes that exist only because your own provider read the artifact while planning. [Git delivery](/fundamentals/artifact-git-delivery) rests on exactly that. Its source must be a `data "aws_s3_object"` block, because the object's `version_id` and `etag` come from an S3 `HeadObject` your provider performs at plan time, and those two values are what tell Tensor9 you have republished the artifact. ### Artifact naming requirements To ensure immutability and prevent race conditions during deployments, Tensor9 requires all vendor artifacts to be uniquely named and immutable. | Artifact Type | Naming Requirement | Why? | | ------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | S3 Objects | Apply a version scheme to the S3 key/filename. | Ensures that a deployment always receives the same, specific artifact version, even during rollbacks. | | ECR Images | Apply a version scheme to the ECR tag. | Mutable tags (like `:latest`) can change, leading to inconsistent deployments. Use immutable tags for reliability. | By enforcing immutable naming, we ensure a deployment to an appliance is always linked to a known-good application version. ### Supported artifact locations Tensor9 supports copying artifacts from two locations within the vendor's control: | Origin Location | Pros | Cons | | ---------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Vendor Controller Account | Tensor9 manages the necessary permissions for appliances to read and copy artifacts. | Requires the vendor to replicate the artifact into this account before initiating deployment. | | Another Vendor-Owned Account | Directly reference artifacts where they are used. | Requires the vendor to manually create roles that the Tensor9 controller can assume to perform the copy operation. May also increase transfer costs by copying once to the Vendor Controller account then on to the customer account. | ### Out-of-scope artifacts Tensor9 does not treat the following as artifact sources: * Secrets (handled by Tensor9 Secret Management) * Code repositories (GitHub, CodeCommit, etc.) * Package mirrors (Maven, NPM, etc.) * Database data **Note**: You can, however, export data from these sources as inert files (e.g., a database snapshot) and copy them as S3 objects. ## Delivering an artifact into a git repository Both artifact kinds above are delivered to a place your application reads from. Some destinations cannot be read from: a hosted build service that builds your application from a git repository has to be pushed to, and nobody is present inside your customer's account to do the push. For that case, the appliance itself pushes an artifact's contents into a git repository in the customer's account as a commit, and the build service builds from what lands there. The artifact is still an ordinary object-store file; only its destination and the party that moves it are different. See [Delivering Artifacts to Git](/fundamentals/artifact-git-delivery). # How Break Glass Works Source: https://docs.tensor9.com/fundamentals/break-glass Your customers run your software inside their own cloud accounts. You have no standing SSH keys, no kubectl context, no database password. The [operations](/fundamentals/operations) surface covers most of what you need (run a vetted command, get the output back), but sometimes you need to reach a specific resource directly: open an interactive `kubectl` session against a crash-looping cluster, or connect to a database to diagnose a live incident. Break glass is the surface for one-time, time-bounded access to a single named resource. You request it, your customer reviews and cryptographically approves that single request, and the system assembles exactly what's needed to reach and authenticate to that resource for the length of the session. When the session ends, everything is torn down. Nothing privileged is left standing in your customer's account between sessions. How break glass works in five steps: an operator requests access to one resource with a reason and time limit, your customer reviews and cryptographically signs the request, a short-lived credential is minted (and a temporary network path if the target is private), the operator works on the resource for the session, then access self-expires and is torn down with every step logged. How break glass works in five steps: an operator requests access to one resource with a reason and time limit, your customer reviews and cryptographically signs the request, a short-lived credential is minted (and a temporary network path if the target is private), the operator works on the resource for the session, then access self-expires and is torn down with every step logged. ## When to use break glass Break glass is deliberately separate from the standing operations channel. Reach for it only when a vetted command isn't enough: | Use the standing channel | Use break glass | | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | [Operations](/fundamentals/operations): run a vetted template against an appliance and get signed output back | Reach a resource directly for an interactive or open-ended session | | The work fits in a known, reviewable command body | You need a live shell, a kubectl context, or a database connection | | No interactive session required | A live incident or deep-debug session where you can't pre-script every step | Both surfaces share the same trust core (a customer-signed approval the appliance controller verifies before anything happens), so the concepts below will feel familiar if you've used operations. ## The two planes Getting an operator onto a customer resource takes two independent things: a network path to reach it, and a credential to authenticate once there. Break glass models these as two planes and resolves each one per target. | Plane | Question it answers | Resolutions | | ---------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | **Network path** | How does the operator's traffic reach the resource? | The resource is already reachable (**Direct**), or it's private and break glass stands up a temporary tunnel (**Tailscale** or **Twingate**). | | **Credential** | How does the operator authenticate once there? | The appliance controller mints one, an approved operational command mints one, or the operator brings their own (out of band). | The network path is **resolved, not chosen**: break glass inspects the target's actual reachability and decides whether a tunnel is needed. You don't pick "directly reachable vs. tunnel"; that follows from the resource. See [Network providers](/fundamentals/break-glass/providers) for how each path is configured. ### How the credential is minted The credential plane is where most of the variation lives: | Credential source | When it applies | Example | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Appliance controller** | The controller has structural authority over the target and can mint a fresh credential itself | A Kubernetes cluster the controller administers: it issues a new, per-session, short-lived ServiceAccount token and deletes it at session end | | **Operational command** | The controller can't mint natively, but the credential exists in your customer's environment and an approved command can fetch or generate it | A database: an approved operational command reads the password from your customer's secret store, or runs `CREATE ROLE ... VALID UNTIL` to generate a short-lived login | | **Out of band** | Break glass supplies no credential; the operator already holds one | A self-hosted service that uses a shared secret, and break glass provides only the network path | The operational-command path is the general mechanism and the expected story for most resources: a credential is whatever a customer-approved operational command produces, and it reuses the same approval machinery as [operations](/fundamentals/operations). A credential recipe is a shell-script command, so it can do whatever a script can: read a secret from your customer's secret store, run `psql` or `kubectl`, launch a one-shot pod, and so on. A recipe is never a Terraform template, so a minted secret is never written into Terraform state. "Out of band" does not mean the resource has no credential. It means break glass isn't the thing supplying it. The operator authenticates with a credential they already hold or that your customer provided directly; break glass only opens the network path. ## The lifecycle Every break glass session moves through the same shape, regardless of which planes it resolves: An operator opens the Break Glass page in your vendor portal, picks one resource, and submits a request with a reason and a requested duration. Your customer reviews the exact request (which resource, what privilege, how long) and signs an approval with a private key that never leaves their environment. They can also reject. The appliance controller verifies the signature locally, then assembles the session: where the credential source mints one, it mints a short-lived credential scoped to exactly what was approved; where the target is private, it stands up a temporary, session-scoped network path. The session goes active only when every plane it needs is ready. The operator uses the resource for the session: fetches a ready-to-use kubeconfig and runs `kubectl`, or connects to the database, and so on. The session has an expiry set from the approved duration. At the end, any temporary credential and network path are removed. A minted credential is also short-lived on its own clock, so access ends even if teardown is interrupted. The request, the signed approval, and teardown are recorded. The requested duration is the window of usable access, and it begins when the session goes active (after your customer approves), not at submission. Time spent waiting for your customer to approve does not count against it. See [Requesting access](/fundamentals/break-glass/lifecycle) for the operator's side of this flow in detail. ## What your customer sees When you request access, your customer is sent (via your existing notification channel) a unique web link to your support portal. The link opens an approval page showing the exact resource, the privilege being requested, the reason, and the duration. Approving means signing the request with your customer's own key; the appliance controller running in their account verifies that signature before it mints anything. This is the same signing keypair your customer uses to approve operations commands. If they've already set it up for operations, break glass approvals work immediately. See [Security model](/fundamentals/break-glass/security) for the trust properties and [operations security](/fundamentals/operations/security) for how the keypair is generated, stored, and pinned. ## Where to go next | If you want to... | Read | | ------------------------------------------------------------------------------- | -------------------------------------------------------- | | Walk the operator's request-to-teardown flow in detail | [Requesting access](/fundamentals/break-glass/lifecycle) | | Configure how break glass reaches private targets (Direct, Tailscale, Twingate) | [Network providers](/fundamentals/break-glass/providers) | | Understand the approval, least-privilege, and audit guarantees underneath | [Security model](/fundamentals/break-glass/security) | | See how the standing, command-based channel works | [How operations work](/fundamentals/operations) | # Requesting Access Source: https://docs.tensor9.com/fundamentals/break-glass/lifecycle A break glass session moves through a state machine on its way from "I need to get into this resource" to "the session is over and everything is torn down." This page walks your side of that flow: how to request access, what your customer sees in parallel, how the session is assembled, and how it ends. ## Requesting access You start a request from the **Break Glass** page in your vendor portal. Pick the appliance and the one resource you need to reach, then submit: | You specify | Notes | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Resource** | The single resource the session targets (a cluster, a database, a service). One session reaches one resource. | | **Reason** | Free-text justification. Shown to your customer at approval time and recorded in the audit trail. | | **Duration** | How long you need. This bounds the session and the lifetime of any credential minted for it, measured from when the session becomes active. | | **Credential source** | How the credential is produced for this resource: minted by the appliance controller, minted by an approved operational command, or supplied out of band. Offered only where more than one applies. | You do not pick the network path. Break glass resolves that from the resource: if the target is already reachable it uses the Direct path; if it's private it stands up a temporary tunnel through your configured provider. See [Network providers](/fundamentals/break-glass/providers). Once submitted, the request is **Pending** and your customer's review begins. The requested duration is the window of usable access, and it begins when the session goes **Active** (after your customer approves), not at submission. Time spent waiting for your customer to approve does not count against the duration. ## Session states The happy path is request, approve, assemble, use, expire. A few terminal states cover the ways a session can stop early. | State | What it means | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Pending** | The request exists and is waiting on your customer's decision. You can withdraw it; it also auto-expires if your customer never acts within the request window. | | **Activating** | Your customer approved. Break glass is assembling the session: minting the credential where break glass supplies one, and (for a private target) bringing the temporary network path online. | | **Active** | Every plane the session needs is ready. You can use the resource. The session expires at a time derived from the approved duration. | | **Ended** | The session finished. The temporary credential and any temporary network path have been removed. | Terminal states for a session that stopped early: | State | What happened | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Rejected** | Your customer declined the request at review. They can include a reason for you with the rejection. | | **Cancelled** | You withdrew the request before your customer acted. | | **Failed** | The session couldn't be fully assembled or torn down (for example, a temporary tunnel never came online). Break glass fails closed: a session that can't be assembled does not go active. | A session goes **Active** only when every plane it needs is ready. For a private target with a minted credential that means the credential is minted *and* the tunnel is online; if either half can't complete, the session does not activate. An out-of-band session resolves only the network path (the operator brings their own credential), so it activates once that path is online. ## What your customer sees When you submit a request, your customer is sent (via your existing notification channel) a unique support-portal link. Opening it shows the exact request: which resource, the privilege being requested, your reason, and the duration. Your customer picks Approve or Reject. Approving means **signing** the request. Your customer signs with a private key that lives in their own environment and never travels to your control plane; the appliance controller running in their account verifies that signature against a public key only your customer controls before it mints anything. This is the same signing keypair your customer uses to approve [operations](/fundamentals/operations) commands, so if they've used operations, break glass approvals work immediately. See [operations security](/fundamentals/operations/security) for how the keypair is generated, stored, and pinned. The approval is bound to this one request. It cannot be redirected at a different resource, widened in privilege, or extended in duration after your customer signs; any such change breaks the signature. Approvals also have a short validity window and are single-use, so a captured approval can't be replayed later. ## Using the session Once the session is **Active**, break glass delivers what the operator needs to connect: * **Kubernetes**: a ready-to-use kubeconfig for the session. It holds the minted, short-lived credential and the cluster endpoint; point `kubectl` at it and work normally. * **Database and other resources**: the connection details produced by the approved credential source (for example, a short-lived database login). * **Out-of-band sessions**: break glass delivers the network path so the otherwise-private endpoint becomes reachable, and the operator authenticates with the credential they already hold. Break glass delivers no credential in this case. A minted credential is delivered to the operator once (consumed on read) and is not durably stored by the control plane. Treat the delivered connection details as the session's secret: they're scoped to this one session and expire with it. A minted credential's lifetime can be capped below the approved duration by your customer's own environment (for example, a Kubernetes cluster's maximum service-account token lifetime). If that cap is shorter than your session, the credential can expire mid-session and is not re-minted automatically; request a fresh session if you need more time. ## Expiry, teardown, and revocation Access ends when the session ends, and it ends two ways, both bounded: * **Self-expiry.** A minted credential is short-lived, with a lifetime tied to the approved duration. Even if teardown is interrupted, the credential stops working on its own clock. * **Explicit teardown.** At session end, break glass removes what it created: it deletes any minted identity (revoking the credential) and tears down any temporary network path. For an out-of-band session there is no minted credential to revoke, so only the network path is removed. Confirmed teardown is recorded. To end a session early, end it from the Break Glass page; teardown runs immediately rather than waiting for the expiry. Because the credential also self-expires, ending early and letting it lapse both converge on the same result: no standing access remains. ## Related * [How break glass works](/fundamentals/break-glass): the two-plane model and where this flow fits. * [Network providers](/fundamentals/break-glass/providers): how the network path for a private target is configured. * [Security model](/fundamentals/break-glass/security): the approval, least-privilege, and audit guarantees behind this lifecycle. # Network Providers Source: https://docs.tensor9.com/fundamentals/break-glass/providers A break glass session needs a network path to the target resource. When the resource is already reachable, no provider is involved. When the resource is private, break glass uses a **network provider** to stand up a temporary, session-scoped path into your customer's environment and tears it down when the session ends. You configure providers once, on the **Break Glass** page in your vendor portal. They apply to every break glass session that needs a tunnel. ## The providers | Provider | Use it when | What you configure | | --------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | | **Direct** (built-in) | The target endpoint is already reachable (for example, a Kubernetes cluster with a public API endpoint) | Nothing. Direct is always available and needs no API key. | | **Tailscale** | The target is private and you use [Tailscale](https://tailscale.com) | A Tailscale API access key for your tailnet. | | **Twingate** | The target is private and you use [Twingate](https://www.twingate.com) | A Twingate API key and your network subdomain. | Direct maps to the "directly reachable" network path; Tailscale and Twingate are the two tunnel providers for private targets. You only need to configure a tunnel provider if you'll be reaching resources that aren't already reachable. ## Direct Direct is the built-in path for targets that are already reachable from where your operators connect. There's no tunnel to stand up and nothing to configure: break glass connects to the resource's own endpoint. Every vendor has Direct available automatically, and it can't be removed. You can disable Direct on the Break Glass page if you want to require a tunnel for every session; re-enable it to allow directly-reachable sessions again. ## Tailscale For a private target, break glass stands up a temporary [Tailscale](https://tailscale.com) subnet router inside your customer's network so the operator can route to the otherwise-private endpoint for the life of the session. The router is created with single-use, ephemeral keys and removed at session end, so it exists only for that one session. To configure it, add a Tailscale provider on the Break Glass page and supply a Tailscale API access key for your tailnet. Break glass uses the key to provision and later remove the per-session router. Unlike the per-session router keys, this API access key is a standing credential you hold; store and rotate it like any other production API key. **Comments in your Tailscale policy file are not preserved.** Tailscale policy files are written in [HuJSON](https://tailscale.com/kb/1018/acls) (JSON that allows comments and trailing commas), but break glass updates your tailnet policy as standard JSON. When you set up the Tailscale provider with your API access key, any comments in the policy file are stripped out. If your policy file contains comments you want to keep, back it up before adding the provider. The per-session router is temporary, but network-level access control within your tailnet is governed by your tailnet's own ACLs, which you control. Scope your tailnet ACLs to match the access you intend break glass sessions to have, the same way you would for any device on your tailnet; don't assume the integration confines a session within your tailnet on its own. ## Twingate [Twingate](https://www.twingate.com) is the alternative tunnel provider and plays the same role as Tailscale: break glass stands up a temporary, session-scoped connector into the private network and removes it at session end. To configure it, add a Twingate provider on the Break Glass page and supply a Twingate API key plus your organization's network subdomain (your `{network}.twingate.com` tenant). Break glass uses these to provision and remove the per-session connector. ## Adding and managing providers On the Break Glass page you can: * **Add** a Tailscale or Twingate provider by supplying its API key (and, for Twingate, your network subdomain). When you add a provider, break glass validates the key against the provider's API so you find out immediately if it's missing a required scope, rather than at session time. * **Enable or disable** any provider, including the built-in Direct path. A disabled provider can't be used to resolve a session. Disabling is reversible. * **Remove** a Tailscale or Twingate provider you no longer use. Direct is built-in and can't be removed. Your provider configuration is stored with your vendor control plane and persists across portal restarts. [Border0](https://www.border0.com) as a break glass provider is planned but not yet available. ## Related * [How break glass works](/fundamentals/break-glass): where the network-path plane fits in the two-plane model. * [Requesting access](/fundamentals/break-glass/lifecycle): how the network path is resolved and torn down per session. * [Security model](/fundamentals/break-glass/security): the session-scoping and teardown guarantees for temporary network paths. # Security Model Source: https://docs.tensor9.com/fundamentals/break-glass/security Break glass lets a vendor operator reach a resource inside your customer's own cloud account. That's access into someone else's account, so it is built to be consented, bounded, and auditable: every session is gated by a customer-signed approval the appliance controller verifies before anything is minted, the access it grants is scoped to one resource for one session, and it self-expires. This page documents those guarantees. Break glass trust chain: an operator requests access to one resource, your customer signs the approval offline with a key only they hold, the appliance controller re-verifies the signature against the pinned public key before minting anything, and the resulting credential is least-privilege and self-expiring. Break glass trust chain: an operator requests access to one resource, your customer signs the approval offline with a key only they hold, the appliance controller re-verifies the signature against the pinned public key before minting anything, and the resulting credential is least-privilege and self-expiring. ## The core guarantee No break glass session happens without an explicit approval your customer signs cryptographically. The approval is bound to one specific request, and the appliance controller running in your customer's account verifies it against a key only your customer controls before it mints a credential or stands up a network path. ## Trust model Two parties touch a break glass session, and only one of them is trusted to grant access: * **The vendor control plane** coordinates the workflow (it relays the request and the approval, drives the session). It is **not** trusted to grant access on its own. * **The appliance controller** (the Tensor9 component running inside your customer's account under your customer's IAM) makes the actual security decision. It re-verifies your customer's signature against the customer's pinned public key and only then mints anything. The consequence: even a fully compromised vendor control plane cannot mint access, widen its privilege, redirect it at a different resource, or extend its duration. None of those is possible without your customer's signing key, which the control plane never holds. The security decision is made on the appliance controller in your customer's environment, not in vendor infrastructure. ## The approval is customer-signed Break glass uses the **same customer-signed approval mechanism as [operations](/fundamentals/operations)**: your customer signs with an Ed25519 private key that lives in their own environment and never travels to your control plane, and the appliance controller verifies against a public key your customer has pinned into the controller's secret store. If your customer has already set up signing for operations, break glass approvals work immediately. The mechanics of that keypair (how it's generated, where the private key lives, how the public key is pinned, and how to rotate it) are documented in the [operations security model](/fundamentals/operations/security). Rather than restate them, this page covers what's specific to break glass. ## What the approval binds A break glass approval is a signature over an explicit, fixed set of request fields, not a blanket grant. The signed set is what defines what your customer is consenting to: | Bound field | What it pins | | ----------------- | ----------------------------------------------------------------------------------- | | Resource | The one appliance and resource the session targets | | Privilege | The access level being requested for that resource | | Duration | How long the session and its credential may live | | Network provider | Which path the session may use to reach the resource | | Credential action | For a command-minted credential, the exact operational command and its exact inputs | Because the signed set is a deliberate allowlist, an approved request can't be silently redirected at a different resource, escalated in privilege, or extended in duration after your customer signs. Any such change alters the signed bytes and fails verification. The appliance controller re-derives the signed data from its own view of the request and compares; it does not trust values handed to it at mint time. Approvals are also single-use and valid only briefly, so a captured approval can't be replayed. When the credential is produced by an operational command, the approval binds the **exact command and its inputs**, and the appliance controller re-checks that the command it's about to run is exactly the one your customer approved before it runs anything. See [operations security](/fundamentals/operations/security) for how command approval and integrity are signed and verified. ## Least privilege and time-boxing Two properties bound every session: * **Scoped to one resource, one session.** A session reaches exactly the resource your customer approved, using a credential minted for that session alone. It does not grant standing access to anything else. * **Time-boxed.** The credential's lifetime is tied to the duration your customer approved. Your customer's own environment may shorten it further (for example, a Kubernetes cluster's maximum token lifetime), but never lengthen it; where that cap is shorter than the session, the credential can expire before the session does. "Least privilege" here means scoped and time-boxed, **not** minimal permissions within the resource. The privilege level inside the resource is whatever the approved request specifies: the shipped Kubernetes path, for instance, mints a cluster-admin-tier credential. What's bounded is that the credential reaches only that one resource, only for that one session, and only at the privilege your customer signed off on. ## No standing or stored credentials When break glass mints a credential (the appliance-controller and operational-command sources), it does not leave it lying around: * **Ephemeral.** The credential is minted fresh for the session, not a pre-existing standing credential handed over. * **Delivered once, never durably stored.** The credential is delivered to the operator exactly once (consumed on read) and is held only transiently until that single fetch; it is never written to logs or persistent storage. Secrets do not appear in error output that leaves the appliance controller. * **Self-expiring.** The credential is short-lived on its own clock, so access ends even if teardown is somehow interrupted. * **Explicitly torn down.** At session end the appliance controller deletes the minted identity (revoking the credential) and removes any temporary network path. Teardown removes only the session's own temporary resources. For an out-of-band session break glass mints no credential, so there is nothing for it to deliver, store, or revoke on the credential plane; it tears down only the temporary network path, if any. Between sessions, nothing privileged that break glass created remains in your customer's account. ## Temporary network paths When a session targets a private resource, break glass stands up a temporary network path through your configured provider and removes it at session end. The path is created with single-use, ephemeral keys and exists only for that session. Network-level access control within your own network remains governed by your network configuration. For Tailscale, that means your tailnet's ACLs, which you control; scope them to match the access you intend break glass sessions to have. See [Network providers](/fundamentals/break-glass/providers) for how each provider's path is configured. ## Fail-closed Break glass denies rather than guesses. If the appliance controller can't verify the approval, it doesn't mint. If a session's network path or credential can't be assembled, the session doesn't go active. If teardown can't complete, the failure is surfaced and the credential still self-expires on its own clock. There is no path where a verification or assembly failure quietly results in standing access. ## Audit The request, your customer's signed approval, and teardown are recorded; credential delivery is logged on a best-effort basis. The customer-signed approval is the durable, non-repudiable evidence of consent: an Ed25519 signature over the fixed set of approved fields, preserved on the session record and re-verifiable in principle against the customer's pinned public key. It is the same approval primitive [operations](/fundamentals/operations/security) uses, though the turnkey `tensor9 ops command audit verify` tool is specific to operations commands and does not cover break glass sessions today. ## Related * [How break glass works](/fundamentals/break-glass): the two-plane model these guarantees apply to. * [Requesting access](/fundamentals/break-glass/lifecycle): the session lifecycle each guarantee attaches to. * [Operations security model](/fundamentals/operations/security): the shared customer-signed approval keypair, signing, and audit mechanics. # Configuring Observability Source: https://docs.tensor9.com/fundamentals/configuring-observability This guide covers configuring observability: setting up your sinks and routing, turning it on per appliance, and optionally instrumenting telemetry collection in your origin stack. For the conceptual model, including [telemetry routing](/fundamentals/observability#telemetry-routing), see [Observability](/fundamentals/observability). ## Configure your sinks and routing Set up sinks and routing in the **Observe** section of the vendor portal (`tensor9 portal`). ### Add a sink Click **Create an observability sink** and follow the three-step wizard. The configuration step differs by sink type: Give the sink a **name** and **display name**, then choose **AWS CloudWatch** as the type. Datadog, Loki, and Prometheus are also available; Elasticsearch and OpenTelemetry are coming soon. Sink wizard Basics step with AWS CloudWatch selected Pick the **region** your logs, metrics, and traces are written to in CloudWatch (defaults to your vendor region) and the **authentication type**. **Default Credentials** writes to CloudWatch in your Tensor9 account with permissions managed automatically; you can also assume a cross-account role. Optionally add **Exclude Dimensions** to stay under CloudWatch's 30-dimension limit and control cost. CloudWatch sink configuration: region, authentication, and exclude dimensions Review the **sink summary** and select **Create Sink**. CloudWatch sink summary with Create Sink Give the sink a **name** and **display name**, then choose **Datadog** as the type. Sink wizard Basics step with Datadog selected **Store your Datadog API key** in your control plane's secret store with the `aws secretsmanager create-secret` command shown in the wizard. Tensor9 reads it at startup, so the key is never part of the sink config and is never deployed to an appliance. Then choose your **Datadog site**. Datadog sink configuration: API key secret command and Datadog site Review the **sink summary** and select **Create Sink**. Datadog sink summary with Create Sink Sink credentials are stored encrypted in your control plane and are never deployed to customer appliances. ### Route your telemetry In the **Observe** section your sinks are listed with the **Routing** view directly below them. It places your **sources on the left** and **sinks on the right**; drag a colored source handle to a sink to route that signal. Connections are color-coded by signal (logs, metrics, and traces), and you can only connect matching signals (a source's logs to a sink that accepts logs). By default each sink receives only its matching source. The Routing view: sources on the left, sinks on the right, connections color-coded by signal To change routing, drag a new connection or select one and remove it. Changes are staged; review the unsaved-changes badge, then select **Save routing**. Routing view with sources wired to sinks and unsaved changes pending ## Turn observability on or off You control observability at two levels: the whole appliance and individual resources within it. Both are managed in the vendor portal, on the appliance's detail page, and both are **orthogonal to [routing](/fundamentals/observability#telemetry-routing)**: these toggles decide *whether* something is observed, while routing decides *which sinks receive which sources* once it is. ### Per appliance Observability is turned on or off for an **entire appliance** with a single switch on the appliance's detail page. This is the master control, and it's **off by default** (observability is opt-in per appliance): * **Off:** nothing is collected from the appliance; no telemetry reaches any sink. * **On:** Tensor9 stands up the collection pipeline for that appliance, and its resources begin reporting (subject to the per-resource toggles below). ### Per resource Once an appliance has observability on, you can turn collection on or off for each **resource** independently (a Lambda function, an ECS service, and so on), each listed with its name and stack alongside a switch. Use this to collect from the resources you care about and leave noisy or high-volume ones off. * **On:** Tensor9 sets up collection for that resource, and its telemetry flows to your sinks (per your routing). * **Off:** collection is removed for that resource, and nothing flows from it. The shared collection pipeline stays in place until the *last* observed resource is turned off, so toggling one resource never disrupts the others. Changes settle in the background rather than instantly, so an appliance or resource briefly shows a **Pending** state while it transitions: filling when you turn it on (until telemetry actually flows) and draining when you turn it off. It clears on its own once the change takes effect. ## Configure telemetry in your origin stack In your origin stack, you can configure telemetry collection however you like: define log groups, add instrumentation libraries, configure metrics exporters, or use any telemetry approach that works for your application. Tensor9 analyzes your origin stack during compilation and automatically configures the appropriate telemetry routing for each appliance based on your observability sink configuration. Here are two common examples: ### Example 1: Send telemetry to CloudWatch in your control plane Define resources with CloudWatch logging in your origin stack: ```terraform theme={null} # Example: Lambda function with CloudWatch logs resource "aws_lambda_function" "api" { function_name = "${var.namespace}myapp-api" handler = "index.handler" runtime = "nodejs18.x" } resource "aws_cloudwatch_log_group" "api" { name = "/aws/lambda/${var.namespace}myapp-api" retention_in_days = 7 } ``` During compilation, Tensor9 detects the CloudWatch log group and automatically configures log forwarding from each appliance to CloudWatch in your Tensor9 AWS account (your control plane). ### Example 2: Send telemetry to Datadog via your control plane Configure resources with Datadog instrumentation in your origin stack: ```terraform theme={null} # Example: Lambda function with Datadog instrumentation resource "aws_lambda_function" "api" { function_name = "${var.namespace}myapp-api" handler = "index.handler" runtime = "nodejs18.x" layers = [var.datadog_lambda_layer_arn] environment { variables = { DD_API_KEY = var.datadog_api_key DD_SITE = var.datadog_site DD_TRACE_ENABLED = "true" DD_SERVICE = "myapp-api" DD_ENV = var.namespace } } } ``` During compilation, Tensor9 strips out the Datadog credentials from your origin stack so they are never deployed to customer appliances. Telemetry flows from each appliance back to your control plane without credentials, and your control plane then forwards the telemetry to your Datadog account using the credentials you configured for your Datadog sink. These are just examples. You can configure telemetry collection in your origin stack using any approach, and Tensor9 will handle the deployment details appropriately for each appliance. # Connection Security Source: https://docs.tensor9.com/fundamentals/connection-security Every appliance maintains a single secure channel back to your [control plane](/fundamentals/control-plane). Deployments and operations ride this channel, and the appliance reports its status back over it. This page explains the mechanics of that channel and the mutual TLS (mTLS) trust model that secures both ends of it. ## The connection at a glance * **One channel per appliance**, between the appliance's [controller](/fundamentals/appliances#controller) and your control plane. * **Outbound-only**: the appliance always dials out. It never opens inbound ports. * **Mutually authenticated**: both ends present and verify certificates before any data flows. * **Long-lived**: established once during appliance setup and re-handshaked as needed. Control traffic runs in both directions over the channel: | Direction | Traffic | | ------------------------- | -------------------------------------------------------------------------------------- | | Control plane → appliance | Deployment instructions, operations commands (remote shell, one-off scripts, restarts) | | Appliance → control plane | Responses to those commands, deployment and health status, and connection liveness | Telemetry (logs, metrics, traces) does not ride this channel - observability data is handled separately. See [Observability](/fundamentals/observability). ## Outbound-only by design The appliance controller initiates the connection to your control plane over outbound HTTPS. It does **not** open inbound network ports, assign public IPs, or create ingress rules for itself. Your control plane never reaches into the customer's network unsolicited; it can only send instructions back down a connection the appliance already opened. This matters for two reasons: * **Customers rarely have to change network policy to allow it.** Most networks already permit outbound HTTPS, so customers don't have to open ingress ports - a request that is often logistically difficult and a security concern in regulated environments. * **It shrinks the attack surface.** There is no listening socket on the appliance for an attacker to reach, and no inbound path from your control plane into customer infrastructure. The connectivity behavior is part of the appliance's [form factor](/fundamentals/key-concepts#form-factor). See [connectivity modes](/fundamentals/appliances#connectivity-modes) for how `Connected` and `Disconnected` appliances differ. ## Mutual TLS: both sides prove who they are Ordinary (one-way) TLS only proves the *server's* identity to the client. mTLS adds the reverse: the client must also present a certificate the server verifies. On the appliance-to-control-plane channel, **both ends authenticate each other** on every handshake: * Your control plane verifies that the connecting appliance is one it actually provisioned. * The appliance verifies that it is talking to *your* control plane, not an impostor endpoint. During appliance [setup](/fundamentals/appliances#customer-signup-flow), Tensor9 provisions each end with the certificates it needs to recognize the other. Neither side will complete a handshake with a party it cannot verify. ### The two identities | Identity | Belongs to | Verified by | | ------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | **Control plane certificate** | Your control plane, running in your own AWS account | The appliance, which is provisioned at setup to trust only your control plane | | **Appliance leaf certificate** | The individual appliance's controller (one per appliance) | Your control plane, which validates it against the CA chain it issued and against the leaf fingerprint it recorded for that appliance | ### The handshake The appliance controller opens an outbound connection to your control plane's endpoint. Your control plane presents its certificate. The appliance validates that it chains to your control plane's CA - the root it pinned when it enrolled (see [Bootstrapping trust with cloud identity](#bootstrapping-trust-with-cloud-identity)). If it doesn't validate, the appliance refuses to proceed - so a spoofed or man-in-the-middle endpoint cannot impersonate your control plane. The appliance presents its leaf certificate. Your control plane validates that the certificate chains to its CA and matches the leaf fingerprint it recorded for that appliance at enrollment. If the certificate doesn't chain, doesn't match the recorded fingerprint (for example, after the appliance was revoked), or has expired, the control plane drops the handshake. With both ends authenticated, an encrypted channel is established and deployments, operations, and status updates can flow over it. ## Bootstrapping trust with cloud identity The handshake above assumes each side already holds a certificate. But a brand-new appliance has none - and handing it a pre-shared secret to start with would just move the problem. Tensor9 closes this gap by bootstrapping the appliance's first certificate off the **platform's own identity system** - AWS, GCP, or Kubernetes - so there is no long-lived shared secret to distribute or leak. Enrollment follows the same four steps on every platform; only the proof of identity differs: The appliance opens a provisional connection to your control plane. Your control plane accepts it to conduct the enrollment exchange, but treats it as unauthenticated - the appliance hasn't proven its identity yet. Over that connection the appliance sends an enrollment request containing a certificate signing request (CSR) for the certificate it wants, plus a **platform-signed proof of identity** (see the table below). The proof is bound to this enrollment, so a captured proof cannot be replayed on a different connection. Your control plane validates the proof against the platform's own authority - AWS STS, Google's token-signing keys, or the cluster's OIDC keys - and confirms the identity it resolves to matches the one recorded for this appliance at provisioning time, set by the customer's own infrastructure. On a match, your control plane signs the CSR with its KMS-backed CA and returns the leaf certificate, the CA chain, and the root. The appliance pins that root and authenticates with its real, chain-validated certificate from then on. The proof of identity is each platform's native primitive: | Platform | What the appliance presents | What your control plane checks it against | | -------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | **AWS** | A presigned STS `GetCallerIdentity` request, signed with the IAM role the appliance runs as | Forwards it to AWS STS, which returns the caller's role ARN; matches that against the role recorded for the appliance | | **GCP** | A Google-signed instance-identity token (JWT) fetched from the instance metadata server | Verifies the signature against Google's public keys; matches the project and service account against those recorded for the appliance | | **Kubernetes** | A projected ServiceAccount token (JWT) mounted into the appliance's pod | Verifies the signature against the cluster's OIDC keys; matches the namespace and service account against those recorded for the appliance | The key property is the same across all three: the appliance proves *"I am this workload, running under this identity"* using credentials the platform already issued it. Nothing secret is transmitted or stored, and the proof is bound to the customer's registered identity and to the specific enrollment. The root of trust is the customer's own cloud or cluster IAM. ## Per-appliance identity Each appliance has its **own** leaf certificate, not a shared secret reused across the fleet. When your control plane issues that certificate, it records the leaf's fingerprint against that appliance. On every handshake it checks both that the presented certificate chains to its CA and that the fingerprint matches the one on record - so an appliance can only ever authenticate as itself, and that recorded fingerprint is the lever for revoking it. The practical consequences: * **No shared bearer secret.** There is no API key or token that, if leaked, authenticates as any appliance. An appliance's credential only authenticates *that* appliance. * **Blast radius is contained.** Compromising one appliance's certificate does not let an attacker present as a different appliance. * **No reaching into customer infrastructure.** Cutting an appliance off is decided on your side - you don't need access to the customer's environment, which is outbound-only and may be unreachable on demand. See [Cutting off an appliance](#cutting-off-an-appliance). ## Certificate lifecycle Every certificate on this channel is issued from a two-tier certificate authority that your control plane operates **inside your own AWS account** - the same dedicated account your control plane runs in. The CA signing keys are held in [KMS](https://docs.aws.amazon.com/kms/) in that account: the private keys never leave KMS, and issuance happens through KMS signing operations. There is no exportable CA key to leak, and the CA stays under your control - Tensor9 never holds it on your behalf. | Certificate | Validity | Role | | -------------------- | -------- | ----------------------------------------------------------------------------------------------------- | | **Root CA** | 10 years | Top of the trust chain. Signs the intermediate CA. Signing key held in KMS. | | **Intermediate CA** | 2 years | Signs leaf certificates. Signing key held in KMS. | | **Leaf certificate** | 1 month | What each end presents on the handshake - both your control plane and the appliance. Rotated monthly. | The tiers trade off security against operational load. The root key is the most sensitive, so it is long-lived and rarely used: it signs only the intermediate. The intermediate handles the day-to-day signing of leaf certificates on a shorter, two-year clock and can be replaced without touching the root. Leaf certificates are deliberately short-lived and rotated monthly, so a leaked or stale leaf is only useful for a brief window. The appliance renews over the existing authenticated channel - it requests a new leaf before the current one expires, without repeating the cloud-identity attestation - so the long-lived channel stays up across rotations and certificate rollover is invisible to deployments and operations. Operator certificates are issued from the same CA but are longer-lived (about a year); see [Relationship to operator authentication](#relationship-to-operator-authentication). ## Cutting off an appliance When your control plane issues an appliance its leaf certificate, it records that leaf's fingerprint against the appliance. Every handshake checks the presented certificate's fingerprint against the recorded one, so revoking an appliance is **immediate**: remove the recorded fingerprint and the cached certificate fails its very next handshake. The appliance cannot reconnect without re-enrolling, which requires a fresh cloud-identity attestation against the role recorded for it. This doesn't rely on a certificate revocation list - revocation is a single change your control plane makes, enforced on the next handshake. ## Relationship to operator authentication Two kinds of mTLS clients terminate at your control plane, and they use the same underlying trust model: | | **Appliance identity** | **Operator identity** | | --------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | **Who** | An appliance's controller (machine) | A person on your vendor team (human) | | **First enrolled by** | A cloud-identity attestation (AWS STS) against the role recorded for the appliance | Redeeming a one-time enrollment bundle | | **Credential** | A CA-issued leaf certificate, rotated \~monthly | A CA-issued leaf certificate, valid \~1 year | | **Cut off by** | Remove its pinned leaf fingerprint; the cached cert fails its next handshake | The same operation, exposed as `iam user revoke` (which also cancels any unredeemed invite) | | **Covered in** | This page | [IAM Commands](/cli/reference#iam-commands) | In both cases your control plane is the certificate authority that issues these certificates and pins each party's leaf fingerprint. Revocation is the same underlying operation for both - remove the pinned fingerprint and the cached certificate fails its next handshake - whether the principal is an appliance or an operator. Appliances connect to receive deployments and operations and to report status; operators connect to drive the CLI. Neither relies on a shared bearer secret. ## What the channel does and doesn't expose The mTLS channel secures *how* the two ends talk; the [permissions model](/fundamentals/permissions-model) and [secrets handling](/fundamentals/secrets) govern *what* the connection is allowed to do. Independent of mTLS: * **Application and business data stay in the appliance.** Only control traffic - deployment instructions, operations commands, and status responses - crosses the channel. Your customer's business data never does. * **Secret values never leave the customer's environment.** The controller detects secrets created in the customer's own secret manager; their values are not transmitted to your control plane. See [Secrets](/fundamentals/secrets). * **Every operation is authenticated and audited.** Commands sent over the channel run through your control plane's [permissions model](/fundamentals/permissions-model) and are logged. ## Related topics * [**Your Control Plane**](/fundamentals/control-plane): What the control plane does * [**Customer Appliances**](/fundamentals/appliances): The appliance and its controller * [**Permissions Model**](/fundamentals/permissions-model): What the connection is allowed to do * [**IAM Commands**](/cli/reference#iam-commands): Operator authentication, also built on mTLS # Connectivity Source: https://docs.tensor9.com/fundamentals/connectivity Network paths between vendor, customer, and appliance A Tensor9 deployment has four distinct network links with configurable paths. Each one is for a different kind of traffic, has different defaults, and exposes different options. This page covers them in one place so you can reason about them together. | Link | Direction | Used for | | --------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------ | | [Application ingress](#application-ingress) | Customer's users to the deployed app | End-user traffic | | [Operator to control plane](#operator-to-control-plane) | Your CLI/IaC tooling to your vendor control plane | Day-to-day operations and deploys | | [Appliance to control plane](#appliance-to-control-plane) | Customer's appliance to your vendor control plane | Deploy instructions and secure remote management | | [Break-glass access](#break-glass-emergency-access) | Emergency operator access into the customer environment | Time-limited elevated support access | The first three are normal day-to-day links. The last is reserved for exceptional situations. ## Application ingress End user connects to the deployed application in the customer environment End user connects to the deployed application in the customer environment Once your application is deployed into a customer appliance, your users reach it over whichever ingress path your [origin stack](/fundamentals/origin-stacks) defines: a public load balancer, a private path, a Tailscale-attached endpoint, or anything else expressible in your IaC. By default, Tensor9 does not modify this link. The shape of customer-facing ingress is determined primarily by your application's IaC and the customer's environment. However, your customer may prefer a narrower scope of access. The customer-facing options Tensor9 surfaces are summarized on the [Private ingress](/customer/configuration/private-ingress) page. ## Operator to control plane Vendor operator connects to the vendor control plane Vendor operator connects to the vendor control plane When you run `tensor9` commands, or when an [origin-stack deploy](/fundamentals/deployments) executes its `plan` and `apply` against an appliance, your CLI talks to your [vendor control plane](/fundamentals/control-plane). The control plane's network surface exposes a small set of mutually-authenticated listeners for this traffic. ### Default: public load balancer with mTLS By default, the control plane's listeners are reachable over the public internet via a network load balancer. Every connection is mutually-TLS authenticated using operator certificates that Tensor9 issues during enrollment. No public access is granted; the listener accepts only certificate-presenting clients on the operator allowlist. ### Optional: route operator traffic over Tailscale If you operate a [Tailscale](https://tailscale.com) tailnet for your engineering team, you can have your vendor control plane join that tailnet and expose the same operator listeners over it. Once joined, your operators can reach the control plane at its tailnet hostname without leaving Tailscale. A step-by-step guide to onboarding is provided in [Common Workflows](/cli/common-workflows#onboard-your-vendor-controller-to-tailscale). ### Optional: remove the public path After your operator tooling is reliably reaching the control plane over Tailscale, you can remove the public-internet path for the operator-side listeners: `CLI`, `Terraform`, and `Appliance` services. Each service is gated individually so you can roll out the change in stages. See our step-by-step guide to onboarding in [Common Workflows](/cli/common-workflows#onboard-your-vendor-controller-to-tailscale) for more details on this process. Enforcing tunnel-only for the appliance-facing listener (the `Appliances` group, which serves the appliance-to-control-plane link below) requires extra care. The command refuses to enforce that listener if customer appliances or appliance-setup links are still using a non-tunnel path, or if AWS PrivateLink is active for the appliance link. See the [Appliance to control plane](#appliance-to-control-plane) section for the relationship between the two. ## Appliance to control plane Customer appliance controllers connect outbound to the vendor control plane Customer appliance controllers connect outbound to the vendor control plane Every customer appliance establishes an outbound, mutually-authenticated connection from its on-prem controller back to your vendor control plane. This connection is the channel for deploy instructions and [operations](/fundamentals/operations) commands. It is always initiated from the customer side; the customer's environment never accepts inbound connections from yours. See the [customer security model](/customer/security/security-model#no-inbound-network-access) for the underlying handshake, and [Connection Security](/fundamentals/connection-security) for the mTLS trust model. Telemetry (logs, metrics, traces) also flows from the appliance back to your control plane, but over a separate stream rather than this connection. See [Observability](/fundamentals/observability). The connection itself is the same regardless of the network path. The available network paths today are: | Network path | Where it works | What it requires | | ------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | **Public internet** | Any combination of vendor and customer environments | Outbound HTTPS from the customer environment | | **AWS PrivateLink** | Vendor control plane and customer environment are both on AWS | Vendor opts in at setup; cross-region attachments supported | | **Tailscale (vendor-provided)** | Any environment where the customer's appliance can run Tailscale | Vendor-supplied tailnet that both the control plane and the customer's appliance join | You select which paths a form factor permits when you author it; the customer (or you, when generating an appliance-setup link) then picks one of the permitted paths for each appliance. See the [AWS form factor](/form-factor/aws#controller-connectivity) page for how the choice is exposed for AWS. ### Vendor-provided Tailscale (`TailscaleVendor`) The vendor operates a tailnet. Both the vendor's control plane and each enrolled customer appliance join it. The appliance-to-control-plane handshake then travels the tailnet instead of the public internet or PrivateLink. This is particularly useful for: * Test appliances and other vendor-operated environments where you want all appliance-to-control-plane traffic on a single private network you control. * Customer environments that allow outbound Tailscale but prefer not to expose direct internet egress to the vendor control plane's public listener. Enrollment is operator-supplied: when you create an appliance-setup link with a `TailscaleVendor`-permitted form factor, you provide the customer's pre-auth key, and the customer's appliance uses it at boot to join your tailnet. ### Customer-provided tailnets A symmetric variant where the appliance joins the customer's tailnet via `tsnet` and the vendor control plane reaches into it is not yet available. Contact us if this would unblock a deployment; we'd be interested to talk through specifics. ## Break-glass emergency access Vendor operator reaches directly into a customer's private app via a time-limited emergency path Vendor operator reaches directly into a customer's private app via a time-limited emergency path In rare situations a vendor operator needs to reach further into the customer environment than the standing operations channel allows: a live incident, an application failure, or a customer-requested deep-debug session. Tensor9 supports time-limited, audited break glass for these cases. The operator requests access to one resource, the customer cryptographically approves it, a short-lived credential (and, for a private target, a temporary network path) is provisioned for the session, and everything is torn down and logged when the session ends. Break-glass is intentionally separate from all other connectivity links defined: it is reserved for situations that the standing channels are not designed to cover, and it is held to stricter approval and audit requirements. See [Break glass](/fundamentals/break-glass) for the full model, the network providers (Direct, Tailscale, Twingate), and the [security model](/fundamentals/break-glass/security). # Your Control Plane Source: https://docs.tensor9.com/fundamentals/control-plane Your Tensor9 **control plane** is the central nervous system of your private software distribution platform. It is **provisioned directly within your own dedicated AWS account** for Tensor9. This architecture ensures that your intellectual property, customer data, and infrastructure credentials always remain under your ownership and control. Your control plane is responsible for orchestrating the entire lifecycle of your applications, from compiling your origin stack into a deployable artifact, to enabling your to manage ongoing operations and observability for every customer appliance. High-level overview of your Tensor9 control plane High-level overview of your Tensor9 control plane ## How it works Once [installed](/fundamentals/install), your control plane acts as the central authority for managing your software distribution. It runs within your cloud account, using read-only IAM roles to interact with your origin stack's infrastructure-as-code, artifacts, and secrets. You interact with your control plane through two interfaces: the **`tensor9` CLI** and the **[Vendor Portal](/fundamentals/key-concepts#vendor-portal)**, a web dashboard for monitoring and managing your apps, appliances, and operations. Deployments are handled through the CLI and your CI/CD pipeline. Its responsibilities are divided into three main areas: **deployments**, **observability**, and **operations**. ### Deployments: From origin stack to appliance Your control plane automates the process of compiling your app into customer-specific **[appliances](/fundamentals/appliances)**. This process begins with your **[origin stack](/fundamentals/origin-stacks)**. An origin stack is the blueprint for your application - it can be a Terraform workspace, a Docker image, or a CloudFormation template. When you publish a new version of your origin stack and create a **release**, your control plane "compiles" it into a **deployment stack**. This compilation step involves: 1. **Validation**: Your control plane inspects the origin stack to ensure it's well-formed and meets Tensor9 requirements. 2. **Porting**: It translates cloud-specific resources in your origin stack to their equivalents in the target customer's environment, based on the appliance's **[form factor](/fundamentals/key-concepts#form-factor)**. For example, if your origin stack uses AWS RDS for its database, but the customer's appliance is set to run on Google Cloud, your control plane will replace RDS with Cloud SQL during compilation. 3. **Observability**: It prepares the stack to be observed when deployed within an appliance by configuring the routing for logs, metrics, and traces. 4. **Packaging**: The result of this compilation process is a **deployment stack**. This is a self-contained, deployable infrastructure-as-code artifact you deploy in your own environment (e.g. `terraform apply` or `tofu apply`) that deploys your application into a specific customer's appliance. Tensor9 Deployment Flow ### Observability: centralized logs, metrics, and traces Your control plane handles **[observability](/fundamentals/observability)** for all deployed appliances by ensuring the instrumentation your application already has continues to function. Tensor9 does not add new agents or require code changes; instead, it configures the routing needed for the telemetry your application is already configured to produce. The appliance runtime captures the telemetry data that your software and its underlying infrastructure generates, including: * **Logs**: App logs that your software writes to standard output. * **Metrics**: Custom metrics your software produces, and system metrics your infrastructure produces (e.g. k8s node CPU utilization, storage bucket size). * **Traces**: Distributed traces your software produces. This telemetry is streamed from each appliance to a secure endpoint within your control plane. Your control plane then acts as a central aggregator, routing each telemetry stream to the observability sinks you've configured (such as Datadog, CloudWatch, or an OpenTelemetry collector), with control over which sources feed which sinks. This provides a centralized way to view data from all appliances. #### Integrating with existing observability Tensor9 works with your existing observability setup. It configures the appliance environment so that your application's existing telemetry is sent to its original destination. For example, consider an origin stack defined in Terraform. You declare a variable annotated with `@namespace`, and Tensor9 fills in a value unique to each appliance when it compiles the stack. ```terraform theme={null} #@namespace(max_size=32, delimiter='-') variable "namespace" { type = string description = "Prefix that keeps globally-unique resource names distinct per install" default = "" } resource "aws_lambda_function" "my_lambda" { function_name = "${var.namespace}my-function" handler = "index.handler" runtime = "nodejs18.x" # ... other lambda configuration ... } resource "aws_cloudwatch_log_group" "lambda_lg" { name = "/aws/lambda/${aws_lambda_function.my_lambda.function_name}" retention_in_days = 14 } ``` By building the function name from the namespace, you ensure that each appliance creates a distinct log group. When an appliance is deployed, the logs from `my-function` will be sent to a unique log group such as `/aws/lambda/myapp-acme-7e-my-function`. This allows you to observe each customer environment in isolation while using the same origin stack for all deployments. Your existing dashboards and analysis tools can then be configured to monitor these per-appliance log groups. Tensor9 Observability Flow Tensor9 Observability Flow ### Operations: secure remote management Your control plane provides **[operational endpoints](/fundamentals/operations)** that allow you to perform day-2 operations on your appliances. This provides an auditable method for remote management. When an appliance is created, it establishes a secure, outbound-only tunnel to your control plane. The tunnel is mutual-TLS authenticated and is established over the public internet by default; [AWS PrivateLink](/customer/security/security-model#no-inbound-network-access) (for AWS-to-AWS deployments) and a vendor-provided Tailscale tailnet are optionally available as alternative network paths. See [Connectivity](/fundamentals/connectivity#appliance-to-control-plane) for a side-by-side comparison of network paths, and [Connection Security](/fundamentals/connection-security) for the mTLS trust model. This tunnel allows you to use the `tensor9` CLI to: * **Access a remote shell** inside an appliance for debugging. * **Run specific, one-off commands** or scripts (e.g., database migrations, data backfills). * **Securely manage secrets** and environment variables for a specific appliance. * **Restart services** or trigger other state changes. Every action taken through these operational endpoints is authenticated via your control plane's permissions model and is fully logged, providing an audit trail of who did what, and when. This allows you to manage appliances remotely while maintaining security and compliance. Tensor9 Operations Flow Tensor9 Operations Flow # Cross-Cloud IAM Source: https://docs.tensor9.com/fundamentals/cross-cloud-iam How AWS IAM in your application adapts to a customer's deployment target. This page describes IAM resources that belong to **your application**: identities, roles, policies and the IAM API calls your application makes. The [Permissions Model](/fundamentals/permissions-model) describes access granted to the Tensor9 control plane. Installation documentation covers the appliance's cloud identity. Applications use cloud IAM to define who may call a service, which actions are allowed and which resources are in scope. Those definitions are specific to the origin cloud. A deployment on another cloud must preserve the application's authorization behavior even when the target uses a different identity system. Tensor9 adapts AWS IAM through two [adaptation tiers](/service-adapters/overview#adaptation-tiers). The **[Infrastructure only adaptation tier](/service-adapters/overview#adaptation-tiers)** maps permissions declared in infrastructure to native grants during deployment. The **[Max adaptation tier](/service-adapters/overview#adaptation-tiers)** serves supported AWS IAM calls and evaluates the adapted policy inside each appliance.
AWS IAM passes through your IAM service adapter into Cedar, Google Cloud IAM or Azure RBAC. AWS IAM passes through your IAM service adapter into Cedar, Google Cloud IAM or Azure RBAC.
## Origin and targets This page covers an AWS origin stack: an application built for AWS, using AWS IAM, deployed to a customer running a different cloud. The IAM service adapter reads identities and policy statements from the AWS model, then applies the adaptation tier selected for the deployment. Infrastructure only emits grants in the target cloud's IAM system. Max retains an adapted policy model and authorizes requests inside the appliance. Google Cloud and Azure are available as native-IAM targets, where deployment emits grants in that cloud's own IAM system. OCI and Private Kubernetes use the [Max adaptation tier](/service-adapters/overview#adaptation-tiers), with [Cedar](https://cedarpolicy.com/en) evaluating the adapted policy inside the appliance. ## Policy compatibility Before deployment, the IAM service adapter evaluates every statement in a policy document against the selected target. The document receives one of four outcomes: | Outcome | Deployment behavior | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Faithful | Deployment proceeds with the policy's restrictions intact. | | Narrower | Deployment proceeds and reports a recommendation. The workload receives less permission than requested and may fail at runtime. | | **Wider** | **Deployment is blocked by default.** You must accept the broader grant explicitly. | | Incompatible | Deployment is refused because the target cannot express the policy. An acceptance cannot override this result. On the native-grant path, examples include `Deny` and `NotAction`. |
For an AWS origin, an AWS IAM policy passes through your IAM service adapter and maps to Cedar or the target cloud's native IAM. For an AWS origin, an AWS IAM policy passes through your IAM service adapter and maps to Cedar or the target cloud's native IAM.

For an AWS origin, an accepted policy maps to Cedar for runtime authorization or to the target cloud's native IAM during deployment.

A failure applies to the entire policy document. The adapter reports the document name and reason; it does not remove an unsupported statement and continue with the rest. ### Accept a broader grant Only the Wider outcome can be accepted. At the **[Infrastructure only adaptation tier](/service-adapters/overview#adaptation-tiers)**, place the annotation immediately above the IAM resource whose target grant may be broader: ```hcl theme={null} # This bucket-scoped grant may become project-scoped on the target cloud. #@iam(compat='widen') resource "aws_iam_role_policy" "object_access" { role = aws_iam_role.application.id policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Action = ["s3:GetObject", "s3:PutObject"] Resource = "arn:aws:s3:::application-data/*" }] }) } ``` The annotation applies to that resource. Other policies remain subject to the default block. Keeping the acceptance next to the policy also makes its owner and scope visible during code review. At the **[Max adaptation tier](/service-adapters/overview#adaptation-tiers)**, the acceptance travels on the create request instead of through Terraform. Put the tuning entry in the request's `Tags` field. For example: ```bash theme={null} aws iam create-role \ --role-name application \ --assume-role-policy-document file://trust-policy.json \ --tags \ Key='t9:tuning:[....]',Value='' ``` The key after `t9:tuning:` identifies the target attribute. Values are strings on the wire and are parsed into the corresponding native type. Lists use comma-delimited values without brackets, and maps are set one scalar entry at a time. Because the tag is part of the IAM create request, no separate configuration file or client protocol is required. Neither acceptance form can release an Incompatible policy. ### Policy forms that are refused At Max, the following forms cause the complete policy document to fail: | Policy form | Reason | | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | `"Action": "s3:Get*"` | Declarable action groups are limited to `*` and `:*`. `s3:*` is accepted; a narrower prefix is refused. | | `"Action": ["s3:GetObject", "s3:List*"]` | One statement combines exact and starred actions. Use separate statements. | | `NotAction`, `NotResource`, `NotPrincipal` | The inverse match has no faithful equivalent. | | `"Principal": "*"` | The statement grants access to an unbounded principal set. | | `"Principal": {"Service": "..."}` | Service principals are not supported. | | A principal in another AWS account | Concrete cross-account principals are refused. | | `${aws:username}` or another policy variable | Policy-variable interpolation is not supported. | | A `?` wildcard | Only `*` wildcards are supported. | | A condition key other than `aws:CurrentTime` or `aws:EpochTime` | The adapter cannot reconstruct a value for the condition from the request. | Attribute-based access control is unavailable at Max. Conditions that depend on `aws:PrincipalTag`, session tags, `aws:SourceIp`, `aws:PrincipalOrgID` or `aws:SecureTransport` cause the complete document to fail. Role tags are stored but are not used during authorization. The runtime catalog does not contain AWS-managed policy documents. Replace a reference such as `AmazonS3ReadOnlyAccess` with a customer-managed policy that uses supported action forms. ## Cedar runtime authorization This section describes the Cedar authorization destination: your AWS policies adapted to Cedar and evaluated inside the appliance, whichever cloud that appliance runs in. At Max, each receiving service adapter verifies the signed AWS request. The principal and resource are synthesized from ARNs in that request and passed to Cedar with the adapted policy set. Cedar applies explicit-deny-wins semantics: a matching deny overrides an allow, and a request with no matching allow is denied.
A signed request has its principal and resource synthesized from the request itself, then Cedar evaluates the adapted policies. A request whose principal or resource cannot be synthesized is denied. A signed request has its principal and resource synthesized from the request itself, then Cedar evaluates the adapted policies. A request whose principal or resource cannot be synthesized is denied.

The request supplies the principal and resource used for authorization. A request that cannot supply either value is denied.

For example, consider a signed `GetObject` request. The receiving S3 adapter verifies the signature, derives the caller and object ARN from the request, and asks Cedar whether the adapted policy allows `s3:GetObject`. A matching deny rejects the call even if another statement allows it. With no matching allow, Cedar also rejects the call. No directory lookup occurs during this evaluation. Each entity has an ARN attribute, so a policy cannot depend on principal or network attributes that are absent from the request. Actions outside the receiving adapter's service catalog are denied. Production adapters enforce the decision. Report-only mode is available before cutover: calls that enforcement would deny are recorded and allowed to continue. This setting belongs to each receiving adapter, which lets services move to enforcement independently. If the appliance has not received a policy set, enforcement denies the request. ## Adaptation tiers The tiers differ in when authorization is decided and whether the application can call the AWS IAM API at runtime. | | [Max adaptation tier](/service-adapters/overview#adaptation-tiers) | [Infrastructure only adaptation tier](/service-adapters/overview#adaptation-tiers) | | ---------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | | IAM administration API | Supported AWS IAM operations are served inside the appliance | IAM administration operations are not served | | Authorization decision | Cedar, inside the appliance | Target cloud's native IAM | | Policy location | Adapted policy set in each appliance | Native grants in the target cloud | | Available targets | Google Cloud, Azure, OCI and Private Kubernetes | Google Cloud and Azure | Infrastructure only fits applications whose infrastructure declares all required permissions and that do not administer IAM at runtime. Max is required when the application makes supported AWS IAM administration calls after deployment. It is also required on OCI and Private Kubernetes. Tier selection is made from the origin stack and target cloud. The service adapter analysis identifies the services and policy constructs that determine the result. ## Cedar IAM behavior Everything below applies wherever Cedar is the authorization destination, whichever cloud the appliance runs in. The AWS names in it, such as ARNs and `AssumeRole`, come from the origin model your application still speaks, not from the target cloud. ### Policy synchronization Each appliance maintains its own policy set. Changes are checked about every 30 seconds, although that interval is not a propagation deadline. There is no fixed upper bound for revocation to take effect. When the policy source is unavailable, the appliance continues to use its last-known-good policy set and reports that the data is stale. The policy does not expire automatically. A change applied to one appliance has no effect on another. ### Accounts and root credentials Account IDs in adapted ARNs identify logical accounts within an appliance. The ARN spelling is inherited from the origin model and names nothing in AWS: these do not create AWS accounts or billing relationships, and AWS Organizations policies do not apply. Each appliance issues one long-lived access key and secret for the logical account root, and shows the secret once. Because this principal has unrestricted access and the IAM API cannot rotate its credential, store it with the controls used for the appliance's most privileged secret. ### API coverage and logging Each appliance serves 50 origin-model IAM administration operations for roles, customer-managed policies, instance profiles and OIDC providers. It also serves `GetCallerIdentity`, `AssumeRole` and `AssumeRoleWithWebIdentity`. A role's trust policy controls session creation; deleting the role invalidates its sessions. Allowed calls are absent from the per-request audit log. Diagnostics contain denied calls and report-only findings. ## Cedar IAM limitations These limitations apply to your AWS policies adapted to Cedar at the [Max adaptation tier](/service-adapters/overview#adaptation-tiers). They are properties of that adaptation, not of any one target cloud. The [AWS IAM service adapter](/service-adapters/aws/security-identity/aws-iam) provides operation-by-operation coverage. | Limitation | Effect | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Attribute-based access control | `aws:PrincipalTag`, session tags, `aws:SourceIp`, `aws:PrincipalOrgID` and `aws:SecureTransport` are unsupported. A policy that uses one fails as a complete document. | | Permissions boundaries | Boundaries are not enforced. Creating a role with one is rejected unless the loss is acknowledged explicitly. | | AWS-managed policies | Their documents are not supplied by the runtime catalog. Re-author them as customer-managed policies. | | Resource-based policies | General resource-policy evaluation is unavailable. | | Organizations SCPs | Service control policies are not enforced. | | Users, groups, access-key administration and SAML federation | These IAM families are not served. | | Audit records for allowed calls | Diagnostics do not record each allowed call or identify the matching policy statement. | | Access Analyzer, credential reports and Access Advisor | These features are unavailable. | | `SimulateCustomPolicy` | The IAM administration endpoint does not implement this operation. `SimulatePrincipalPolicy` is limited to the workload-identity endpoint, its configured principal and concrete resources. | `AssumeRole` does not accept role chaining, session policies, `ExternalId` or MFA inputs. A design that depends on `ExternalId` for confused-deputy protection must provide that protection elsewhere. Cross-account role assumption is supported between configured logical accounts when both the caller's policy and the target role's trust policy allow it. A policy document that names a concrete principal from another AWS account is refused. ## Related topics * [**AWS IAM service adapter**](/service-adapters/aws/security-identity/aws-iam): Coverage for each supported IAM and STS operation * [**Permissions Model**](/fundamentals/permissions-model): Access granted to the Tensor9 control plane inside a customer's environment * [**Security Model**](/fundamentals/security-model): Trust relationships between Tensor9, you and your customer * [**Service Adapters**](/service-adapters): Supported services and target-cloud mappings # Deployments Source: https://docs.tensor9.com/fundamentals/deployments **Deployments** are how you deliver your application to customer [appliances](/fundamentals/appliances). In Tensor9, a deployment is the result of creating a **release** for a specific appliance and then applying that release using standard infrastructure-as-code tooling. The **release** process compiles your [origin stack](/fundamentals/origin-stacks) into an appliance-specific **deployment stack**. You then use that deployment stack to deploy the release to an appliance. Deployment workflow showing origin stack compilation to deployment stack Deployment workflow showing origin stack compilation to deployment stack ## How deployments work Deployments in Tensor9 follow a three-stage process: **publish**, **release**, and **deploy**. First, you publish your [origin stack](/fundamentals/origin-stacks) to your [control plane](/fundamentals/control-plane). This makes your origin stack available for release: ```bash theme={null} tensor9 stack publish \ -stackType TerraformWorkspace \ -stackS3Key my-stack \ -dir /path/to/terraform ``` This command uploads your infrastructure code to your control plane's artifact storage and returns a **native stack ID** (e.g., `s3://t9-ctrl-000001/my-stack.tf.tgz`). **Important**: You only need to bind your stack to your app once using `tensor9 stack bind`. After the initial bind, you can publish new versions without re-binding. Next, you create a **release** for a specific appliance. This triggers your control plane to compile your origin stack into a deployment stack tailored to that appliance's [form factor](/fundamentals/key-concepts#form-factor): ```bash theme={null} tensor9 stack release create \ -appName my-app \ -testApplianceName my-test-appliance \ -vendorVersion "1.0.0" \ -description "Initial production release" \ -notes "Deployed by engineer@company.com" ``` During compilation, your control plane: 1. **Validates** your origin stack to ensure it's well-formed 2. **Ports** cloud-specific resources to match the appliance's form factor (e.g., AWS RDS → Google Cloud SQL) 3. **Instruments** the stack for [observability](/fundamentals/observability) (logs, metrics, traces) 4. **Identifies artifacts** (container images, S3 objects) and rewrites references to point to appliance-local locations 5. **Generates** a deployment stack - a ready-to-deploy infrastructure-as-code artifact For **Terraform/OpenTofu** origin stacks, after a few minutes the compiled deployment stack downloads into a new directory named after your appliance (e.g., `./my-test-appliance/`). For **CloudFormation** origin stacks, the control plane automatically creates the deployment stack in your control plane's AWS account. Finally, you deploy the compiled stack. During deployment, Tensor9 copies any referenced artifacts (container images, S3 objects) to the appliance's local environment. The deployment process depends on your stack type: **For Terraform/OpenTofu:** You deploy the compiled stack using standard tooling: ```bash theme={null} cd acme-corp-production tofu init tofu apply ``` **For CloudFormation:** Your control plane automatically creates the CloudFormation stack in your control plane's AWS account. You can monitor deployment progress using: ```bash theme={null} # View deployment status tensor9 report -customerName acme-corp # View CloudFormation stack events in your control plane's account aws cloudformation describe-stack-events \ --stack-name myapp-stack-000000007e ``` The deployment executes in the target appliance, creating all the infrastructure resources your application needs. ## Creating releases for different appliance types The release creation process differs slightly depending on whether you're deploying to a test appliance or a customer appliance. ### Releasing to test appliances Test appliances are environments you control, used for validation before production deployments: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -testApplianceName my-test-appliance \ -vendorVersion "1.0.0" \ -description "Testing new feature" \ -notes "QA validation build" ``` ### Releasing to customer appliances Customer appliances are production environments running in your customer's infrastructure. To create a release for a customer appliance, use the customer name: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -customerName \ -vendorVersion "1.0.0" \ -description "Production release" \ -notes "Deployed after successful QA validation" ``` You can find customer names using `tensor9 report`. ## Version management The `-vendorVersion` parameter allows you to track which version of your application is deployed to each appliance. This should match your internal versioning scheme (e.g., semantic versioning): ```bash theme={null} # Initial release tensor9 stack release create \ -appName my-app \ -testApplianceName my-test \ -vendorVersion "1.0.0" \ -description "Initial release" # Bug fix release tensor9 stack release create \ -appName my-app \ -testApplianceName my-test \ -vendorVersion "1.0.1" \ -description "Fix authentication bug" # Feature release tensor9 stack release create \ -appName my-app \ -testApplianceName my-test \ -vendorVersion "1.1.0" \ -description "Add user analytics dashboard" ``` Version numbers are stored with each release and visible in `tensor9 report`, making it easy to track which version is deployed where. ## Deployment stack structure The deployment stack structure depends on your origin stack type: ### Terraform/OpenTofu deployment stacks After creating a release, Tensor9 downloads a deployment stack into a directory named after your appliance: ``` my-test-appliance/ ├── main.tf # Compiled Terraform configuration ├── variables.tf # Variable declarations ├── outputs.tf # Outputs from your origin stack ├── modules/ # Any child modules from your origin stack └── .terraform/ # Created after `tofu init` ``` The deployment stack is self-contained and ready to deploy with no additional configuration required. ### CloudFormation deployment stacks For CloudFormation origin stacks, your control plane automatically creates the compiled deployment stack as a CloudFormation stack in your control plane's AWS account when you create a release. There's no local directory to download - the stack is created and managed directly in CloudFormation. ## Compilation process When you create a release, your control plane compiles your origin stack through several transformation steps: ### Service equivalents During compilation, Tensor9 compiles cloud-specific services in your origin stack to their functional equivalents in the target appliance's environment. This compilation is based on a **service adapter registry** that maps services across cloud providers. For the full guide to service equivalents, including detailed examples and best practices, see [**Service adapters**](/service-adapters/overview). Tensor9 groups services into equivalent sets based on their function. The full mapping, with the tier each service lands at and which target clouds it reaches, lives in one place: [service adapters](/service-adapters/overview), with the generated per-Service Catalog at [Service Catalog](/service-adapters/catalog). Third-party managed equivalents (Backblaze B2, Neon, PlanetScale, MongoDB Atlas, Redis Enterprise Cloud, Confluent Cloud, Astronomer) require your customers to bring their own credentials and accounts with these services. EC2, DynamoDB, and EFS are all offered. Some services are not adapted yet, including Step Functions, API Gateway, Cognito, AppSync, and Redshift. See [Service Catalog](/service-adapters/catalog#services-we-do-not-adapt-yet). When you compile an origin stack for a specific form factor, Tensor9 automatically replaces services with their equivalents in the target environment. **Example: AWS to Google Cloud** ```terraform theme={null} # Origin stack (AWS) resource "aws_db_instance" "postgres" { engine = "postgres" instance_class = "db.t3.micro" } resource "aws_s3_bucket" "data" { bucket = "my-app-data" } # Compiled deployment stack (Google Cloud) resource "google_sql_database_instance" "postgres" { database_version = "POSTGRES_15" tier = "db-f1-micro" } resource "google_storage_bucket" "data" { name = "my-app-data" } ``` **Example: AWS to private Kubernetes** ```terraform theme={null} # Origin stack (AWS) resource "aws_eks_cluster" "app" { name = "my-app-cluster" } resource "aws_db_instance" "postgres" { engine = "postgres" } # Compiled deployment stack (Kubernetes private) resource "kubernetes_cluster" "app" { name = "my-app-cluster" } # CloudNative PostgreSQL operator resource "kubectl_manifest" "postgres" { yaml_body = <<-YAML apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: postgres spec: instances: 3 storage: size: 20Gi YAML } ``` Service equivalents ensure your application works consistently across different cloud providers and deployment environments, without requiring you to maintain separate infrastructure code for each target platform. ### Parameterization Declare a variable annotated with `@namespace`. Tensor9 replaces its default during compilation with a value unique to each appliance: ```terraform theme={null} #@namespace(max_size=32, delimiter='-') variable "namespace" { type = string description = "Prefix that keeps globally-unique resource names distinct per install" default = "" } ``` Your origin stack should use this variable to make all resource names unique: ```terraform theme={null} resource "aws_s3_bucket" "data" { bucket = "${var.namespace}my-app-data" } ``` ### Artifact identification and rewriting During compilation, Tensor9 identifies artifact references (container images, S3 objects) in your origin stack and rewrites them to point to appliance-local locations: ```terraform theme={null} # Origin stack resource "aws_lambda_function" "api" { image_uri = "123456789012.dkr.ecr.us-west-2.amazonaws.com/my-app:v1.0.0" } # Compiled deployment stack (for customer appliance in account 999888777666) resource "aws_lambda_function" "api" { image_uri = "999888777666.dkr.ecr.us-west-2.amazonaws.com/my-app:v1.0.0" } ``` The actual copying of artifacts from your origin account to the appliance happens during deployment (when you run `tofu apply`). This ensures your artifacts are available locally within the appliance without requiring cross-account permissions. ### Observability instrumentation Your control plane configures telemetry routing so logs, metrics, and traces flow back to your [observability sink](/fundamentals/key-concepts#observability-sink): ```terraform theme={null} # Origin stack resource "aws_cloudwatch_log_group" "api_logs" { name = "/aws/lambda/${var.namespace}my-function" } # Compiled deployment stack resource "aws_cloudwatch_log_group" "api_logs" { name = "/aws/lambda/${var.namespace}my-function" # Tensor9 automatically configures log forwarding to your sink } ``` ## Deploying updates To deploy changes to an existing appliance, publish a new version of your origin stack and create a new release: 1. **Make changes** to your origin stack (add resources, update configurations, etc.) 2. **Publish the updated origin stack**: **For Terraform/OpenTofu:** ```bash theme={null} tensor9 stack publish \ -stackType TerraformWorkspace \ -stackS3Key my-stack \ -dir /path/to/terraform ``` **For CloudFormation:** ```bash theme={null} aws cloudformation update-stack \ --stack-name myapp-origin-stack \ --template-body file://template.yaml \ --capabilities CAPABILITY_IAM ``` 3. **Create a new release** with an incremented version: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -testApplianceName my-test \ -vendorVersion "1.1.0" \ -description "Add caching layer" ``` 4. **Deploy the update**: **For Terraform/OpenTofu:** ```bash theme={null} cd acme-corp-production tofu init tofu apply ``` Terraform/OpenTofu will compute the diff between the current state and the new deployment stack, applying only the necessary changes. **For CloudFormation:** The control plane automatically updates the CloudFormation stack in your control plane's AWS account. Monitor the update progress using `tensor9 report -customerName ` or by viewing CloudFormation stack events. ## Testing strategy Always test releases in test appliances before deploying to customer appliances: ### 1. Create a test appliance ```bash theme={null} tensor9 test appliance create \ -appName my-app \ -formFactorName aws-connected \ -region aws:us-west-2 ``` ### 2. Deploy and validate in test ```bash theme={null} # Create release for test appliance tensor9 stack release create \ -appName my-app \ -testApplianceName my-test \ -vendorVersion "1.2.0-rc1" \ -description "Release candidate for testing" # Deploy cd my-test tofu init && tofu apply # Validate functionality curl https://api.my-test.my-app.acme.com/health ``` ### 3. Deploy to production after validation ```bash theme={null} # Create release for customer appliance tensor9 stack release create \ -appName my-app \ -customerName \ -vendorVersion "1.2.0" \ -description "Production release with caching" # Deploy to customer cd customer-appliance tofu init && tofu apply ``` ## Multi-stack deployments Some applications consist of multiple independently deployable components. You can bind multiple origin stacks to a single app and deploy them separately: ```bash theme={null} # Publish API stack tensor9 stack publish \ -stackType TerraformWorkspace \ -stackS3Key api-stack \ -dir /path/to/api # Publish worker stack tensor9 stack publish \ -stackType TerraformWorkspace \ -stackS3Key worker-stack \ -dir /path/to/worker # Bind both to the same app tensor9 stack bind -appName my-app -stackType TerraformWorkspace -nativeStackId s3://t9-ctrl-000001/api-stack.tf.tgz tensor9 stack bind -appName my-app -stackType TerraformWorkspace -nativeStackId s3://t9-ctrl-000001/worker-stack.tf.tgz # Create releases for each stack tensor9 stack release create -appName my-app -testApplianceName my-test -vendorVersion "1.0.0" -nativeStackId s3://t9-ctrl-000001/api-stack.tf.tgz tensor9 stack release create -appName my-app -testApplianceName my-test -vendorVersion "1.0.0" -nativeStackId s3://t9-ctrl-000001/worker-stack.tf.tgz ``` Each stack compiles and deploys independently, allowing you to update components without redeploying the entire application. ## Integration with CI/CD Tensor9 integrates with standard CI/CD tools and practices. Here's an example GitHub Actions workflow: ```yaml theme={null} name: Deploy to Test Appliance on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install Tensor9 CLI run: | curl -sSL https://t9-artifacts-prod-1.s3.us-west-2.amazonaws.com/install-latest.sh | sh echo "$HOME/.tensor9/bin" >> $GITHUB_PATH - name: Publish origin stack env: T9_API_KEY: ${{ secrets.T9_API_KEY }} run: | tensor9 stack publish \ -stackType TerraformWorkspace \ -stackS3Key my-stack \ -dir ./terraform - name: Create release env: T9_API_KEY: ${{ secrets.T9_API_KEY }} run: | tensor9 stack release create \ -appName my-app \ -testApplianceName ci-test \ -vendorVersion "${GITHUB_SHA:0:7}" \ -description "CI build from commit ${GITHUB_SHA}" - name: Deploy to test appliance run: | cd ci-test tofu init tofu apply -auto-approve ``` You can also use specialized tools like [Atlantis or Spacelift](/integrations/atlantis-spacelift) for Terraform automation. ## Backend configuration Tensor9 **does not modify backend configuration** in your origin stack. Any backend configuration you include in your origin stack is preserved in the compiled deployment stack, giving you full control over Terraform state management. You are responsible for managing backend configuration for your deployments. You can include backend configuration directly in your origin stack, or provide it at deployment time: **Option 1: Include in origin stack** ```terraform theme={null} # In your origin stack's backend.tf terraform { backend "s3" { bucket = "my-terraform-state" key = "appliances/terraform.tfstate" region = "us-west-2" dynamodb_table = "terraform-locks" } } ``` This backend configuration is preserved in the compiled deployment stack. Backend blocks cannot interpolate variables, so a backend written this way uses one fixed state path. To give each appliance its own state file, use Option 2 and pass the key at deployment time. **Option 2: Provide at deployment time** ```bash theme={null} # No backend config in origin stack # Provide via CLI arguments when deploying tofu init \ -backend-config="bucket=my-terraform-state" \ -backend-config="key=appliances/customer-123/terraform.tfstate" \ -backend-config="region=us-west-2" tofu apply ``` **Option 3: Separate backend config file** ```bash theme={null} # Create backend.tf in deployment directory after compilation cat > backend.tf < Update your origin stack to the state before the problematic changes in 1.4.6. This could mean: * Checking out the git commit from version 1.4.5 (the last working version) * Reverting the problematic changes in your repository * Restoring from a backup of your infrastructure code ```bash theme={null} # Example: revert to the commit before the broken changes git revert abc123 ``` Create a new release from the restored origin stack. Note that this is version **1.4.7**, not 1.4.5 - you're rolling forward, not backward: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -testApplianceName my-test \ -vendorVersion "1.4.7" \ -description "Roll forward to restore working configuration from 1.4.5" ``` Deploy the new release to the appliance: **For Terraform/OpenTofu:** ```bash theme={null} cd acme-corp-production tofu init tofu apply ``` Terraform will compute the difference between the current state (1.4.6) and the desired configuration (1.4.7, which restores 1.4.5's config), reverting any changes introduced in the failed 1.4.6 release. **For CloudFormation:** The control plane automatically updates the CloudFormation stack when you create the release. Monitor the rollback progress using `tensor9 report` or CloudFormation stack events. CloudFormation will compute the changes needed to restore the working configuration. This is a **roll forward** approach rather than a traditional rollback - you're creating a new release (1.4.7) that happens to restore a previous working configuration (1.4.5). This ensures all changes go through the same compilation and deployment workflow, maintaining consistency and auditability. ### Alternative recovery approaches For emergency recovery with **Terraform/OpenTofu**, you can also: * **Re-deploy a previous deployment stack directory** if you've retained it (bypasses compilation but uses known-good deployment stack) * **Use Terraform state management** (`tofu state pull`, `tofu state push`) to manually revert state (advanced users only) For **CloudFormation**, you can: * **Use CloudFormation stack rollback features** in the AWS console or CLI to revert to a previous stack state * **View stack change sets** to understand what changes were applied in each release ## Monitoring deployments Track deployment status and health using the CLI or your observability platform. ### Tensor9 report ```bash theme={null} tensor9 report ``` Shows all appliances, active releases, and deployment status. ### Terraform output After deployment, view outputs defined in your origin stack: ```bash theme={null} tofu output ``` ### Observability sink Once deployed, your appliance forwards logs, metrics, and traces to your configured [observability sink](/fundamentals/observability). Monitor application health in your preferred tool (Datadog, New Relic, etc.). ## Best practices Adopt a consistent versioning scheme for the `-vendorVersion` parameter: * **Major version** (1.0.0 → 2.0.0): Breaking changes * **Minor version** (1.0.0 → 1.1.0): New features, backward compatible * **Patch version** (1.0.0 → 1.0.1): Bug fixes Always create and validate releases in test appliances before deploying to customer appliances. This catches issues early and reduces customer-facing incidents. Include meaningful descriptions and notes with every release: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -testApplianceName my-test \ -vendorVersion "1.2.0" \ -description "Add user analytics dashboard with real-time metrics" \ -notes "Deployed by jane@company.com after QA sign-off on JIRA-123" ``` This creates an audit trail and makes it easy to understand what changed in each release. Document your deployment process, including: * Required backend configuration * Post-deployment validation steps * Rollback procedures * Contact information for escalations Use CI/CD pipelines to automate the publish → release → deploy workflow. This reduces manual errors and ensures consistent deployments across all appliances. Archive deployment stack directories after successful deployments. This allows quick rollbacks and serves as a historical record of what was deployed. ## Troubleshooting **Symptom**: `tensor9 stack release create` fails with compilation errors. **Solutions**: * Run `tofu validate` on your origin stack to catch syntax errors * Check that all required variables are defined * Verify artifact references (container images, S3 objects) are accessible * Review Tensor9 logs for specific compilation errors **Symptom**: `tofu apply` fails with resource creation errors. **Solutions**: * Check that the appliance has necessary permissions (IAM roles, service accounts) * Verify resource names don't conflict (prefix them with the `@namespace` annotated variable) * Check cloud provider quotas (e.g., VPC limits, compute limits) * Review Terraform error output for specific resource failures **Symptom**: Release created but deployment doesn't occur. **For Terraform/OpenTofu** - deployment stack doesn't download: * Verify appliance is in "Live" status using `tensor9 report` * Check network connectivity between your environment and control plane * Ensure API key is valid: `echo $T9_API_KEY` * Wait a few minutes - compilation can take time for large stacks **For CloudFormation** - CloudFormation stack not created in control plane: * Verify the control plane has necessary permissions to create CloudFormation stacks * Check CloudFormation events in your control plane's AWS account for errors: `aws cloudformation describe-stack-events --stack-name ` * Verify the release was successfully created: `tensor9 report -customerName ` * Check AWS service quotas for CloudFormation stacks in your control plane's account **Symptom**: `tofu init` fails with backend errors. **Solutions**: * Ensure backend configuration is provided (either in origin stack or at deployment time) * If using CLI arguments, verify all required backend parameters are specified * Ensure state bucket exists and is accessible * Check that each appliance is given its own backend state path at deployment time ## Next steps Now that you understand deployments, explore these related topics: * [**Observability**](/fundamentals/observability): Monitor deployed appliances * [**Operations**](/fundamentals/operations): Perform day-2 operations on deployments * [**Testing**](/fundamentals/testing): Advanced testing strategies * [**Atlantis/Spacelift Integration**](/integrations/atlantis-spacelift): Automate Terraform deployments # Endpoints, DNS, and Load Balancing Source: https://docs.tensor9.com/fundamentals/endpoints Tensor9 manages DNS hostnames, load balancers, and TLS certificates across cloud and Kubernetes environments. When you define these resources in your [origin stack](/fundamentals/origin-stacks), Tensor9 compiles them to the appropriate [service adapter](/service-adapters/overview) for the target [form factor](/fundamentals/key-concepts#form-factor). This includes private Kubernetes clusters where cloud-managed services aren't available. You can also assign publicly-accessible custom DNS hostnames to deployed [appliances](/fundamentals/appliances) by configuring a **vanity domain** for your app. This enables touch-free SSL certificate generation and branded endpoints for your customers. There are two ways to assign vanity domains to appliances: 1. **Vendor-Supplied**: Tensor9 automatically assigns subdomains of the app's root vanity domain to customers (e.g., `customer-a.ai-chat.playground.tensor9.app`). 2. **Customer-Supplied**: Your customer brings their own domain (e.g., `app.internal.customer.com`) and delegates it to the appliance. In both cases, your Terraform origin stack remains exactly the same. Tensor9 creates the hosted zone during appliance setup *before* the deployment stack is deployed. The deployment stack simply looks up the pre-existing zone to create records. ## Enable Vanity Domains To enable vanity domains, specify a root domain during app creation: ```bash theme={null} tensor9 app create \ -name ai-chat \ -vanityDomain ai-chat.playground.tensor9.app ``` This triggers Tensor9 to create a hosted zone for this domain. The vanity domain is scoped to the app and all installs of this app share the same root domain. Perform a one-time manual delegation from your DNS provider to the nameservers of the hosted zone created by Tensor9. This allows Tensor9 to manage subdomains on your behalf. ## Vanity Domain Assignment Once the app is created with a vanity domain, your customers can choose to use either the vendor-supplied or customer-supplied vanity domain option during appliance setup. ### Vendor-Supplied Domains In this model, Tensor9 automatically manages the assignment and delegation of a subdomain of your app's root vanity domain to an appliance. When a customer installs your app, Tensor9 automatically: Tensor9 assigns a unique subdomain to the install (e.g., `customer-a.ai-chat.playground.tensor9.app`). The appliance setup process creates a hosted zone for this subdomain within the customer's environment. Tensor9 delegates the subdomain from your root hosted zone to the appliance's hosted zone. ### Customer-Supplied Domains In this model, your customer brings their own domain name. During appliance setup, the customer specifies their desired domain (e.g., `portal.corp.com`). The appliance setup process creates a hosted zone for this domain within the customer's environment. The customer adds an NS record at their DNS provider to delegate the domain to their appliance's hosted zone. ## DNS Provider Support Tensor9 uses the **native DNS service** of each target environment where possible. For environments without a built-in DNS service, Tensor9 supports external DNS providers. | Target Environment | DNS Provider | Notes | | ------------------ | ---------------------- | ------------------------------------------------------- | | AWS | Route 53 | Native AWS DNS service | | Google Cloud | Cloud DNS | Native GCP DNS service | | Azure | Azure DNS | Native Azure DNS service | | Private Kubernetes | Route 53 or Cloudflare | External DNS provider configured during appliance setup | Your origin stack doesn't change based on the DNS provider. During compilation, Tensor9 handles provider selection automatically based on the target form factor. For private Kubernetes appliances, the DNS provider is configured at the appliance level. Credentials are stored securely, never leave the customer's environment, and are used by Tensor9 to manage DNS records on their behalf. ## Origin Stack: DNS To support both vanity domain workflows, your Terraform code should **look up** a hosted zone rather than create one. ### 1. Receive the Domain Name Use the `@vanity_domain_root()` annotation on a variable. Tensor9 injects the install's assigned fully qualified domain name (FQDN) at deploy time. ```hcl theme={null} # @vanity_domain_root() variable "domain_root" { type = string default = "local.example.com" } ``` ### 2. Look Up the Hosted Zone Reference the hosted zone using a Terraform **data source** instead of a resource. The zone already exists (created during appliance setup) by the time your stack is deployed. ```hcl AWS theme={null} data "aws_route53_zone" "app_zone" { name = var.domain_root } resource "aws_route53_record" "app" { zone_id = data.aws_route53_zone.app_zone.zone_id name = "www.${var.domain_root}" type = "CNAME" ttl = 300 records = [aws_lb.app.dns_name] } ``` ```hcl GCP theme={null} data "google_dns_managed_zone" "app_zone" { name = var.domain_root } resource "google_dns_record_set" "app" { managed_zone = data.google_dns_managed_zone.app_zone.name name = "www.${var.domain_root}." type = "CNAME" ttl = 300 rrdatas = [google_compute_global_address.app.address] } ``` ```hcl Azure theme={null} data "azurerm_dns_zone" "app_zone" { name = var.domain_root } resource "azurerm_dns_cname_record" "app" { name = "www" zone_name = data.azurerm_dns_zone.app_zone.name resource_group_name = var.resource_group_name ttl = 300 record = azurerm_public_ip.app.fqdn } ``` During compilation, all DNS records are compiled to use the DNS provider configured on the appliance. For private Kubernetes targets, Route 53 and Cloudflare records are managed automatically via the configured DNS provider. ## Load Balancer Endpoints Tensor9 compiles load balancer resources from your origin stack to equivalent resources in the target environment using the [service adapters](/service-adapters/overview) model. For example, you can define load balancers using AWS resources, and Tensor9 will map them to Kubernetes-native load balancing when compiling for private environments. ### How It Works Vendors define load balancer resources in their origin stack using AWS convention. During compilation, Tensor9: Tensor9 detects the use of NLBs, ALBs, or Kubernetes Services with AWS Load Balancer Controller annotations and Ingress resources with load balancer annotations. Each load balancer resource is mapped to its functional equivalent based on the target form factor. Port mappings, health checks, routing rules, and TLS settings are preserved in the mapping. Required infrastructure (Traefik, cert-manager) is declared as service dependencies and installed automatically in the customer's environment. ### Equivalence Table | Origin (AWS) | Private Kubernetes | Notes | | --------------------------------------- | ------------------------------------------- | ----------------------------------------- | | Kubernetes Service with NLB annotations | Kubernetes Service with MetalLB annotations | L4 TCP/UDP load balancing | | Kubernetes Ingress with ALB annotations | Traefik IngressRoute | L7 HTTP/HTTPS routing | | Health check annotations | Kubernetes probes | Automatic mapping | | ACM certificate (via annotation) | cert-manager Certificate | See [TLS Certificates](#tls-certificates) | You do not need to define Traefik or cert-manager resources in your origin stack. Tensor9 automatically declares these as service dependencies and manages their installation in the target environment. ### Origin Stack Example Here is a typical Kubernetes service with AWS NLB annotations from an AWS origin stack: ```hcl theme={null} resource "kubernetes_service_v1" "app" { metadata { name = "app-lb" namespace = var.namespace annotations = { "service.beta.kubernetes.io/aws-load-balancer-type" = "nlb" "service.beta.kubernetes.io/aws-load-balancer-scheme" = "internet-facing" } } spec { type = "LoadBalancer" port { port = 443 target_port = 8080 protocol = "TCP" } selector = { app = "my-app" } } } ``` When compiled for a private Kubernetes form factor, Tensor9 replaces the AWS annotations with MetalLB annotations and adds the necessary service dependency declarations. The port mappings and selectors are preserved. ### `@kubernetes_service()` Annotation Use `@kubernetes_service(type='LoadBalancer')` on a `data "kubernetes_service_v1"` data source when your stack needs to reference a load balancer service's external address. This annotation tells the compiler how to handle platform-specific behavior. For example, MetalLB assigns IP addresses (not hostnames), so the compiler creates a DNS A record and rewrites `.hostname` references accordingly. ```hcl theme={null} # @kubernetes_service(type='LoadBalancer') data "kubernetes_service_v1" "traefik" { metadata { name = "traefik" namespace = "traefik" } } resource "aws_route53_record" "app" { zone_id = data.aws_route53_zone.app_zone.zone_id name = "app.${var.domain_root}" type = "CNAME" ttl = 300 records = [data.kubernetes_service_v1.traefik.status.0.load_balancer.0.ingress.0.hostname] } ``` ## TLS Certificates Tensor9 compiles TLS certificate resources to the appropriate equivalent for each target environment. In AWS, certificates are managed by ACM. In private Kubernetes environments, Tensor9 uses [cert-manager](https://cert-manager.io/) with Let's Encrypt DNS-01 validation. ### Origin Stack: ACM Certificates Define TLS certificates using AWS ACM resources. Since the vanity domain is properly delegated, you can use automatic DNS validation: ```hcl theme={null} resource "aws_acm_certificate" "cert" { domain_name = "www.${var.domain_root}" validation_method = "DNS" } resource "aws_route53_record" "cert_validation" { for_each = { for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => { name = dvo.resource_record_name record = dvo.resource_record_value type = dvo.resource_record_type } } allow_overwrite = true name = each.value.name records = [each.value.record] ttl = 60 type = each.value.type zone_id = data.aws_route53_zone.app_zone.zone_id } resource "aws_acm_certificate_validation" "cert" { certificate_arn = aws_acm_certificate.cert.arn validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn] } ``` ### Equivalence: cert-manager When compiling for private Kubernetes, Tensor9 replaces ACM resources with cert-manager equivalents: | Origin (AWS) | Private Kubernetes | Notes | | --------------------------------- | ------------------------------ | ----------------------------------------------- | | `aws_acm_certificate` | cert-manager `Certificate` CRD | Domain name and SANs preserved | | `aws_acm_certificate_validation` | Removed | cert-manager handles validation automatically | | `aws_route53_record` (validation) | Removed | DNS-01 challenge handled by cert-manager solver | The cert-manager `ClusterIssuer` is managed automatically by Tensor9's service dependency system and vendors do not need to define it. The issuer uses Let's Encrypt with DNS-01 validation via the appliance's configured DNS provider. ### Referencing External Certificates When an ACM certificate is passed as a variable (rather than defined in-stack), the compiler cannot follow the reference to extract domain information. Instead, use a `data "aws_acm_certificate"` data source to look up the certificate by domain. This allows Tensor9 to extract the domain information and automatically convert it to a Kubernetes data source when compiling for private Kubernetes. ```hcl theme={null} # @vanity_domain_root() variable "domain_root" { description = "Root domain name (e.g., example.com)" type = string } data "aws_acm_certificate" "cert" { domain = var.domain_root statuses = ["ISSUED"] } ``` ## Annotations Reference | Annotation | Applies To | Description | | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------- | | `@vanity_domain_root()` | `variable` | Injects the install's vanity domain FQDN at deploy time | | `@vanity_domain_provided()` | `variable` (bool) | Controls conditional hosted zone creation (see below) | | `@kubernetes_service(type='LoadBalancer')` | `data "kubernetes_service_v1"` | Marks a load balancer data source for platform-specific handling | ### Additional Annotations #### @vanity\_domain\_provided() If you need to keep the hosted zone definition inside your origin stack (e.g., for self-contained single-file stacks), use `@vanity_domain_provided()` to handle this conditionally. Annotate a boolean variable with `@vanity_domain_provided()`. Leave it `false` (the default) so the hosted zone is created when you deploy the stack directly. During compilation, Tensor9 sets the variable to `true` to skip hosted zone creation (since it was created during appliance setup) and allows the deployment stack to look up the zone instead. ```hcl theme={null} # @vanity_domain_provided() variable "zone_provided" { type = bool default = false } # Conditionally create the zone ONLY if it wasn't provided resource "aws_route53_zone" "created_zone" { count = var.zone_provided ? 0 : 1 name = var.domain_root } # Look up the zone if it WAS provided data "aws_route53_zone" "lookup_zone" { count = var.zone_provided ? 1 : 0 name = var.domain_root } ``` ## Best Practices If your app is hosted at `saas.com`, pick an alternative like `saas-customers.com` or `saas.app` for your vanity domain root. This ensures that the cookie space for your hosted offering is entirely separate from your customer's appliances. The maximum length of the Common Name in a certificate is 64 characters. Vanity domains assigned to customers include the appliance ID as well as any endpoints defined in your origin stack. The total length of the subdomain for which a certificate is requested cannot exceed the limit. Always reference hosted zones using data sources (`data "aws_route53_zone"`) instead of creating them with resources. The zone is created during appliance setup before your stack is deployed. Using a data source ensures your stack works with both vendor-supplied and customer-supplied vanity domains. Do not manually define Traefik or cert-manager resources in your origin stack. Use standard AWS load balancer annotations and ACM certificates. Tensor9 compiles these to the correct equivalents for each target environment and manages the required infrastructure automatically. ## Questions You Might Ask Both modes create a DNS hosted zone in the customer's cloud account during appliance setup, and both flow through the same [`@vanity_domain_root()`](/fundamentals/endpoints#vanity_domain_root) variable in your origin stack. Your origin stack does not need to distinguish between the two. The difference is in who owns the domain and who performs the NS delegation: * **Vendor-supplied**: You register a domain (e.g., `your-domain.app`) and delegate it to Tensor9. Tensor9 auto-generates a subdomain for each customer (e.g., `abc123.your-domain.app`) and handles NS delegation to the customer's hosted zone automatically. * **Customer-supplied**: The customer provides their own domain during appliance setup and creates the NS delegation on their side. Tensor9 configures the customer's hosted zone accordingly. In the customer's cloud account, during appliance setup. For vendor-supplied domains, Tensor9 then delegates the subdomain from your app's root hosted zone (in your Tensor9 Cloud Account) to the customer's hosted zone by creating NS records automatically. For customer-supplied domains, the customer handles NS delegation themselves. Here is the delegation chain for vendor-supplied domains: Vanity domain DNS delegation chain Vanity domain DNS delegation chain Your domain registrar delegates to the Tensor9-managed hosted zone in your Tensor9 Cloud Account. Tensor9 then delegates each customer's subdomain to their own hosted zone in their account. You define a TLS certificate resource once in your origin stack, referencing the [`@vanity_domain_root()`](/fundamentals/endpoints#vanity_domain_root) annotated domain variable. Tensor9 compiles that resource into each customer's deployment with the correct domain and the appropriate certificate type for the target environment (e.g., AWS ACM certificates on AWS, cert-manager Certificates on Kubernetes). This works the same for both vendor-supplied and customer-supplied domains. Do not hardcode certificate identifiers - the compiler operates on Terraform resources, not raw strings. In the customer's account, alongside the load balancers and CDN distributions that reference them. DNS validation records are created in the customer's hosted zone during deployment. Because the subdomain is delegated to the customer's zone, certificate authorities can resolve the validation records through the delegation chain. TLS certificate and DNS validation flow TLS certificate and DNS validation flow No. For vendor-supplied domains, Tensor9 creates it automatically during app creation. You perform a one-time NS delegation so that DNS queries for your vanity domain resolve to the Tensor9-managed hosted zone. Where you configure that delegation depends on the domain you chose: * **Root domain** (e.g., `your-domain.app`): Update the NS records at your domain registrar to point to the nameservers Tensor9 returns. * **Subdomain** (e.g., `self-host.your-domain.app`): Create an NS record in whatever DNS provider hosts the parent domain (e.g., your `your-domain.app` zone) delegating the subdomain to the Tensor9 nameservers. For customer-supplied domains, the customer creates and delegates their own hosted zone during appliance setup. No. To use a different vanity domain, create a new app with the desired domain. Yes, for vendor-supplied domains. If your cloud product runs at `your-domain.com`, use a separate domain like `your-domain.app` for vanity domains. This keeps the cookie space for your cloud offering entirely separate from customer appliances. ## Related Topics * [**Service adapters**](/service-adapters/overview): Full reference for how Tensor9 maps services across cloud providers * [**Origin Stacks**](/fundamentals/origin-stacks): How to define portable infrastructure code * [**Deployments**](/fundamentals/deployments): How compilation and deployment works * [**Appliances**](/fundamentals/appliances): Deploy to different cloud environments # How Tensor9 works Source: https://docs.tensor9.com/fundamentals/how-tensor9-works Tensor9 delivers your existing app to your customers as a private **appliance** installed directly into their environment. A Tensor9 **controller** in your cloud account creates a control plane that orchestrates **deployments**, and allows you to **operate** and **observe** your customers' appliances: Here are the steps you'll take to use Tensor9: Connect your app's **origin stack** to Tensor9. Tensor9 supports origin stacks defined in: **Terraform/OpenTofu**, **CloudFormation**, **Kubernetes Manifests/Helm**, and **Docker Compose/Containers**. Create an **appliance** for the customer environment you want to deploy to. An appliance is defined by its **form factor**, which specifies the target environment like **Amazon Web Services**, **Google Cloud**, **Microsoft Azure**, or a customer-provided **Kubernetes** cluster, as well as which managed cloud services are required. You create an appliance using `tensor9 CLI` or the [**Vendor Portal**](/fundamentals/key-concepts#vendor-portal). Your customer completes setup through a guided install wizard, a white-labeled web interface branded with your logo and company name. Use `tensor9 CLI` to **compile** your origin stack into a **deployment stack** for your new appliance. Tensor9 compiles your stack for the **form factor** of that appliance. For example, a Google Cloud deployment can map an AWS load balancer to Google Cloud Load Balancing and AWS Aurora PostgreSQL to Cloud SQL for PostgreSQL. The selected service adapters determine which features are preserved and which require changes. Use standard tooling to **deploy** your deployment stack to a customer appliance. For example, if your origin stack is defined using Terraform/OpenTofu, then invoke `terraform apply` or `tofu apply` on a deployment stack to deploy that stack to the appliance it was compiled for. **Observe** the state, performance, and usage of deployed resources in customer appliances; with metrics, logs, and traces asynchronously synchronized back to your **observability sink** of choice. Monitor appliance health from the [**Vendor Portal**](/fundamentals/key-concepts#vendor-portal) dashboard or the CLI. You can also **operate** deployed resources within customer appliances. For example, execute `kubectl` commands or request temporary, scoped access to cloud resources via IAM. Your customers review and approve operations requests through their [**Customer Portal**](/fundamentals/key-concepts#customer-portal). ## Per-customer configuration The same origin stack compiles into different deployment stacks for different customers, driven by customer-supplied configuration. Each customer can pick their ingress posture (public, allowlisted, Tailscale, or fully private), substitute their own managed services for default-shipped ones (for example, the customer's own managed Temporal), and choose how the appliance reaches your control plane (public internet, AWS PrivateLink, or Tailscale). You ship one stack; each customer gets a build shaped to their environment. See [Auto-Customizations](/customizations/overview) for the full picture. ## How Tensor9 compares to building it yourself | Feature | With Tensor9 | Without Tensor9 | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Service Equivalents** | Maintain a single origin stack. Tensor9 maps cloud services to their equivalents in the target environment during compilation (e.g., RDS → Cloud SQL, S3 → GCS). | Maintain separate infrastructure code for each cloud provider. Port resources manually and keep feature parity across cloud-specific APIs. | | **Deployments** | Tensor9 compiles your origin stack into a deployment stack for a private appliance in your customer's environment. Deploy using standard tooling locally (`terraform apply`, `tofu apply`), and Tensor9 applies changes to the corresponding appliance through a secure channel. | Manually set up a deployment pipeline unique to each customer environment. Request temporary access to customer infrastructure (VPN, IAM roles) for each deployment and execute operations manually. | | **Observability** | Telemetry (logs, metrics, traces) flows from appliances to your observability sink. Tensor9 configures forwarding automatically based on your origin stack. | Set up monitoring agents, configure log forwarding, and manage telemetry infrastructure separately in each customer environment. | | **Operations** | Execute operations commands remotely through your control plane with customer approval workflows and audit logging. | Request and manage temporary access credentials for each operation. Coordinate with customers for access to their infrastructure. | | **Artifacts** | Tensor9 identifies artifacts in your origin stack and copies them to appliance-local storage during deployment (container images, S3 objects). | Build artifact replication and distribution systems. Manage credentials for artifact access across customer environments. | | **Secrets** | Store customer-specific secrets in your control plane with access controls defining vendor vs. customer-only access. | Manage secrets across multiple systems. Coordinate with customers for secret injection and rotation. | | **Endpoints and DNS** | Tensor9 provisions DNS records for each appliance under a domain you specify (vendor-owned or customer-owned) with delegation to customer infrastructure where appropriate. | Coordinate with customer network teams to configure DNS for each deployment. | # Installing Tensor9 Source: https://docs.tensor9.com/fundamentals/install This guide explains how to install the `tensor9` CLI and use it to create a Tensor9 control plane in a dedicated AWS account you own. ## Prerequisites Before you can use Tensor9, you need to ensure your environment meets the following requirements: * **Tensor9 API Key**: You must have an API key to use the `tensor9` CLI. If you don't have one, please send an email to [hello@tensor9.com](mailto:hello@tensor9.com) to request one. * **A dedicated AWS account**: This account should be used exclusively for Tensor9 to avoid conflicts with other resources. ## Install the Tensor9 CLI Install the **tensor9** CLI via Homebrew (recommended): ```bash theme={null} brew tap tensor9ine/tensor9 brew install tensor9 ``` Alternatively, install via the install script: ```bash theme={null} curl -sSL https://t9-artifacts-prod-1.s3.us-west-2.amazonaws.com/install-latest.sh | sh ``` Then set your Tensor9 API key: ```bash theme={null} export T9_API_KEY= ``` **Note:** An API key is required. If you do not have an API key, send email to [hello@tensor9.com](mailto:hello@tensor9.com) to request one. ## Set up your Tensor9 control plane Your Tensor9 control plane is the engine that manages your customer appliances. It consists of a set of resources that are provisioned directly into your own dedicated Tensor9 AWS account. Your control plane is responsible for: * Managing your **origin stacks**. * Creating and managing **appliances**. * Performing **deployments** to appliances. * Synchronizing telemetry from appliances back to your **observability sink**. * Creating **operations endpoints** so that you can remotely operate appliances. To set up your Tensor9 control plane, run the following command. This process can take several minutes to complete. ```bash theme={null} tensor9 vendor setup \ -cloud aws \ -region \ -awsProfile ``` Replace `` with the AWS region you are using and `` with the name of your configured AWS CLI profile. If your environment is AWS, setup automatically provisions an PrivateLink endpoint service that makes [AWS PrivateLink](/customer/security/security-model#no-inbound-network-access) available as a network path for the appliance-to-control-plane channel. Pass `-noAwsPrivateLinkRdv` on the setup command to suppress the endpoint service, which removes PrivateLink as a network-path option for your form factors. ### Verify your control plane Once the setup command completes, you can verify that your control plane is up and running by using the `tensor9 report` command. This command provides a summary of your Tensor9 control plane, including its health status. ```bash theme={null} tensor9 report ``` For a newly created control plane with no apps or appliances yet, the output will look like this: ``` Vendor: Acme Software [id: 000000000000003b]: Name: Acme Software Apps: (0) Customer Appliances: (0) Test Appliances: (0) ``` This confirms your control plane is running and ready to use. You can now proceed to create your first app and begin deploying to customer appliances. ## Next steps Now that your control plane is installed and running, explore these topics to start using Tensor9: * [**Quick Start: Terraform**](/getting-started/quick-start-terraform): Deploy your first application using Terraform * [**How Tensor9 Works**](/fundamentals/how-tensor9-works): Understand the deployment workflow * [**Origin Stacks**](/fundamentals/origin-stacks): Learn how to define your application infrastructure * [**Key Concepts**](/fundamentals/key-concepts): Understand the core terminology and concepts # Key Concepts Source: https://docs.tensor9.com/fundamentals/key-concepts ## Vendor A **vendor** refers to you: a software vendor that seeks to deliver your software into customer environments. Tensor9 helps vendors like you to package, deploy, and support your application as a Tensor9 **app** installed into a customer-controlled environment called an **appliance**. Your vendor metadata defines your app's branding, presentation to customers and deployment configuration. ## Customer A **customer** represents your end customer who purchases and runs your Tensor9 app in their own infrastructure. Customers are organizations such as enterprises, financial institutions, healthcare providers, and other regulated organizations that require software to run securely within their own controlled environments. Each customer is associated with metadata that defines their organization, preferences, and any custom configurations related to their appliances. ## App A Tensor9 **app** represents the software product that you package and deliver to your customers. An app includes the application code, infrastructure, and any associated services required to run your software in a customer's appliance. An app is designed to be deployed in customer-controlled environments, allowing customers to run your software securely within their own infrastructure. ## Appliance An **appliance** is a secure, self-contained system that Tensor9 deploys into a customer's infrastructure. The appliance runs in the customer's cloud account or data center while you maintain the ability to deploy, observe, and operate it remotely. Each appliance mirrors your origin stack configuration and provides the necessary compute, storage, and networking resources while ensuring that all data remains under the customer's control. ## Install An **install** represents a specific app running on a specific appliance. ## Service adapter A **service adapter** lets your app use a service from its origin stack in a different customer environment. For example, an adapter can translate your app's S3 requests into Azure Blob Storage operations. Depending on the mapping and adaptation tier, Tensor9 adapts your infrastructure definitions, runtime API calls, or the service's state and behavior. Each mapping documents the supported operations and differences that affect your app. See [How Service Adapters Work](/service-adapters/overview) for adaptation tiers and examples. ## Form factor A **form factor** defines the environment and constraints in which an appliance runs. It specifies the cloud provider, connectivity, available services and security requirements that the customer's environment must satisfy. A form factor describes essential attributes such as: * whether the appliance runs in AWS, Azure, Google Cloud, or private; * whether it is connected to the internet; * whether managed services like AWS S3, Kubernetes, MongoDB Atlas, or Lucenia OpenSearch are available - as well as which versions are available; * and any regulatory requirements such as FIPS or CMMC compliance. ### Service requirements Form factors can specify **service requirements** - the services your app needs to run in that environment. Service requirements can be specified in two modes: * **Exact**: A single, specific service (e.g., "CloudNative PostgreSQL >= 14.0") * **OneOf**: Multiple options where the customer chooses (e.g., "CloudNative PostgreSQL OR Bring Your Own PostgreSQL") OneOf requirements give customers flexibility to use their preferred service while ensuring your app's needs are met. Which services reach which environments, and what your customer depends on Tensor9 for in each case, is covered in [service adapters](/service-adapters/overview). ### Form factor versioning Form factors are versioned to track how your app's infrastructure requirements evolve over time. Each version has a **status** that controls its availability: * **Preferred**: The recommended version for new installs. Exactly one version is Preferred at any time. * **Active**: Supported but not recommended for new installs. New versions start as Active until validated and promoted to Preferred. * **Retiring**: Being phased out. Existing installs continue to work, but customers are signaled to upgrade. * **Retired**: No longer in use. A version is automatically retired when no installs reference it. When you create a new form factor version, existing installs stay pinned to their original version until explicitly upgraded. This ensures stability for your customers while allowing you to evolve requirements over time. ## Origin stack An **origin stack** represents the original definition of the software, infrastructure, and services that make up your app. The origin stack is defined as infrastructure-as-code (e.g., Terraform/OpenTofu, Kubernetes, CloudFormation) and serves as the source of truth for deployments. Tensor9 compiles the origin stack into a deployment stack for the customer's form factor, such as a cloud account or private environment. The origin stack specifies all the infrastructure components (e.g., databases, storage, compute clusters) and configurations that Tensor9 synchronizes within customer appliances. ## Deployment stack A **deployment stack** is the environment-specific, deployable infrastructure-as-code artifact generated by your control plane from an origin stack. It contains all the necessary service equivalents and configuration adjustments required to deploy an application into a specific form factor. The deployment stack is the final output of the compilation process and is what gets executed by the controller in the target environment to create an appliance. ## Audit stack An **audit stack** is a companion copy of the deployment stack that your control plane produces during compilation, with the Tensor9 runtime plumbing stripped out. It contains only the application infrastructure the customer is being asked to host - no Tensor9 Terraform provider, runtime links, or reflection resources - so it can be reviewed with the customer's standard infrastructure-as-code security, policy, and compliance tooling before the deployment stack is applied. The audit stack is downloaded side-by-side with the deployment stack and is intended for review only, not for `apply`. See [Stack audit](/fundamentals/stack-audit) for the review workflow. ## Stack tuning A **stack tuning document** is an optional configuration file that allows you to customize deployment-specific settings without modifying your origin stack. It enables you to adjust parameters like resource allocations (CPU, memory), custom DNS endpoints, and other appliance-specific configurations on a per-release basis. Stack tuning documents allow you to maintain a single origin stack while customizing deployments for different customer tiers, environments, or requirements. For example, you might allocate more resources for enterprise customers or use different endpoints for development versus production appliances. ## Vendor Portal The **Vendor Portal** is a web-based dashboard where you manage your Tensor9 setup. You can create and configure apps, define form factors with service mappings, generate customer signup links, customize the customer experience, monitor appliance health, configure observability sinks, manage operations templates and commands, handle break-glass sessions, and manage your team. Deployments are handled through the CLI and your CI/CD pipeline. ## Customer Portal The **Customer Portal** is a web-based interface that your customers use to interact with their appliance. It covers the full lifecycle: setting up a new appliance (selecting their environment, applying infrastructure templates, configuring DNS, secrets, and services), and ongoing management (viewing appliance health, reviewing and approving operations requests, viewing deployed infrastructure, configuring release windows, and managing upgrades). The Customer Portal is white-labeled with your branding. Your logo and company name appear in the interface, and your customers see it as part of your product experience. ## Control plane Your Tensor9 **control plane** is your central management plane hosted within your designated cloud account. It is provisioned directly within your own dedicated AWS account, ensuring that your code, data, and infrastructure credentials always remain under your ownership and control. Your control plane is responsible for orchestrating the entire lifecycle of your applications, from compiling your origin stack into a deployable artifact, to enabling you to manage ongoing operations and observability for every customer appliance. ## Controller A Tensor9 **controller** is Tensor9-provided software that runs in both your control plane and your customer's appliances. It is responsible for coordinating deployments, observability, and operations between your control plane and your customer's appliances. When running in your control plane, it manages the origin stack and compilation process. When running in a customer's appliance, it receives commands, executes actions, and coordinates sending telemetry back to your control plane. ## Observability sink An **observability sink** is a destination your appliance telemetry (logs, metrics, and traces) is forwarded to: Datadog, CloudWatch, Loki, Prometheus, or any OpenTelemetry-compatible backend. You can configure multiple sinks and control which telemetry sources feed each one, per signal (see [Telemetry routing](/fundamentals/observability#telemetry-routing)). This allows you to use your existing monitoring and analysis tools to get a unified view of your entire fleet of appliances. ## Operations endpoint An **operations endpoint** is a secure API in your control plane that lets you issue remote commands to appliance-hosted resources. It supports both asynchronous operations and synchronous operations, with approval workflows and audit logs so your customers stay in control. ## Auto-Customization **Auto-Customization** is the agreement between you and each customer that shapes their install. At appliance setup time, your customer declares a small set of properties about their environment, and the compiler emits a deployment stack that honors them. Your application code does not change between customers; the same origin stack compiles into a per-customer build. The form factor defines the permitted set of choices, and Auto-Customization is your customer picking from what the form factor allows. Three configuration choices are available: * **Ingress posture**: how end users reach the deployed application (public, allowlisted, Tailscale). * **Controller connectivity**: how the appliance reaches your control plane (public internet, AWS PrivateLink, Tailscale). * **Customer-provided services**: which managed services the install uses your customer's existing instance of, instead of provisioning a default equivalent. See [Auto-Customizations](/customizations/overview) for details of each choice. # Observability Source: https://docs.tensor9.com/fundamentals/observability Observability in Tensor9 enables you to monitor all your customer appliances from a single observability platform. Logs, metrics, and traces flow from each customer's infrastructure to your control plane, which then routes each telemetry stream to the observability sinks you've configured, giving you unified visibility across all deployments regardless of where they run. ## How observability works When you deploy applications through Tensor9, each customer appliance runs in isolated infrastructure (their AWS account, Google Cloud project, or private environment). Without observability, you would have no visibility into how these appliances are performing, whether deployments succeeded, or how customers are using your application. Tensor9's observability system solves this by collecting telemetry from each appliance and forwarding it to your centralized observability platform: Resources in the customer appliance (containers, Lambda functions, databases, load balancers) generate logs, metrics, and traces during normal operation. Tensor9 uses [steady-state permissions](/fundamentals/permissions-model#steady-state-permissions) to collect telemetry from appliance resources. Collection runs inside the appliance, tapping the telemetry your resources already emit and forwarding it to your control plane (see [How telemetry flows](#how-telemetry-flows) for the per-runtime mechanism). Collected telemetry is forwarded from the customer appliance to your control plane over secure connections. Your control plane then routes each telemetry stream to the observability sink (or sinks) you've configured. You decide which sources feed which sinks, per signal (see [Telemetry routing](#telemetry-routing)). Your team monitors all customer appliances from your observability platform. You can track deployment health, investigate incidents, analyze usage patterns, and troubleshoot issues across all customers from one place. Observability collection uses steady-state permissions, which are always active and read-only. Customers do not need to approve observability access; it runs continuously to ensure you maintain visibility into appliance health. ## What you can observe Tensor9 collects telemetry from customer appliances: ### Application logs Logs from your application components running in customer appliances: * **Container logs** from your workloads in EKS, GKE, AKS, or private Kubernetes, captured through your existing log agent (Datadog, OpenTelemetry, or Loki) * **Function logs**: Execution logs from Lambda and from whatever it compiles to on the target, such as Cloud Run or Container Apps * **Application logs**: Custom application logs written to CloudWatch, Cloud Logging, or other logging services Logs include the `t9_appliance_id` and `t9_customer_name` (plus `t9_service_name` for CloudWatch-sourced logs), allowing you to filter and correlate logs across customers. ### Infrastructure metrics Performance and health metrics from infrastructure resources: * **Compute metrics**: CPU, memory, network for containers, VMs, or functions * **Database metrics**: Connections, queries per second, replication lag, storage utilization * **Storage metrics**: Object count, storage used, request rates for S3, GCS, or Azure Blob Storage * **Load balancer metrics**: Request count, latency, error rates, healthy/unhealthy targets ### Custom metrics Application-level metrics you instrument in your code: * **Business metrics**: User signups, API calls, feature usage * **Performance metrics**: Request duration, queue depth, cache hit rates * **Error tracking**: Exception rates, failed operations, validation errors ### Distributed traces Request traces across your application components: * **Cross-service traces**: Track requests across microservices, databases, and external APIs * **Performance analysis**: Identify slow operations and bottlenecks * **Dependency mapping**: Visualize how services communicate within an appliance ## Observability sinks An observability sink is a destination your telemetry is forwarded to. You can configure **multiple sinks** and route different telemetry to each. For example, application logs can go to Datadog while high-volume infrastructure logs go to CloudWatch. Tensor9 supports these sink types natively: | Sink | Logs | Metrics | Traces | Configuration | | ---------------------------------------- | :--: | :-----: | :----: | ----------------------------------------- | | **Datadog** | ✓ | ✓ | ✓ | API key and site | | **CloudWatch** | ✓ | ✓ | | Default credentials or cross-account role | | **OpenTelemetry (OTLP)** *(coming soon)* | ✓ | ✓ | | OTLP endpoint and optional authentication | | **Loki** | ✓ | | | Endpoint and credentials | | **Elasticsearch** *(coming soon)* | ✓ | | | Cluster endpoint(s) and credentials | | **Prometheus Remote Write** | | ✓ | | Endpoint and credentials | Any backend that speaks **OTLP** (New Relic, Sumo Logic, Honeycomb, Grafana Cloud, and most modern observability platforms) can be used through the **OpenTelemetry** sink. Grafana stacks are reached directly through the **Loki** (logs) and **Prometheus Remote Write** (metrics) sinks. You configure sinks in your control plane, and Tensor9 applies the configuration to all of that app's appliances automatically. ## Telemetry routing By default, each sink receives only the telemetry from its **matching source**: a Datadog sink receives Datadog telemetry, a CloudWatch sink receives CloudWatch logs, a Loki sink receives Loki logs. This keeps high-volume infrastructure logs (such as Kubernetes or CloudWatch control-plane logs) out of your SaaS sinks, where they would inflate cost, unless you deliberately send them there. When you need a different topology, you control exactly which sources feed which sinks, independently for logs, metrics, and traces. You edit routing visually in the portal's Routing view; see [Route your telemetry](/fundamentals/configuring-observability#route-your-telemetry). ### Telemetry sources Tensor9 recognizes telemetry from these sources in your appliances: | Source | Logs | Metrics | Traces | | --------------------------- | :--: | :-----: | :----: | | **Datadog** (Datadog Agent) | ✓ | ✓ | ✓ | | **OpenTelemetry** (OTLP) | ✓ | ✓ | ✓ | | **Loki** | ✓ | | | | **Prometheus** | | ✓ | | | **CloudWatch** | ✓ | | | ### Per-signal routing Routing is **per signal**. You can send a source's logs to one sink and its metrics to another, or fan a single source out to several sinks. For each sink, logs, metrics, and traces are routed independently: * **Default** (no routes set): the sink receives only its matching source for each signal. OpenTelemetry and Elasticsearch sinks have no matching source (OTLP is vendor-neutral, so it is routed explicitly rather than matched by default), so they receive nothing until you route something to them. Both sink types are coming soon. * **Routed**: the sink receives exactly the sources you connect, for that signal. * **Disabled**: remove every route for a signal and that signal is no longer delivered to that sink. You can configure and manage observability sink settings from the **Vendor Portal** under **Observability**, or via the CLI as shown below. ## Configuring observability Observability is **off by default**; you turn it on per appliance, and optionally per resource, in the vendor portal. Add sinks, wire up routing, control which appliances and resources are observed, and instrument telemetry in your origin stack. ## How the pipeline works Under the hood, telemetry moves through a fixed pipeline: Observability pipeline: native cloud logging, to a forwarder, to a vendor-owned stream, to a router, to your sinks 1. **Buffered in native cloud logging.** Collected logs, metrics, and traces are written to the customer's native logging service (CloudWatch Logs on AWS, Cloud Logging on Google Cloud, Azure Monitor Logs on Azure), which buffers them and doubles as the [customer audit trail](#customer-audit-trail). 2. **Forwarded to your control plane.** A forwarder tags each record at the edge with its [appliance and customer identity](#appliance-identification), then sends it on to a stream that you (the vendor) own. 3. **Pushed to your sinks.** A router reads the stream, applies your [routing](#telemetry-routing), and pushes each telemetry stream to its configured sink. Sink credentials are applied in your control plane and never leave it; they are never deployed to a customer appliance. The observability pipeline scales automatically with the volume of telemetry, so it absorbs traffic spikes without any tuning on your part. ## Customer audit trail Before any telemetry leaves the customer's environment, it is recorded in their own native log service: CloudWatch Logs on AWS, Cloud Logging on Google Cloud, and Azure Monitor Logs on Azure. The collected telemetry passes through this local record on its way to your control plane, so the customer keeps a complete, independent copy of exactly what was captured and forwarded out of their account. They can audit everything that crosses their boundary. In the future, customers will also be able to redact and filter this telemetry before it is forwarded, giving them direct control over what leaves their environment. ## Observability across form factors Observability collection adapts to each appliance's [form factor](/fundamentals/key-concepts#form-factor): | Form Factor | Log Collection | Metrics Collection | Trace Collection | | ---------------------- | --------------------------- | ------------------------------------------------ | --------------------------------------------------- | | **AWS** | CloudWatch Logs | CloudWatch Metrics, resource-specific metrics | X-Ray or application instrumentation | | **Google Cloud** | Cloud Logging | Cloud Monitoring, resource-specific metrics | Cloud Trace or application instrumentation | | **Azure** | Azure Monitor Logs | Azure Monitor Metrics, resource-specific metrics | Application Insights or application instrumentation | | **Private Kubernetes** | Logs via Fluent Bit/Fluentd | Prometheus metrics | OpenTelemetry Collector | | **On-prem** | Logs via Fluent Bit/Fluentd | Prometheus metrics | OpenTelemetry Collector | Tensor9 provisions the appropriate collection infrastructure for each environment during compilation. ## How telemetry flows Tensor9 captures the telemetry your application already emits and forwards it to your sinks, without new agents or application-code changes. How it taps in depends on the runtime: ### Kubernetes Tensor9 deploys a lightweight **collection DaemonSet** to the cluster and redirects your existing telemetry agents to it. Whatever your workloads already run (the **Datadog Agent**, an **OpenTelemetry Collector**, **Prometheus**, or **Loki**) keeps running unchanged but sends through the Tensor9 collector, which tags its logs, metrics, and traces with the appliance and customer metadata and forwards them to your control plane. No changes to your workloads. ### AWS Lambda Tensor9 injects a **Lambda extension** into your functions during compilation. The extension intercepts the function's telemetry (Datadog, OpenTelemetry, and so on), tags it, and forwards it, again without changing your function code. In both cases the capture happens inside the customer's environment; only the resulting telemetry leaves it. ### Native cloud logs Both paths above work by emitting your application's logs, metrics, and traces into the **native cloud log service** (CloudWatch Logs on AWS, and the equivalent on other clouds). Tensor9 collects by **mirroring that service**, so anything written to it flows to your sinks. A useful consequence: the cloud's own logs are forwarded too, with no extra setup. Control-plane logs (for example, EKS control-plane logs) and VPC flow logs reach your sinks through the same path. ## Appliance identification Every forwarded telemetry record is stamped with two Tensor9 identity tags so you can attribute it to an appliance and customer: * **t9\_appliance\_id**: Tensor9's unique identifier for the appliance * **t9\_customer\_name**: Customer that owns the appliance The emitting **service** is identified by the source's own convention: CloudWatch-sourced telemetry adds a **`t9_service_name`** tag (derived from the log group), while Datadog, Loki, and Prometheus telemetry name the service in their native tag/label (Datadog `service`, Kubernetes `app`/`container`). Tensor9 doesn't override those. These tags let you filter, group, and correlate telemetry across customers and services. They're distinct from the `t9-*` tags Tensor9 stamps onto the resources themselves (see [Configure telemetry in your origin stack](/fundamentals/configuring-observability#configure-telemetry-in-your-origin-stack)). ### Example: Filtering logs by customer In Datadog: ``` service:myapp-api t9_customer_name:acme-corp ``` In Grafana Loki: ``` {t9_customer_name="acme-corp", app="myapp-api"} ``` ## Unified dashboards With telemetry from all appliances flowing to your observability sink, you can create unified dashboards that aggregate metrics across customers: * **Deployment health**: Track successful vs. failed deployments across all appliances * **Performance trends**: Compare response times and error rates across customers * **Resource utilization**: Monitor database CPU, storage usage, function execution counts * **Version adoption**: See which customers are running which versions You can also create customer-specific dashboards filtered to a single `t9_appliance_id` or `t9_customer_name` for troubleshooting individual appliances. ## Telemetry and customer data **Your responsibility**: Tensor9 does not guarantee that your logs do not contain customer data. It is your responsibility as the vendor to ensure that your application does not log sensitive customer data (PII, financial information, proprietary content, or customer business data) that will be forwarded to your observability sink. Observability telemetry should contain application logs and infrastructure metrics, not customer business data. While logs may include operational metadata (timestamps, user IDs, API endpoints, error codes), they should never include sensitive customer information. **You must take precautions to prevent customer data from appearing in logs:** * **Sanitize logs**: Remove or redact sensitive information before logging. Never log request/response payloads containing customer data. * **Use structured logging**: Log metadata and identifiers, not full payloads. Log `user_id: 12345` instead of the entire user object. * **Configure log levels**: Use DEBUG/INFO for development, WARN/ERROR for production. Avoid verbose logging that may capture customer data. * **Review what you collect**: Audit what data flows to your observability sink. Test your logging to ensure no customer data leaks through. * **Filter at the source**: Configure log filters to exclude patterns that may contain sensitive data (credit card numbers, SSNs, API keys). Tensor9 forwards whatever telemetry your application emits; it is your responsibility to ensure that telemetry does not contain customer data. ## Observability permissions Telemetry collection requires [steady-state permissions](/fundamentals/permissions-model#steady-state-permissions) in customer appliances. These permissions are: * **Read-only**: Cannot modify infrastructure or customer data * **Always active**: Observability runs continuously without customer approval * **Scoped to vendor resources**: Can only access resources deployed by your application Example steady-state role for observability in AWS: ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "cloudwatch:GetMetricData", "cloudwatch:ListMetrics" ], "Resource": "*", "Condition": { "StringEquals": { "aws:ResourceTag/t9-appliance-id": "" } } } ] } ``` This role allows reading metrics only from resources tagged with the appliance's `t9-appliance-id`. ## Alerting and incident response Once telemetry flows to your observability platform, you can configure alerts that notify your team when issues occur across customer appliances: * **Deployment failures**: Alert when a deployment to any appliance fails * **High error rates**: Notify when error rates exceed thresholds * **Performance degradation**: Alert on slow response times or database latency * **Resource exhaustion**: Warn when databases approach storage limits Alerts can include the `t9_appliance_id` and `t9_customer_name`, allowing you to quickly identify which customer is affected and route incidents to the right team. ## Best practices It is your responsibility to ensure your application does not log sensitive customer data (PII, financial information, customer business data). Tensor9 forwards whatever telemetry your application emits; it does not filter or sanitize logs for customer data. Implement log sanitization in your application code, avoid logging request/response payloads, and regularly audit what data flows to your observability sink. Tensor9 stamps `t9-appliance-id`, `t9-buyer-name`, `t9-app-name`, and related tags onto every resource whose provider schema supports tags. This is what enables filtering telemetry by appliance and scoping observability permissions, so you don't need to add appliance-identifying tags of your own. ## Related topics * [**Permissions Model**](/fundamentals/permissions-model): Understanding steady-state permissions for observability * [**Appliances**](/fundamentals/appliances): Customer environments where telemetry is collected * [**Deployments**](/fundamentals/deployments): Tracking deployment success through observability * [**Operations**](/fundamentals/operations): Using observability to inform remote operations # How Operations Work Source: https://docs.tensor9.com/fundamentals/operations Your customers run your software inside their own cloud accounts. You have no SSH keys, no kubectl context, no AWS credentials. When something goes wrong (a pod crash-looping, a disk filling up, a backfill that needs kicking) you cannot just `ssh` in. Operations is the surface that bridges that gap. You ship vetted **templates** to your customers, submit a request to run one against one of your customer's appliances, and your customer reviews and approves the request through a web link. The command runs inside their appliance, the output is held encrypted in the appliance's secret store, and your customer signs a release before you see anything. How operations work in five steps: define a reusable script with named variables, request an execution against your customer's appliance, your customer reviews and approves it, the script executes on the appliance, and your customer signs again to release the output back to you. How operations work in five steps: define a reusable script with named variables, request an execution against your customer's appliance, your customer reviews and approves it, the script executes on the appliance, and your customer signs again to release the output back to you. ## The four entities Operations works in terms of four objects. Knowing what each one is makes the rest of the docs much shorter. | Entity | What it is | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Template** | A small file you author (`*.tensor9.tf`, `*.t9.sh`, or `*.t9.kubectl`) describing one runnable operation: its body, declared data access, variables, and required permission tier. | | **Source** | A Git repository whose templates are auto-imported into your control plane. You re-sync to pull new and modified templates as the repo evolves. | | **Ops command** | A single submission of a template against a single customer appliance, with values bound for the template's variables. Runs through a state machine from submission to released output. | | **Pre-approval** | A customer-signed manifest that lets you run a specific template against a specific appliance up to N times within a validity window without per-command approval. | Templates and sources are write-once authoring artifacts. Ops commands are the per-execution objects. Pre-approvals are an optional optimization your customer can apply on top. ## The four-step lifecycle Every ops command moves through the same shape, regardless of which template flavor it uses or which appliance it targets: Your customer reads the template description, the declared data-access tags, your justification reason, and the exact command body and variable values. They pick Approve, Reject, or close. On approval, the appliance prepares to execute. Pre-approved templates skip Steps 1 and 2 silently. The command body runs inside the appliance's sandboxed working directory. Output (stdout, stderr, exit code) is captured and encrypted with a per-command key the appliance holds in its vault. Your customer reviews the decrypted output (decrypted on the appliance, which lives in their own cloud account under their IAM), then signs an Ed25519 release manifest. Only after the signed release does the control plane forward the plaintext output to you. The control plane never sees the plaintext output before release; you never see ciphertext. Every transition is signed, and the chain is verifiable independently with `tensor9 ops command audit verify`. ## What your customer sees When you submit an ops command, your customer is sent (via your existing notification channel) a unique `/support/` web link to their [Customer Portal](/fundamentals/key-concepts#customer-portal). Clicking the link opens a guided approval wizard where they review the command details, approve or reject it, watch it execute, and then sign a release to share the output with you. Your customer can also review and approve pending operations from the **Operations** section of their Customer Portal. You never see your customer's portal session. The only thing that crosses the boundary is the plaintext output (and only after your customer signs the release manifest). On your side, you can track the status of ops commands in the **Vendor Portal** under **Operate → Commands**, or via the CLI with `tensor9 ops command retrieve`. ## Where to go next | If you want to... | Read | | ------------------------------------------------------------------------------- | -------------------------------------------------------------- | | Author a template (file format, variables, permission tiers, data-access tags) | [Authoring templates](/fundamentals/operations/templates) | | Maintain a Git repo of templates as a re-syncable source (the `cmdlib` pattern) | [Git template sources](/fundamentals/operations/sources) | | Submit and track an ops command against your customer's appliance | [Running commands](/fundamentals/operations/lifecycle) | | Let your customer pre-approve a template once for repeated runs | [Standing pre-approvals](/fundamentals/operations/preapproval) | | Understand the keys, signatures, and audit guarantees underneath | [Security model](/fundamentals/operations/security) | ## A minimal end-to-end To make the rest of the docs concrete, here is the shortest possible path from "no templates yet" to "you see released output": ```bash theme={null} # 1. Register a Git repo of templates as a source tensor9 ops template source create \ --sourceType GitHub \ --sourceUrl https://github.com/tensor9ine/cmdlib \ --appName my-app \ --sourceName cmdlib # 2. Submit one of those templates against a customer's appliance tensor9 ops command create \ --appName my-app \ --customerName acme-corp \ --template linux-disk-usage \ --vars MOUNT_PREFIX=/var/lib/myapp \ --commandName check-myapp-disk \ --reason "investigating disk pressure" # 3. Your customer follows the /support/ link sent to them. # Watch lifecycle progress while you wait: tensor9 ops command retrieve --appName my-app --commandName check-myapp-disk ``` Each of those three commands is unpacked in detail on the corresponding subpage. # Built-In Commands Source: https://docs.tensor9.com/fundamentals/operations/cmdlib `cmdlib` is the open-source reference repository of operational command templates that ship ready-to-register against any Tensor9 app. Most teams start by registering it as a source, then layer their own app-specific templates on top. It lives at [github.com/tensor9ine/cmdlib](https://github.com/tensor9ine/cmdlib), Apache 2.0 licensed. How to use cmdlib: register the repo as a source, browse the categories, submit any template like your own. How to use cmdlib: register the repo as a source, browse the categories, submit any template like your own. ## Why use it Three reasons to register `cmdlib` on day one: 1. **Common ops are already written.** Disk usage, top processes, pod restarts, EBS snapshots, Temporal workflow termination, etc. The templates declare appropriate `data_access` tags and permission tiers so your customers can review them quickly. 2. **Templates evolve from real ops incidents.** Bug fixes and new templates land via PR in the cmdlib repo and propagate to every install that re-syncs. 3. **They're good reference for your own templates.** When you write an app-specific template, the closest match in `cmdlib` is usually a good starting point. Browse the directory that matches your target (linux, k8s, aws, etc.) and copy the closest file. ## What's in it Templates are organized by target environment. Each category is a directory under `src/`; every `.tensor9.tf` file inside contributes one template. | Category | Examples | Use when | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | `linux/` | `disk-usage`, `top-processes`, `journalctl-tail`, `dmesg-tail`, `network-connections`, `host-info`, `read-file`, `tail-app-log` | Triaging a Linux appliance host. Read-only diagnostics. | | `darwin/` | `disk-usage`, `disk-list`, `top-processes`, `log-tail`, `pmset-status`, `launchd-list`, `system-profile`, `host-info`, `network-connections`, `read-file`, `tail-app-log` | Triaging a macOS appliance host. Read-only diagnostics. | | `k8s/` | `list-pods`, `list-nodes`, `list-services`, `describe-pod`, `describe-deployment`, `tail-pod-logs`, `tail-deployment-logs`, `top-pods`, `top-nodes`, `get-events`, `list-stuck-pods`, `restart-deployment`, `restart-pods-by-label`, `scale-deployment`, `rollback-deployment`, `delete-pod`, `delete-failed-evicted-pods`, `drain-node`, `read-pod-file`, `read-deployment-file` | Standard SRE operations on the appliance's Kubernetes cluster. | | `aws/` | `find-idle-instances`, `find-untagged-resources`, `find-unused-security-groups`, `list-public-s3-buckets`, `list-old-snapshots`, `rotate-iam-access-key`, `snapshot-ebs-volume`, `rds-snapshot`, `terminate-stopped-instances` | Direct AWS resource operations (EC2, EBS, S3, RDS, IAM). | | `node/` | `cpu-profile`, `heap-snapshot`, `event-loop-lag`, `gc-trace`, `open-handles`, `tail-stderr`, `npm-list`, `process-info` | Debugging a Node.js process inside the appliance. | | `temporal/` | `list-running-workflows`, `list-failed-workflows`, `describe-workflow`, `signal-workflow`, `reset-workflow`, `terminate-workflow` | Inspecting and intervening in Temporal workflows. | | `orchestration/` | `pre-deploy-backup`, `snapshot-then-restart`, `snapshot-then-resize-volume`, `drain-then-terminate-node`, `rollout-restart-then-tail` | Multi-step compositions that chain primitives from the other categories. | Open the category-level `README.md` in the repo for the full per-file inventory and the trade-off notes for each template. ## Registering it ```bash theme={null} tensor9 ops template source create \ --sourceType GitHub \ --sourceUrl https://github.com/tensor9ine/cmdlib \ --appName my-app \ --sourceName cmdlib ``` Source registration walks every `.tensor9.tf` under `src/`, persists each as a template prefixed with its parent directory name (`linux/disk-usage.tensor9.tf` becomes `linux-disk-usage`, so it does not collide with `darwin/disk-usage.tensor9.tf` which becomes `darwin-disk-usage`). You can scope to a subset of categories with `--dirs`: ```bash theme={null} # Only the Kubernetes and AWS templates tensor9 ops template source create \ --sourceType GitHub \ --sourceUrl https://github.com/tensor9ine/cmdlib \ --appName my-app \ --sourceName cmdlib-k8s-aws \ --dirs src/k8s,src/aws ``` After registration, see [Git template libraries](/fundamentals/operations/sources) for the resync / upgrade / retire commands as the upstream evolves. ## Reading a template Every file is a small standalone Terraform template. The canonical shape is documented in [Authoring templates](/fundamentals/operations/templates); `linux/disk-usage.tensor9.tf` is the example used there. Open any file in the repo to see the same pattern applied to a different operation. ## Contributing `cmdlib` accepts pull requests. Useful contributions tend to fall in one of three buckets: * **A new template for an existing category.** Follow the conventions in that category's `README.md`. Match the existing data-access tags and permission-tier choices unless you have a reason not to. * **A new category.** Discuss in an issue first; new categories imply new conventions and shouldn't be added without coordination. * **A bug fix or hardening change to an existing template.** Include a one-line reproducer in the PR description so reviewers can confirm the fix. Templates that mutate state (`ReadWrite` or `Admin` tier) require an extra reviewer and a dry-run plan in the PR description. The bias is toward conservative defaults: prefer `ReadOnly` unless the operation genuinely cannot be expressed that way. ## License Apache 2.0. You can fork the repo, register your fork as a source instead of upstream, and curate the template set that matches your deployment shape. ## Related * [Authoring templates](/fundamentals/operations/templates): the file format used by every `cmdlib` template. * [Git template sources](/fundamentals/operations/sources): how to keep your registered `cmdlib` source up to date as the upstream evolves. # Running Commands Source: https://docs.tensor9.com/fundamentals/operations/lifecycle A submitted ops command moves through a state machine on its way from "you want to run this" to "you have the released output." This page walks your side of that flow: how to submit, how to watch progress, what your customer sees in parallel on the `/support/` link they receive, and how to verify the audit chain after the fact. How a command moves: you submit, your customer approves, the output is released to you. How a command moves: you submit, your customer approves, the output is released to you. ## Submitting a command ```bash theme={null} tensor9 ops command create \ --appName my-app \ --customerName acme-corp \ --template linux-disk-usage \ --vars MOUNT_PREFIX=/var/lib/myapp \ --commandName check-myapp-disk \ --reason "investigating disk pressure on tenant alerts" ``` Required flags: | Flag | Purpose | | ---------------- | --------------------------------------------------------------------------------------------------- | | `--appName` | The Tensor9 app the command targets. | | `--customerName` | Which of your customers' appliances will execute the command. | | `--commandName` | A memorable identifier (3-64 chars, lowercase + hyphens). You'll use this to retrieve, cancel, etc. | One of the following picks the body of the command: | Flag | What it does | | ------------ | ------------------------------------------------------------------------------------------ | | `--template` | Reference an already-imported template by id. Most common. | | `--command` | Inline ad-hoc command body. Useful for one-off shell snippets that don't merit a template. | Common modifier flags: | Flag | Purpose | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--vars` | Comma-separated `KEY=VALUE` pairs for template variables (`--vars NAMESPACE=prod,DEPLOYMENT=api`). See "A note on `--vars` escaping" below. | | `--permissions` | `ReadOnly` (default), `ReadWrite`, or `Admin`. Drives the role minted on the appliance for Kubectl-tier commands. | | `--reason` | Free-text justification. Shown to your customer at approval time and recorded in the audit trail. | | `--timeout` | How long to wait for customer approval before the command times out. Default `7d`. | | `--originRsxId` | Required only for Kubectl ad-hoc and Kubectl templates: the Terraform resource address of the target cluster (e.g. `aws_eks_cluster.production`). Ignored for Tf and Script paths. The id is resolved against your **latest published release** of this app, not against the version currently running on your customer's appliance; if those differ and the resource was renamed across versions, the cluster lookup may fail. There is no flag today to pin the resolution to a specific release. | The `--commandType` flag selects between command shapes. It's almost always inferred from `--template` or auto-defaults; only set explicitly if you're authoring tooling that needs to be specific: | `--commandType` value | When the system uses it | | --------------------- | ----------------------------------------------------------------- | | `Kubectl` | Ad-hoc kubectl invocation (no template). Default for ad-hoc. | | `KubectlFromTmpl` | Auto-selected when `--template` resolves to a Kubectl template. | | `ScriptFromTmpl` | Auto-selected when `--template` resolves to a Script template. | | `TfFromTmpl` | Auto-selected when `--template` resolves to a Terraform template. | #### A note on `--vars` escaping Today `--vars` is a single comma-separated string, which means values cannot contain commas or `=` characters. This is a known limitation; support for repeated `--var KEY=VALUE` flags is planned. Until then, work around with templates whose variable values are constrained to simple alphanumerics + path characters. The action prints the assigned `commandName` and the initial state, then returns. The command is now `Submitted`; your customer's review experience begins next. ## Lifecycle at a glance Ops command lifecycle: three lanes (Command Approval, Execution, Output Release) with happy-path states across the top, terminal unhappy states across the bottom, and an intermediate Cancelling state reachable from Submitted, CmdApproving, or Executing. Ops command lifecycle: three lanes (Command Approval, Execution, Output Release) with happy-path states across the top, terminal unhappy states across the bottom, and an intermediate Cancelling state reachable from Submitted, CmdApproving, or Executing. The happy path has six in-flight states, two terminal happy states, and five terminal unhappy states. One intermediate state (`Cancelling`) covers the brief window where a cancel request has landed but the appliance is still tearing down. ### Happy path in words The command exists; your customer's appliance has not picked it up yet. You see this immediately after `tensor9 ops command create`. Your customer's appliance has the command in its inbox and is waiting on a human review decision. Your customer sees the `/support/` link and walks the four-step approval UI. Your customer approved execution and the appliance is preparing to run the command. The command body is running inside the appliance's sandboxed working directory. The appliance captures stdout / stderr / exit code, uploads each output stream to your blob store (S3 in your customer's account) and stores a small `[blob: ...]\\n` payload on the command record. That payload is then encrypted with a per-command key. Execution finished. The appliance is now waiting for your customer to review the output and decide whether to release it to you. Your customer signed an Ed25519 release manifest. The control plane surfaces the decrypted blob-payload to you; calling `tensor9 ops command retrieve` returns the payload, and you fetch the actual bytes by curling the presigned URL it contains. State advances to `Completed` next. ### Terminal unhappy states | State | What happened | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `CmdRejected` | Your customer rejected the command at review (Step 1 of their approval UI). | | `OutputRejected` | Your customer approved execution, but rejected releasing the output. You never see stdout / stderr. | | `ExecutionFailed` | The command ran but the appliance reported a non-zero exit code or an internal error (or the staleness recovery fired). Terminal: no transition out, no output release path. The failure stderr is uploaded to your blob store like any other output. | | `Cancelled` | You explicitly cancelled (`tensor9 ops command cancel`) before the command was approved. | | `Timeout` | Your customer never decided within the `--timeout` window. | ### Encryption mechanism Output is encrypted with a per-command AES-256-GCM key that the appliance generates on-the-fly. The ciphertext includes a SHA-256 fingerprint of the key (in the AAD), so the decrypt path can find the right key in the appliance's secret store even after a re-execution overwrites the per-scope slot. Keys live in the appliance's secret store under `/t9-private/projection/.../ops-cmd/...` and are deleted after successful release or output rejection. When you see a "decryption failed" alert from the appliance, the likely causes are: (a) a re-execution clobbered the scope-keyed slot before your customer released the previous run's output, (b) the secret store is unreachable, (c) your customer rotated keys mid-flight. The fingerprint addressing is the defense for (a); see the appliance audit log for the specific failure mode. ## Watching progress ```bash theme={null} # Snapshot of every command across this app tensor9 ops command list --appName my-app # Same, including completed and rejected history tensor9 ops command list --appName my-app --history # Drill into one command tensor9 ops command retrieve --appName my-app --commandName check-myapp-disk ``` `retrieve` shows the current state, full audit chain (who approved what, when), and (once `Completed`) the released stdout / stderr / exit code. Both commands accept `--output json` for scripting; pipe into `jq .lifecycle` to poll a single state value. ## What your customer sees When you submit, your customer is sent (via your existing notification channel) a unique `/support/` web link. Clicking it opens a four-step approval UI: If your customer hasn't subscribed a notification channel for ops-command events, submission silently produces no notification. The command still appears in your `tensor9 ops command list` (state: `Submitted`), and your customer would only see it if they happened to visit the support portal directly. Confirm channel subscription with each customer at onboarding; otherwise an "unanswered" command is more likely to mean "your customer doesn't know about it" than "your customer is ignoring it." | Step | What your customer does | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Review** | Reads the template description, declared `data_access` tags, your reason, and the exact template body and variable values you submitted. Picks Approve, Reject, or close. | | **Approve** | Confirms the approval. State advances to `CmdApproved`; the appliance executes immediately. | | **Execute** | Watches a status pane while the appliance runs the command. State stays `Executing` until the output lands. | | **Release** | Reviews the decrypted stdout / stderr / exit code, then signs an Ed25519 release manifest. Picks Release or Reject. | ### What "review" actually shows For Terraform templates, your customer sees the literal HCL source plus the values they're submitting for each `variable`. They do **not** see a `tofu plan` output: a plan would require evaluating data sources, which can only run after the appliance is authorized to do so. Your customer is reviewing "the shape of what will execute" plus the declared `data_access`, `side_effects`, and permission tier, not a fully-resolved diff. Things the HCL surface does NOT pre-evaluate for the reviewer: * **`${var.X}` interpolations stay as literal strings in the displayed HCL.** Your customer sees `${var.MOUNT_PREFIX}` in the command body and the submitted value of `MOUNT_PREFIX` separately; they have to substitute mentally at review time. The approval UI shows the submitted variable values next to the HCL. * **`for_each` cardinality is invisible at review time.** A `for_each = toset(data.aws_s3_buckets.all.buckets[*].name)` does not show whether it will iterate over 3 buckets or 30,000. Bound the potential blast radius via `data_access` + `side_effects` declarations and use `description` to explain the cardinality semantics in plain English. * **`local-exec` heredocs are reviewed as shell.** A multi-line `command = <<-EOT ... EOT` is shown verbatim. Customers reviewing a `kubectl drain ... && kubectl ...` heredoc are reviewing a shell program, not a Terraform plan. Keep heredocs short and named in the `description`. * **`null_resource.triggers` are not re-evaluated against prior state** (there is no prior state; see [Authoring templates](/fundamentals/operations/templates)). A `triggers = { mount_prefix = var.MOUNT_PREFIX }` block makes the resource look like it fires only on change, which is misleading. Document the behavior in the template's `description` or omit the triggers block. Because the review surface is the HCL, the `description` field on `tensor9_command` is what your customer actually reads before approving. Treat it as the plain-English equivalent of the HCL: name the exact APIs called, the expected output shape, the cardinality of any fan-out, and the intended side effects. For Script and Kubectl templates the review surface is the literal script body or kubectl invocation. Same caveats: `${VAR}` references are unsubstituted; the customer reads the script and the variable values side-by-side. ### Trust properties for the release step Release has two properties to explain to your customer: * **The plaintext output passes through the appliance your customer already controls before you see it.** Decryption happens on the appliance (which lives in your customer's cloud account under their IAM); the control plane only ever sees the ciphertext before release. Once your customer signs release, the control plane forwards the plaintext to you. * **The release decision is non-repudiable.** Your customer's signed release manifest is preserved in the audit chain and can be verified independently with [`tensor9 ops command audit verify`](#audit-and-forensics). Pre-approved templates skip the per-command Review and Approve steps; see [Pre-approvals](/fundamentals/operations/preapproval). ## Resource limits and queueing Operations is for diagnostics and short-lived interventions, not for bulk data extraction. The appliance enforces a few limits that you should size your templates against: | Limit | Value | Implication | | --------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Concurrent commands per appliance | 10 | Approved commands dispatch onto the appliance's bounded execution pool (max 10 worker threads). Commands beyond the cap stay in `CmdApproved` and are picked up on the next polling cycle (\~10s) as workers free up. | | Per-command runtime cap | 10 minutes | A command running past 10 minutes will be killed and surfaced as `ExecutionFailed`. | | Stuck-execution recovery | 20 minutes (2 × runtime cap) | A command that's been in `Executing` for 20 minutes (e.g. because the appliance restarted mid-run) is auto-transitioned to `ExecutionFailed` with stderr noting the likely cause. | | Per-stream output cap | 5 GiB | stdout and stderr each route through your blob store (uploaded by the appliance, fetched by the customer's release script and your `retrieve` call via a presigned S3 URL). 5 GiB is the AWS S3 single-PUT ceiling. | | Children per batch | 50 | See "Batches" below. | The 5 GiB cap is high enough that you generally don't think about it. The appliance uploads each stream to your S3 bucket and stores only a small `[blob: bucket=..., key=..., size=..., sha256=...]\\n` payload (encrypted) on the command record. The customer's release script fetches the actual bytes, sha256-verifies them, and shows them in the local preview. Your `tensor9 ops command retrieve` returns the same payload; curling the URL gives you the bytes back. Bulk log dumps (`journalctl --since 24h`, `kubectl logs deployment/...`) flow through without the per-template `| tail -c` self-capping templates used to need. On Kubernetes-form-factor appliances whose blob store does not yet support presigned URLs (MinIO is in this category as of this writing), the appliance falls back to an inline 4 MiB cap with a marker like `[stdout truncated - 4 MB cap]`; the release script's preview still works, but the customer and you see only the first 4 MiB of any stream over the cap. If you're on-call and you see a command stuck in `Executing` for more than five minutes, you can either wait for the 20-minute staleness recovery (automatic) or interrupt it manually with `tensor9 ops command cancel --commandName `. Cancelling an `Executing` command transitions it to `Cancelling` while the appliance tears down; the eventual terminal state depends on what the appliance was doing at the time. See "Cancelling" below for the full state-machine view. ### Not yet enforced Two limits ship in the codebase as constants but no enforcement call site exists today. Treat these as documentation-of-intent, not as guarantees: * **Submissions per hour: 100 per appliance.** Plan around it; do not rely on it. The 101st submission this hour will succeed. * **Cooldown between submissions: 60 seconds per appliance.** Same caveat: not enforced today. Enforcement will land in a future release; until then, rate-limiting in your own scripts is the only real bound. ## Cancelling ```bash theme={null} tensor9 ops command cancel --commandName check-myapp-disk ``` What happens depends on the command's current state: | State at cancel time | Outcome | | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Submitted`, `CmdApproving` | Transitions directly to `Cancelled`. | | `CmdApproved` | Transitions to `Cancelled`; the appliance never picks the command up to execute. | | `Executing` | Transitions to `Cancelling`; the appliance tears down its sandbox while the cancel intent propagates. The eventual terminal state depends on what the appliance was doing (typically `Cancelled`, but may surface as `ExecutionFailed` if the process had already produced partial state that needed teardown). | | `Executed`, `OutputApproving`, or any terminal state | No-op. The cancel arrives too late; output release proceeds normally. | The Cancelling intermediate state exists specifically because a running `tofu apply` or `kubectl` invocation needs a moment to wind down cleanly. In-flight side effects (a partially-created cloud resource, a partially-applied K8s manifest) may or may not be backed out depending on the template; if you cancel a mutating template mid-execution, expect to inspect your customer's environment afterwards. If you submitted many commands by accident and need to cancel them all in one shot: ```bash theme={null} # Cancel everything against this customer regardless of when submitted tensor9 ops command batch cancel-bulk \ --appName my-app \ --customerName acme-corp \ --yes # Or scope to a time window tensor9 ops command batch cancel-bulk \ --appName my-app \ --submittedAfter "2026-05-09T13:55:00Z" ``` `cancel-bulk` lists the commands it's about to cancel and prompts for confirmation; pass `--yes` to skip the prompt for scripts. Only commands still in cancellable states are touched; anything past `CmdApproving` is reported as "skipped" so you know what's still running. ## Batches When you need to fan a command out across multiple customers or appliances, use the batch surface: ```bash theme={null} tensor9 ops command batch submit --appName my-app --file ./batch-spec.json tensor9 ops command batch list --appName my-app tensor9 ops command batch retrieve --appName my-app --batchId tensor9 ops command batch cancel --appName my-app --batchId ``` A batch creates one underlying ops command per appliance. The lifecycle tracks each child command independently, so different customers approving at different times is normal. `batch retrieve` rolls the children up into a single status summary. A single batch is capped at **50 child commands** (one per appliance). For larger fleets, submit multiple batches with a small delay between them to avoid a thundering-herd against the notification path. We are working on a higher cap; let us know what your steady-state fan-out looks like. ## Audit and forensics Three Ed25519 signatures protect every ops command. Together they form a non-repudiation chain your customer can verify independently: | Signature | Signed by | What it proves | | ----------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `commandApproval` | Appliance signing key | "The command body you submitted, with these specific variable values, was approved by this person at this time." | | `outputIntegrity` | Appliance signing key | "Exactly these stdout / stderr / exitCode bytes came out of the command's execution, before any encryption or transit." | | `outputApproval` | Appliance signing key | "These specific output bytes were approved for release to you by this person at this time." | The signatures are stored on the command's audit record and survive the encrypt/decrypt cycle (the integrity signature is computed over plaintext before encryption, then preserved in the ciphertext metadata). Either you or your customer can verify the full chain on a specific command: ```bash theme={null} tensor9 ops command audit verify \ --appName my-app \ --commandName check-myapp-disk ``` The action retrieves the command record + the appliance's pinned signing public key, reconstructs the canonical signed-data for each of the three signatures, and verifies. The three checks use two different trust anchors: * `commandApproval` and `outputApproval` verify the customer's signature on the approval / release manifest, using the public key embedded in the manifest itself. The output reports the `signerPublicKeyFingerprint`, which you (or the customer) should cross-reference against the customer-signing pubkey currently pinned on the appliance vault, which is the actual trust anchor. * `outputIntegrity` verifies the appliance's signature over the plaintext output, against the customer's pinned `opsCmdPubKey`. Output (in the healthy case): ``` Audit chain for check-myapp-disk (Completed) Appliance signer fingerprint: 3a8c4b1f... ✓ commandApproval [OK] ✓ outputIntegrity [OK] ✓ outputApproval [OK] ✓ Audit chain verified. ``` Any failure is surfaced as `[FAIL]` with a one-line reason, and the process exits non-zero. Customers running compliance audits should script this against their full ops-command history; you should run it whenever a customer reports "you ran something I didn't approve" so the disagreement turns into a verifiable record very quickly. `UNSIGNED_LEGACY` records (commands authored before the signature chain was required, with no customer-signed manifest attached) are reported but do not cause a failure exit by default; pass `--strict` to fail on those too. For programmatic use, pass `--output json`. Each entry in the JSON `checks` array has a `trustAnchor`, `signerPublicKeyFingerprint`, `signedBy`, and `signedAt`, which let you distinguish auto-approved-by-pre-approval from manually-approved commands and archive the chain independently. ## Related * [Authoring templates](/fundamentals/operations/templates): the templates this command body comes from. * [Pre-approvals](/fundamentals/operations/preapproval): skip per-command approval for repeated runs. * [Security model](/fundamentals/operations/security): the keys, signatures, and storage-side audit guarantees that make the audit chain non-repudiable. * [Permissions model](/fundamentals/permissions-model): how permission tiers map onto appliance-side roles. # Standing Pre-approvals Source: https://docs.tensor9.com/fundamentals/operations/preapproval Per-command approval is the safe default, but for routine operations (daily disk-usage checks, log tails, health probes) the back-and-forth gets old fast. A pre-approval lets a customer sign once: you can then run the same template against their appliance up to N times within a validity window without per-command human review. Pre-approval is offered to your customer inside the support-portal approval UI as an optional step after a regular command's output is released. Your customer clicks through Step 5 of the approval UI, picks a scope, and pastes a few bash snippets we render into their own terminal. Your job is to ask your customer to enable a pre-approval and explain what it does. How pre-approval works: your customer signs once, you run N times within scope, your customer revokes or the window expires. How pre-approval works: your customer signs once, you run N times within scope, your customer revokes or the window expires. ## What a pre-approval is A pre-approval is an Ed25519-signed manifest your customer creates on their own workstation that authorizes one of your templates to run against one specific appliance, capped by: * **Max runs.** Default 100. (Note: enforcement of this counter is scheduled but not yet shipping; see "Known limits" below.) * **Validity window.** Default 90 days. After expiry, runs fall back to per-command approval. * **Approval level.** `CommandsOnly` (auto-approve the command; customer still releases output per run) or `FullyPreApprove` (auto-approve both command and output release). * **Variable constraints.** Optional regex patterns that variable values you submit must match. A submission whose `--vars` would violate any constraint falls back to per-command approval. Your customer's Ed25519 private key lives in their own secret store: AWS SSM Parameter Store (SecureString) on AWS appliances, or a Kubernetes Secret on Kube appliances. The approval UI renders the bash snippets that put it there. Other backends (GCP, Azure, on-prem) are on the roadmap; the approval UI refuses to advance setup on those today. The matching public key is pinned into the appliance's secret store by a one-time command your customer runs against their own cloud account. Your control plane never sees the private key, and you never see either. ## Approval levels | Level | Command step | Output release step | | ------------------------ | ------------- | ------------------------------------------- | | `CommandsOnly` (default) | Auto-approved | Customer still reviews and releases per run | | `FullyPreApprove` | Auto-approved | Auto-approved | `CommandsOnly` is the right default for most templates: your customer loses the per-command review burden but still controls what bytes you see. `FullyPreApprove` is appropriate for purely operational templates where the output content is uninteresting (a health probe whose output is just `ok`) or where your customer has strong upstream trust in you. ## Granting a pre-approval Pre-approval is offered as Step 5 of the support-portal approval UI, after your customer releases the output of a regular per-command flow. The approval UI walks them through four sub-steps: Your customer picks what to pre-approve: this specific template only, or a broader scope (e.g., any read-only command). They also pick the approval level (`CommandsOnly` or `FullyPreApprove`), validity window (default 90 days), and optional variable constraints. If the appliance has no pinned signing pubkey yet, the approval UI renders three bash snippets your customer pastes into their terminal: 1. `openssl genpkey -algorithm Ed25519 -out priv.pem` to generate the keypair on their workstation. 2. `aws ssm put-parameter --type SecureString` (or `kubectl create secret`) to store the private key in your customer's own secret store. 3. A second `put-parameter` / `create secret` to pin the public key where the appliance can read it. The approval UI polls until the appliance reports the pubkey is visible, then advances. Subsequent pre-approvals on this appliance skip this step. The approval UI shows the canonical manifest your customer is about to sign (so they can audit the exact bytes), plus a bash snippet that: 1. Fetches the private key from your customer's chosen storage. 2. Runs `openssl pkeyutl -sign` over the canonical bytes. 3. Prints a base64 signature. The private key never enters the browser. The signing is local to your customer's workstation. Your customer pastes the base64 signature into the approval UI's text field. The approval UI submits the signed manifest to your control plane, which mirrors it to the appliance's vault on the appliance's next poll. From this point forward, runs matching the pre-approval's scope auto-approve. What you'll see: from the next ops command submission onward, runs that match the pre-approval's scope auto-approve. Your customer sees no notification per run; runs that fall outside the scope still produce a regular `/support/` link. ## Variable constraints Without constraints, a `MOUNT_PREFIX` variable can be set to any string at submit time. The approval UI's Step 5a scope picker accepts per-variable regex patterns that pin acceptable values, e.g.: ``` MOUNT_PREFIX = ^/var/log/.* LIMIT = ^([1-9][0-9]?|100)$ ``` If you submit `tensor9 ops command create` whose `--vars` would violate any constraint, the appliance rejects auto-approval for that submission and falls back to per-command manual approval. The customer still sees the request; they just have to approve it by hand. Constraint patterns are stored in the manifest, signed alongside the rest of the scope, and re-verified on every run. ## Listing active pre-approvals You can list the pre-approvals your customer has granted on a specific appliance: ```bash theme={null} tensor9 ops template approvals list \ --appName my-app \ --applianceId appl_xyz789 ``` Walks every template you've published on this app and reports any pre-approval scoped to the appliance, including: * Template id and current version * Max runs, used runs, runs remaining (subject to the "Known limits" caveat below) * Validity window (`validUntil` timestamp) * Approval level * Signer public-key fingerprint (so an audit can confirm the manifest was signed with the customer's current key, not a leaked older one) * Runtime state (`expired` or active) Read-only. Pass `--output json` to script against the output. This is a vendor-side surface; your customer reads the same information from their support-portal approval UI. ## Verifying a pre-approval is actually active The most common failure: your customer finishes the approval UI's pre-approval flow but the pin step never actually completed (the cloud command was copied wrong, or your customer doesn't have the IAM permission to write to the secret store). Without a pinned public key on the appliance, every pre-approval verification falls back to manual, silently. You keep getting per-command notifications even though your customer thinks they enabled auto-approval. The approval UI polls the appliance for pubkey visibility before letting your customer advance past the setup step, so this failure mode is mostly caught at setup time. But if your customer skipped the poll (closed the tab early, or the approval UI was bypassed by a customer-side script), you can confirm from the vendor side by running audit-verify in JSON mode against a recent command: ```bash theme={null} tensor9 ops command audit verify \ --appName my-app \ --commandName \ --output json | jq '.checks[] | select(.name=="commandApproval") | .approvedBy' ``` If pre-approval is working, recent commands print `"BuyerSignedPreapproval:"`. If pre-approval is silently falling back to manual, the same field will be your customer's email or whatever signer string they used at approval time. A pre-approval configured but consistently falling back almost always means the pinning step from grant didn't take. ## Revoking a pre-approval There are two revocation paths (one targeted, one nuclear). Both happen directly against the customer's own cloud secret store with their own credentials and do not involve your control plane, so revocation works even if your control plane is unreachable. ### Surgical: revoke one pre-approval When your customer opens an active or recently-finished support link, the approval UI's terminal page renders a revocation snippet for the pre-approval they granted in that flow. The snippet is the matching `aws secretsmanager delete-secret` (or `kubectl delete secret`) for the specific pre-approval; your customer pastes it into their terminal and runs it with their own cloud credentials. `tensor9 ops template revoke` is the vendor-side surface to render the same snippet (useful when you need to instruct a customer who isn't actively in a support session): ```bash theme={null} tensor9 ops template revoke \ --appName my-app \ --templateId tmpl_abc123 \ --templateSemver 1.0.0 \ --applianceId appl_xyz789 ``` `--templateSemver` is required because pre-approvals are scoped to a specific template version; if the same template has been evolved, each version has its own pre-approval that has to be revoked independently. On the appliance's next polling cycle (within seconds), the appliance reads the revocation record and stops honoring that specific pre-approval. Other pre-approvals on the same appliance, including those for other of your templates, are unaffected. Runs of the revoked template fall back to per-command approval. The revocation record itself is unsigned by design: anyone with write access to the appliance's secret store can already rotate or delete keys, and writing a fake revocation record can only force a pre-approval into the more-strict per-command path. Adding signatures would add complexity without security gain. Don't try to construct the revocation command by hand from this doc. The exact secret-store path is install-scoped and key-id-suffixed, and the wire format is non-trivial. Always use the approval UI's rendered snippet (or `tensor9 ops template revoke` from the vendor side); the path it produces is the one the appliance actually consults. ### Nuclear: revoke every pre-approval signed by this key To revoke every pre-approval signed by a particular workstation's key (for example, on a workstation that may be compromised), the customer deletes the pinned public key from the appliance's secret store. The approval UI's Setup-Signing-Keypair step prints the exact path on first run; your customer can re-open any support link and walk through Step 2 setup to see the path again (the snippet shape is the inverse of the pin command, just `delete-secret` / `delete secret` in place of `put-secret-value` / `create secret`). ```bash theme={null} # AWS aws ssm delete-parameter --name # Kubernetes kubectl delete secret -n ``` Once the pinned public key is gone, every signed pre-approval whose signer matches the deleted key immediately fails verification on the appliance and falls back to per-command approval. To start granting pre-approvals again, your customer opens a support link, the approval UI detects no pinned key, and walks them back through Step 2 setup with a fresh keypair. ## Lost-key recovery The private key lives in your customer's own SSM Parameter Store or Kubernetes Secret, so workstation loss is not a key-loss event: any workstation with your customer's cloud credentials can refetch the key. The actual lost-key scenarios are: * Your customer accidentally deletes the SecureString / Secret holding the private key (`aws ssm delete-parameter` on the wrong name, `kubectl delete secret` on the wrong target). * Your customer's cloud account is compromised and the private key may have been exfiltrated; they want to rotate to a new key. * The workstation that generated the key is suspected of compromise and your customer wants to invalidate every pre-approval ever signed from it. There is no escrow on our side; the private key never leaves your customer's environment. Recovery is the same shape as nuclear revoke: 1. From any workstation your customer trusts, run the nuclear revoke step above to delete the pinned public key from the appliance. Every pre-approval signed by the lost key immediately stops auto-approving. 2. From the new workstation, open a fresh support link. The approval UI detects no pinned key and walks your customer through Step 2 setup with a new keypair, then through Step 5 to re-grant pre-approvals for each template they want to keep. ## Supported appliance backends Today the pin and revoke command rendering supports: * **AWS Secrets Manager**, for appliances running in AWS accounts. * **Kubernetes secrets**, for appliances running on a Kubernetes cluster your customer controls. Other backends (GCP Secret Manager, Azure Key Vault, HashiCorp Vault, on-prem / air-gapped, multi-region appliances) fall back to a "`# Pinning command not yet templated for ApplianceEnv=$env`" placeholder; your customer cannot use the rendered-command flow on those today. We are adding them as customer demand surfaces. If your deal hinges on one of these, let us know. ## Validity-mid-flight semantics The validity window is checked **twice** per command: once when the command is approved, and again when output is released (for `FullyPreApprove` only). This matters for two cases: * **`FullyPreApprove` cmd whose manifest expires between approve and release.** The command executes (validity check passed at command-approve time) but the output is held for manual release. You see a command stuck at `Executed` waiting for human release, even though everything was supposed to be auto-approved. Restoring auto-release means refreshing the pre-approval and releasing the held output by hand. * **Signature check fails between approve and release.** Per appliance-side policy, signature failures at output release fall back to manual rather than rejecting outright. The reasoning: rejecting output on a command that already ran adds no safety and blocks your customer from reviewing the output. Same symptom on your end: an `Executed` cmd waiting on human release. If a `FullyPreApprove` workload starts producing held outputs after months of clean runs, the most likely cause is one of those two edges, not a bug in your code. ## Trust properties When asking your customer to enable a pre-approval, these are the properties worth naming explicitly: * **Signing keys live entirely in your customer's environment.** The private key sits in your customer's chosen secret store (SSM, Kubernetes Secret, or whatever they pick); the actual signing runs locally in their terminal via `openssl pkeyutl`. The browser and your control plane never touch the private key. A compromised control plane cannot fabricate a pre-approval. * **Pinning is gated by your customer's own cloud credentials.** Only your customer can install or delete the pinned public key on an appliance. * **Constraints are part of the signed scope.** You cannot expand the set of acceptable variable values after the fact without the customer re-signing. * **Revocation is independent of your control plane.** Even if your control plane is down or compromised, a customer can revoke any pre-approval through their own cloud console. ## Known limits * **`maxRuns` is not yet enforced.** The counter that should decrement on each auto-approved run is wired into the data model but the increment call site is not yet shipping. In practice this means a pre-approval with `maxRuns: 100` will auto-approve unlimited times within its validity window. Treat `maxRuns` as documentation-of-intent until enforcement lands. Validity-window expiry, signature check, and revocation all work correctly. ## Related * [Running commands](/fundamentals/operations/lifecycle): how individual ops commands flow through the lifecycle, with and without pre-approval. * [Authoring templates](/fundamentals/operations/templates): how to write templates whose `data_access`, `side_effects`, and permission tier give a customer enough information to pre-approve them. * [Security model](/fundamentals/operations/security): the keys, signatures, and storage-side audit guarantees that underpin pre-approval (including why revocation cannot rewrite history). # Security Model Source: https://docs.tensor9.com/fundamentals/operations/security Operations is a remote-execution surface running inside your customer's own cloud account. This page documents the cryptography that makes it auditable: which keys exist, where they live, which transitions get signed, where signed evidence is preserved, and how anyone (you, your customer, an auditor) can verify the chain after the fact. Trust chain: customer pins their pub key to the appliance controller, the appliance controller signs every lifecycle transition with its own key, signed manifests mirror to the vendor control plane, anyone with the pinned pubkey can verify the chain after the fact. Trust chain: customer pins their pub key to the appliance controller, the appliance controller signs every lifecycle transition with its own key, signed manifests mirror to the vendor control plane, anyone with the pinned pubkey can verify the chain after the fact. ## The trust chain at a glance Two Ed25519 keypairs anchor the system: * **Your customer's signing key**, generated on your customer's workstation when they first sign anything (a pre-approval, a release manifest). The private key lives in a local keychain on their workstation; the public key is pinned into the appliance controller's secret store by a cloud-native write your customer runs in their own cloud account. * **The appliance controller's signing key**, generated by the appliance controller the first time the install runs. The private key lives in the appliance controller's secret store under your customer's IAM (Tensor9 cannot read it). The public key is registered on the install record in your control plane so anyone can fetch it to verify signatures. At every lifecycle transition that needs non-repudiation, the appliance controller signs canonical bytes with its key. The signed manifest is then mirrored to your control plane at sign time. Anyone with the appliance controller's pinned pubkey can replay the verification later and prove what was signed, by whom, at what time. ## What the three signatures prove The lifecycle has three signature transitions. Each covers different bytes and proves a different fact. | Signature | Signed when | What bytes are covered | What it proves | | ----------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `commandApproval` | Your customer approves the request in Step 1 of the approval UI | Canonical form of `{cmdId, decision, at, approver, reason, sha256(rawCommand)}` | The exact command body and variable values were approved by this person at this time | | `outputIntegrity` | Output capture finishes on the appliance controller, before encryption | Canonical form of `{cmdId, executedAt, exitCode, sha256(stdout + stderr)}` where `stdout` / `stderr` are the small `[blob: bucket=..., key=..., size=..., sha256=...]\\n` payloads stored on the command record | The blob-payload bytes the vendor reads on `retrieve` are the exact payloads the appliance controller produced. The payload's embedded `sha256` then binds the URL to the actual stream bytes, which the vendor independently re-verifies on curl. | | `outputApproval` | Your customer releases output in Step 4 | Canonical form of `{cmdId, decision, at, approver, reason, sha256(exitCode + stdout + stderr)}` over the same payloads | The release decision covers these specific blob-payload bytes (and, transitively via the embedded `sha256`, the actual stream bytes) and was made by this person at this time | All three signatures are Ed25519 and live on the command's audit record in your control plane. They survive the encrypt/decrypt cycle: the integrity signature is computed over the plaintext payload before encryption, then preserved alongside the ciphertext metadata. The integrity guarantee chains: 1. The appliance controller signs the cmd record's stdout / stderr (the small blob-payload strings). 2. Inside each payload, a `sha256=` field binds the presigned URL to specific bytes. 3. When the customer or vendor fetches the URL and re-computes the sha256, any byte-level tamper between the appliance controller's upload and the read is detected (the release script exits non-zero on mismatch; you can do the same client-side). The signatures compose: a customer disputing "you ran something I didn't approve" has to either repudiate `commandApproval` (which is signed against their pinned pubkey) or repudiate the pinning step itself (which they did with their own cloud credentials). Neither is plausible without their key material. ## Where keys live ### Your customer's signing key Your customer owns and stores their own private signing key. The support-portal approval UI handles the setup on first approval (the **Set up your signing keypair** step) and renders the bash snippets your customer pastes into their own terminal. Today the approval UI supports two storage backends, matched to the appliance environment: * **AWS appliances**: private key in your customer's AWS SSM Parameter Store as a SecureString (KMS-encrypted at rest). * **Kubernetes appliances**: private key as a Kubernetes Secret in the cluster namespace. The approval UI refuses to advance the setup step on other appliance environments today (GCP, Azure, on-prem). Support for those is on the roadmap. | Property | Value | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Algorithm | Ed25519 | | Private key | Lives in your customer's own secret store: AWS SSM Parameter Store SecureString on AWS appliances, or a Kubernetes Secret on Kube appliances. The browser never touches the private key. | | Public key | Pinned to the appliance controller's secret store at a path the appliance controller verifier reads on every poll. The approval UI's "Pin the public key" snippet writes to the same backend (SSM or Kubernetes Secret) under a parallel path the appliance controller has IAM to read. See [Standing pre-approvals](/fundamentals/operations/preapproval) for the exact paths. | | How it gets there | One-time per appliance: your customer opens any support link, the approval UI detects no pinned key, and walks them through three bash snippets they paste into their terminal: `openssl genpkey -algorithm Ed25519` on their workstation to generate the keypair, an `aws ssm put-parameter` / `kubectl create secret` to store the private key, and a second `put-parameter` / `create secret` to pin the public key. The approval UI polls until the appliance controller reports the pubkey is visible, then advances. | | Used to sign | Pre-approval grant manifests and per-command approval / release manifests. Signing is local: the approval UI's Step 5 / release snippets fetch the private key from your customer's storage, sign the canonical bytes with `openssl pkeyutl`, and emit a base64 signature your customer pastes back into the approval UI. | | Recovery | The private key lives in your customer's own SSM Parameter Store or Kubernetes Secret, so it survives workstation loss: any workstation with your customer's cloud credentials can refetch it and resume signing. If the secret itself is deleted (e.g., your customer accidentally wipes the parameter), see "Lost-key recovery" in [Standing pre-approvals](/fundamentals/operations/preapproval). | ### The appliance controller's signing key | Property | Value | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Algorithm | Ed25519 | | Private key | Lives in the appliance controller's secret store under your customer's IAM. Your control plane cannot read it; only the appliance controller process can. | | Public key | Registered on the install record in your control plane so both you and your customer can fetch it to verify signatures. | | How it gets there | The appliance controller generates the keypair when the install starts and writes the private key to its own vault before any ops command can run. | | Used to sign | Every `commandApproval`, `outputIntegrity`, and `outputApproval` transition the appliance controller acts on. | | Recovery | Lost only if the appliance is destroyed. A fresh install mints a new keypair and registers it on the install record (overwriting the previous pubkey slot). Signatures produced under the old key stop verifying once that overwrite happens; the install record keeps only the current pubkey, not a history. Plan for: if you need to verify old signatures across an appliance rebuild, archive the customer's `opsCmdPubKey` before the rebuild. | The customer-side key proves the human signed ("I, the customer, approved this"); the appliance controller's key proves the bytes were produced inside the appliance ("this output came out of the box at this time, not from elsewhere"). A customer signature without an appliance controller signature would prove approval but not provenance, and the reverse would prove production but not consent, so the audit chain requires both. ## How non-repudiation survives revocation Pre-approval involves two distinct artifacts stored in two distinct places. They serve different purposes and have different deletion properties. | Artifact | Stored in | Purpose | Can your customer delete it? | | ---------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | Signed pre-approval manifest | Your control plane (on the template lineage record) | The non-repudiation evidence: the Ed25519-signed bytes plus the signer's embedded pubkey + fingerprint. The appliance controller fetches it from your control plane at decide time to verify against the controller-pinned customer-signing pubkey. | **No**, your customer has no access to vendor infrastructure. | | Revocation record | Appliance controller vault (your customer's own cloud account) | Drives runtime enforcement: the appliance controller reads the revocation list on every decide cycle and refuses to act on a manifest that's been revoked. The revocation record is unsigned. | **Yes**, this is how revocation works. | **Revocation removes future enforcement but does not erase the historical signature.** If your customer later claims "I never signed that pre-approval," you produce the signed manifest from your control plane, the Ed25519 signature verifies against the embedded signer pubkey (with the fingerprint recorded inline on the manifest, in case the key has since been rotated), and the dispute resolves cryptographically. The non-repudiation property holds because the manifest lives where the customer can't unilaterally erase it: vendor-side infrastructure. If the manifest lived on the appliance controller vault instead, customer-side deletion would be both revocation AND history-rewrite, and a customer could plausibly claim they never signed anything that the appliance controller briefly acted on. ## Key rotation Customers rotate their signing key for a few reasons: retiring a workstation, suspected compromise of the laptop holding the key, an employee with access leaving, or routine rotation per their own security policy. The mechanics: 1. **Generate a fresh keypair** on the new workstation. Your customer opens any support link; the approval UI's Step 2 setup detects no pinned key and walks them through `openssl genpkey` followed by the `put-parameter` / `create secret` snippets to store the private key and pin the public key. 2. **Pin the new pub key** to the appliance controller using the same cloud-native command pattern as the initial pin. Your customer can pin alongside the old key or replace it. 3. **Decide what happens to the old key**: * **Leave it pinned**. Pre-approvals signed by the old key keep auto-approving until they expire. New approvals get signed by the new key. * **Unpin it**. Every pre-approval signed by the old key immediately fails verification. Auto-approval reverts to manual for all of them. Historical verification stays intact either way: your control plane records the signer's pubkey fingerprint at sign time, so signatures on past commands continue to verify against the *recorded* fingerprint, not against whatever is pinned now. `tensor9 ops command audit verify` walks each record using the recorded fingerprint. ## What revocation does and doesn't do Three distinct operations get called "revocation" loosely. They have different effects. | Operation | What it does | What it does NOT do | | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | Delete the appliance controller vault entry for one pre-approval | Stops auto-approval for that pre-approval on the appliance controller's next decide cycle (within seconds) | Does not erase the signed manifest from your control plane. Does not affect other pre-approvals. | | Delete the pinned customer pubkey | Invalidates every pre-approval signed by that key. Subsequent verification on the appliance controller fails closed; the cmd falls back to manual. | Does not erase any history. Does not retroactively undo commands that already executed. | | Reject a per-command approval (in the approval UI) | Sets the command's lifecycle to `CmdRejected`. The command never executes. | Does not erase the (rejected) approval record from your control plane. A future audit can show the customer was offered the command and declined. | Across all three operations, revocation only blocks future enforcement; it does not rewrite history. ## Independent verification Both you and your customer can verify the full audit chain on a specific ops command: ```bash theme={null} tensor9 ops command audit verify \ --appName my-app \ --commandName check-myapp-disk ``` The action: 1. Pulls the command record from your control plane. 2. Fetches the appliance controller's signing pubkey (recorded at sign time, so later key rotation does not invalidate older signatures). 3. Reconstructs the canonical signed-data for each of the three signatures and verifies them against that pubkey. 4. Exits zero if all signatures verify; non-zero on any failure. Output in the healthy case names every check with `[OK]` and prints the appliance controller's signer fingerprint at the top. Failures appear as `[FAIL]` with a one-line reason. For compliance pipelines that need to fail on legacy unsigned records (commands authored before the signature chain was required), pass `--strict`. For programmatic use, pass `--output json` and read the per-check signer fingerprints + signed-payload digests so the chain can be archived independently. A customer disputing "you ran something I didn't approve" doesn't have to take your word. They run `tensor9 ops command audit verify` themselves and the Ed25519 signatures either hold or they don't. ## Related * [Running commands](/fundamentals/operations/lifecycle): the lifecycle states each signature attaches to. * [Standing pre-approvals](/fundamentals/operations/preapproval): pre-approval grant + revocation mechanics in detail. * [Authoring templates](/fundamentals/operations/templates): how data-access and side-effect declarations bound what a customer is consenting to at approval time. # Git Template Sources Source: https://docs.tensor9.com/fundamentals/operations/sources A template source is a Git repository whose `*.tensor9.{tf,sh,kubectl}` and `*.t9.{tf,sh,kubectl}` files become Tensor9 ops command templates in your control plane. You register the source once; later, as the repo evolves, you re-sync to discover new templates, upgrade modified ones, or retire those removed upstream. This is the canonical way to maintain a library. The open-source `tensor9ine/cmdlib` repo is laid out exactly this way and is a good starting point if you're new to authoring templates. How a Git template source works: register the repo, resync to see drift, upgrade to mint new versions. How a Git template source works: register the repo, resync to see drift, upgrade to mint new versions. Three properties keep authoring sane: 1. **Templates live in code review.** Pull requests against the repo gate every change before customers ever see it. 2. **Resync surfaces drift.** When the repo lands new templates or modifies existing ones, `tensor9 ops template source resync` shows the four-bucket diff (unchanged, modified, new, removed) without committing anything. 3. **Upgrades are explicit.** `tensor9 ops template source upgrade` is a separate step. Diff first, then apply only the picks you want, scoped by file or action kind. ## A reference layout The `cmdlib` repo groups templates by category. Each subdirectory contributes one template per `.tensor9.*` file inside it: ``` src/ aws/ find-idle-instances.tensor9.tf list-public-s3-buckets.tensor9.tf ... k8s/ drain-node.tensor9.tf scale-deployment.tensor9.tf ... linux/ disk-usage.tensor9.tf host-info.tensor9.tf ... darwin/ disk-usage.tensor9.tf host-info.tensor9.tf ... ``` Both `linux/` and `darwin/` contain a `disk-usage.tensor9.tf`. They coexist because Tensor9 prefixes the parent directory name onto the imported template, producing `linux-disk-usage` and `darwin-disk-usage` as distinct templates. See "Conflict resolution" below for the cases where the prefix alone does not disambiguate. ## Registering a source ```bash theme={null} tensor9 ops template source create \ --sourceType GitHub \ --sourceUrl https://github.com/tensor9ine/cmdlib \ --appName my-app \ --sourceName cmdlib ``` The action shells out to `gh` to clone the repo at HEAD, walks every matching file in the tree, ships the raw bytes to the appliance for parsing, and persists one template per file plus a single `OpsCmdTmplSrc` row binding the source to its templates. Useful flags: | Flag | Purpose | | ------------------- | ---------------------------------------------------------------------------------------- | | `--branch` | Override the branch (default: the repo's default branch). | | `--dirs` | Comma-separated directories to track (default: every dir containing `.tensor9.*` files). | | `--dry-run` | Walk and print what would be imported. No control-plane writes. | | `--conflict-policy` | How to handle template-name conflicts in the target app. See below. | `gh auth login` must succeed first. The action will fail with a clear message if the GitHub CLI is missing or unauthenticated. ## Conflict resolution The persisted template name is the dir-prefixed file stem (`linux-disk-usage` from `linux/disk-usage.tensor9.tf`). A name conflict happens when that stem already exists in the target app because, say, the same source was registered before and you are re-importing. `--conflict-policy` controls the bulk behavior: | Value | Effect | | ---------------- | --------------------------------------------------------------------------------------------- | | `fail` (default) | Abort the whole register on the first conflicting name. Safest for first-time imports. | | `skip-all` | Skip every conflicting file. The non-conflicting subset still imports. | | `duplicate-all` | Auto-rename each conflicting file with a `-N` suffix (`linux-disk-usage-2`, then `-3`, etc.). | Example: ```bash theme={null} # Re-import cmdlib, dropping anything we already track and importing only the new files tensor9 ops template source create \ --sourceType GitHub \ --sourceUrl https://github.com/tensor9ine/cmdlib \ --appName my-app \ --sourceName cmdlib-v2 \ --conflict-policy skip-all ``` The action's output lists the skipped or renamed files explicitly so you can audit what landed. ## Resyncing against HEAD ```bash theme={null} tensor9 ops template source resync --sourceName cmdlib ``` Resync clones the repo, lifts each file's spec, and reports the four-bucket diff against what's already imported: | Bucket | Meaning | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `Unchanged` | Tracked template whose source file at HEAD has the same content hash. No action needed. | | `Modified` | Tracked template whose source file at HEAD has a different content hash. Run `tensor9 ops template source upgrade` to mint a new lineage version. | | `New available` | File at HEAD that no tracked template owns. Run `tensor9 ops template source upgrade --actions add` to import. | | `Removed upstream` | Tracked template whose source file vanished from HEAD. Run `tensor9 ops template source upgrade --actions retire-here` to drop the source binding. | Resync is read-only; it never persists anything. Run it as often as you like. Resync walks the **whole** repo, not just the directories you selected at register time. If a new sibling directory landed upstream after you registered (a `darwin/` next to your originally-selected `linux/`), its files surface as `New available` automatically. You do not need to re-register the source to track new dirs. ## Applying the diff: upgrade ```bash theme={null} tensor9 ops template source upgrade --sourceName cmdlib ``` By default `tensor9 ops template source upgrade` applies every action the diff implies: evolve modified templates, add newly-available ones, retire-here those removed upstream. Tracked template lineages are versioned, so an upgrade mints `v1.1.0` (or the appropriate semver bump) without breaking customers who pre-approved `v1.0.0`. ### Scoping with `--only` and `--actions` Two filters narrow what the upgrade does: ```bash theme={null} # Only evolve modified templates; don't add new files or retire missing ones tensor9 ops template source upgrade \ --sourceName cmdlib \ --actions upgrade # Only add newly-discovered files; leave existing tracked templates alone tensor9 ops template source upgrade \ --sourceName cmdlib \ --actions add # Apply the diff but only for two specific files tensor9 ops template source upgrade \ --sourceName cmdlib \ --only linux-disk-usage.tensor9.tf,linux-host-info.tensor9.tf ``` `--actions` accepts any subset of `upgrade`, `add`, `retire-here`. `--only` restricts to a CSV of file names matching what `tensor9 ops template source resync` listed. ### What "retire-here" means When a source file vanishes from upstream, customers who ran the template before still have valid pre-approvals against the persisted template versions. `retire-here` deprecates the latest version (so new runs surface a deprecation notice) and drops the binding from the source's tracked id list. The template lineage itself stays in your control plane until you explicitly revoke or evolve it. Customers can keep using already-approved versions until pre-approvals expire. ## Inventory ```bash theme={null} # All sources you've registered tensor9 ops template source list # A single source's full detail tensor9 ops template source retrieve --sourceName cmdlib ``` `retrieve` shows the GitHub coordinates, last-synced commit, selected directories, and the imported template id list. ## Retiring a source ```bash theme={null} tensor9 ops template source retire --sourceName cmdlib ``` Soft-delete. The source row is marked retired; the imported templates stay. They become orphan lineages: usable, evolveable, and revokable on their own, but no longer reachable through `tensor9 ops template source resync` or `tensor9 ops template source upgrade`. Use `--auto` to skip the confirmation prompt in scripts. ## End-to-end example ```bash theme={null} # 1. Register the public cmdlib repo as a template source for `my-app` tensor9 ops template source create \ --sourceType GitHub \ --sourceUrl https://github.com/tensor9ine/cmdlib \ --appName my-app \ --sourceName cmdlib # 2. Some weeks later, a new `darwin/` dir landed upstream. # See what's changed: tensor9 ops template source resync --sourceName cmdlib # 3. Pull only the new darwin templates (don't touch existing linux ones) tensor9 ops template source upgrade \ --sourceName cmdlib \ --actions add # 4. Review what you now expose: tensor9 ops template list --appName my-app ``` ## Related * [Authoring templates](/fundamentals/operations/templates): the file format, variables, data access tags, permission tiers. * [Submitting and tracking ops commands](/fundamentals/operations/lifecycle): use a registered template to actually run something. # Authoring Templates Source: https://docs.tensor9.com/fundamentals/operations/templates An ops command template is a small file you check into a Git repo (or ship straight to your control plane) that describes one runnable operation, the data it touches, and the side effects it has. Customers review the template once and either approve a single execution or pre-approve repeated runs within constraints. The actual work happens inside your customer's appliance, not on your laptop. Why templates: source from Git so PR review gates every change, your customer pre-approves the body and variable constraints once, then you fill in variable values per run with no per-run approval within the signed scope. Why templates: source from Git so PR review gates every change, your customer pre-approves the body and variable constraints once, then you fill in variable values per run with no per-run approval within the signed scope. ## Three template flavors There are three template kinds, each identified by file extension. Both `.tensor9.*` and `.t9.*` are accepted. | Flavor | Extension | Wire-level type | When to use | | ------------- | ---------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------ | | **Terraform** | `.tensor9.tf` / `.t9.tf` | `TfFromTmpl` | Mutating cloud or Kubernetes work, anything that benefits from declarative HCL plus provisioners or data-source queries. | | **Script** | `.tensor9.sh` / `.t9.sh` | `ScriptFromTmpl` | One-shot bash. Metadata lives in a `: <<'TENSOR9' ... TENSOR9` heredoc at the top of the file. | | **Kubectl** | `.tensor9.kubectl` / `.t9.kubectl` | `KubectlFromTmpl` | Plain `kubectl` invocations against the appliance's K8s cluster. | The wire-level type names appear in the audit log and in `tensor9 ops command list` output; you'll mostly see them when correlating events across systems. Most teams reach for Terraform. The provisioner-driven shape works across cloud APIs, Kubernetes, and arbitrary local-exec, and the data-source-driven shape is even more reviewable for read-only queries against a cloud provider's API. The script flavor exists for cases where bash is genuinely simpler. ## Anatomy of a Terraform template There are two canonical shapes for `.tensor9.tf` templates, distinguished by where the actual work happens. Both are common in the open-source `tensor9ine/cmdlib` reference repo. ### Shape A: provisioner-driven (shell pipeline wrapped in HCL) The simplest example is `linux/disk-usage`. It runs `df -h` on the appliance host and surfaces the output. Read-only. ```terraform theme={null} terraform { required_providers { tensor9 = { source = "tf-providers.prod-1.tensor9.com/tensor9/tensor9", version = "~> 2.41" } null = { source = "hashicorp/null", version = "~> 3.2" } } } provider "tensor9" { mode = "ops" } variable "MOUNT_PREFIX" { type = string default = "/" description = "Filesystem path prefix; only mounts under this prefix are reported" validation { condition = startswith(var.MOUNT_PREFIX, "/") error_message = "MOUNT_PREFIX must be an absolute path beginning with /" } } resource "tensor9_command" "this" { name = "disk-usage" display = "Disk usage" description = "Show human-readable disk usage (df -h) for mounts under MOUNT_PREFIX. Read-only." icon = "disk" data_access = ["Storage"] } resource "null_resource" "df" { triggers = { mount_prefix = var.MOUNT_PREFIX } provisioner "local-exec" { command = "df -h | awk 'NR==1 || $6 ~ \"^${var.MOUNT_PREFIX}\"'" } } ``` Your customer reviews the metadata block (`tensor9_command "this"`), the variable, and the literal shell pipeline. On approval, the appliance shells out via `local-exec`, and Tensor9 captures every line prefixed with ` (local-exec): ` from the apply log as the command's stdout, uploads the captured bytes to your blob store (S3 in your customer's account), and stores only a small `[blob: ...]\\n` payload on the command record. Templates can write to stdout freely up to the 5 GiB cap; the release script fetches the actual bytes via the URL during preview, sha256-verifies, and shows them to the customer. ### Shape B: data-source-driven (cloud API queries) For read-only questions that map cleanly onto a cloud provider's API (list buckets, find idle instances, describe a workflow), you can write a template with no `null_resource` at all. Use `data` blocks to query, `locals` to filter / transform, and `output` blocks to emit JSON. ```terraform theme={null} terraform { required_providers { tensor9 = { source = "tf-providers.prod-1.tensor9.com/tensor9/tensor9", version = "~> 2.41" } aws = { source = "hashicorp/aws", version = "~> 5.0" } } } provider "tensor9" { mode = "ops" } variable "REGION" { type = string default = "us-east-1" description = "AWS region for the provider" } provider "aws" { region = var.REGION } resource "tensor9_command" "this" { name = "list-public-s3-buckets" display = "List public S3 buckets" description = "Diagnostic: list S3 buckets whose ACL grants READ or WRITE to AllUsers / AuthenticatedUsers. Read-only." icon = "search" data_access = ["Infrastructure"] } data "aws_s3_buckets" "all" {} data "aws_s3_bucket_acl" "by_bucket" { for_each = toset(data.aws_s3_buckets.all.buckets[*].name) bucket = each.value } locals { public_buckets = [/* ... filtering logic ... */] } output "public_buckets" { value = local.public_buckets } ``` When the template has `output { ... }` blocks and no `local-exec`, the appliance captures `tofu output -json` as the command's stdout. The surface a customer sees at review time names the exact AWS APIs that will be called, which is the information they need to approve. The trade-off is more verbose HCL and per-API network round trips at execution time. ### Three things doing real work in either shape **`provider "tensor9" { mode = "ops" }`**. `mode = "ops"` tells the Tensor9 provider that this template is metadata-only from its perspective. The provider does not need to reach the control plane, and the `endpoint` argument is not required. Use this mode for every ops template; the only time you'd set `mode` to something else is for non-template stacks the same provider also serves. The full provider reference (other modes, every `tensor9_*` resource and data source) is published alongside the provider releases at `tf-providers.prod-1.tensor9.com/tensor9/tensor9`. **`resource "tensor9_command" "this"`**. Exactly one of these per template. It is the metadata block customers review at approval time: | Attribute | Required | Purpose | | -------------- | -------- | ---------------------------------------------------------------------------------------------------------- | | `name` | yes | Machine-readable identifier (lowercase, hyphenated, 1-64 chars). Defaults to the filename stem if omitted. | | `display` | yes | Human-readable label shown on your customer's review screen. | | `description` | yes | One or two sentences describing what the command does. Your customer reads this before approving. | | `icon` | no | Hint for the review UI (e.g. `disk`, `server`, `activity`). | | `data_access` | yes | List of data categories the command can see. See "Data access tags" below. | | `side_effects` | no | List of side-effect tags. See "Side-effect tags" below. | **The execution shape itself**, which is either `null_resource` + `provisioner "local-exec"` (Shape A), or `data` blocks + `output` blocks (Shape B), or a mix. See "Execution model" below for the operational details. #### Source of truth: HCL bytes are canonical The HCL bytes the appliance executes and the metadata your customer sees in the review pane come from the same source. At template-import time, the bytes are parsed once, the parsed `tensor9_command "this"` metadata is rendered into the review pane, and a content hash binds the parsed view to the executed bytes. If the parser ever produced a different result than the executor (a Tensor9 bug), the content hash check would catch the divergence. When you author a template, the literal HCL `description = "..."`, `data_access = [...]`, etc. are exactly what your customer reads at approval time. There is no separate "review-display" layer to keep in sync. #### Outputs are surfaced verbatim; `sensitive = true` does not redact Terraform's `output { value = ...; sensitive = true }` controls how `tofu` displays the value in its own CLI; it does **not** affect the Tensor9 release flow. Any `output` block's value flows through the release pipeline as-is: your customer sees the raw decrypted value in the support portal before signing release, and you see whatever your customer releases. If your template legitimately emits a secret (e.g. `aws/rotate-iam-access-key.tensor9.tf` returns a freshly-minted secret access key), call this out in the template's `description` so your customer's reviewer knows what they're approving: "Emits a fresh AWS access key in the released output." Your customer still sees the value, and the audit chain still records that it was released, but the surface-level expectation is now clear. ## Variables Each `variable` block becomes a parameter you set at submission time: ```bash theme={null} tensor9 ops command create \ --appName my-app \ --customerName acme-corp \ --template linux-disk-usage \ --vars MOUNT_PREFIX=/var/lib/myapp \ --commandName check-myapp-disk \ --reason "investigating disk pressure" ``` Validation runs on the appliance before the template executes. If the value you pass fails the `condition`, the command fails with the `error_message` you wrote. Validation is your customer's only guarantee that the values you supply are constrained, so spend a moment writing real conditions on every variable that can vary. ## Data access tags `data_access` declares the categories of appliance data the command can touch. Your customer sees this list at approval time and can reject if a template asks for more than they expect. Values must come from the canonical enum: | Tag | What it covers | | ----------------- | ----------------------------------------------------------------------------------- | | `Secrets` | API keys, credentials, signing keys. | | `Pii` | Personally-identifiable information. | | `Rbac` | Identity and access bindings. | | `Logs` | Application logs, system logs (`log show`, `journalctl`). | | `Configs` | Application configuration files. | | `Infrastructure` | Pod / node / cluster metadata, AWS resource state, system processes. | | `Network` | Network connections, routing, security-group state. | | `Storage` | Disk usage, file listings, snapshot metadata, EBS volumes. | | `CustomResources` | Application-defined resources (Temporal workflows, custom CRDs, app-owned objects). | | `Metrics` | Application metrics, telemetry endpoints, performance counters. | Pick the smallest set that honestly describes what the command can see. A `kubectl get pods` is `Infrastructure`. A `tail` on an app log is `Logs`. A `df` is `Storage`. Adding more tags than necessary makes templates harder to approve. ## Side-effect tags `side_effects` declares what the command does to the system, beyond just reading. The shape is a free-form list of strings you write; your customer's review screen surfaces each tag verbatim. The convention in `cmdlib` is kebab-case verb-object phrases: `pod-restarts`, `node-drain`, `pod-evictions`, `iam-key-rotation`, `ebs-snapshot`, `rds-snapshot`. ```terraform theme={null} resource "tensor9_command" "this" { name = "drain-node" display = "Drain node" description = "Cordon the node and evict its pods so it can be safely terminated." icon = "server" data_access = ["Infrastructure"] side_effects = ["node-drain", "pod-evictions"] } ``` Read-only templates omit `side_effects` entirely. Mutating templates list every distinct visible-to-customer effect. If you add templates against `cmdlib`, match the existing tag vocabulary in that repo to keep your customer-side review experience consistent. ## Permission tiers Three tiers govern what a Kubectl-tier command can do, set at submission time on `tensor9 ops command create`: | Tier | Intent | | ----------- | --------------------------------------------------------------------------------------------------- | | `ReadOnly` | Inspect-only. The template's RBAC role grants `get` / `list` / `describe` verbs in Kubernetes. | | `ReadWrite` | Mutate state your customer can re-create (restart deployments, scale replicas, rotate credentials). | | `Admin` | Destructive or platform-touching (drop database, delete namespace, rotate root keys). | The tier selects one of three pre-provisioned ServiceAccounts on the appliance (one per tier, set up at install time). Each tier SA has a fixed cluster-wide role granting that tier's verbs; the actuator does not yet narrow the role to the specific namespace or resource list your template declares. Your customer's pre-approval review is where scope is discussed; per-command role minting bounded by the template's declarations is on the roadmap. Choose the lowest tier that still lets the template do its job. The tier is only consulted for Kubectl-tier commands today; Tf and Script templates run with the appliance host's process identity and rely on `data_access` plus `side_effects` for customer-side review. ## Versioning Pin every `required_providers` constraint with `~>` (pessimistic lower bound), not `>=`. The execution model below explains why constraint discipline matters more here than in normal Terraform: ```terraform theme={null} terraform { required_providers { tensor9 = { source = "tf-providers.prod-1.tensor9.com/tensor9/tensor9", version = "~> 2.41" } null = { source = "hashicorp/null", version = "~> 3.2" } aws = { source = "hashicorp/aws", version = "~> 5.0" } kubernetes = { source = "hashicorp/kubernetes", version = "~> 2.20" } } } ``` `~> 2.41` accepts `2.41.x` patch updates but rejects a `3.0.0` major. `~> 5.0` accepts `5.x` minor updates but rejects a `6.0`. Pin every external provider, not just `tensor9`. The provider source URL for the `tensor9` provider is published as part of each release; use the exact URL above. There is no committed lock file (`.terraform.lock.hcl`); the execution model uses a fresh working directory per run, so lock files have nowhere to persist. Constraint discipline is the only thing standing between a clean re-execution and a surprise breaking change in a transitive provider. ## Execution model The operational details of how the appliance turns a template into output are worth understanding, because the model is intentionally narrower than normal Terraform. ### Per-execution working directory Every command execution gets a fresh temporary directory: 1. Appliance creates a unique tempdir. 2. Writes the template's HCL to `main.tf` in that tempdir. 3. Runs `tofu init -input=false` (downloads providers; consults public registry and `tf-providers.prod-1.tensor9.com`). 4. Runs `tofu apply -auto-approve -input=false -no-color -refresh=false`. 5. Runs `tofu output -json`. 6. Captures local-exec stdout (if any) and the JSON output (if any). 7. Deletes the tempdir. State is born and dies with each invocation. There is no remote backend, no state lock, no `terraform.tfstate` reused between runs, no `terraform import` step, no workspaces. ### Network requirements for `tofu init` Because step 3 runs every time and there's no shared plugin cache, the appliance must be able to reach **two registry endpoints over HTTPS on every command**: * `registry.opentofu.org` (or the equivalent Terraform registry your external provider sources resolve through), to fetch `hashicorp/aws`, `hashicorp/null`, `hashicorp/kubernetes`, etc. * `tf-providers.prod-1.tensor9.com`, to fetch the `tensor9` provider. Customers running their appliance in a restricted-egress VPC must whitelist both endpoints. Without that, every ops command fails at the `tofu init` step with a generic "provider download failed" error, which the appliance surfaces as `ExecutionFailed` with the init stderr in the output. Local plugin caching (to amortize download cost across runs) is a roadmap item; for now, every execution pays the full init cost. ### What this implies for what you can write * **Every `resource` block looks like a fresh `Create` to Terraform** on every execution. There is no prior state to compute drift against. For naturally-stateful resources (`aws_iam_user`, `aws_iam_access_key.old_disabled` that depends on something imported), the apply will attempt to create from scratch and fail with `EntityAlreadyExists`. The right strategy depends on the resource: AWS APIs vary, and there is no single workaround. Examples that work in practice: * `RunInstances` and `CreateSnapshot` accept idempotency tokens. * `kubernetes_annotations` is an upsert against an existing object. * `aws s3 cp` honors conditional headers (`--if-none-match`). * `CreateUser` requires a check-then-create via `null_resource + local-exec` shelling out to `aws iam get-user` first. * `cmdlib`'s `aws/snapshot-ebs-volume.tensor9.tf` and `aws/rotate-iam-access-key.tensor9.tf` are good reference patterns. The general rule: assume your `apply` runs against an empty state, and design the body to either upsert correctly or to no-op cleanly when the resource already exists. * **`-refresh=false` is set deliberately.** The appliance does not call provider `read` APIs to verify state before computing the diff (there's no prior state to refresh). Three consequences worth designing for: 1. **No drift detection.** A `kubernetes_annotations` (or similar) resource just calls the upsert; it does not first check whether the upstream object already has the value. This is the right model for at-least-once upserts; it does not give you compare-and-set semantics. 2. **No transactional rollback across resources.** If a `null_resource` runs a multi-step `local-exec` that succeeds halfway and then fails, the next execution starts from an empty state again. Partial-mutation rollback is the template author's responsibility: design idempotent steps or use a single all-or-nothing `local-exec`. 3. **`data` blocks still run every invocation.** They're the only refresh-equivalent and they hit the cloud API on every command, not just when something changes. Bill accordingly. The general rule: every mutating template is at-least-once. Assume you'll re-execute on partial failure and design the body to either upsert correctly or to no-op cleanly when the resource already exists. * **Your customer at review time sees the template's HCL, not a `tofu plan`.** Plans depend on data-source results which can only run after the appliance is authorized to do so. Customers are reviewing "what this template will do" plus the declared `data_access` and `side_effects`, not a fully-resolved diff. For a `for_each` over a data source, the cardinality is not visible at review time; bound the blast radius via `data_access` and `side_effects` declarations rather than relying on review to catch fan-out. * **`timestamp()` in `triggers` is not necessary.** Because state is empty every run, `null_resource` will Create on every execution regardless of triggers. The cargo-cult `run_at = timestamp()` pattern adds nothing here. Use `timestamp()` only when you need a timestamp value at execution time (e.g. `kubectl.kubernetes.io/restartedAt = timestamp()` for K8s rolling restart annotations, or `timeadd(timestamp(), "-${N}h")` for date-window filtering). ### Appliance runtime environment The local-exec environment ships with a known set of binaries and environment variables. Templates can rely on: | Binary | Notes | | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `tofu` | OpenTofu (the apply runner). Always present. | | `bash`, `awk`, `sed`, `grep`, `df`, `tail`, `head`, `find`, `xargs` | Standard POSIX tools. Always present. | | `kubectl` | Pre-configured against the appliance's cluster (no `update-kubeconfig` needed for in-cluster operations). | | `aws` | Available; AWS credentials are injected as env vars (see below) when the template targets an AWS-backed appliance. | | `jq` | Available for JSON pipelines in local-exec. | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN` are injected into the local-exec environment for AWS appliances. The Terraform AWS provider picks them up automatically from the ambient environment. If your template needs a binary not in the list above (e.g. `helm`, `psql`, a custom CLI), it will fail at execution time. We are adding more binaries as patterns surface; flag what you need and we'll discuss. ## Direct-create alternative If you don't want a Git source for a one-off template, you can ship a single template directly from a JSON spec file: ```bash theme={null} tensor9 ops template create \ --appName my-app \ --file ./my-template.json ``` The JSON shape mirrors the on-disk template lifted into a structured form. For a Tf template, the file contains: ```json theme={null} { "name": "disk-usage", "displayName": "Disk usage", "description": "Show human-readable disk usage (df -h). Read-only.", "spec": { "kind": "Tf", "hcl": "terraform {\n required_providers { ... }\n}\n\n..." }, "dataAccess": ["Storage"], "sideEffects": [], "variables": [ { "name": "MOUNT_PREFIX", "default": "/", "description": "Filesystem path prefix", "pattern": "^/.*" } ] } ``` For Script templates, replace the `spec` block with `{"kind":"Script","script":"#!/bin/bash\n..."}`; for Kubectl, with `{"kind":"Kubectl","invocation":"kubectl get pods -n ${NAMESPACE}"}`. #### HCL ↔ JSON field name mapping The on-disk HCL `tensor9_command` block uses snake\_case field names because that's HCL convention; the JSON envelope uses camelCase because that's the wire-protocol convention. Both shapes express the same information; only the spelling differs: | HCL (`tensor9_command "this"`) | JSON envelope | Notes | | ------------------------------ | ------------- | --------------- | | `name` | `name` | identical | | `display` | `displayName` | rename + suffix | | `description` | `description` | identical | | `icon` | `icon` | identical | | `data_access` | `dataAccess` | snake → camel | | `side_effects` | `sideEffects` | snake → camel | Templates imported from a Git source go through HCL → JSON lifting automatically, so authors only deal with one shape at a time. You only see the JSON shape if you use the direct-create path explicitly. Most teams instead register a Git source (see [Git template libraries](/fundamentals/operations/sources)) so the templates live next to the rest of their infra code and benefit from PR review. The JSON path is best reserved for prototypes and test scaffolding. The persisted template name is the dir-prefixed filename stem when the template comes in via a Git source. `linux/disk-usage.tensor9.tf` becomes a template called `linux-disk-usage`. This keeps siblings across directories from colliding (e.g. `linux/disk-usage` and `darwin/disk-usage` coexist as `linux-disk-usage` and `darwin-disk-usage`). The HCL's `tensor9_command "this" { name = ... }` is overridden by the dir-prefixed stem when imported from a source. ## Listing and retrieving templates ```bash theme={null} # List all templates this app exposes tensor9 ops template list --appName my-app # Inspect a single template in full tensor9 ops template retrieve --appName my-app --templateId ``` Both accept `--output json` for scripting. ## Related * [Git template sources](/fundamentals/operations/sources): how a folder of templates becomes a re-syncable source. * [Submitting and tracking ops commands](/fundamentals/operations/lifecycle): how customers see a command and release output. * [Pre-approvals](/fundamentals/operations/preapproval): how to let customers approve a template once and let you run it many times. # Origin Stacks Source: https://docs.tensor9.com/fundamentals/origin-stacks An **origin stack** is the blueprint for your application: the infrastructure-as-code, container definitions, or configuration files that define how your application is built and deployed. Your origin stack represents the canonical version of your application that Tensor9 compiles into customer-specific [deployment stacks](/fundamentals/key-concepts#deployment-stack) for each appliance. ## What is an origin stack? Your origin stack is the source of truth for your application's infrastructure and configuration. It contains all the resources, dependencies, and settings needed to run your application. When you publish an origin stack to Tensor9, your control plane uses it as the template to generate [deployment stacks](/fundamentals/key-concepts#deployment-stack) tailored to each customer's specific environment and form factor. Think of your origin stack as the infrastructure-as-code you already use to deploy your application. It defines everything from compute resources (like Lambda functions, containers, or VMs) to databases, storage buckets, networking configuration, IAM roles, and any other cloud resources your application needs. Tensor9 takes this single origin stack and uses it as a template to generate customized deployments for each customer appliance, adapting the infrastructure to match each customer's target environment. Tensor9 uses your existing origin stack as-is. You don't need to define a new origin stack specifically for Tensor9 deployment - simply publish the same Terraform, Docker Container, Docker Compose, or CloudFormation code you already use to deploy your product. ### Example: A Terraform/OpenTofu origin stack Consider a typical application with an API, database, and storage. Your origin stack might look like this: ```terraform theme={null} #@namespace(max_size=32, delimiter='-') variable "namespace" { type = string description = "Prefix that keeps globally-unique resource names distinct per install" default = "" } # API Lambda function resource "aws_lambda_function" "api" { function_name = "${var.namespace}myapp-api" handler = "index.handler" runtime = "nodejs18.x" role = aws_iam_role.api_role.arn environment { variables = { DB_HOST = aws_db_instance.postgres.endpoint BUCKET_NAME = aws_s3_bucket.data.id NAMESPACE = var.namespace } } } # PostgreSQL database resource "aws_db_instance" "postgres" { identifier = "${var.namespace}myapp-db" engine = "postgres" engine_version = "15.3" instance_class = "db.t3.micro" allocated_storage = 20 db_name = "myapp" username = "admin" password = var.db_password } # S3 bucket for application data resource "aws_s3_bucket" "data" { bucket = "${var.namespace}myapp-data" } # IAM role for Lambda resource "aws_iam_role" "api_role" { name = "${var.namespace}myapp-api-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "lambda.amazonaws.com" } }] }) } ``` This origin stack defines a complete application. When you publish your origin stack, then create a release of it: 1. **For customer A on AWS**: Tensor9 generates a deployment stack that creates these same resources in Customer A's AWS account\` 2. **For customer B on Google Cloud**: Tensor9 translates the resources (RDS becomes Cloud SQL, S3 becomes Cloud Storage, Lambda becomes Cloud Run) and generates a deployment stack for Customer B's Google Cloud account 3. **For customer C private**: Tensor9 translates the resources to use private equivalents (e.g., RDS becomes CloudNativePG, S3 becomes MinIO) and generates a deployment stack for Customer C's private environment 4. **For a test appliance D**: Tensor9 generates a deployment stack that creates these resources in that test appliance in your Tensor9 AWS account\` The origin stack remains unchanged - you maintain a single source of truth while Tensor9 handles the complexity of deploying to multiple customers across different [form factors](/fundamentals/key-concepts#form-factor). ## Supported stack types Tensor9 supports multiple infrastructure-as-code and container formats as origin stacks: | Stack Type | Example | | ---------------------- | --------------------------------------------------------------------------- | | **Terraform/OpenTofu** | `s3://my-bucket/my-tf-workspace.tf.tgz` | | **Docker Container** | `123456789012.dkr.ecr.us-west-2.amazonaws.com/my-app:latest` | | **Docker Compose** | `s3://t9-ctrl-000001/my-app-compose.yml` | | **CloudFormation** | `arn:aws:cloudformation:us-west-2:123456789012:stack/my-app-stack/a1b2c3d4` | | **Kubernetes** | *Manifest or Helm chart embedded in an origin stack of a different type* | ## How origin stacks work When you create a release for an appliance, Tensor9 performs a compilation process that transforms your origin stack into a deployment stack: 1. **Validation**: Your control plane inspects the origin stack to ensure it's well-formed and meets Tensor9 requirements 2. **Porting**: Resources are translated to their equivalents in the target customer's environment based on the appliance's form factor 3. **Observability**: The stack is instrumented to route logs, metrics, and traces back to your observability sink 4. **Packaging**: The result is a deployment stack - a self-contained artifact you deploy using standard tooling Alongside the deployment stack, the compiler also produces an [**audit stack**](/fundamentals/stack-audit): a companion copy of the same infrastructure with the Tensor9 runtime plumbing stripped out, so your customers can review it with their standard IaC security and compliance tooling before applying the deployment stack. ## Publishing origin stacks To make your origin stack available to Tensor9, you publish it to your control plane. The publishing process depends on the stack type: ### Publishing a Terraform origin stack ```bash theme={null} tensor9 stack publish \ -stackType TerraformWorkspace \ -stackS3Key my-stack \ -dir ``` This command: * Compresses your Terraform workspace into a `.tf.tgz` archive * Uploads it to your control plane's S3 bucket * Returns a **native stack id** you'll use to **bind** the origin stack to your app **Example output:** ``` Creating archive of .tf files in /path/to/your/terraform Uploading /tmp/my-stack.tf.tgz to s3://t9-ctrl-000001/terraform-stacks/origins/my-stack.tf.tgz Uploading to s3://t9-ctrl-000001/terraform-stacks/origins/my-stack.tf.tgz...........100% Successfully uploaded stack. The native stack ID is s3://t9-ctrl-000001/terraform-stacks/origins/my-stack.tf.tgz ``` In the above example, the stack's **native stack id** is: `s3://t9-ctrl-000001/terraform-stacks/origins/my-stack.tf.tgz` Pass this native stack id into the `tensor9 stack bind` command to bind this Terraform origin stack to your app. ### Publishing a Docker container origin stack For Docker containers, you push your container image to your Tensor9 AWS account's Elastic Container Registry (ECR): ```bash theme={null} # Authenticate to ECR aws ecr get-login-password --region | docker login --username AWS --password-stdin .dkr.ecr..amazonaws.com # Tag your image docker tag my-app:latest .dkr.ecr..amazonaws.com/my-app:latest # Push to ECR docker push .dkr.ecr..amazonaws.com/my-app:latest ``` In the above example, the stack's **native stack id** is: `.dkr.ecr..amazonaws.com/my-app:latest` Pass this native stack id into the `tensor9 stack bind` command to bind this Docker container origin stack to your app. ### Publishing a Docker Compose origin stack For Docker Compose applications, you publish your docker-compose.yml file to your control plane: ```bash theme={null} tensor9 stack publish \ -stackType DockerCompose \ -stackS3Key my-app-compose \ -file docker-compose.yml ``` This command: * Uploads your docker-compose.yml file to your control plane's S3 bucket * Returns a **native stack id** you'll use to **bind** the origin stack to your app **Example output:** ``` Uploading docker-compose.yml to s3://t9-ctrl-000001/my-app-compose.yml Successfully uploaded stack. The native stack ID is s3://t9-ctrl-000001/my-app-compose.yml ``` In the above example, the stack's **native stack id** is: `s3://t9-ctrl-000001/my-app-compose.yml` Pass this native stack id into the `tensor9 stack bind` command to bind this Docker Compose origin stack to your app. When you create a release, Tensor9 compiles your docker-compose.yml file into a complete Terraform deployment stack with Kubernetes resources. Services with exposed ports get LoadBalancer services, while internal services use ClusterIP. All container images are automatically copied to the appliance's container registry. ### Using a CloudFormation origin stack For CloudFormation stacks, you manage and deploy your stack using the AWS CLI, not `tensor9 stack publish`. Tensor9 references your existing CloudFormation stack and uses its template as your origin stack. First, deploy your CloudFormation stack using the AWS CLI: ```bash theme={null} aws cloudformation create-stack \ --stack-name my-app-stack \ --template-body file://my-template.yaml \ --region us-west-2 ``` Once deployed, get the stack ARN: ```bash theme={null} aws cloudformation describe-stacks \ --stack-name my-app-stack \ --region us-west-2 \ --query 'Stacks[0].StackId' \ --output text ``` This returns your stack ARN, which is your **native stack id**: `arn:aws:cloudformation:us-west-2:123456789012:stack/my-app-stack/a1b2c3d4` You'll use this ARN to bind the CloudFormation origin stack to your app. Tensor9 will use the template from this stack as the blueprint for generating deployment stacks. ## Binding an origin stack to an app After the first time you publish an origin stack, you must **bind** it to your app. Binding registers the stack with your app so you can create **releases**: ```bash theme={null} tensor9 stack bind \ -appName my-app \ -stackType TerraformWorkspace \ -nativeStackId ``` **Important**: You only need to bind once per app. Future publishes of the same stack (with updated code) don't require re-binding. ### Multiple origin stacks per app Some applications consist of multiple independently deployable components. You can bind multiple origin stacks to a single app: ```bash theme={null} # Bind the API stack tensor9 stack bind \ -appName my-app \ -stackType TerraformWorkspace \ -nativeStackId s3://your-bucket/api-stack.tf.tgz # Bind the worker stack tensor9 stack bind \ -appName my-app \ -stackType TerraformWorkspace \ -nativeStackId s3://your-bucket/worker-stack.tf.tgz ``` ## Origin stack requirements To work with Tensor9, your origin stack must meet certain requirements: ### Terraform/OpenTofu requirements * **Valid Terraform**: Your configuration must be valid and pass `tofu validate` * **Backend Configuration**: You can optionally include backend configuration in your origin stack. Tensor9 preserves any backend configuration you provide, giving you control over state management. See [Backend Configuration](/fundamentals/deployments#backend-configuration) for details. * **Root Module Location**: If your root module is in a subdirectory within the archive, specify the path using `//` notation: * `s3://your-bucket/your-tf-workspace.tf.tgz` - root module at archive root * `s3://your-bucket/your-tf-workspace.tf.tgz//infrastructure/terraform` - root module in `infrastructure/terraform/` subdirectory * **Namespace Variable**: Your stack should declare a variable annotated with `@namespace`, which Tensor9 fills in with a value unique to each appliance: ```terraform theme={null} #@namespace(max_size=32, delimiter='-') variable "namespace" { type = string description = "Prefix that keeps globally-unique resource names distinct per install" default = "" } resource "aws_s3_bucket" "data" { bucket = "${var.namespace}my-app-data" # ... } ``` ### Docker container requirements * **Platform Support**: Containers must support the `linux/amd64` architecture * **Registry Access**: Images must be accessible from your Tensor9 AWS account's ECR * **Stateless Design**: Follow container best practices for stateless, immutable deployments ### Docker Compose requirements * **Valid Compose File**: Your docker-compose.yml must be valid (v2.x or v3.x format) * **Container Images in Registries**: All images referenced in your compose file must be pushed to container registries before creating a release * **Named Volumes Only**: Use named volumes for persistent storage (bind mounts are not supported) * **External Secrets**: Secrets must be defined as `external: true` and pre-created in the appliance namespace * **No Build Directive**: The `build:` directive is not supported - all services must reference pre-built images ### CloudFormation requirements * **Valid Template**: Your CloudFormation template must be valid and deployable * **Deployed in Tensor9 AWS Account**: The CloudFormation stack must be deployed in your Tensor9 AWS account * **Parameterized Resources**: Use Parameters to make resource names unique per appliance (the CloudFormation counterpart of Terraform's `@namespace` annotation) * **No Nested Stacks**: Nested CloudFormation stacks are not supported ## Updating origin stacks When you need to release changes to your application, publish a new version of your origin stack: 1. **Make Changes**: Update your infrastructure code or container image 2. **Publish**: Run `tensor9 stack publish` (for Terraform) or push a new container image 3. **Release**: Create a release targeting the appliances you want to update The new origin stack version becomes the source for all future releases. Previously deployed releases continue to run their original stack version until you deploy a new release. ## Best practices Your origin stack must be designed to support multiple independent deployments without resource name collisions. This is called **"parameterization"** - making your infrastructure unique per appliance instance. When you deploy the same origin stack to multiple customer appliances, each deployment must create its own isolated copy of every resource. Without parameterization, multiple deployments would attempt to create resources with identical names, causing conflicts and failures. Declare a variable annotated with `@namespace`. Tensor9 fills it in during compilation, and you prefix resource identifiers with it to make them unique: ```terraform theme={null} #@namespace(max_size=32, delimiter='-') variable "namespace" { type = string description = "Prefix that keeps globally-unique resource names distinct per install" default = "" } # ✓ CORRECT: Resource names begin with the namespace prefix resource "aws_s3_bucket" "data" { bucket = "${var.namespace}myapp-data" } resource "aws_db_instance" "postgres" { identifier = "${var.namespace}myapp-db" } # ✓ CORRECT: Secret names stay literal - Tensor9 isolates them per install data "aws_secretsmanager_secret_version" "api_key" { secret_id = "prod/api/key" } # ✗ INCORRECT: Hard-coded names will cause collisions resource "aws_s3_bucket" "data" { bucket = "myapp-data" # Multiple deployments will conflict } ``` **What needs to be parameterized:** * Resource names and identifiers (S3 buckets, databases, Lambda functions, etc.) * Secret paths in external secret stores * Log group names * IAM role and policy names * Any other globally unique identifiers Without proper parameterization, attempting to deploy to multiple appliances will result in resource creation failures as Terraform tries to create duplicate resources. Always test new origin stack versions in test appliances before releasing to customer appliances: 1. Publish the new origin stack 2. Create a release to a test appliance 3. Deploy and validate the release against the test appliance 4. Create a new release for customer appliances 5. Deploy the release to customer appliances ## Next steps Now that you understand origin stacks, explore these related topics: * [**Appliances**](/fundamentals/appliances): Where your origin stack gets deployed * [**Deployments**](/fundamentals/deployments): How to release your origin stack to appliances * [**Control Plane**](/fundamentals/control-plane): How Tensor9 compiles and manages your origin stacks * [**Quick Start: Terraform**](/getting-started/quick-start-terraform): Step-by-step guide for Terraform origin stacks * [**Quick Start: Docker Compose**](/getting-started/quick-start-docker-compose): Step-by-step guide for Docker Compose origin stacks # Permissions Model Source: https://docs.tensor9.com/fundamentals/permissions-model This page covers the permissions **your control plane** needs inside a customer's environment. The identity and access rules inside your application's stack are adapted separately from AWS, Azure or Google Cloud onto the customer's deployment target. See [Cross-Cloud IAM](/fundamentals/cross-cloud-iam). Tensor9's permissions model defines how your control plane interacts with customer appliances. Since appliances run in customer-owned infrastructure, permissions govern what operations the control plane can perform, when it can perform them, and how customers approve or audit those operations. ## Permissions context When deploying applications through Tensor9: * **Customer appliances** run entirely in the customer's cloud account or private infrastructure * **Your control plane** orchestrates deployments and operations from your vendor account * **Permissions** control what your control plane can do within each customer's appliance The permissions model balances operational capability (your ability to deploy and operate) with customer control (their ability to approve, audit, and restrict access). ## Four-phase permissions model Tensor9 uses a four-phase permissions model where different lifecycle phases require different permission levels: | Phase | Purpose | Permission Level | Customer Control | | ---------------- | ----------------------------------------------------- | ------------------------------------------ | ---------------------------------------- | | **Install** | Initial appliance setup, major infrastructure changes | Highest (full infrastructure provisioning) | Customer-approved, one-time or rare | | **Steady-state** | Observability, monitoring, read-only operations | Minimal (read-only) | Active by default (customer can disable) | | **Deploy** | Deployments, updates, configuration changes | Elevated (read-write on vendor resources) | Customer-approved, time-bounded | | **Operate** | Remote operations, troubleshooting, debugging | Elevated (read-write for debugging) | Customer-approved, time-bounded | Each phase corresponds to an IAM role (in cloud environments) or service account (in Kubernetes environments) with specific permissions scoped to that phase's requirements. ## Install permissions Install permissions are the highest level of permissions, used for initial appliance setup or major infrastructure changes: ### What install permissions allow * **Full infrastructure provisioning**: Create VPCs, networking, IAM roles, databases, and all application resources * **Initial configuration**: Set up observability forwarding, secrets management, DNS * **Major upgrades**: Perform infrastructure migrations or significant architectural changes ### When install permissions are used * **Initial appliance provisioning**: When Tensor9 first creates a customer appliance * **Major version upgrades**: When a new release requires fundamental infrastructure changes * **Infrastructure migrations**: When moving to a different cloud region or architecture Install permissions are rarely used after initial setup. Most deployments use deploy permissions, not install permissions. ### Access control Install permissions are typically: * **Explicitly approved** by the customer for each use * **Time-bounded** to a specific maintenance window * **Audited** with full CloudTrail or audit logging * **Assumed directly** by authorized operators, not via role chaining ## Steady-state permissions Steady-state permissions are the baseline permissions your control plane uses for automated observability collection: ### What steady-state permissions allow * **Observability**: Collect logs, metrics, and traces from the appliance * **Monitoring**: Read resource state, health checks, and configuration for observability * **Discovery**: Enumerate deployed resources and their status for telemetry collection ### What steady-state permissions prevent * Modifying infrastructure (creating, updating, or deleting resources) * Changing IAM policies or security configurations * Accessing customer data or application secrets * Performing destructive operations Steady-state permissions are active by default - unless a customer disables them. Your control plane uses these permissions continuously to maintain observability and report appliance status. ### Implementation In AWS, steady-state permissions are implemented as an IAM role that your control plane assumes: ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:DescribeLogGroups", "logs:DescribeLogStreams", "logs:GetLogEvents", "cloudwatch:GetMetricData", "cloudwatch:ListMetrics", "ec2:Describe*", "rds:Describe*", "s3:ListBucket" ], "Resource": "*" }, { "Effect": "Deny", "Action": [ "iam:*", "*:Delete*", "*:Terminate*" ], "Resource": "*" } ] } ``` The role includes explicit denies for destructive or privilege-escalating actions. ## Deploy permissions Deploy permissions are elevated permissions required for deployments, updates, and configuration changes: ### What deploy permissions allow * **Infrastructure changes**: Create, update, and delete vendor-owned resources * **Deployments**: Apply Terraform/CloudFormation changes to the appliance * **Configuration updates**: Modify application configuration, environment variables * **Scaling operations**: Adjust compute, storage, or other resource capacity ### What deploy permissions prevent * Modifying customer IAM policies or roles * Changing network boundaries or security groups outside vendor resources * Accessing resources not owned by the vendor application ### Conditional access Deploy permissions are not continuously active. Your control plane can only assume the deploy role when specific conditions are met: 1. **Customer approval**: The customer grants temporary deploy access 2. **Time-bounded**: Deploy access expires after a defined window (e.g., 1 hour) 3. **Tagged requests**: Assumption requests include specific tags that customers validate #### Example: AWS conditional assume role ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::VENDOR_ACCOUNT:role/SteadyStateRole" }, "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "aws:RequestTag/DeployAccess": "enabled" }, "DateLessThan": { "aws:CurrentTime": "2024-12-31T23:59:59Z" } } } ] } ``` The steady-state role can only assume the deploy role when: * The `DeployAccess` tag is set to "enabled" * The current time is within the allowed window Customers control when and for how long deploy access is granted. ### Deployment workflow Customer approves a deployment (manually or via automated approval workflow). This sets the `DeployAccess` tag and defines a time window. Your control plane assumes the deploy role using the steady-state role. The assumption succeeds because the conditions are met. Your control plane performs the deployment (e.g., `terraform apply`) using deploy permissions. Infrastructure changes are applied to the appliance. After the time window expires, the deploy role can no longer be assumed. Control plane reverts to steady-state permissions. ## Operations permissions Operations permissions control what operations commands your control plane can execute on appliances: ### Operations types All operations require Operate permissions. Steady-state permissions only allow automated observability collection (logs, metrics, traces), not interactive operations or queries. | Operation Type | Example | Permission Required | | -------------------------- | --------------------------------------------------------------- | -------------------------------- | | **Read-only operations** | `kubectl get pods`, `kubectl describe`, database SELECT queries | Operate | | **Non-destructive write** | Restart a pod, drain a node, clear cache | Operate | | **Resource modifications** | Scale a deployment, update a config, database UPDATE queries | Operate | | **Destructive operations** | Delete resources, terminate instances, DROP tables | Operate (with customer approval) | ### Customer approval for operations Customers can require approval for operations commands: ```bash theme={null} # Vendor initiates an operation tensor9 ops kubectl \ -appName my-app \ -customerName acme-corp \ -originResourceId "aws_eks_cluster.main_cluster" \ -command "kubectl rollout restart deployment/api" # Output: # Operation request submitted. Waiting for customer approval... # Approval granted by jane@acme-corp.com # Executing: kubectl rollout restart deployment/api # deployment.apps/api restarted ``` Customers see the exact command before approving, providing full transparency. ## Operate permissions Operate permissions are elevated permissions required for remote operations, troubleshooting, and debugging: ### What operate permissions allow * **Remote command execution**: Execute kubectl commands, database queries, and cloud CLI commands * **Interactive access**: Create ops endpoints for kubectl, SSH, and database access * **Resource inspection**: Read detailed resource state, logs, and configurations * **Troubleshooting operations**: Restart pods, run diagnostic commands, query databases * **Debugging access**: SSH into VMs, exec into containers, run queries against databases ### What operate permissions prevent * Modifying infrastructure definitions or Terraform state * Deploying new application versions or configuration changes * Changing IAM policies or roles * Accessing resources not owned by the vendor application * Permanent infrastructure modifications (changes are operational, not persistent) ### Conditional access Operate permissions are not continuously active. Your control plane can only assume the operate role when specific conditions are met: 1. **Customer approval**: The customer grants temporary operate access 2. **Time-bounded**: Operate access expires after a defined window (e.g., 1 hour) 3. **Tagged requests**: Assumption requests include specific tags that customers validate #### Example: AWS conditional assume role ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::VENDOR_ACCOUNT:role/SteadyStateRole" }, "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "aws:RequestTag/OperateAccess": "enabled" }, "DateLessThan": { "aws:CurrentTime": "2024-12-31T23:59:59Z" } } } ] } ``` The steady-state role can only assume the operate role when: * The `OperateAccess` tag is set to "enabled" * The current time is within the allowed window Customers control when and for how long operate access is granted. ### Operations workflow Customer approves an operations request (manually or via automated approval workflow). This sets the `OperateAccess` tag and defines a time window. Your control plane assumes the operate role using the steady-state role. The assumption succeeds because the conditions are met. Your control plane performs the operation (e.g., `tensor9 ops kubectl`, `tensor9 ops endpoint create`) using operate permissions. Commands are executed in the appliance. After the time window expires, the operate role can no longer be assumed. Control plane reverts to steady-state permissions. ## Customer approval workflows Customers can configure approval workflows that gate access to elevated permissions: ### Manual approval Customer administrators manually approve deployment requests: 1. Vendor initiates a deployment through the control plane 2. Customer receives a notification (email, Slack, PagerDuty) 3. Customer reviews the deployment details 4. Customer grants deploy access by setting the approval tag 5. Deployment proceeds ### Automated approval Your customers can automate approvals using their cloud provider's access control features. In AWS, this is done using IAM condition keys directly in the trust policy. For example, to allow deploy role assumptions only during business hours: ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::VENDOR_ACCOUNT:role/SteadyStateRole" }, "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "aws:RequestTag/DeployAccess": "enabled" }, "DateGreaterThan": { "aws:CurrentTime": "2024-01-01T09:00:00Z" }, "DateLessThan": { "aws:CurrentTime": "2024-01-01T17:00:00Z" }, "StringEquals": { "aws:RequestedRegion": "us-east-1" } } } ] } ``` For more complex automation (e.g., different windows for weekdays vs. weekends), your customers can use AWS EventBridge to trigger Lambda functions that update the IAM trust policy or set the `DeployAccess` tag based on their schedule. Similar automation can be built in other environments using Google Cloud IAM conditions or Azure Conditional Access policies. ### Release windows Your customers control when deployments are allowed by combining their cloud provider's access control features with their own automation: * **Time-based conditions**: Use condition keys (like `aws:CurrentTime` in AWS) in IAM policies to restrict deployments to specific time ranges * **Tag-based gating**: Control approval tags programmatically via scheduled automation or manual approval workflows * **Always require manual approval**: Omit time-based conditions entirely and rely on manual tag updates This approach provides flexibility while keeping access control entirely within customer-managed policies. ## Permissions across form factors Permission models vary by form factor: | Form Factor | Identity Mechanism | Conditional Access | Audit Logging | Access Management | | ---------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ----------------------------------------------------- | | **AWS** | IAM roles for all permission phases | Conditional assume role for deploy and install permissions | CloudTrail logging for full auditability | Integration with AWS SSO for operator access | | **Google Cloud** | Service accounts for all permission phases | IAM conditions on service account impersonation | Cloud Audit Logs for full auditability | Integration with Google Workspace for operator access | | **Azure** | Managed identities for all permission phases | Conditional access policies for deploy and install permissions | Azure Monitor logging for full auditability | Integration with Azure AD for operator access | | **Private Kubernetes** | Kubernetes service accounts for all permission phases | RBAC roles and role bindings define permissions. Customer-defined approval mechanisms (custom operators, external approval systems) | Audit logging to customer SIEM | Customer-controlled access management | ## Audit logging All permission assumptions and operations are logged: ### What gets logged | Event Type | What Gets Logged | | ----------------------- | ----------------------------------------------------------------------------------------------------------------- | | **Role assumptions** | When steady-state assumes deploy or install roles, including timestamps, requesting identity, and approval status | | **Operations commands** | Every kubectl, database query, or other operation executed on the appliance | | **Deployments** | Terraform/CloudFormation executions, including what resources were created, updated, or deleted | | **Permission denials** | Failed assumption attempts or unauthorized operations, including the reason for denial | ### Where logs are stored | Type | Log System | What It Contains | | ---------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Appliance audit logs** | CloudTrail (AWS), Cloud Audit Logs (GCP), Azure Monitor (Azure), Kubernetes Audit Logs to SIEM (private) | All API calls, role assumptions, resource modifications, and operations within the customer's appliance. Stored in customer-controlled infrastructure. | | **Control plane audit logs** | Vendor-maintained audit trail | All operations initiated by the vendor control plane, deployment history, approval workflows, and permission requests. Stored in vendor infrastructure. | Customers have complete visibility into what your control plane does within their infrastructure. ## Best practices Design your application to operate with steady-state permissions whenever possible. Minimize the frequency of deployments requiring deploy permissions. When requesting deploy access, include clear descriptions of what the deployment does and why elevated permissions are needed. This helps customers make informed approval decisions. Honor customer-defined release windows. Don't request deploy access outside approved time windows unless it's a genuine emergency. After initial appliance setup, avoid operations requiring install permissions. Use deploy permissions for routine updates and only request install permissions for major architectural changes. Maintain your own audit trail of what operations your team performs on customer appliances. This helps with customer support and incident investigation. Clearly document what permissions your application requires for each phase. This helps customers understand and approve your permission model. ## Related topics * [**Appliances**](/fundamentals/appliances): Understanding customer appliances * [**Deployments**](/fundamentals/deployments): How deployments use deploy permissions * [**Operations**](/fundamentals/operations): Remote operations on appliances * [**Observability**](/fundamentals/observability): How observability uses steady-state permissions * [**Cross-Cloud IAM**](/fundamentals/cross-cloud-iam): How your application's own identities, roles and policies are adapted from its origin cloud onto the customer's target cloud * [**AWS IAM service adapter**](/service-adapters/aws/security-identity/aws-iam): Operation-by-operation coverage for AWS-origin IAM adaptation # Secrets Source: https://docs.tensor9.com/fundamentals/secrets This document outlines how Tensor9 manages sensitive data (secrets) within your Infrastructure as Code (IaC) to ensure security and support various ownership models for the services your product relies on. ## Secret ownership models Tensor9 distinguishes between two core models for managing secrets, based on who owns the external service or resource: | Secret Type | Definition | Managed By | Visibility | Example Use Case | | ------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------ | -------------------------------------------------------------- | | Shared Secrets | Used for external services the vendor owns and needs for the product to function. | The vendor (configured on the vendor's side and "tunneled" into the customer's appliance). | Customer may be able to view these secrets. | Sentry.io token for vendor-owned monitoring. | | Customer-Supplied Secrets | Used for external services the customer fully controls and is providing. | The customer (configured within the appliance during setup). | Vendor has no visibility into the actual secret value. | MongoDB Atlas connection string for a customer-owned database. | Vendors and customers must agree on the ownership of all external services and their associated secrets. **Vendor Recommendation**: While Tensor9 supports customer-supplied secrets at the infrastructure (Terraform) level, we strongly encourage migrating to a runtime secret model where the vendor's application exposes an admin setting page for the customer to provide optional secrets after initial setup. This avoids requiring a new deployment for every secret change and makes secrets easier to manage as optional. ## Terraform secret patterns Tensor9 supports the following common industry patterns for integrating secrets into your Terraform configuration: ### 1. Runtime fetching via data source (recommended for external secrets) This is the most common and recommended pattern for external services. Secrets are stored in an external secret store and retrieved during Terraform execution using a data block. | Action | Vendor Workflow | | --------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Define | Create a data block in your Terraform to reference the secret by name. | | Reference | Reference the data source's output (e.g., `data.aws_secretsmanager_secret_version.sentry.secret_string`) in your compute resource definition. | | Storage | Secrets are stored in the respective external system (e.g., AWS Secrets Manager, AWS SSM Parameter Store). | #### Important constraints: * **Do not use ARNs**: Tensor9 does not support referencing secrets by Amazon Resource Name (ARN). Use name-based identifiers instead, as ARNs are absolute and may not be accessible from the customer side of the appliance. * **Use a literal secret name**: Name the secret by the plain path it lives at in your own account (e.g., `prod/sentry/token`). Don't build the name from a variable. Tensor9 reads the name at compile time to identify the secret, and gives each install its own isolated copy on the appliance side, so you don't parameterize the path yourself. ### 2. Variable injection Secrets are passed into the Terraform configuration as variables, typically injected via a CI/CD pipeline or `.tfvars` file. | Action | Vendor Workflow | | --------- | ----------------------------------------------------------------------------- | | Define | Define a variable block with `sensitive = true` and `ephemeral = true`. | | Reference | Reference the variable (e.g., `var.db_password`) in your resource definition. | | Storage | The secret is passed directly as a value into the pipeline. | **Important Constraint**: Tensor9 requires that injected variables contain the actual secret value, not a reference (like a Secrets Manager secret ID). If you need to pass a reference, use the Runtime Fetching (Data Source) pattern instead. ### 3. Platform/infrastructure generated passwords (no Tensor9 support needed) This pattern is for secrets that are automatically generated and managed by the cloud provider or Terraform itself (e.g., a database master password created by AWS RDS). Since the secret is fully encapsulated within the IaC, no special Tensor9 annotation or transfer logic is required. ## Secret annotation and configuration For the Runtime Fetching and Variable Injection patterns, you must explicitly tell Tensor9 which variables and data sources represent secrets and who owns them using a configuration file (via the tuning document or a separate JSON file). Without this annotation, Tensor9 defaults all secret-holding data sources/variables to be Vendor-owned (Shared). ## Secret lifecycle and rotation | Secret Type | When Collected | Vendor Access | | ---------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | Shared Secrets | When the vendor executes `tofu apply` on their compiled stack. | The vendor places the secret in their external secret store; Tensor9 fetches and copies it to the appliance. | | Customer Secrets | During the customer's onboarding/setup process via the UI. | The vendor never has access to the secret value. | Before deployment, Tensor9 performs a pre-flight check to validate that all configured secrets meet the requirements. ### Secret rotation The process for rotating secrets depends on how the secret is referenced in your Terraform: 1. **If passing by Value**: If the secret value is directly embedded in a resource definition (e.g., `password = var.db_password`), an update to the secret will be detected by Terraform, and a normal build and apply (deployment) will update the resource. 2. **If passing by Reference**: If the infrastructure retrieves the secret by reference (e.g., a data source fetching from Secrets Manager), the infrastructure itself will not detect a change to the secret value. * **Vendor Action Required**: The vendor must trigger a refresh or a new deployment. * **Application Logic Required**: The application consuming the secret must have refresh logic built-in to periodically check for updated values. For secrets managed via AWS Secrets Manager, Tensor9 supports the use of rotation Lambda functions. The vendor's controller will copy the rotation Lambda function to the appliance side. This Lambda must use the ARN passed into it at runtime and must not rely on hardcoded ARNs or other references that point to the vendor's original stack resources, since those will not resolve in the appliance. ## Secrets by origin stack type The approach to managing secrets varies by origin stack type. All approaches share the same principle: **store secrets in AWS Secrets Manager or SSM Parameter Store, then pass them to your application as environment variables**. ### Terraform origin stacks Define secrets in AWS Secrets Manager or SSM Parameter Store, then inject them into your compute resources as environment variables. **Defining the secret**: ```terraform theme={null} resource "aws_secretsmanager_secret" "db_password" { name = "prod/db/password" } resource "aws_secretsmanager_secret_version" "db_password" { secret_id = aws_secretsmanager_secret.db_password.id secret_string = var.db_password } ``` **Injecting into ECS Fargate**: ```terraform theme={null} resource "aws_ecs_task_definition" "app" { family = "${var.namespace}myapp" container_definitions = jsonencode([ { name = "app" image = "myapp:latest" # Inject secret as environment variable secrets = [ { name = "DB_PASSWORD" valueFrom = aws_secretsmanager_secret.db_password.arn } ] } ]) } ``` **Injecting into Lambda**: ```terraform theme={null} resource "aws_lambda_function" "api" { function_name = "${var.namespace}myapp-api" environment { variables = { DB_PASSWORD = data.aws_secretsmanager_secret_version.db_password.secret_string } } } data "aws_secretsmanager_secret_version" "db_password" { secret_id = aws_secretsmanager_secret.db_password.id } ``` See [Terraform Origin Stacks](/origin-stack/terraform) for complete documentation. ### CloudFormation origin stacks Use CloudFormation dynamic references to fetch secrets from Secrets Manager during stack deployment, then pass them to your resources. ```yaml theme={null} Parameters: InstanceId: Type: String Resources: # Secret in Secrets Manager DBPasswordSecret: Type: AWS::SecretsManager::Secret Properties: Name: !Sub '${InstanceId}/prod/db/password' SecretString: !Ref DBPassword # ECS Task Definition with secret injection TaskDefinition: Type: AWS::ECS::TaskDefinition Properties: Family: !Sub 'myapp-${InstanceId}' ContainerDefinitions: - Name: app Image: myapp:latest Secrets: - Name: DB_PASSWORD ValueFrom: !Ref DBPasswordSecret ``` See [CloudFormation Origin Stacks](/origin-stack/cloudformation) for complete documentation. ### Docker Compose origin stacks Define secrets in the tuning document, then reference them as environment variables in your compose file. **docker-compose.yml**: ```yaml theme={null} services: api: image: myapp/api:latest environment: - DB_PASSWORD=${DB_PASSWORD} - API_KEY=${API_KEY} ``` **tuning.json**: ```json theme={null} { "version": "V1", "secrets": { "db_password": { "source": "aws_secretsmanager", "secretId": "prod/db/password", "environmentVariable": "DB_PASSWORD" }, "api_key": { "source": "aws_ssm_parameter", "parameter": "/prod/api/key", "environmentVariable": "API_KEY" } } } ``` **Create release with tuning document**: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -customerName acme-corp \ -vendorVersion "1.0.0" \ -tuningDoc tuning.json ``` See [Docker Compose Origin Stacks](/origin-stack/docker-compose#managing-secrets) for complete documentation. ### Docker Container origin stacks Similar to Docker Compose, define secrets in the tuning document: **tuning.json**: ```json theme={null} { "version": "V1", "containerResources": { "cpu": "2", "memory": "4Gi" }, "secrets": { "db_password": { "source": "aws_secretsmanager", "secretId": "prod/db/password", "environmentVariable": "DB_PASSWORD" } } } ``` The secrets are automatically injected as environment variables into your container. See [Docker Container Origin Stacks](/origin-stack/docker#managing-secrets) for complete documentation. ### Kubernetes origin stacks Define secrets in AWS Secrets Manager within your Terraform/CloudFormation wrapper, then reference them in Kubernetes Deployment environment variables: ```terraform theme={null} # Define secret in AWS Secrets Manager resource "aws_secretsmanager_secret" "db_password" { name = "prod/db/password" } # Reference in Kubernetes Deployment resource "kubernetes_deployment" "app" { spec { template { spec { container { env { name = "DB_PASSWORD" value_from { secret_key_ref { name = aws_secretsmanager_secret.db_password.name key = "password" } } } } } } } } ``` Avoid using Kubernetes Secrets directly for sensitive data. Use AWS Secrets Manager and inject values as environment variables. See [Kubernetes Origin Stacks](/origin-stack/kubernetes) for complete documentation. ### Application code (all origin stack types) Regardless of origin stack type, your application code reads secrets from environment variables: ```python theme={null} import os # Read secrets from environment variables db_password = os.environ['DB_PASSWORD'] api_key = os.environ['API_KEY'] ``` Environment variables provide secret values when the application starts. Applications that need to fetch a value while running can keep using AWS Secrets Manager SDK calls such as `get_secret_value()`. For cross-cloud deployments, the [Secrets Manager service adapter](/service-adapters/aws/security-identity/secrets-manager) serves supported calls through the configured adapter endpoint. Review its target-specific limits and configure the application's access before deployment. # Security Model Source: https://docs.tensor9.com/fundamentals/security-model How Tensor9 protects your customers' environments, what you can and cannot access, and what controls your customers have. Your customers have their own view of this model. Share the [Customer Security Model](/customer/security/security-model) with their security teams during procurement. It covers the same architecture from their perspective. ## The Trust Boundary Trust boundary between customer infrastructure and vendor infrastructure Trust boundary between customer infrastructure and vendor infrastructure Three parties are involved, each with a scoped role: | Party | Runs | Can see | Cannot see | | ----------------- | ------------------------------------------ | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | | **Tensor9** | Management plane | Deployment coordination metadata, platform health | Your code, customer data, any credentials | | **You (vendor)** | Control plane (in your AWS account) | Deployment status, health, logs/metrics/traces, secret existence | Secret values, customer application data, customer infrastructure credentials | | **Your customer** | Application environment (in their account) | Full audit log of all vendor activity, their own data, access control settings | None | ## Data Sovereignty Your customers' data never leaves their environment. This is enforced by architecture, not policy. Your software is deployed *into* the customer's account or datacenter. Their data, credentials, and secrets stay inside their boundary. Your control plane receives deployment metadata (status, health, version state) but never application data or credentials. Tensor9's management plane sees even less: only the coordination metadata needed to orchestrate deployments. It has no access to your application code, your customers' data, or anyone's credentials. ### Customer isolation Each of your customers gets a fully isolated environment: * Their own controller instance * Their own application deployment * Their own infrastructure (AWS account, datacenter, or cluster) * No network connectivity to any other customer's environment There is no shared tenancy. A security event at one customer has zero blast radius to another. ## Connectivity and Identity Customer environments never accept inbound connections, not from you and not from Tensor9. The appliance in each customer's environment initiates all communication outbound over mutual TLS, using a unique per-appliance certificate that rotates monthly. Your customers choose the network transport (public HTTPS, AWS PrivateLink, or Tailscale). New appliances authenticate using platform-native identity (AWS STS, GCP JWTs, or Kubernetes OIDC). There are no pre-shared secrets to distribute. See [Connection Security](/fundamentals/connection-security) for the mTLS protocol, certificate lifecycle, and bootstrap process. See [Connectivity](/fundamentals/connectivity) for transport options and network architecture. ## Permissions Your access to each customer's environment is scoped into phases: Install, Observe, Deploy, and Operate. Steady-state access is read-only. Elevated access for deployments and operations is time-bounded and requires customer approval. Explicit deny statements prevent destructive actions regardless of phase. Your customers can disable or revoke any permission tier at any time. Changes take effect immediately. See [Permissions Model](/fundamentals/permissions-model) for the full phase breakdown and IAM implementation. See [Revoking Access](/customer/security/revoking-access) for step-by-step instructions your customers can follow. ## Secrets Your customers create and manage their own credentials in their own infrastructure: Kubernetes Secrets, AWS Secrets Manager, or SSM Parameter Store. You never handle their secrets. The controller detects configured secrets automatically without viewing, transmitting, or storing actual values. See [Secrets](/fundamentals/secrets) for the vendor-side configuration model. See [Credentials and Secrets](/customer/security/credentials-and-secrets) for the customer-facing documentation. ## Customer-Provided Services and Ingress Your customers can substitute Tensor9-managed service equivalents with their own existing databases, message queues, and search engines. They use their own backup policies, patch schedules, and security controls. They also choose how end users access the deployed application: public, IP allowlist, or private network via Tailscale. The same application compiles differently based on their selection. No code changes required on your side. See [Customer-Provided Services](/customizations/customer-provided-services) for supported services and configuration. See [Ingress Control](/customizations/ingress) for available postures. ## Audit and Transparency All vendor activity in customer environments is logged on both sides. Customer-side logging uses their existing tools: CloudTrail, Cloud Audit Logs, or Kubernetes audit. Your control plane maintains its own audit trail. For remote operations, Tensor9 adds a cryptographic signing chain. Customers authorize commands with their own key, the appliance signs output, and the resulting manifest is independently verifiable by either party. See [Operations Security](/fundamentals/operations/security) for the signing protocol and verification commands. ## Tensor9's Security Posture Tensor9 is SOC 2 Type II certified. An independent auditor observed our controls over a sustained period and confirmed they work as designed. The report covers access management, change management, incident response, availability, data protection, and vendor management. Existing customers and prospects can request a copy by [contacting us](https://www.tensor9.com/book-a-demo/). ## What to Share with Your Customers When your customers' security teams evaluate your self-hosted offering, point them to these pages. They describe the same architecture covered here, written for the customer's audience: * [Security Model](/customer/security/security-model) - the trust boundary from their perspective * [Permissions](/customer/security/permissions) - what the controller can and cannot do * [Revoking Access](/customer/security/revoking-access) - how to disable or revoke access * [Credentials and Secrets](/customer/security/credentials-and-secrets) - how secrets are handled * [Connection Security](/fundamentals/connection-security) - mTLS, certificate lifecycle, and bootstrap # Stack Audit Source: https://docs.tensor9.com/fundamentals/stack-audit When you create a release, your control plane compiles your [origin stack](/fundamentals/origin-stacks) into two artifacts: a [**deployment stack**](/fundamentals/key-concepts#deployment-stack) that gets applied into the customer's [appliance](/fundamentals/appliances), and an [**audit stack**](/fundamentals/key-concepts#audit-stack) that is produced for the customer to review. The audit stack exists so that customers can satisfy their own security, policy, and compliance review before any Tensor9-managed infrastructure is applied in their environment. ## What's in the audit stack The audit stack describes the same application infrastructure as the deployment stack, but with the Tensor9 runtime plumbing stripped out: * No Tensor9 Terraform provider * No Tensor9 runtime links or reflection resources What remains is the vendor's application infrastructure as the customer will see it in their account: the compute, storage, networking, and managed service resources that the deployment stack provisions. The audit stack is intended for **review only**. Do not `apply` it. The deployment stack is what actually provisions a working appliance - the audit stack is a companion artifact for inspection. ## How it's produced Audit stack compilation happens automatically as part of `tensor9 stack release create`. No extra flags or configuration are required. If audit stack compilation fails for any reason, the release itself is not blocked - the deployment stack is still produced. Audit stacks are currently produced for Terraform and OpenTofu origin stacks. ## Where the audit stack lands When `tensor9 stack release create` finishes, it writes the audit stack to disk alongside the deployment stack, under a directory named after the target appliance: ``` acme-corp-appliance/ ├── my-app-stack/ # deployment stack - apply this │ └── ...terraform files... └── my-app-stack.audit/ # audit stack - review only └── ...terraform files... ``` The deployment stack directory is what you (or your customer) feed to `tofu init` / `tofu apply`. The `.audit` directory mirrors it for inspection. ## Review workflow A typical pre-deployment review loop: Creating a release for an appliance also writes the compiled deployment and audit stacks to the local filesystem: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -customerName acme-corp \ -vendorVersion "1.0.0" \ -description "Release 1.0.0" \ -notes "Initial release" ``` This produces both the deployment and audit directories for the customer's appliance. Point your existing IaC review tooling at the `.audit` directory. For example: ```bash theme={null} cd acme-corp-appliance/my-app-stack.audit tofu init tofu plan -out=plan.tfplan # Security / policy scans tfsec . checkov -d . ``` Because the audit stack has no Tensor9 providers or runtime plumbing, these tools can plan and scan it standalone and see exactly the resources your customer will be asked to host. Share the `.audit` directory with your customer's security or platform team. They can run it through their own review pipeline - including tools like [Atlantis or Spacelift](/integrations/atlantis-spacelift) - without any Tensor9-specific context. Once review is complete, apply the deployment stack (not the audit stack): ```bash theme={null} cd acme-corp-appliance/my-app-stack tofu init tofu apply ``` ## Related * [Deployment stack](/fundamentals/key-concepts#deployment-stack) - the artifact that actually gets applied * [Origin stacks](/fundamentals/origin-stacks) - the source that the compiler transforms * [Deployments](/fundamentals/deployments) - end-to-end release and deploy workflow * [Atlantis and Spacelift](/integrations/atlantis-spacelift) - gating deployments on IaC review # Testing Source: https://docs.tensor9.com/fundamentals/testing Testing with Tensor9 involves validating releases in **test appliances** before deploying to customer appliances. Test appliances are isolated environments that mirror customer deployment targets, allowing you to verify functionality, performance, and compatibility without affecting production customers. ## Test appliances A **test appliance** is an automatically managed environment that Tensor9 provisions for testing purposes. Test appliances: * Run in vendor-controlled infrastructure (your cloud account) * Mirror the form factor and configuration of customer appliances * Allow you to validate releases before production deployments * Can be created and retired on demand * Support the full deployment lifecycle (compile, deploy, observe, operate) Test appliances function identically to customer appliances but are designed for pre-production validation. This ensures what you test matches what customers will experience. ## Why test in test appliances Testing in test appliances before deploying to customers provides several safeguards: | What to Test | What You Validate | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | **Compilation** | Origin stack compiles correctly for the target form factor. Catch compilation errors, missing resources, or incompatible service mappings. | | **Deployment** | Deployment stack deploys successfully. Identify issues with resource dependencies, configuration errors, or provider-specific constraints. | | **Functionality** | Application works correctly in the target environment. Test API endpoints, database connections, storage access, and other application features. | | **Observability** | Telemetry (logs, metrics, traces) flows correctly to your observability sink. Verify monitoring dashboards and alerts work as expected. | | **Performance** | Application performance in the target form factor. Identify performance differences across cloud providers or managed service implementations. | | **Form Factor Compatibility** | Application works consistently across all target environments when supporting multiple form factors (AWS, Google Cloud, Azure, private). | ## Creating test appliances Create a test appliance using the Tensor9 CLI: ```bash theme={null} tensor9 test appliance create \ -appName \ -formFactorName \ -region ``` ### Example: Create test appliance for AWS ```bash theme={null} tensor9 test appliance create \ -appName my-app \ -formFactorName aws-connected \ -region aws:us-west-2 ``` This creates a test appliance named after your form factor and region. Tensor9 automatically provisions the infrastructure and the appliance becomes available within 10-15 minutes. ### Example: Create test appliances for multiple form factors To test across different cloud providers, create multiple test appliances: ```bash theme={null} # Test appliance for AWS tensor9 test appliance create \ -appName my-app \ -formFactorName aws-connected \ -region aws:us-west-2 # Test appliance for Google Cloud tensor9 test appliance create \ -appName my-app \ -formFactorName gcp-connected \ -region gcp:us-central1 # Test appliance for Azure tensor9 test appliance create \ -appName my-app \ -formFactorName azure-connected \ -region azure:eastus ``` Each test appliance operates independently, allowing you to validate your application across all supported environments. ## Viewing test appliances Use `tensor9 report` to see all your test appliances and their status: ```bash theme={null} tensor9 report ``` Example output showing test appliances: ``` Test Appliances: (2) Test Appliance: test-aws-us-west-2 [id: 000000000000009a]: Status: Live Name: test-aws-us-west-2 Customer: Acme Software Test [id: 000000000000003c] Cloud Details: Aws(us-west-2) Form Factor: aws-connected Appliance Id: 000000000000009a Test Appliance Name: test-aws-connected Test Appliance Id: 000000000000003b:0000000000000006 Installs: Acme Software/my-app → Acme Software Test [id: 0000000000000213:000000000000009a:000000000000016a] Vendor: Acme Software [id: 000000000000003b] App: my-app [id: 0000000000000213] Customer: Acme Software Test [id: 000000000000003c] Release: Acme Software/my-app → Acme Software Test [version: 1.3.0-rc1] Version: 1.3.0-rc1 Outputs: api_endpoint: https://api.test-aws-us-west-2.my-app.acme.co Releases: Effective Releases: (1) Acme Software/my-app → Acme Software Test [version: 1.3.0-rc1] Version: 1.3.0-rc1 Lifecycle: Submitted Deployment: Deployed Created: 4 hours ago Updated: 4 hours ago Description: Release candidate for testing Origin: s3://t9-ctrl-000001/my-stack.tf.tgz Id: s3://t9-ctrl-000001/my-stack.tf.tgz:5ef42c62:0000000000000812 Notes: Testing new features Prepped Releases: (0) Hardware: (updated 45 seconds ago) Uptime: 6 hours 23 minutes Capacity Machines: 1 Test Appliance: test-gcp-us-central1 [id: 000000000000009b]: Status: Live Name: test-gcp-us-central1 Customer: Acme Software Test [id: 000000000000003c] Cloud Details: Gcp(us-central1) Form Factor: gcp-connected Appliance Id: 000000000000009b Installs: Releases: Effective Releases: (0) Prepped Releases: (0) ``` The report shows: * **Status**: Whether the appliance is Live and ready for deployments * **Cloud Details**: The cloud provider and region * **Form Factor**: Which form factor the appliance uses * **Installs**: Apps deployed to the appliance * **Releases**: Deployed releases with version, deployment status, and outputs * **Hardware**: Appliance uptime and capacity ## Testing workflow Follow this workflow to test releases before deploying to customers: Create a test appliance for the target form factor if one doesn't exist: ```bash theme={null} tensor9 test appliance create \ -appName my-app \ -formFactorName aws-connected \ -region aws:us-west-2 ``` Wait for the appliance to reach "Live" status (check with `tensor9 report`). Publish your origin stack to your control plane: ```bash theme={null} tensor9 stack publish \ -stackType TerraformWorkspace \ -stackS3Key my-stack \ -dir /path/to/terraform ``` This uploads your infrastructure code and returns a native stack ID. Create a release targeting your test appliance: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -testApplianceName test-aws-us-west-2 \ -vendorVersion "1.5.0-rc1" \ -description "Testing new analytics feature" ``` Tensor9 compiles your origin stack for the test appliance and downloads the deployment stack into a directory named after your test appliance. Deploy the compiled stack using standard tooling: ```bash theme={null} cd test-aws-us-west-2 tofu init tofu apply ``` The deployment executes in your test appliance. Test your application in the test appliance: ```bash theme={null} # Check outputs from deployment tofu output # Test API endpoints curl https://api.test-aws-us-west-2.my-app.acme.com/health # Run integration tests ./scripts/integration-test.sh https://api.test-aws-us-west-2.my-app.acme.com # Verify observability # Check logs, metrics, and traces in your observability sink ``` Validate functionality, performance, and observability before proceeding. Once validated, deploy the same release to customer appliances: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -customerName acme-corp \ -vendorVersion "1.5.0" \ -description "New analytics feature" cd acme-corp-production tofu init tofu apply ``` The tested release deploys to the customer appliance. ## Testing across form factors When supporting multiple cloud providers or deployment environments, create test appliances for each form factor you support: ```bash theme={null} # Create test appliances for all supported form factors tensor9 test appliance create -appName my-app -formFactorName aws-connected -region aws:us-west-2 tensor9 test appliance create -appName my-app -formFactorName gcp-connected -region gcp:us-central1 tensor9 test appliance create -appName my-app -formFactorName azure-connected -region azure:eastus ``` Then test your release in each environment: ```bash theme={null} # Test in AWS tensor9 stack release create -appName my-app -testApplianceName test-aws-us-west-2 -vendorVersion "1.5.0-rc1" cd test-aws-us-west-2 && tofu init && tofu apply # Validate... # Test in Google Cloud tensor9 stack release create -appName my-app -testApplianceName test-gcp-us-central1 -vendorVersion "1.5.0-rc1" cd test-gcp-us-central1 && tofu init && tofu apply # Validate... # Test in Azure tensor9 stack release create -appName my-app -testApplianceName test-azure-eastus -vendorVersion "1.5.0-rc1" cd test-azure-eastus && tofu init && tofu apply # Validate... ``` This validates that service equivalents work correctly and your application performs consistently across all target environments. ## Integration testing Integrate test appliances into your CI/CD pipeline for automated testing: ```yaml theme={null} # Example GitHub Actions workflow name: Test Release on: push: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install Tensor9 CLI run: | curl -sSL https://t9-artifacts-prod-1.s3.us-west-2.amazonaws.com/install-latest.sh | sh echo "$HOME/.tensor9/bin" >> $GITHUB_PATH - name: Publish origin stack env: T9_API_KEY: ${{ secrets.T9_API_KEY }} run: | tensor9 stack publish \ -stackType TerraformWorkspace \ -stackS3Key my-stack \ -dir ./terraform - name: Create release for test appliance env: T9_API_KEY: ${{ secrets.T9_API_KEY }} run: | tensor9 stack release create \ -appName my-app \ -testApplianceName test-ci \ -vendorVersion "${GITHUB_SHA:0:7}" \ -description "CI test build" - name: Deploy to test appliance run: | cd test-ci tofu init tofu apply -auto-approve - name: Run integration tests run: | cd test-ci API_ENDPOINT=$(tofu output -raw api_endpoint) ./scripts/integration-test.sh $API_ENDPOINT ``` This automatically validates every change in a test appliance before deploying to customers. ## Versioning for test releases Use version suffixes to distinguish test releases from production releases: ```bash theme={null} # Release candidate tensor9 stack release create \ -appName my-app \ -testApplianceName test-aws \ -vendorVersion "1.5.0-rc1" # Beta release tensor9 stack release create \ -appName my-app \ -testApplianceName test-aws \ -vendorVersion "1.5.0-beta" # Development build tensor9 stack release create \ -appName my-app \ -testApplianceName test-aws \ -vendorVersion "1.5.0-dev" ``` After validation, create the production release without the suffix: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -customerName acme-corp \ -vendorVersion "1.5.0" ``` This makes it clear which releases are for testing vs. production. ## Retiring test appliances When you no longer need a test appliance, retire it to clean up resources: ```bash theme={null} tensor9 test appliance retire \ -testApplianceName test-aws-us-west-2 ``` This decommissions the test appliance and cleans up all associated infrastructure. ## Best practices Maintain long-running test appliances for each form factor you support rather than creating them on demand. This speeds up testing and provides consistent validation environments. Always validate releases in test appliances before deploying to customer appliances. This catches issues early and reduces customer-facing incidents. Integrate test appliances into CI/CD pipelines. Automatically deploy to test appliances and run validation tests on every commit or pull request. Regularly deploy to test appliances to keep them in sync with your latest changes - stale test appliances may not accurately reflect production behavior. If you support multiple cloud providers, test in all of them. Service equivalents may behave differently across providers, and testing catches these differences. Use the same observability setup for test appliances as you do for customer appliances. This validates that telemetry flows correctly and your monitoring works. Retire test appliances you no longer need to reduce costs and clutter in your reports. ## Related topics * [**Deployments**](/fundamentals/deployments): How to create releases and deploy * [**Appliances**](/fundamentals/appliances): Understanding customer vs. test appliances * [**Observability**](/fundamentals/observability): Monitoring test appliances * [**Operations**](/fundamentals/operations): Operating test appliances # Quick start: CloudFormation Source: https://docs.tensor9.com/getting-started/quick-start-cloudformation This quick start guide demonstrates how to use Tensor9 with an existing application stack that is already modeled in CloudFormation. Tensor9 provides a click-to-install UI experience for your customers, as well as an AWS console and Tensor9 CLI control plane experience for your team. As you follow this guide, you will: * Set up a Tensor9 control plane in your AWS account, create a Tensor9 app, and bind an AWS CloudFormation stack as the origin for that app. In this guide, we use Nginx as an example app. * Test your app running natively in AWS before rolling it out to customers. * Try the customer click-to-install experience. * Explore your control plane in the AWS Console that allows you to observe and operate all customer appliances. * Deploy code changes and monitor them rolling out to customer appliances. ## Prerequisites * Send an email to [hello@tensor9.com](mailto:hello@tensor9.com) and request: * An API key. You must have an API key to complete the quick start. * A CloudFormation template to create your origin stack for the Nginx sample app. * Create an AWS account for Tensor9. We will refer to this as the Tensor9 AWS account. **Important:** * Your Tensor9 AWS account should be a dedicated AWS account used only for Tensor9. This reduces the risk of conflicts between your app deployed in a Tensor9 appliance and any other software, infrastructure, or resources you might have in a general-purpose AWS account. * Your Tensor9 AWS account must be located in a United States region. Support for non-US regions will be available in the near future. ## Launch an AWS EC2 instance To use the Tensor9 CLI, you need to set up an AWS EC2 instance. 1. Go to your [AWS EC2 dashboard](http://console.aws.amazon.com/ec2/). 2. Select any **United States** region. (**Note:** Support for other regions is coming soon.) 3. Click the **Launch instances** button. 4. Provide a name for your new instance. 5. Under **Application and OS images**, choose your desired configuration. **Important:** You must use an x86 Linux or MacOS machine image. Windows is not supported. For the purposes of deploying Nginx, we recommend using an Ubuntu x86 Amazon Machine Image (AMI) with a t2.small instance type and at least 20GB of storage, but the needs of other apps may vary. 6. Select a new or existing **Key pair** to log in via SSH, if needed. (You can alternatively access your instance via Session Manager.) 7. Review the **Network settings** section. These can be left at their defaults in most environments, but you can customize them, if needed. 8. In the **Configure storage** section, increase your storage to at least 20GB. 9. Under **Advanced details**, select an IAM instance profile that has the **AdministratorAccess** policy assigned to the role. If you do not have such a profile: 1. Click the **Create new IAM profile** link, then click the **Create role** button. 2. Select **EC2** as **Service or use case**. 3. Search for and select **AdministratorAccess** to add the policy to the role. 4. Name the role and click **Create Role**. 5. Return to your **Launch an instance** tab and select the new instance profile that you just created. 10. Click **Launch instance**. ## Install the Tensor9 CLI 1. Connect to your new AWS EC2 instance. 2. [Install the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). 3. Install the Tensor9 CLI and set your API key: Install the **tensor9** CLI via Homebrew (recommended): ```bash theme={null} brew tap tensor9ine/tensor9 brew install tensor9 ``` Alternatively, install via the install script: ```bash theme={null} curl -sSL https://t9-artifacts-prod-1.s3.us-west-2.amazonaws.com/install-latest.sh | sh ``` Then set your Tensor9 API key: ```bash theme={null} export T9_API_KEY= ``` **Note:** An API key is required. If you do not have an API key, send email to [hello@tensor9.com](mailto:hello@tensor9.com) to request one. ## Set up a Tensor9 control plane and Docker container 1. Set up a Tensor9 control plane in your new AWS account (this takes several minutes to complete): ```bash theme={null} tensor9 vendor setup \ -cloud aws \ -region ``` 2. Create a new Tensor9 app. As an example, we use Nginx: `tensor9 app create -name nginx-example -displayName "Nginx example app"` **Note:** The length of the `displayName` field must be 32 characters or fewer. 3. Install Docker, if it is not already installed: `sudo apt install docker.io` 4. Verify that Docker is running: `sudo docker version` 5. Prepare a docker container with your app. In this example, we download Nginx: `sudo docker pull nginx` 6. Push your container to a repository in the Elastic Container Registry (ECR) in your Tensor9 AWS account. For more information and for a list of push commands, go to [your list of private repositories](http://console.aws.amazon.com/ecr/private-registry/repositories), click a repository link, and then click the **View push commands** button. **Note:** The list of push commands includes a `build` step, but because you are pulling down an existing Docker container for Nginx, you do not need to complete the `build` step. The `build` step is only required if you are building your own container. If you are building your own container, make sure the container supports the linux/amd64 architecture. ## Set up a CloudFormation origin stack Now you will create a new CloudFormation stack from your Tensor9 template. 1. Go to the AWS CloudFormation Console in your region. 2. Click the **Create stack** button, then click **With new resources**. 3. Click **Choose an existing template**, then click **Upload a template file**. Upload the CloudFormation template that you previously obtained from Tensor9, then click **Next**. If you do not have a CloudFormation template, send email to [hello@tensor9.com](mailto:hello@tensor9.com). 4. Enter a stack name for your new stack (32 characters or fewer, such as `t9-quickstart-origin-stack`), then click **Next**. 5. On the **Configure Stack Options** screen, you can leave all settings at their defaults. Check the checkbox next to **I acknowledge that AWS CloudFormation might create IAM resources** and click **Next**. 6. Review all settings, then click the **Submit** button to create your stack. The stack takes several minutes to be created. 7. After your new stack is created, click on its name to find its Amazon Resource Name (ARN). The ARN of the stack shows as the **Stack ID**. 8. Bind your stack as the new origin stack: ```bash theme={null} tensor9 stack bind \ -appName nginx-example \ -stackType CloudFormation \ -nativeStackId ``` ## Create a Tensor9 test appliance and test your app 1. Create a test appliance: ```bash theme={null} tensor9 test appliance create \ -appName nginx-example \ -name aws-test ``` 2. View the output of `tensor9 report` to determine when your test appliance is ready for a release. While the test appliance is creating, `tensor9 report` displays output such as: When the appliance is ready, `tensor9 report` displays output such as: 3. Create a release to your test appliance: ```bash theme={null} tensor9 stack release create \ -appName nginx-example \ -testApplianceName aws-test \ -vendorVersion "" \ -description "" \ -notes "" ``` 4. After a few minutes, go to the AWS CloudFormation Console in your region and search for your test appliance name. This is the deployment stack for your test appliance. 5. Select the deployment stack, then click **Events** and **Timeline View**. This view shows the deployment timeline of your stack to your test appliance. 6. Wait several minutes for the deployment to complete. 7. Click the **Resources** tab. Search for **ECSCluster** to find the ECS cluster resource in your test appliance's deployment stack. 8. Click the link in the **Service name** column. Here you can explore deployments, see which containers are running, and view logs and metrics. **Note:** It takes at least 10–15 minutes for the appliance to provision hardware, download the docker container, and start publishing logs and metrics to your deployment stack. When startup logs for Nginx appear in the container, then Nginx is running: The load balancer in your deployment stack should also show as healthy: 8. Run `tensor9 report` to find the DNS name of your appliance. The output has a line labeled **DnsNameForLbAppliance**, which is the DNS name. 9. Visit `http://` to verify that Nginx is working. ## Testing the customer install process As a vendor providing services to customers, you can test your environment by going through the process of installing your app from the perspective of a customer. 1. Create a separate AWS account to simulate your customer. 2. Log into the AWS console as your simulated customer. Verify that you have admin permissions. 3. Create a customer signup link: `tensor9 app signup-link -appName nginx-example` 4. Visit the signup link in the same browser window that you used to log into the AWS console as a simulated customer. A signup screen displays and prompts you for your company information and email address. 5. Enter an email address and a company name, such as [data@examplecompany.com](mailto:data@examplecompany.com) and ExampleCompany, then click the link that appears. The AWS **Quick create stack** screen opens in a new browser tab. 6. On the **Quick create stack** screen, check the checkbox next to **I acknowledge that AWS CloudFormation might create IAM resources** and click the **Create stack** button. 7. Return to the signup page browser tab to click the **Continue** button. 8. Wait approximately 10 minutes for the infrastructure to finish provisioning. The customer test appliance is now ready for use. Next, we will explore the control plane for this customer appliance. ## Explore your control plane in AWS AWS is your control plane for all your customer appliances. You can view the status of your customer appliances in AWS. 1. Visit the deployment stack for a customer appliance by searching for your customer name in the AWS CloudFormation console. Two deployment stacks are displayed: One for your test appliance and the other for your customer appliance. **Note:** The customer deployment stack might not appear immediately. It takes approximately 10 minutes for your deployment stack to deploy for the first time after the appliance is provisioned. 2. Click the link to the customer app deployment stack. 3. Click the **Resources** tab, search for "service", then click the **Service** link to view the ECS cluster for your customer. **Note:** Logs and metrics might not appear immediately. It takes several minutes for your customer's appliance to provision hardware, download your docker container, and start publishing logs and metrics to your deployment stack. When you see startup logs appearing in your container, then you know the customer app is running. 4. Run `tensor9 report` and look for a line labeled **DnsNameForLbAppliance** in the **Customer Appliances** section of the output. This is the DNS name for the customer appliance. For example, `nginx--LbApp-tAUAT8tDV5hn-0c7199c912e8827d.elb.us-west-2.amazonaws.com` . 5. Visit `http://:80` to use Nginx as if you were the customer. ## Release a code change At any time, you can make a code change, build the latest container, and push it to Elastic Container Registry in your Tensor9 AWS account. You must use a Docker image tag that is different from the tag you originally used to deploy the container. **Important:** Verify that your container supports the linux/amd64 platform. 1. Prepare your updated Docker container, then tag the container and push it to your ECR repository just as you did in the [Set up a Tensor9 control plane and Docker container](#set-up-a-tensor9-control-plane-and-docker-container) section. 2. Modify the CloudFormation stack template that you used in the [Set up a CloudFormation origin stack](#set-up-a-cloudformation-origin-stack) section so that it has the URI of the new container you just pushed to your ECR repository. 3. Go to your AWS CloudFormation console and select your Tensor9 origin stack, then click **Update stack** > **Make a direct update**. On the next screen, click **Replace existing template**. Select the template you modified in the previous step, then click the **Next** button. 4. On the following screen, make any modifications that are appropriate for your environment, then click **Next**. On the **Configure stack options** screen, check the checkbox next to **I acknowledge that AWS CloudFormation might create IAM resources**, then click **Next**. 5. On the **Review** screen, verify that all the changes look correct. When you are ready, click the **Submit** button. The stack takes several minutes for update to complete. 6. Retire the current release: ```bash theme={null} tensor9 stack release retire \ -appName nginx-example \ -customerName examplecompany \ -vendorVersion "" ``` 7. Create a new release: ```bash theme={null} tensor9 stack release create \ -appName nginx-example \ -customerName examplecompany \ -vendorVersion "" \ -description "" \ -notes "" ``` **Important:** You must use a different `-vendorVersion` than the currently deployed app. 8. Inspect your customer's deployment stack in the AWS CloudFormation console to find the ECS cluster. A recent blue or green deployment for your change appears. The old software will not be turned off until the new software is up and passing health checks. 9. Run `tensor9 report` and look for a line labeled **DnsNameForLbAppliance** in the **Customer Appliances** section. This is the DNS name for the customer appliance. For example, `nginx-LbApp-6SOn9F6auFHS-bb813efec3dd0c2c.elb.us-west-2.amazonaws.com` . 10. Visit `http://` to test Nginx on the customer appliance and verify that your code changes were applied. # Quick start: Docker Container Source: https://docs.tensor9.com/getting-started/quick-start-docker This quick start guide explains how to use Tensor9 to deploy your existing Docker container as a private, customer-owned appliance. Tensor9 provides a click-to-install UI experience for your customers, as well as an AWS console and Tensor9 CLI control plane experience for your team. As you follow this guide, you will: * Set up a Tensor9 control plane in your AWS account, create a Tensor9 app, and bind your Docker container as the origin for that app. In this guide, we use Nginx as an example app. * Test your app running natively in AWS before rolling it out to customers. * Try the customer click-to-install experience. * Explore your control plane in the AWS Console that allows you to observe and operate all customer appliances. * Deploy code changes and monitor them rolling out to customer appliances. ## Prerequisites * Send an email to [hello@tensor9.com](mailto:hello@tensor9.com) and request an API key. You must have an API key to complete the quick start. * Create an AWS account for Tensor9. We will refer to this as the Tensor9 AWS account. **Important:** * Your Tensor9 AWS account should be a dedicated AWS account used only for Tensor9. This reduces the risk of conflicts between your app deployed in a Tensor9 appliance and any other software, infrastructure, or resources you might have in a general-purpose AWS account. * Your Tensor9 AWS account must be located in a United States region. Support for non-US regions will be available in the near future. ## Launch an AWS EC2 instance To use the Tensor9 CLI, you need to set up an AWS EC2 instance. 1. Go to your [AWS EC2 dashboard](http://console.aws.amazon.com/ec2/). 2. Select any **United States** region. (**Note:** Support for other regions is coming soon.) 3. Click the **Launch instances** button. 4. Provide a name for your new instance. 5. Under **Application and OS images**, choose your desired configuration. **Important:** You must use an x86 Linux or MacOS machine image. Windows is not supported. For the purposes of deploying Nginx, we recommend using an Ubuntu x86 Amazon Machine Image (AMI) with a t2.small instance type and at least 20GB of storage, but the needs of other apps may vary. 6. Select a new or existing **Key pair** to log in via SSH, if needed. (You can alternatively access your instance via Session Manager.) 7. Review the **Network settings** section. These can be left at their defaults in most environments, but you can customize them, if needed. 8. In the \*\*Configure storage section, increase your storage to at least 20GB. 9. Under **Advanced details**, select an IAM instance profile that has the **AdministratorAccess** policy assigned to the role. If you do not have such a profile: 1. Click the **Create new IAM profile** link, then click the **Create role** button. 2. Select **EC2** as **Service or use case**. 3. Search for and select **AdministratorAccess** to add the policy to the role. 4. Name the role and click **Create Role**. 5. Return to your **Launch an instance** tab and select the new instance profile that you just created. 10. Click **Launch instance**. ## Install the Tensor9 CLI 1. Connect to your new AWS EC2 instance. 2. [Install the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). 3. Install the Tensor9 CLI and set your API key: Install the **tensor9** CLI via Homebrew (recommended): ```bash theme={null} brew tap tensor9ine/tensor9 brew install tensor9 ``` Alternatively, install via the install script: ```bash theme={null} curl -sSL https://t9-artifacts-prod-1.s3.us-west-2.amazonaws.com/install-latest.sh | sh ``` Then set your Tensor9 API key: ```bash theme={null} export T9_API_KEY= ``` **Note:** An API key is required. If you do not have an API key, send email to [hello@tensor9.com](mailto:hello@tensor9.com) to request one. ## Set up a Tensor9 control plane and Docker container 1. Set up a Tensor9 control plane in your new AWS account (this takes several minutes to complete): ```bash theme={null} tensor9 vendor setup \ -cloud aws \ -region ``` 2. Create a new Tensor9 app. As an example, we use Nginx: `tensor9 app create -name nginx-example -displayName "Nginx example app"` **Note:** The length of the `displayName` field must be 32 characters or fewer. 3. Install Docker, if it is not already installed: `sudo apt install docker.io` 4. Verify that Docker is running: `sudo docker version` 5. Prepare a docker container with your app. In this example, we download Nginx: `sudo docker pull nginx` 6. Push your container to a repository in the Elastic Container Registry (ECR) in your Tensor9 AWS account. For more information and for a list of push commands, go to [your list of private repositories](http://console.aws.amazon.com/ecr/private-registry/repositories), click a repository link, and then click the **View push commands** button. **Note:** The list of push commands includes a `build` step, but because you are pulling down an existing Docker container for Nginx, you do not need to complete the `build` step. The `build` step is only required if you are building your own container. If you are building your own container, make sure the container supports the linux/amd64 architecture. 7. Bind your container as your new origin stack: ```bash theme={null} tensor9 stack bind \ -appName nginx-example \ -stackType DockerContainer \ -nativeStackId ``` You can find your container URI by checking the chosen docker image in the ECR repository. The URI should look like this: `.dkr.ecr..amazonaws.com/:` ## Create a Tensor9 test appliance and test your app 1. Create a test appliance: ```bash theme={null} tensor9 test appliance create \ -appName nginx-example \ -name aws-test ``` 2. View the output of `tensor9 report` to determine when your test appliance is ready for a release. While the test appliance is creating, `tensor9 report` displays output such as: When the appliance is ready, `tensor9 report` displays output such as: 3. Create a release to your test appliance: ```bash theme={null} tensor9 stack release \ -appName nginx-example \ -testAppliance aws-test \ -vendorVersion "" \ -description "" \ -notes "" ``` 4. After a few minutes, go to the AWS CloudFormation Console in your region and search for your test appliance name. This is the deployment stack for your test appliance. 5. Select the deployment stack, then click **Events** and **Timeline View**. This view shows the deployment timeline of your stack to your test appliance. 6. Wait several minutes for the deployment to complete. 7. Click the **Resources** tab. Search for **ECSCluster** to find the ECS cluster resource in your test appliance's deployment stack. 8. Click the link in the **Service name** column. Here you can explore deployments, see which containers are running, and view logs and metrics. **Note:** It takes at least 10–15 minutes for the appliance to provision hardware, download the docker container, and start publishing logs and metrics to your deployment stack. When startup logs for Nginx appear in the container, then Nginx is running: The load balancer in your deployment stack should also show as healthy: 8. Run `tensor9 report` to find the DNS name of your appliance. The output has a line labeled **DnsNameForLbAppliance**, which is the DNS name. 9. Visit `http://` to verify that Nginx is working. ## Testing the customer install process As a vendor providing services to customers, you can test your environment by going through the process of installing your app from the perspective of a customer. 1. Create a separate AWS account to simulate your customer. 2. Log into the AWS console as your simulated customer. Verify that you have admin permissions. 3. Create a customer signup link: `tensor9 app signup-link -appName nginx-example` 4. Visit the signup link in the same browser window that you used to log into the AWS console as a simulated customer. A signup screen displays and prompts you for your company information and email address. 5. Enter an email address and a company name, such as [data@examplecompany.com](mailto:data@examplecompany.com) and ExampleCompany. 6. Click the link to start the install process and provision infrastructure. Wait approximately 10 minutes for the infrastructure to finish provisioning. After this short waiting period, the customer test appliance is ready for use. Next, we will explore the control plane for this customer appliance. ## Explore your control plane in AWS AWS is your control plane for all your customer appliances. You can view the status of your customer appliances in AWS. 1. Visit the deployment stack for a customer appliance by searching for your customer name in the AWS CloudFormation console. Two deployment stacks are displayed: One for your test appliance and the other for your customer appliance. **Note:** The customer deployment stack might not appear immediately. It takes approximately 10 minutes for your deployment stack to deploy for the first time after the appliance is provisioned. 2. Click the link to the customer app deployment stack. 3. Click the **Resources** tab, search for "service", then click the **Service** link to view the ECS cluster for your customer. **Note:** Logs and metrics might not appear immediately. It takes several minutes for your customer's appliance to provision hardware, download your docker container, and start publishing logs and metrics to your deployment stack. When you see startup logs appearing in your container, then you know the customer app is running. 4. Run `tensor9 report` and look for a line labeled **DnsNameForLbAppliance** in the **Customer Appliances** section of the output. This is the DNS name for the customer appliance. For example, `nginx--LbApp-tAUAT8tDV5hn-0c7199c912e8827d.elb.us-west-2.amazonaws.com` . 5. Visit `http://:80` to use Nginx as if you were the customer. ## Release a code change At any time, you can make a code change, build the latest container, and push it to Elastic Container Registry in your Tensor9 AWS account using the same Docker image tag you used to deploy the container. **Important:** Verify that your container supports the linux/amd64 platform. 1. Retire the current release: ```bash theme={null} tensor9 stack release retire \ -appName nginx-example \ -customerName examplecompany \ -vendorVersion "" ``` 2. Create a new release: ```bash theme={null} tensor9 stack release \ -appName nginx-example \ -customerName examplecompany \ -vendorVersion "" \ -description "" \ -notes "" ``` **Important:** You must use a different `-vendorVersion` than the currently deployed app. 3. Inspect your customer's deployment stack in the AWS CloudFormation console to find the ECS cluster. A recent blue or green deployment for your change appears. The old software will not be turned off until the new software is up and passing health checks. 4. Run `tensor9 report` and look for a line labeled **DnsNameForLbAppliance** in the **Customer Appliances** section. This is the DNS name for the customer appliance. For example, `nginx-LbApp-6SOn9F6auFHS-bb813efec3dd0c2c.elb.us-west-2.amazonaws.com` . 5. Visit `http://:80` to test Nginx on the customer appliance and verify that your code changes were applied. # Quick start: Docker Compose Source: https://docs.tensor9.com/getting-started/quick-start-docker-compose This quick start guide explains how to use Tensor9 to deploy your existing Docker Compose app as a private appliance. As you follow this guide, you will: * Set up a Tensor9 control plane in your AWS account and bind a Docker Compose file as the origin stack for your app. * Test your app running in an appliance. * Release infrastructure changes. ## Prerequisites * Send an email to [hello@tensor9.com](mailto:hello@tensor9.com) and request an API key. You must have an API key to complete the quick start. * Create an AWS account for Tensor9. We will refer to this as the Tensor9 AWS account. **Important:** * Your Tensor9 AWS account should be a dedicated AWS account used only for Tensor9. This reduces the risk of conflicts between your app deployed in a Tensor9 appliance and any other software, infrastructure, or resources you might have in a general-purpose AWS account. * Your Tensor9 AWS account must be located in a United States region. Support for non-US regions will be available in the near future. * Install [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html) in your environment, and [set up an AWS CLI profile](https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-files.html) that has admin permissions to your new AWS account. * Have a docker-compose.yml file ready with your multi-container application. * Ensure all container images referenced in your compose file are pushed to container registries (ECR, Docker Hub, GitHub Container Registry, etc.). ## Install OpenTofu or Terraform In your environment, you'll need to install Terraform/OpenTofu CLI. This guide assumes you are using OpenTofu and uses the `tofu` command throughout. ## Install Tensor9 CLI Install the **tensor9** CLI via Homebrew (recommended): ```bash theme={null} brew tap tensor9ine/tensor9 brew install tensor9 ``` Alternatively, install via the install script: ```bash theme={null} curl -sSL https://t9-artifacts-prod-1.s3.us-west-2.amazonaws.com/install-latest.sh | sh ``` Then set your Tensor9 API key: ```bash theme={null} export T9_API_KEY= ``` **Note:** An API key is required. If you do not have an API key, send email to [hello@tensor9.com](mailto:hello@tensor9.com) to request one. ## Set up a Tensor9 control plane and create a new app 1. Set up a Tensor9 control plane in your new AWS account (this takes several minutes to complete): ```bash theme={null} tensor9 vendor setup \ -cloud aws \ -region \ -awsProfile ``` 2. Create a new Tensor9 **app**. ```bash theme={null} tensor9 app create -name compose-quickstart -displayName "Compose example app" ``` **Note:** The length of the `displayName` field must be 32 characters or fewer. ## Publish your Docker Compose file and bind it to your app Tensor9 works by compiling your docker-compose.yml file for each appliance you want to deploy to. Your next step is to **publish** your compose file to your Tensor9 control plane: ```bash theme={null} tensor9 stack publish \ -stackType DockerCompose \ -stackS3Key my-app-compose \ -file docker-compose.yml ``` This will return a **native stack id**, which will look something like this: `s3://t9-ctrl-000001/my-app-compose.yml` The next step is to **bind** your published stack to your app: ```bash theme={null} tensor9 stack bind \ -appName compose-quickstart \ -stackType DockerCompose \ -nativeStackId "" ``` This registers your stack with your app so that you can release your app/stack combination to appliances. In the future, you can publish new versions of your stack (using the tensor9 stack publish command) without having to bind the app again. ## Create a test appliance and test your app 1. Create a test appliance: ```bash theme={null} tensor9 test appliance create \ -appName compose-quickstart \ -name compose-quickstart-test ``` 2. View the output of `tensor9 report` to determine when your test appliance is ready for a release. While the test appliance is creating, `tensor9 report` displays output such as: ``` Vendor: My Company [id: 000000000165ebb8]: Name: My Company Apps: (1) compose-quickstart [id: 0000000000000213]: Name: compose-quickstart Domain: - Stacks: (1) DockerCompose: (1) my-app-compose | s3://t9-ctrl-000001/my-app-compose.yml Customer Appliances: (0) Test Appliances: (1) Creating: (1) compose-quickstart-test [id: 000000000000007e] ``` When the appliance is ready, `tensor9 report` displays output such as: ``` Vendor: My Company [id: 000000000165ebb8]: Name: My Company Apps: (1) compose-quickstart [id: 0000000000000213]: Name: compose-quickstart Domain: - Stacks: (1) DockerCompose: (1) my-app-compose | s3://t9-ctrl-000001/my-app-compose.yml Customer Appliances: (0) Test Appliances: (1) Test Appliance: compose-quickstart-test [id: 0000000000000001]: Status: Live Name: compose-quickstart-test Customer: My Company [id: 000000000165ebb8] Cloud Details: AWS us-west-2 Form Factor: AWS Connected Appliance Id: 0000000000000001 Installs: Releases: Effective Releases: (0) Prepped Releases: (0) Hardware: (updated 1 second ago) Uptime: 3 minutes Capacity Machines: 2 ``` 3. Create a release to your test appliance: ```bash theme={null} tensor9 stack release create \ -appName compose-quickstart \ -testApplianceName compose-quickstart-test \ -vendorVersion "1.0.0" \ -description "First release of my Docker Compose app via Tensor9" \ -notes "By engineer@vendor.co" ``` After a few minutes, the deployment stack downloads into a new directory that is named after your appliance. 4. Change into the new directory that contains the deployment stack for your test appliance: ```bash theme={null} cd compose-quickstart-test ``` 5. Deploy as normal by running `tofu init` followed by `tofu apply`. 6. View the deployed services: ```bash theme={null} kubectl get deployments kubectl get services kubectl get pods ``` 7. Access your application through the load balancer. Get the load balancer endpoint for services with exposed ports: ```bash theme={null} kubectl get service -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' ``` ## Release an infrastructure change You can release infrastructure changes to your Docker Compose stack at any time. Make any desired changes to your compose file. For example, you could: * Add a new service * Change resource limits * Add environment variables * Update container image versions **Important:** The next step will overwrite the previous origin stack. All new releases will come from the most recently published version of your origin stack. ```bash theme={null} tensor9 stack publish \ -stackType DockerCompose \ -stackS3Key my-app-compose \ -file docker-compose.yml ``` If the origin stack is published successfully, the following message is displayed: ```bash theme={null} Your origin stack is ready to be released. Use the following native stack id s3://t9-ctrl-000001/my-app-compose.yml ``` Your updated origin stack is now ready. You don't need to rebind it - new releases will automatically use the updated version. ```bash theme={null} tensor9 stack release create \ -appName compose-quickstart \ -testApplianceName compose-quickstart-test \ -vendorVersion "1.0.1" \ -description "Added caching layer and updated API resources" \ -notes "By engineer@vendor.co" ``` After a few minutes, the deployment stack for your updated compose file downloads into the appliance directory. Change into the directory that contains the deployment stack for your test appliance: ```bash theme={null} cd compose-quickstart-test ``` Deploy the updated infrastructure: ```bash theme={null} tofu apply ``` ```bash theme={null} kubectl get deployments kubectl get services kubectl get pods ``` You should see your new services, updated resource limits, or other changes reflected in the Kubernetes resources. ## Next steps Now that you've deployed a Docker Compose app with Tensor9: * Review the [Docker Compose origin stack documentation](/origin-stack/docker-compose) for advanced features * Learn about [stack tuning documents](/origin-stack/docker-compose#tuning-container-resources) to customize deployments per customer * Explore [form factors](/fundamentals/key-concepts#form-factor) to deploy to different cloud providers * Set up [observability](/fundamentals/observability) to monitor your appliances # Quick start: Terraform/OpenTofu Source: https://docs.tensor9.com/getting-started/quick-start-terraform This quick start guide explains how to use Tensor9 to deploy your Terraform/OpenTofu (**TF**) app as a private appliance. As you follow this guide, you will: * Set up a Tensor9 control plane in your AWS account and bind a TF workspace as the origin stack for your app. * Test your app running in an appliance. * Release infrastructure changes. ## Prerequisites * Send an email to [hello@tensor9.com](mailto:hello@tensor9.com) and request an API key. You must have an API key to complete the quick start. * Create an AWS account for Tensor9. We will refer to this as the Tensor9 AWS account. **Important:** * Your Tensor9 AWS account should be a dedicated AWS account used only for Tensor9. This reduces the risk of conflicts between your app deployed in a Tensor9 appliance and any other software, infrastructure, or resources you might have in a general-purpose AWS account. * Your Tensor9 AWS account must be located in a United States region. Support for non-US regions will be available in the near future. * Install [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html) in your environment, and [set up an AWS CLI profile](https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-files.html) that has admin permissions to your new AWS account. ## Install OpenTofu or Terraform In your environment, you'll need to install Terraform/OpenTofu CLI. This guide assumes you are using OpenTofu and uses the `tofu` command throughout. ## Install Tensor9 CLI Install the **tensor9** CLI via Homebrew (recommended): ```bash theme={null} brew tap tensor9ine/tensor9 brew install tensor9 ``` Alternatively, install via the install script: ```bash theme={null} curl -sSL https://t9-artifacts-prod-1.s3.us-west-2.amazonaws.com/install-latest.sh | sh ``` Then set your Tensor9 API key: ```bash theme={null} export T9_API_KEY= ``` **Note:** An API key is required. If you do not have an API key, send email to [hello@tensor9.com](mailto:hello@tensor9.com) to request one. ## Set up a Tensor9 control plane and create a new app 1. Set up a Tensor9 control plane in your new AWS account (this takes several minutes to complete): ```bash theme={null} tensor9 vendor setup \ -cloud aws \ -region \ -awsProfile ``` 2. Create a new Tensor9 **app**. ```bash theme={null} tensor9 app create -name tofu-quickstart -displayName "Tofu example app" ``` **Note:** The length of the `displayName` field must be 32 characters or fewer. ## Publish your TF and bind it to your new app Tensor9 works by compiling your TF for each appliance your want to deploy to. So, your next step is to **publish** your TF to your Tensor9 control plane: ```bash theme={null} tensor9 stack publish \ -stackType TerraformWorkspace \ -stackS3Key your-stack \ -dir ``` This will return a **native stack id**, which will will look something like this: `s3://t9-ctrl-000001/your-stack.tf.tgz` The next step is to **bind** your published stack to your app: ```bash theme={null} tensor9 stack bind \ -appName tofu-quickstart \ -stackType TerraformWorkspace \ -nativeStackId ``` This registers your stack with your app so that you can release your app/stack combination to appliances. In the future, you can publish new versions of your stack (using the tensor9 stack publish command) without having to bind the app again. ## Create a test appliance and test your app 1. Create a test appliance: ```bash theme={null} tensor9 test appliance create \ -appName tofu-quickstart \ -name tofu-quickstart-test ``` 2. View the output of `tensor9 report` to determine when your test appliance is ready for a release. While the test appliance is creating, `tensor9 report` displays output such as: When the appliance is ready, `tensor9 report` displays output such as: 3. Create a release to your test appliance: ```bash theme={null} tensor9 stack release create \ -appName tofu-quickstart \ -testApplianceName tofu-quickstart-test \ -vendorVersion "1.0.0" \ -description "First release of my origin stack via Tensor9" \ -notes "By engineer@vendor.co" ``` After a few minutes, the workspace bundle for your TF root module downloads into a new directory that is named after your appliance. 4. Change into the new directory that contains the deployment stack for your test appliance: ```bash theme={null} cd tofu-quickstart-test ``` 5. Deploy as normal by running `tofu init` followed by `tofu apply`. 6. Run `tofu show` to review the resulting created resources. ## Release an infrastructure change You can release infrastructure changes to your stack at any time. 1. Update your origin stack with any desired change. For example, you could enable versioning on the AWS S3 bucket that was created when initially publishing your origin stack. **Important:** The next step will overwrite the previous origin stack. All new releases will come from the most recently published version of your origin stack. 2. **Re-publish** the origin stack to Tensor9: ```bash theme={null} tensor9 stack publish \ -stackType TerraformWorkspace \ -stackS3Key your-stack \ -dir ``` If the origin stack is published successfully, the following message is displayed: ```bash theme={null} Your origin stack is ready to be released. Use the following native stack id [stack ID URL] ``` Your native stack id is displayed. You will need this stack id in the next step. 3. Create a new release: ```bash theme={null} tensor9 stack release create \ -appName tofu-quickstart \ -testApplianceName tofu-quickstart-test \ -vendorVersion "1.0.1" \ -description "Turning on versioning for the main bucket" \ -notes "By engineer@vendor.co" ``` After a few minutes, the workspace bundle for your TF stack downloads into a new directory that is named after your app. 4. Change into the new directory that contains the deployment stack for your test appliance: ```bash theme={null} cd tofu-quickstart-test ``` 5. Deploy as normal by running `tofu init` followed by `tofu apply`. 6. Run `tofu show` to review the resulting created resources. # Tensor9 Overview Source: https://docs.tensor9.com/index Tensor9 is an **any-prem** platform. It enables software/AI vendors (like you) to deliver their existing products directly into any customer-owned environment: **BYOC**, **private VPC**, or **on-prem**.
Learn about Tensor9's approach to any-prem deployments. Deploy your existing Terraform stack in minutes. Understand the core concepts and terminology. White-label documentation your customers see during installation and operations. ## Why use Tensor9? Tensor9 solves hard problems related to delivering software/AI products into diverse customer-owned environments: * **Deployment** across customer environments with continuous sync. * **Portability** of managed cloud service by replacing them with equivalents. * **Observability** through synchronized logs, metrics and traces. * **Customer control** over maintenance and vendor access. * **[Per-customer configuration](/customizations/overview)** without per-customer forks. Each customer's install is shaped by their own ingress, managed-service, and connectivity choices, compiled from your single origin stack. # Deploying with Atlantis or Spacelift Source: https://docs.tensor9.com/integrations/atlantis-spacelift Tensor9 is designed to integrate with your preferred automation tools (Atlantis, Spacelift, etc.) for deployment. ## 1. Deployment configuration: * Tensor9 preserves any backend configuration you include in your origin stack. You are responsible for managing Terraform state according to your deployment requirements. * You should give each appliance its own backend state file location. Terraform backend blocks cannot interpolate variables, so pass the state path through your deployment tool's configuration and keep it stable across deployment runs for a given appliance. ## 2. Execution (example: Atlantis): * Your CI/CD tool (e.g., Atlantis) fetches the Compiled Stack from Tensor9. * It executes the standard Terraform commands: * `tofu init` * `tofu plan` (for review) * `tofu apply` (upon approval) * **Best Practice**: Maintain a separate Git repository for the compiled stacks to keep your source code history clean. ## Integration with Atlantis Atlantis is an open-source tool that automates Terraform via pull requests (PRs). | Workflow Step | Action with Tensor9 | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1. Create PR | A vendor (or an automated script) makes a change in the Origin Stack repository and runs the Tensor9 stack build step. This generates a new version of the compiled stacks. | | 2. Push Compiled Stacks | Push the new compiled stack files to the separate repository and open a new Pull Request (PR). | | 3. Automated Plan | Atlantis detects the PR in the compiled stacks Repository and automatically runs `tofu plan`. The output is posted as a comment on the PR. | | 4. Configure Workspace ID | The Origin Stack should contain the configuration for Atlantis. The vendor must pass a consistent Appliance ID as a workspace ID environment variable to Atlantis. This ensures that the Terraform state is managed correctly for each customer appliance. | | 5. Apply | Once the plan is reviewed and approved, a user comments `atlantis apply` on the PR. | | 6. Update/Rollback | Update is the same as a new deploy. For a Rollback, you must check out the desired old version of the Origin Stack, re-compile it with Tensor9, and then open a new PR. | ## Integration with Spacelift Spacelift is a complete CI/CD platform for Infrastructure as Code, using Stacks for deployment. | Workflow Step | Action with Tensor9 | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | 1. Connect Repository | Create a Spacelift Stack connected to your compiled stacks repository. | | 2. Trigger Run | Similar to Atlantis, pushing a new version of the compiled stacks to the repository branch, or opening a PR, triggers a run in Spacelift. | | 3. Plan & Apply | Spacelift executes the `tofu plan` and `tofu apply` workflow. Spacelift's Stacks combine the configuration (compiled code), variables, and state data for each customer appliance. | | 4. Customization | Spacelift allows for extensive customization of the workflow, which can be used to set up environment variables for the required Appliance ID (analogous to the workspace ID in Atlantis). | **Note**: Regardless of the tool, you are responsible for managing backend configuration. You can include backend configuration in your origin stack (which Tensor9 preserves), or provide it through your automation tool's configuration. Ensure your backend configuration gives each appliance its own state file. See [Backend Configuration](/fundamentals/deployments#backend-configuration) for details. ## Updating applications To update an appliance, create a new build (compile) using the latest source code and then trigger a new deployment run via your CI/CD tool. Here is an example update scenario: A vendor is using Tensor to deploy a VM into customer environment. They realize the instance type is too small; they need more memory and CPU. Using Tensor9, they can change to an instance type with more memory and CPU. They simplify run `tofu apply` with the new instance type, resulting in an update wherein the instance type changes, e.g., from t2.micro to t5g.large. **Rollback**: To roll back, you must check out the desired previous version of your Origin Stack code, create a new compiled stack from that old code, and then deploy the resulting compiled stack. # CloudFormation Source: https://docs.tensor9.com/origin-stack/cloudformation AWS CloudFormation is a native AWS infrastructure-as-code tool that can be used with Tensor9. A CloudFormation origin stack is a standard CloudFormation template that Tensor9 compiles into customer-specific deployment stacks for each appliance. ## What is a CloudFormation origin stack? A CloudFormation origin stack is your existing CloudFormation template - the YAML or JSON file that defines your application's AWS infrastructure. Tensor9 uses this as the blueprint to generate deployment stacks tailored to each customer's appliance. When you publish a CloudFormation origin stack to Tensor9, you use the AWS CLI to: 1. Create or update a CloudFormation stack in your control plane's AWS account 2. Store the template as the source for generating deployment stacks 3. Use it as the blueprint for creating appliance-specific stacks The key difference from standard CloudFormation usage: **you maintain one origin stack** that Tensor9 compiles into many deployment stacks - one per customer appliance. Your origin stack should be your existing CloudFormation template. Tensor9 is designed to work with the infrastructure-as-code you already have - you don't need to write a new template just for Tensor9. The goal is to maintain a single template that works for both your cloud deployment and private customer deployments. CloudFormation origin stacks can only be deployed to AWS appliances. If you need to support multiple cloud providers, use Terraform or OpenTofu as your origin stack format. ## How CloudFormation origin stacks work Using CloudFormation with Tensor9 follows a straightforward workflow: You publish your CloudFormation template by using the AWS CLI to create or update a CloudFormation stack in your control plane's AWS account. This stack serves as the origin stack that Tensor9 will use as the blueprint for all appliance deployments. When you want to deploy to an appliance, you create a release using `tensor9 stack release create`. During release creation, your control plane **compiles** your origin stack into a **deployment stack** tailored to that specific appliance, and **automatically creates the CloudFormation stack in your control plane**. The compilation process: * Injects the `InstanceId` parameter to ensure resource uniqueness * Instruments the stack for observability (logs, metrics, traces) * Rewrites artifact references to point to appliance-local locations * Updates resource names to include the instance ID The result is a **deployment stack** - a new CloudFormation stack that is automatically created in your control plane. Your control plane automatically deploys the compiled deployment stack by creating a CloudFormation stack in your control plane's AWS account. You can monitor the deployment progress using: ```bash theme={null} # View deployment status tensor9 report -customerName acme-corp # View CloudFormation stack events in your control plane's account aws cloudformation describe-stack-events \ --stack-name myapp-stack-000000007e ``` The CloudFormation stack creates all the infrastructure resources in your control plane's AWS account automatically. You write and maintain **one origin stack**. Tensor9 compiles it into **many deployment stacks** (one per appliance), each customized for that appliance. The control plane **automatically creates and manages these CloudFormation stacks** in your control plane's AWS account - you don't need to manually download or deploy anything. ## Prerequisites Before using CloudFormation as an origin stack, ensure you have: * **AWS CLI installed**: Version 2.0+ recommended for publishing your origin stack and monitoring deployments * **Valid CloudFormation template**: Your template must pass CloudFormation validation * **Tensor9 CLI installed**: For publishing your origin stack and creating releases * **Tensor9 API key configured**: Set as `T9_API_KEY` environment variable * **AWS credentials configured**: For publishing your origin stack to your control plane's AWS account ## CloudFormation template structure Your CloudFormation origin stack should follow standard CloudFormation conventions: ### YAML template example ```yaml theme={null} AWSTemplateFormatVersion: '2010-09-09' Description: My Application Infrastructure Parameters: InstanceId: Type: String Description: Uniquely identifies the instance to deploy into Resources: # Your AWS resources here Outputs: # Your stack outputs here ``` ### JSON template example ```json theme={null} { "AWSTemplateFormatVersion": "2010-09-09", "Description": "My Application Infrastructure", "Parameters": { "InstanceId": { "Type": "String", "Description": "Uniquely identifies the instance to deploy into" } }, "Resources": { }, "Outputs": { } } ``` ## Publishing your CloudFormation origin stack To make your CloudFormation template available to Tensor9, use the AWS CLI to create a CloudFormation stack in your control plane's AWS account: ```bash theme={null} aws cloudformation create-stack \ --stack-name myapp-origin-stack \ --template-body file://template.yaml \ --capabilities CAPABILITY_IAM ``` Or if updating an existing origin stack: ```bash theme={null} aws cloudformation update-stack \ --stack-name myapp-origin-stack \ --template-body file://template.yaml \ --capabilities CAPABILITY_IAM ``` ### What gets published When you create or update the CloudFormation stack: 1. CloudFormation validates and stores your template in your control plane's AWS account 2. The stack name becomes the **native stack ID** you'll use to bind the stack to your app 3. Tensor9 uses this stack as the source template for generating deployment stacks **Example output:** ``` { "StackId": "arn:aws:cloudformation:us-west-2:123456789012:stack/myapp-origin-stack/abcd1234" } ``` The native stack ID for binding is simply the stack name: `myapp-origin-stack` ### Publishing updates When you make changes to your CloudFormation template, update the stack: ```bash theme={null} # Update your template file # Then update the CloudFormation stack aws cloudformation update-stack \ --stack-name myapp-origin-stack \ --template-body file://template.yaml \ --capabilities CAPABILITY_IAM ``` The updated template becomes available for creating new releases. Previously deployed appliances continue running their current version until you create and deploy a new release. ## Binding your origin stack to an app After publishing for the first time, bind your origin stack to your app using the CloudFormation stack name as the native stack ID: ```bash theme={null} tensor9 stack bind \ -appName my-app \ -stackType CloudFormation \ -nativeStackId myapp-origin-stack ``` The native stack ID is simply the CloudFormation stack name you used when creating the stack with `aws cloudformation create-stack`. **Important**: You only need to bind once. Future publishes of the same stack don't require re-binding. ## Parameterization Parameterization is the process of making your origin stack capable of being deployed to multiple appliances without resource naming conflicts. This is the most critical requirement for a CloudFormation origin stack in Tensor9. ### The InstanceId parameter Tensor9 automatically provides an `InstanceId` parameter to every deployment to ensure resource uniqueness across appliances. Your origin stack should declare this parameter: ```yaml theme={null} Parameters: InstanceId: Type: String Description: Uniquely identifies the instance to deploy into ``` Tensor9 automatically provides this value during compilation - you never need to manually set it. ### Using InstanceId for resource naming Use `!Ref InstanceId` to make all resource names unique. This prevents conflicts when deploying to multiple customer appliances: ```yaml theme={null} Resources: # ✓ CORRECT: Unique per appliance DataBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub 'myapp-data-${InstanceId}' Database: Type: AWS::RDS::DBInstance Properties: DBInstanceIdentifier: !Sub 'myapp-db-${InstanceId}' Engine: postgres ApiFunction: Type: AWS::Lambda::Function Properties: FunctionName: !Sub 'myapp-api-${InstanceId}' Runtime: nodejs18.x Handler: index.handler # ✗ INCORRECT: Will cause conflicts across appliances DataBucket: Type: AWS::S3::Bucket Properties: BucketName: 'myapp-data' # Multiple appliances will try to create the same bucket ``` ### What to parameterize Use `InstanceId` for: * **Resource identifiers**: S3 bucket names, RDS identifiers, Lambda function names * **IAM resources**: Role names, policy names * **Networking**: VPC names, subnet tags, security group names * **Logging**: CloudWatch log group names * **Secret paths**: Secrets Manager secret names **DNS names are managed automatically**: Tensor9 automatically generates DNS names for your appliances using either your vendor vanity domain or the customer's vanity domain (if they specified one). You don't need to include `InstanceId` in DNS records. See [Endpoints and DNS](/fundamentals/endpoints) for details. Without proper parameterization, attempting to deploy to multiple appliances will result in resource creation failures as CloudFormation tries to create duplicate resources. ## Complete example origin stack Here's a complete CloudFormation origin stack for a typical application: ```yaml theme={null} AWSTemplateFormatVersion: '2010-09-09' Description: My Application Infrastructure Parameters: InstanceId: Type: String Description: Uniquely identifies the instance to deploy into ApiImage: Type: String Description: Container image for the API DbPassword: Type: String Description: Database master password NoEcho: true Resources: # VPC VPC: Type: AWS::EC2::VPC Properties: CidrBlock: 10.0.0.0/16 EnableDnsHostnames: true EnableDnsSupport: true Tags: - Key: Name Value: !Sub 'myapp-vpc-${InstanceId}' # Private subnets PrivateSubnet1: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC CidrBlock: 10.0.1.0/24 AvailabilityZone: !Select [0, !GetAZs ''] Tags: - Key: Name Value: !Sub 'myapp-private-1-${InstanceId}' PrivateSubnet2: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC CidrBlock: 10.0.2.0/24 AvailabilityZone: !Select [1, !GetAZs ''] Tags: - Key: Name Value: !Sub 'myapp-private-2-${InstanceId}' # ECS Cluster ECSCluster: Type: AWS::ECS::Cluster Properties: ClusterName: !Sub 'myapp-cluster-${InstanceId}' # ECS Task Definition TaskDefinition: Type: AWS::ECS::TaskDefinition Properties: Family: !Sub 'myapp-${InstanceId}' NetworkMode: awsvpc RequiresCompatibilities: - FARGATE Cpu: '256' Memory: '512' ContainerDefinitions: - Name: api Image: !Ref ApiImage PortMappings: - ContainerPort: 8080 Protocol: tcp Environment: - Name: INSTANCE_ID Value: !Ref InstanceId - Name: DB_HOST Value: !GetAtt Database.Endpoint.Address LogConfiguration: LogDriver: awslogs Options: awslogs-group: !Ref LogGroup awslogs-region: !Ref AWS::Region awslogs-stream-prefix: api # ECS Service ECSService: Type: AWS::ECS::Service Properties: ServiceName: !Sub 'myapp-service-${InstanceId}' Cluster: !Ref ECSCluster TaskDefinition: !Ref TaskDefinition DesiredCount: 2 LaunchType: FARGATE NetworkConfiguration: AwsvpcConfiguration: Subnets: - !Ref PrivateSubnet1 - !Ref PrivateSubnet2 SecurityGroups: - !Ref ECSSecurityGroup # RDS PostgreSQL DBSubnetGroup: Type: AWS::RDS::DBSubnetGroup Properties: DBSubnetGroupName: !Sub 'myapp-db-subnet-${InstanceId}' DBSubnetGroupDescription: Database subnet group SubnetIds: - !Ref PrivateSubnet1 - !Ref PrivateSubnet2 Database: Type: AWS::RDS::DBInstance Properties: DBInstanceIdentifier: !Sub 'myapp-db-${InstanceId}' Engine: postgres EngineVersion: '15.3' DBInstanceClass: db.t3.micro AllocatedStorage: 20 DBName: myapp MasterUsername: admin MasterUserPassword: !Ref DbPassword DBSubnetGroupName: !Ref DBSubnetGroup VPCSecurityGroups: - !Ref DBSecurityGroup # S3 bucket DataBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub 'myapp-data-${InstanceId}' VersioningConfiguration: Status: Enabled # ElastiCache Redis CacheSubnetGroup: Type: AWS::ElastiCache::SubnetGroup Properties: Description: Cache subnet group SubnetIds: - !Ref PrivateSubnet1 - !Ref PrivateSubnet2 RedisCluster: Type: AWS::ElastiCache::CacheCluster Properties: CacheClusterId: !Sub 'myapp-redis-${InstanceId}' Engine: redis CacheNodeType: cache.t3.micro NumCacheNodes: 1 CacheSubnetGroupName: !Ref CacheSubnetGroup VpcSecurityGroupIds: - !Ref RedisSecurityGroup # CloudWatch Log Group LogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub '/aws/ecs/myapp-${InstanceId}' RetentionInDays: 7 # Security Groups ECSSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub 'myapp-ecs-sg-${InstanceId}' GroupDescription: Security group for ECS tasks VpcId: !Ref VPC DBSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub 'myapp-db-sg-${InstanceId}' GroupDescription: Security group for database VpcId: !Ref VPC SecurityGroupIngress: - IpProtocol: tcp FromPort: 5432 ToPort: 5432 SourceSecurityGroupId: !Ref ECSSecurityGroup RedisSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub 'myapp-redis-sg-${InstanceId}' GroupDescription: Security group for Redis VpcId: !Ref VPC SecurityGroupIngress: - IpProtocol: tcp FromPort: 6379 ToPort: 6379 SourceSecurityGroupId: !Ref ECSSecurityGroup # Secrets DBPasswordSecret: Type: AWS::SecretsManager::Secret Properties: Name: !Sub '${InstanceId}/prod/db/password' SecretString: !Ref DbPassword Outputs: ClusterName: Description: ECS cluster name Value: !Ref ECSCluster DatabaseEndpoint: Description: RDS database endpoint Value: !GetAtt Database.Endpoint.Address RedisEndpoint: Description: Redis cache endpoint Value: !GetAtt RedisCluster.RedisEndpoint.Address DataBucketName: Description: S3 data bucket name Value: !Ref DataBucket ``` ## Parameters Define parameters for values that vary per deployment: ```yaml theme={null} Parameters: InstanceId: Type: String Description: Uniquely identifies the instance to deploy into ApiImage: Type: String Description: Container image URI DbPassword: Type: String Description: Database master password NoEcho: true ``` When creating a release, Tensor9 automatically provides the `InstanceId` parameter. Other parameters like container images are detected and injected during compilation. Sensitive parameters like `DbPassword` can be stored in AWS Secrets Manager in the customer's account and referenced via `!Sub '{{resolve:secretsmanager:${InstanceId}/prod/db/password}}'`. ## Outputs Define outputs to expose important values after deployment: ```yaml theme={null} Outputs: ApiEndpoint: Description: API endpoint URL Value: !GetAtt LoadBalancer.DNSName DatabaseEndpoint: Description: Database connection endpoint Value: !GetAtt Database.Endpoint.Address DataBucketName: Description: S3 data bucket name Value: !Ref DataBucket ``` After deployment, view outputs using the AWS CLI: ```bash theme={null} aws cloudformation describe-stacks \ --stack-name myapp-stack \ --query 'Stacks[0].Outputs' ``` Outputs are also visible in `tensor9 report`. ## Best practices Every AWS resource with a name or identifier should include `InstanceId` to prevent conflicts across customer appliances: ```yaml theme={null} # ✓ CORRECT Resources: DataBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub 'myapp-data-${InstanceId}' LambdaRole: Type: AWS::IAM::Role Properties: RoleName: !Sub 'myapp-lambda-${InstanceId}' # ✗ INCORRECT - Will cause collisions Resources: DataBucket: Type: AWS::S3::Bucket Properties: BucketName: 'myapp-data' ``` You don't need to add appliance-identifying tags yourself. Tensor9 stamps `t9-appliance-id` and `t9-projection-id` onto every taggable resource in the compiled deployment stack. This enables: * IAM permission scoping * CloudWatch filtering * Cost tracking * Resource discovery Never hardcode secrets. Use Secrets Manager with parameterized names: ```yaml theme={null} ApiKeySecret: Type: AWS::SecretsManager::Secret Properties: Name: !Sub '${InstanceId}/prod/api/key' SecretString: !Ref ApiKey ``` ## Troubleshooting **Symptom**: `aws cloudformation create-stack` fails with CloudFormation validation errors. **Solutions**: * Run `aws cloudformation validate-template` locally to identify syntax errors * Ensure all required parameters are declared * Check that all resource references are valid * Verify resource property names and types match CloudFormation specification **Symptom**: Release creation shows CloudFormation stack creation failed with "AlreadyExists" errors in your control plane. **Solutions**: * Ensure all resource names include `!Sub` with `${InstanceId}` * Verify the `InstanceId` parameter is being properly used in your origin stack * Check that no hardcoded resource names exist in your template * For S3 buckets, remember they must be globally unique - include both app name and InstanceId * Review CloudFormation stack events in your control plane's account: `aws cloudformation describe-stack-events --stack-name ` **Symptom**: "InsufficientCapacity" or quota limit errors during deployment. **Solutions**: * Check AWS service quotas for your control plane's account * Reduce initial resource counts and scale up after deployment * Deploy resources across multiple availability zones to increase capacity * Request quota increases from AWS if needed **Symptom**: "Template too large" error when publishing or deploying. **Solutions**: * Use CloudFormation modules for reusable components * Store large template bodies in S3 and reference by URL * Consider using Terraform instead of CloudFormation for complex infrastructure ## Limitations and considerations CloudFormation origin stacks can only be deployed to AWS appliances. CloudFormation is an AWS-specific infrastructure-as-code tool that only works within AWS environments. If you need to support multiple cloud providers (Google Cloud, Azure) or private Kubernetes environments, use Terraform or OpenTofu as your origin stack format instead. Tensor9 does not currently support CloudFormation nested stacks (stacks that reference other stacks using `AWS::CloudFormation::Stack`). If your infrastructure requires modularity, consider using CloudFormation modules, or migrating to Terraform which supports module composition. ## Next steps Now that you understand CloudFormation origin stacks, explore these topics: * [**Quick Start: CloudFormation**](/getting-started/quick-start-cloudformation): Step-by-step guide to your first CloudFormation deployment * [**Deployments**](/fundamentals/deployments): How to create releases and deploy * [**AWS Form Factor**](/form-factor/aws): Deploy to AWS customer environments * [**Testing**](/fundamentals/testing): Validate your origin stack in test appliances # Docker Container Source: https://docs.tensor9.com/origin-stack/docker Docker containers can be used as origin stacks with Tensor9. A Docker container origin stack is simply a container image URI that Tensor9 compiles into complete infrastructure stacks for each appliance across various customer environments. ## What is a Docker container origin stack? A Docker container origin stack is your existing Docker container image. Tensor9 takes your container image URI and automatically generates all the necessary infrastructure (container orchestration, networking, load balancers) to run it in customer environments - whether that's AWS, Google Cloud, Azure, or private Kubernetes clusters. Tensor9 reads your container image metadata directly from the image. If your container includes `EXPOSE` directives, Tensor9 automatically configures load balancers for those ports. You simply bind your app to the container image URI, and Tensor9 uses this as the blueprint to generate complete deployment stacks for each customer appliance. Your origin stack should be your existing Docker container. Tensor9 is designed to work with the container images you already have - you don't need to rebuild your containers just for Tensor9. The goal is to maintain a single container image that works for both your cloud deployment and private customer deployments. ## How Docker container origin stacks work Your container image must be available in a container registry (Amazon ECR, Docker Hub, GitHub Container Registry, Google Artifact Registry, etc.). Tensor9 will reference this image when creating deployment stacks. When you want to deploy to an appliance, you create a release using `tensor9 stack release create`. During release creation, your control plane **compiles** your Docker container specification into a **complete Terraform deployment stack** tailored to the appliance's cloud environment. The compilation generates different infrastructure based on the appliance's form factor: | Form Factor | Generated Infrastructure | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **AWS** | - ECS Fargate cluster, task definition, and service
- VPC with subnets, internet gateway, and route tables
- Network load balancer with listeners and target groups for each exposed port
- Security groups with ingress rules for exposed ports
- IAM roles with appropriate permissions
- CloudWatch log groups for container logs | | **Google Cloud** | - Cloud Run service or Compute Engine instance group
- VPC network and firewall rules
- Load balancer with backend services for each exposed port
- Service accounts with appropriate permissions
- Cloud Logging configuration | | **Azure** | - Container Instances or Azure Kubernetes Service
- Virtual network and network security groups
- Load balancer with rules for each exposed port
- Managed identities with appropriate permissions
- Azure Monitor configuration | | **Private Kubernetes** | - Kubernetes Deployment with pod specifications
- Kubernetes Service (LoadBalancer or NodePort) for each exposed port
- Resource requests and limits | The compilation process: * Automatically creates appropriate load balancing resources for each exposed port * Configures routing to your container based on the cloud provider * Sets up security rules to allow inbound traffic on exposed ports * Maps container ports to load balancer ports * Configures the container orchestration system to run your container The result is a **deployment stack** - a Terraform configuration that defines all the infrastructure needed to run your container in the target appliance's environment. When deployed, the deployment stack will copy your container image from its original registry into the appliance's container registry.
Download the compiled deployment stack and deploy it using Terraform or OpenTofu: ```bash theme={null} # Navigate to the deployment stack directory cd my-test-appliance # Initialize Terraform tofu init # Deploy the infrastructure tofu apply ``` The Terraform deployment creates all the infrastructure resources (container orchestration, load balancer, networking, etc.) automatically and starts your container. Monitor the deployment using Terraform output and your cloud provider's console: ```bash theme={null} # View deployment status tensor9 report -customerName acme-corp # View Terraform output tofu output # For AWS: Check ECS service aws ecs describe-services --cluster --services # For GCP: Check Cloud Run service gcloud run services describe # For Kubernetes: Check deployment kubectl get deployments kubectl get pods ```
You maintain **one container image**. Tensor9 compiles it into **many deployment stacks** (one per appliance), each customized for that appliance's cloud environment. Each deployment stack is a Terraform configuration that creates the appropriate infrastructure for the target cloud provider. ## Prerequisites Before using Docker as an origin stack, ensure you have: * **Container image in a registry**: Your image must be pushed to a container registry (the deployment stack will copy it to the appliance's registry) * **Tensor9 CLI installed**: For creating releases * **Tensor9 API key configured**: Set as `T9_API_KEY` environment variable ## Docker container origin stack format A Docker container origin stack is simply your container image URI: ``` 210620017265.dkr.ecr.us-west-2.amazonaws.com/my-app:latest ``` Tensor9 reads the image metadata directly from your container image. If the image includes `EXPOSE` directives, Tensor9 automatically configures load balancers for those ports. **Publishing workflow**: You bind your app to a container image URI (typically with the `:latest` tag). Then, each time you want to release a new version, you simply push a new container image to that same tag. Tensor9 will pull the latest image when you create a release. This means you don't need to rebind your app every time you update your container - just push the new image and create a release. ### Supported registries Your container image can be in any container registry: * **Amazon ECR**: `123456789.dkr.ecr.us-west-2.amazonaws.com/my-app:latest` * **Docker Hub**: `docker.io/library/nginx:latest` * **GitHub Container Registry**: `ghcr.io/myorg/app:latest` * **Google Artifact Registry**: `us-docker.pkg.dev/project/repo/image:latest` * **Azure Container Registry**: `myregistry.azurecr.io/app:latest` * **Private registries**: Any OCI-compatible registry ### Exposed ports (optional) If your container image includes `EXPOSE` directives, Tensor9 will automatically configure load balancers for those ports. For example, if your Dockerfile contains: ```dockerfile theme={null} FROM node:18-alpine WORKDIR /app COPY . . EXPOSE 8080 EXPOSE 8443 CMD ["node", "server.js"] ``` Tensor9 will automatically: * Create a **load balancer listener** for each exposed port * Configure **routing** from the load balancer to your container * Set up **security rules** to allow inbound traffic on exposed ports * Map container ports to external load balancer ports If your container has no `EXPOSE` directives, Tensor9 will deploy the container without a load balancer. Currently, only TCP ports are supported. UDP and other protocols are not yet supported for Docker container origin stacks. ## Publishing and deploying ### Initial setup (one-time) Build your origin Docker image and push it to a registry: ```bash theme={null} # Example: Push to Amazon ECR docker build -t my-app:latest . docker tag my-app:latest 210620017265.dkr.ecr.us-west-2.amazonaws.com/my-app:latest docker push 210620017265.dkr.ecr.us-west-2.amazonaws.com/my-app:latest ``` Bind your app to the container image URI: ```bash theme={null} tensor9 stack bind \ -appName my-app \ -stackType DockerContainer \ -nativeStackId "210620017265.dkr.ecr.us-west-2.amazonaws.com/my-app:latest" ``` This only needs to be done once per app. ### Releasing new versions Each time you want to release a new version: ```bash theme={null} # Build and push new version to the same tag docker build -t my-app:latest . docker tag my-app:latest 210620017265.dkr.ecr.us-west-2.amazonaws.com/my-app:latest docker push 210620017265.dkr.ecr.us-west-2.amazonaws.com/my-app:latest ``` ```bash theme={null} tensor9 stack release create \ -appName my-app \ -testApplianceName my-test-appliance \ -vendorVersion "1.0.0" ``` Your control plane compiles the Docker image URI into a complete Terraform deployment stack tailored to the appliance's cloud environment. Download and deploy the compiled deployment stack: ```bash theme={null} # Download the deployment stack cd my-test-appliance # Deploy with Terraform/OpenTofu tofu init tofu apply ``` Once deployed, you can access your application through the load balancer endpoint. The exact method depends on the cloud provider: ```bash theme={null} # For AWS: Get the load balancer DNS tofu output load_balancer_dns # For GCP: Get the Cloud Run URL or load balancer IP tofu output service_url # For Azure: Get the load balancer IP tofu output load_balancer_ip # For Kubernetes: Get the service endpoint kubectl get services ``` Your application will be accessible at the endpoint shown, on each exposed port. ## Tuning container resources You can customize the CPU and memory resources allocated to your container by providing a **stack tuning document** when creating a release. This allows you to adjust resources per deployment without modifying your origin stack. ### Creating a stack tuning document Create a JSON or YAML file that specifies the container resources: ```json theme={null} { "version": "V1", "containerResources": { "cpu": "4", "memory": "8Gi" } } ``` **CPU format**: * Whole numbers: `"2"` (2 CPUs) * Millicores: `"2000m"` (2000 millicores = 2 CPUs) * Fractional: `"0.5"` (half a CPU) **Memory format**: * Gibibytes: `"4Gi"` (4 GiB) * Gigabytes: `"4G"` (4 GB) * Mebibytes: `"4096Mi"` (4096 MiB) * Megabytes: `"4096M"` (4096 MB) ### Using the stack tuning document Pass the stack tuning document when creating a release: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -testApplianceName my-test-appliance \ -vendorVersion "1.0.0" \ -tuningDoc tuning.json ``` You can also use YAML format: ```yaml theme={null} version: V1 containerResources: cpu: "4" memory: "8Gi" ``` ```bash theme={null} tensor9 stack release create \ -appName my-app \ -testApplianceName my-test-appliance \ -vendorVersion "1.0.0" \ -tuningDoc tuning.yaml \ -tuningDocFmt Yaml ``` ### When to use resource tuning Resource tuning is useful when: * **Different customer tiers**: Allocate more resources for enterprise customers * **Performance optimization**: Increase resources for high-load deployments * **Cost optimization**: Reduce resources for development/testing environments * **Workload requirements**: Match resources to specific customer workload patterns The stack tuning document overrides the default resource allocation for that specific release. You can use different stack tuning documents for different appliances, allowing you to customize resources per customer without changing your origin stack. ## Managing secrets Pass sensitive data to your container as environment variables using secrets defined in the tuning document. This allows you to reference secrets from AWS Secrets Manager or SSM Parameter Store. ### Defining secrets in the tuning document Add a `secrets` section to your tuning document: ```json theme={null} { "version": "V1", "containerResources": { "cpu": "2", "memory": "4Gi" }, "secrets": { "db_password": { "source": "aws_secretsmanager", "secretId": "prod/db/password", "environmentVariable": "DB_PASSWORD" }, "api_key": { "source": "aws_ssm_parameter", "parameter": "/prod/api/key", "environmentVariable": "API_KEY" } } } ``` When you create a release with this tuning document, Tensor9 will automatically fetch the secrets and inject them as environment variables into your container. ### Accessing secrets in your application Your application reads secrets from environment variables: ```python theme={null} import os # Read secrets from environment variables db_password = os.environ['DB_PASSWORD'] api_key = os.environ['API_KEY'] ``` Environment variables supply values when the container starts. If the application needs runtime reads, it can keep using the AWS Secrets Manager SDK through the configured [Secrets Manager service adapter](/service-adapters/aws/security-identity/secrets-manager). Check the target-specific limits and application access; fetching secrets through the adapter does not require replacing those calls with a provider SDK. ## Exposing ports Tensor9 detects which ports your container exposes by reading the `EXPOSE` directives in your Dockerfile. When ports are detected, Tensor9 automatically provisions cloud-native load balancers to route traffic to your container. ### Defining exposed ports in your Dockerfile Use the `EXPOSE` directive in your Dockerfile to declare which ports your application listens on: ```dockerfile theme={null} FROM node:18-alpine WORKDIR /app COPY . . RUN npm install # Declare exposed ports EXPOSE 8080 EXPOSE 8443 CMD ["node", "server.js"] ``` When you bind your app to this container image, Tensor9 reads the image metadata and automatically detects that ports 8080 and 8443 need to be exposed. ### How Tensor9 provisions load balancers When Tensor9 detects `EXPOSE` directives in your container image, it creates infrastructure appropriate for each cloud provider: | Environment | Container Orchestration | Load Balancer | Configuration | | ---------------------- | ----------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------- | | **Private Kubernetes** | Kubernetes Deployment | Kubernetes LoadBalancer Service | Service with a port for each exposed port; the cluster needs a load balancer implementation | | **AWS** | ECS Fargate Service | Network Load Balancer (NLB) | Target groups and listeners for each exposed port | | **Google Cloud** | Cloud Run service or Compute Engine | Cloud Load Balancing or Cloud Run | Backend services for each exposed port (TCP) or automatic HTTPS routing (Cloud Run) | | **Azure** | Container Instances or AKS | Azure Load Balancer | Backend pools and rules for each exposed port | ### Accessing your exposed ports After deployment, retrieve the public endpoint from the cloud provider: **For Kubernetes-based environments:** ```bash theme={null} kubectl get service my-app-service NAME TYPE EXTERNAL-IP PORT(S) my-app-service LoadBalancer abc123-lb.us-east-1.elb.amazonaws.com 8080:31234/TCP,8443:31235/TCP ``` Access your application at `http://:8080` and `https://:8443`. **For AWS ECS:** ```bash theme={null} aws elbv2 describe-load-balancers --names my-app-nlb # Returns NLB DNS name: my-app-nlb-123456.elb.us-east-1.amazonaws.com ``` Access your application at `http://my-app-nlb-123456.elb.us-east-1.amazonaws.com:8080`. **For Google Cloud Run:** ```bash theme={null} gcloud run services describe my-app --region us-central1 # Returns service URL: https://my-app-abc123-uc.a.run.app ``` Cloud Run automatically handles HTTPS and routes to your application. ### Multiple ports Expose multiple ports for different purposes by adding multiple `EXPOSE` directives: ```dockerfile theme={null} FROM python:3.11-slim WORKDIR /app COPY . . RUN pip install -r requirements.txt # Expose multiple ports for different purposes EXPOSE 8080 # HTTP API EXPOSE 8443 # HTTPS API EXPOSE 9090 # Prometheus metrics endpoint CMD ["python", "app.py"] ``` Each `EXPOSE` directive creates a corresponding listener/target group on the load balancer. ### Protocol support **Supported:** * Standard TCP ports (HTTP, HTTPS, custom TCP services) * Multiple ports per container * Ports in any range (1-65535) **Not supported:** * UDP protocols (only TCP is supported) * Port range specifications in EXPOSE (e.g., `EXPOSE 8000-8010`) * SCTP or other non-TCP protocols If you need UDP or advanced networking, use a Terraform origin stack with custom Kubernetes manifests. ### Best practices for ports **Use standard ports for common protocols:** ```dockerfile theme={null} # Good: Standard HTTP/HTTPS ports EXPOSE 80 EXPOSE 443 ``` **Minimize exposed ports:** * Each exposed port may incur load balancer costs * Only expose ports that need external access from outside the appliance * Use a single port with path-based routing when possible **Document what each port does:** Add comments in your Dockerfile to explain the purpose of each port: ```dockerfile theme={null} EXPOSE 8080 # HTTP API - main application endpoint EXPOSE 8443 # HTTPS API - secure application endpoint EXPOSE 9090 # Prometheus metrics EXPOSE 9091 # Health check endpoint ``` **Ensure your application listens on all interfaces:** Your application must bind to `0.0.0.0` (all network interfaces), not `localhost` or `127.0.0.1`: ```python theme={null} # ✓ CORRECT: Listen on all interfaces app.run(host='0.0.0.0', port=8080) # ✗ INCORRECT: Only accessible from within the container app.run(host='localhost', port=8080) ``` **Consider using an API gateway:** * For complex routing needs * To consolidate multiple services behind a single endpoint * To reduce load balancer costs ### Internal-only containers If your container doesn't need external access (e.g., background workers, queue processors), don't include any `EXPOSE` directives in your Dockerfile: ```dockerfile theme={null} FROM python:3.11-slim WORKDIR /app COPY . . RUN pip install -r requirements.txt # No EXPOSE directives = no load balancer provisioned CMD ["python", "worker.py"] ``` The container will run but won't have a public endpoint, reducing infrastructure costs. ## Generated infrastructure by form factor When Tensor9 compiles your Docker container origin stack, it generates infrastructure appropriate for the target cloud provider: | Infrastructure | AWS | Google Cloud | Azure | Private Kubernetes | | --------------------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | **Container Orchestration** | ECS Cluster
ECS Task Definition with resource limits
ECS Service (1 replica) | Cloud Run service with automatic scaling
Configured resource limits | Container Instances or AKS cluster
Configured resource limits | Kubernetes Deployment
Resource requests and limits
1 replica | | **Networking** | VPC with public subnets
Internet Gateway and route tables
Security groups for exposed ports | VPC network with subnet
Firewall rules for exposed ports
Cloud NAT for outbound connectivity | Virtual network and subnet
Network security groups for exposed ports | Kubernetes Service (LoadBalancer or NodePort)
Service ports for exposed ports | | **Load Balancing** | Network Load Balancer
Target groups per exposed port
Listeners per exposed port | Cloud Load Balancer (HTTP(S) or TCP)
Backend services per exposed port
URL maps and forwarding rules | Azure Load Balancer or Application Gateway
Backend pools per exposed port
Rules per exposed port | Service endpoint (LoadBalancer or NodePort) | | **Security & Logging** | IAM task execution role and task role
CloudWatch log group (90-day retention) | Service account with permissions
Cloud Logging configuration | Managed identity with permissions
Azure Monitor and Log Analytics workspace | Standard Kubernetes RBAC
Container logs | | **Container Registry** | Amazon ECR in appliance's AWS account | Google Artifact Registry in appliance's GCP project | Azure Container Registry in appliance's Azure subscription | Appliance-local container registry (bundled with appliance) | ## Best practices For Docker container origin stacks, use the `:latest` tag (or another consistent tag) and push updates to the same tag. This allows you to release new versions by simply pushing a new image and creating a release, without needing to rebind your app. Tensor9's vendorVersion field in releases provides version tracking. Docker container origin stacks support a single container per deployment. If your application requires multiple containers (sidecars, service meshes, separate frontend/backend, caching layers, etc.), use a Terraform origin stack instead. Terraform allows you to define complex multi-container architectures using Kubernetes Deployments or ECS task definitions with multiple container specifications. While Docker container origin stacks provide simple resource tuning via stack tuning documents, use a Terraform origin stack if you need advanced configuration options like auto-scaling policies, custom health check intervals, placement constraints, capacity providers, or fine-grained networking controls. Terraform gives you full control over infrastructure parameters that Docker container origin stacks don't expose. ## Limitations and considerations Currently, only TCP ports are supported for exposed ports. UDP, SCTP, and other protocols are not yet supported. If your application requires non-TCP protocols, consider using Kubernetes resources embedded in Terraform as your origin stack. Docker container origin stacks deploy with default resource limits, but you can customize CPU and memory using a stack tuning document (see "Tuning container resources" section above). For more complex resource configurations or different resource types (GPU, ephemeral storage), use Terraform or CloudFormation to define your container infrastructure directly. Docker container origin stacks support a single container. If you need multi-container deployments (sidecars, service meshes, init containers), embed Kubernetes Deployment resources in a Terraform origin stack instead. ## Troubleshooting **Symptom**: Container orchestration system shows unhealthy or continuously restarting containers. **Cause**: Container image not found, incorrect exposed ports, or application crashes on startup. **Solution**: * Verify the container image exists in the registry * Check that exposed ports match what your application listens on * View container logs: `tensor9 report -customerName acme-corp` * Test the container locally: `docker run -p 8080:8080 your-image` * For Kubernetes: Use `kubectl logs` and `kubectl describe pod` to diagnose * For cloud services: Check the cloud provider's console for detailed error messages **Symptom**: Load balancer endpoint resolves but connection times out or is refused. **Cause**: Security rules not configured correctly, or application not listening on the right port. **Solution**: * Verify the `EXPOSE` directives in your Dockerfile match the ports your application listens on * Check security group/firewall rules allow inbound traffic on exposed ports * Confirm your application binds to `0.0.0.0` (all interfaces) not `localhost` or `127.0.0.1` * Check health check status in the cloud provider's console * For Kubernetes: Use `kubectl port-forward` to test direct connectivity to the pod **Symptom**: Container orchestration fails with image pull errors. **Cause**: Image doesn't exist, registry is unreachable, or authentication issues. **Solution**: * Verify the image exists: `docker pull your-image` * Ensure the image is in a publicly accessible registry or properly authenticated * Check that the registry is accessible from the appliance's cloud environment * For private registries, verify that registry credentials are configured correctly * Review the cloud provider's logging for detailed error messages about the pull failure ## Related topics * [**Terraform**](/origin-stack/terraform): For custom container infrastructure or multi-container deployments * [**Kubernetes**](/origin-stack/kubernetes): For embedding Kubernetes resources in Terraform * [**Deployments**](/fundamentals/deployments): How to create releases and deploy * [**Form Factors**](/fundamentals/key-concepts#form-factor): Understand different cloud environments # Docker Compose Source: https://docs.tensor9.com/origin-stack/docker-compose Docker Compose configurations can be used as origin stacks with Tensor9. A Docker Compose origin stack is a docker-compose.yml file that Tensor9 compiles into complete Kubernetes infrastructure stacks for each appliance across all cloud providers. ## What is a Docker Compose origin stack? A Docker Compose origin stack is your existing docker-compose.yml file. Tensor9 takes your compose configuration and automatically generates all the necessary infrastructure (Kubernetes deployments, services, networking, storage) to run your multi-container application in customer environments - whether that's AWS, Google Cloud, Azure, or private Kubernetes clusters. Tensor9 reads your compose file and maps each service to Kubernetes resources. Services with exposed ports get external load balancers, while internal services use Kubernetes service discovery for inter-service communication. Your origin stack should be your existing Docker Compose configuration. Tensor9 is designed to work with the compose files you already have - you don't need to rebuild your application just for Tensor9. The goal is to maintain a single compose file that works for both your local development and private customer deployments. ## How Docker Compose origin stacks work Your docker-compose.yml file is published to your control plane using `tensor9 stack publish`. Container images referenced in your compose file must be available when creating a release (but not when publishing). When you create a release using `tensor9 stack release create`, your control plane **compiles** your Docker Compose configuration into a **complete Terraform deployment stack** that uses Kubernetes. The compilation generates Kubernetes resources for each service: **For each service in the compose file**: * **Kubernetes Deployment**: Runs your container with specified replicas and resource limits * **Container image**: Copied to the appliance's container registry * **Service with `ports:`**: Gets a LoadBalancer Service for external access * **Service with `expose:` only**: Gets a ClusterIP Service for internal-only access * **Named volumes**: Mapped to PersistentVolumeClaims * **Health checks**: Mapped to liveness and readiness probes * **Secrets**: Mapped to Kubernetes Secrets **Service dependencies**: * Services with `depends_on` are deployed in order using Terraform dependencies **Service discovery**: * All services deployed in the same namespace * Services can reach each other by service name (e.g., `http://api:8080`) The result is a **deployment stack** - a Terraform configuration that defines all the Kubernetes resources needed to run your multi-container application in the target appliance's environment. When deployed, the deployment stack copies all container images to the appliance's container registry. Download the compiled deployment stack and deploy it using Terraform or OpenTofu: ```bash theme={null} # Navigate to the deployment stack directory cd my-test-appliance # Initialize Terraform tofu init # Deploy the infrastructure tofu apply ``` The Terraform deployment creates all the Kubernetes resources (deployments, services, persistent volumes, etc.) automatically and starts your containers. Monitor the deployment using Terraform output and Kubernetes: ```bash theme={null} # View deployment status tensor9 report -customerName acme-corp # View Terraform output tofu output # Check deployments and pods kubectl get deployments kubectl get pods kubectl get services kubectl get pvc ``` You maintain **one docker-compose.yml file**. Tensor9 compiles it into **many deployment stacks** (one per appliance), each customized for that appliance's cloud environment. Each deployment stack is a Terraform configuration that creates Kubernetes resources appropriate for the target cloud provider. ## Prerequisites Before using Docker Compose as an origin stack, ensure you have: * **Docker Compose file**: A valid docker-compose.yml file (v2.x or v3.x) * **Container images in registries**: All images referenced in your compose file must be pushed to container registries (the deployment stack will copy them to the appliance's registry) * **Tensor9 CLI installed**: For creating releases * **Tensor9 API key configured**: Set as `T9_API_KEY` environment variable ## Docker Compose origin stack format A Docker Compose origin stack is your docker-compose.yml file. Here's an example: ```yaml theme={null} version: '3.8' services: web: image: 210620017265.dkr.ecr.us-west-2.amazonaws.com/web:latest ports: - "80:8080" environment: - API_URL=http://api:3000 depends_on: - api deploy: replicas: 2 resources: limits: cpus: '1' memory: 2G api: image: 210620017265.dkr.ecr.us-west-2.amazonaws.com/api:latest expose: - "3000" environment: - DB_HOST=db - DB_PORT=5432 depends_on: - db healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 3 db: image: postgres:15 expose: - "5432" environment: - POSTGRES_PASSWORD_FILE=/run/secrets/db_password volumes: - db-data:/var/lib/postgresql/data secrets: - db_password volumes: db-data: secrets: db_password: external: true ``` Tensor9 reads this compose file and generates Kubernetes resources automatically. **Publishing workflow**: You bind your app to a published compose file. Then, each time you want to release a new version, you update your compose file, republish it, and create a release. Tensor9 will read the updated compose file and generate a new deployment stack. ### Supported compose features **Services**: * `image:` - Container image reference (copied to appliance registry) * `ports:` - External ports (creates LoadBalancer Service) * `expose:` - Internal-only ports (creates ClusterIP Service) * `environment:` - Environment variables (preserved in deployments) * `depends_on:` - Service dependencies (enforced via Terraform ordering) * `deploy.replicas:` - Number of container replicas * `deploy.resources:` - CPU and memory limits * `healthcheck:` - Health check configuration (maps to K8s probes) * `secrets:` - Secrets (map to Kubernetes Secrets) **Volumes**: * Named volumes - Map to PersistentVolumeClaims **Secrets**: * External secrets - Map to Kubernetes Secrets (must be pre-created in namespace) ### Unsupported features (will create StackIssue) The following Docker Compose features are not supported and will create a **StackIssue** during compilation: * `build:` - Building images from Dockerfile * `extends:` - Service inheritance * `profiles:` - Conditional service activation * Bind mounts (e.g., `./local-path:/container-path`) - Use named volumes instead StackIssues can be overridden using a stack tuning document if you need to bypass validation. However, unsupported features will not function even if the issue is overridden. ## Publishing and deploying ### Initial setup (one-time) Publish your compose file to your control plane: ```bash theme={null} tensor9 stack publish \ -stackType DockerCompose \ -stackS3Key my-app-compose \ -file docker-compose.yml ``` This returns a native stack ID like `s3://t9-ctrl-000001/my-app-compose.yml` Bind your app to the published compose file: ```bash theme={null} tensor9 stack bind \ -appName my-app \ -stackType DockerCompose \ -nativeStackId "s3://t9-ctrl-000001/my-app-compose.yml" ``` This only needs to be done once per app. ### Releasing new versions Each time you want to release a new version: ```bash theme={null} # Update your docker-compose.yml file, then republish tensor9 stack publish \ -stackType DockerCompose \ -stackS3Key my-app-compose \ -file docker-compose.yml ``` ```bash theme={null} tensor9 stack release create \ -appName my-app \ -testApplianceName my-test-appliance \ -vendorVersion "1.0.0" ``` Your control plane compiles the Docker Compose file into a complete Terraform deployment stack with Kubernetes resources. Download and deploy the compiled deployment stack: ```bash theme={null} # Navigate to the deployment stack directory cd my-test-appliance # Deploy with Terraform/OpenTofu tofu init tofu apply ``` Once deployed, you can access services with external ports through the load balancer endpoint: ```bash theme={null} # Get the load balancer endpoint for a service kubectl get service web -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' # Or if using an IP kubectl get service web -o jsonpath='{.status.loadBalancer.ingress[0].ip}' ``` Internal services are accessible only from within the cluster using service names. ## Tuning container resources You can customize deployment-specific settings using a **stack tuning document**. This allows you to override compose file settings on a per-release basis without modifying your origin stack. ### Creating a stack tuning document Create a JSON or YAML file that specifies service-specific overrides: ```json theme={null} { "version": "V1", "dockerCompose": { "services": { "web": { "replicas": 4, "resources": { "cpu": "2", "memory": "4Gi" }, "env": { "LOG_LEVEL": "debug" } }, "api": { "replicas": 3, "resources": { "cpu": "1", "memory": "2Gi" } } } } } ``` ### Using the stack tuning document Pass the stack tuning document when creating a release: ```bash theme={null} tensor9 stack release create \ -appName my-app \ -testApplianceName my-test-appliance \ -vendorVersion "1.0.0" \ -tuningDoc tuning.json ``` ### When to use resource tuning Resource tuning is useful when: * **Different customer tiers**: Allocate more resources for enterprise customers * **Performance optimization**: Increase replicas and resources for high-load deployments * **Cost optimization**: Reduce resources for development/testing environments * **Environment-specific configuration**: Add environment variables for specific deployments The stack tuning document overrides settings from your docker-compose.yml for that specific release. You can use different stack tuning documents for different appliances, allowing you to customize resources per customer without changing your origin stack. ## Generated Kubernetes resources When Tensor9 compiles your Docker Compose origin stack, it generates Kubernetes resources for each service: | Compose Feature | Kubernetes Resource | | ---------------------------------- | --------------------------------------------------------- | | `services.{name}` | Deployment with DNS-safe name (lowercase, no underscores) | | `services.{name}.ports` | LoadBalancer Service (external access) | | `services.{name}.expose` | ClusterIP Service (internal-only) | | `services.{name}.deploy.replicas` | Deployment replica count | | `services.{name}.deploy.resources` | Container resource limits and requests | | `services.{name}.healthcheck` | Liveness and readiness probes | | `services.{name}.environment` | Container environment variables | | `services.{name}.secrets` | References to Kubernetes Secrets | | `services.{name}.depends_on` | Terraform resource dependencies | | `volumes.{name}` | PersistentVolumeClaim | | `secrets.{name}` | Kubernetes Secret (must be pre-created) | All services are deployed in the same Kubernetes namespace, enabling service-to-service communication using service names. ## Managing secrets Pass sensitive data to your containers as environment variables using secrets defined in the tuning document. This allows you to reference secrets from AWS Secrets Manager or SSM Parameter Store without embedding sensitive values in your compose file. ### Defining secrets in the tuning document Create a tuning document that defines secrets alongside your compose file: **docker-compose.yml**: ```yaml theme={null} services: api: image: myapp/api:latest environment: - DB_PASSWORD=${DB_PASSWORD} - API_KEY=${API_KEY} worker: image: myapp/worker:latest environment: - DB_PASSWORD=${DB_PASSWORD} - QUEUE_TOKEN=${QUEUE_TOKEN} ``` **tuning.json**: ```json theme={null} { "version": "V1", "dockerCompose": { "secrets": { "db_password": { "source": "aws_secretsmanager", "secretId": "prod/db/password", "environmentVariable": "DB_PASSWORD" }, "api_key": { "source": "aws_ssm_parameter", "parameter": "/prod/api/key", "environmentVariable": "API_KEY" }, "queue_token": { "source": "aws_secretsmanager", "secretId": "prod/queue/token", "environmentVariable": "QUEUE_TOKEN" } } } } ``` ### Publishing with secrets When you publish and create a release, pass the tuning document: ```bash theme={null} # Publish your compose file tensor9 stack publish \ -appName my-app \ -type DockerCompose \ -vendorVersion "1.0.0" \ -composeFile docker-compose.yml # Create release with tuning document tensor9 stack release create \ -appName my-app \ -customerName acme-corp \ -vendorVersion "1.0.0" \ -tuningDoc tuning.json ``` Tensor9 will automatically: 1. Fetch the secrets from AWS Secrets Manager or SSM Parameter Store 2. Inject them as environment variables into your containers 3. Name each secret by the path it lives at in your own AWS account ### Accessing secrets in your application Your application reads secrets from environment variables: ```python theme={null} import os # Read secrets from environment variables db_password = os.environ['DB_PASSWORD'] api_key = os.environ['API_KEY'] ``` Use environment variables for values needed when each service starts. Services that fetch secrets while running can use supported AWS SDK calls through the configured [Secrets Manager service adapter](/service-adapters/aws/security-identity/secrets-manager). Configure access for those services and review the selected target's limits. ### Alternative: Docker Compose secrets (not recommended) Docker Compose `secrets:` map to Kubernetes Secrets, which must be pre-created in the appliance namespace: ```yaml theme={null} services: api: image: myapp/api:latest secrets: - db_password secrets: db_password: external: true ``` This approach is not recommended because: * Kubernetes Secrets must be manually created in each appliance * It doesn't work consistently across all deployment targets * The tuning document approach provides better secret management ## Exposing ports Docker Compose provides two ways to expose ports: `ports:` for external access and `expose:` for internal service-to-service communication. Tensor9 compiles these to appropriate Kubernetes Services. ### External access with `ports:` Use `ports:` to make a service accessible from outside the cluster: ```yaml theme={null} services: api: image: myapp/api:latest ports: - "8080:8080" # Host port:container port - "443:8443" # Map 443 → 8443 ``` **How Tensor9 compiles this:** When you use `ports:` in Docker Compose, Tensor9 creates a Kubernetes **LoadBalancer Service**, which automatically provisions a cloud-native load balancer: | Cloud Provider | Load Balancer Type | What Gets Created | | ---------------- | -------------------------------------------------------------- | ----------------------------------------------- | | **AWS** | Network Load Balancer (NLB) or Application Load Balancer (ALB) | Elastic Load Balancing resource with public DNS | | **Google Cloud** | Cloud Load Balancing | Global/regional load balancer with public IP | | **Azure** | Azure Load Balancer | Public load balancer with frontend IP | **The flow:** 1. Docker Compose `ports:` → Kubernetes LoadBalancer Service 2. Kubernetes LoadBalancer Service → Cloud load balancer provisioning 3. Cloud load balancer → Routes traffic to your pods 4. Public endpoint exposed automatically **Access the service:** ```bash theme={null} # View the external endpoint kubectl get service api-service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) api-service LoadBalancer 10.100.200.50 abc123-1234567890.us-east-1.elb.amazonaws.com 8080:31234/TCP ``` The `EXTERNAL-IP` is your public endpoint that routes to your containers. **Alternative: Kubernetes Ingress** For HTTP/HTTPS services, you can optionally use Kubernetes Ingress instead of LoadBalancer Services. Ingress provides: * Path-based routing (e.g., `/api` → api service, `/admin` → admin service) * TLS/SSL termination * Single load balancer for multiple services (cost savings) However, Docker Compose doesn't have native Ingress support. If you need Ingress, use a Terraform origin stack with explicit Kubernetes resources: ```terraform theme={null} # Terraform approach with Ingress resource "kubernetes_ingress_v1" "main" { spec { rule { host = "myapp.example.com" http { path { path = "/api" backend { service { name = "api-service" port { number = 8080 } } } } } } } } ``` For Docker Compose, Tensor9 uses LoadBalancer Services by default, which is simpler but creates one load balancer per exposed service. ### Internal-only access with `expose:` Use `expose:` for services that should only be accessible from within the cluster: ```yaml theme={null} services: api: image: myapp/api:latest expose: - "8080" # Only accessible internally worker: image: myapp/worker:latest environment: - API_URL=http://api:8080 # Can access via service name ``` **How Tensor9 compiles this:** * Creates a Kubernetes **ClusterIP Service** * Only accessible within the Kubernetes cluster * No external load balancer provisioned * Other services can access via DNS name (e.g., `http://api:8080`) ### Port mapping syntax Docker Compose supports several port mapping formats: ```yaml theme={null} services: api: ports: - "8080:8080" # Simple mapping - "443:8443" # Map different ports - "8080" # Container port only (host port assigned randomly) - "127.0.0.1:8080:8080" # Bind to specific IP (not supported in Tensor9) ``` **IP binding not supported**: Port mappings with IP addresses (e.g., `127.0.0.1:8080:8080`) are not supported. Use simple port mappings like `8080:8080` instead. ### Protocol support Currently, only **TCP** is supported for port mappings: ```yaml theme={null} services: api: ports: - "8080:8080" # ✅ TCP (default) - "8080:8080/tcp" # ✅ Explicit TCP - "53:53/udp" # ❌ UDP not supported ``` If your application requires UDP or other protocols, use a Terraform origin stack with custom Kubernetes manifests. ### Multiple ports You can expose multiple ports from a single service: ```yaml theme={null} services: api: image: myapp/api:latest ports: - "8080:8080" # HTTP - "8443:8443" # HTTPS - "9090:9090" # Metrics endpoint ``` Each port mapping creates a corresponding port definition in the Kubernetes LoadBalancer Service. ### Best practices for ports **Use standard ports for common protocols:** ```yaml theme={null} services: web: ports: - "80:8080" # HTTP - "443:8443" # HTTPS ``` **Separate public and internal services:** ```yaml theme={null} services: api: ports: - "443:8443" # External API (LoadBalancer) database: expose: - "5432" # Internal only (ClusterIP) cache: expose: - "6379" # Internal only (ClusterIP) ``` **Document exposed ports clearly:** * External ports incur cloud load balancer costs * Use `expose:` for internal services to avoid unnecessary load balancers * Consider consolidating external endpoints through an API gateway ### Service-to-service communication Services can communicate with each other using service names as DNS hostnames: ```yaml theme={null} services: api: expose: - "8080" worker: environment: - API_URL=http://api:8080 - CACHE_URL=redis://cache:6379 cache: image: redis:latest expose: - "6379" ``` All services in your compose file are deployed in the same Kubernetes namespace, enabling seamless service discovery via DNS. ## Best practices Always use specific version tags for container images (`:v1.0.0` or `:latest`) consistently. This ensures reproducible deployments across customer appliances. Define health checks in your compose file for each service. These map to Kubernetes readiness and liveness probes, ensuring traffic is only routed to healthy containers. Pass sensitive data as environment variables using the tuning document. Define secrets in AWS Secrets Manager or SSM Parameter Store and reference them as environment variables in your compose file. See the [Managing secrets](#managing-secrets) section for complete details and examples. Always use named volumes (not bind mounts) for persistent data. Named volumes map to PersistentVolumeClaims and work across all cloud providers. Test your compose file locally with `docker-compose up` before publishing. Verify that services can communicate, health checks work, and volumes persist data correctly. ## Limitations and considerations Docker Compose origin stacks deploy using Kubernetes. All form factors support Kubernetes, so this works everywhere, but the generated infrastructure will always use Kubernetes resources (Deployments, Services, PVCs). Currently, only TCP ports are supported for exposed ports. UDP, SCTP, and other protocols are not yet supported. If your application requires non-TCP protocols, use Terraform with custom Kubernetes manifests. Do not use Docker Compose `secrets:` in your compose file. Instead, use the tuning document to define secrets from AWS Secrets Manager or SSM Parameter Store: ```yaml theme={null} # docker-compose.yml services: api: environment: - DB_PASSWORD=${DB_PASSWORD} ``` ```json theme={null} // tuning.json { "secrets": { "db_password": { "source": "aws_secretsmanager", "secretId": "prod/db/password", "environmentVariable": "DB_PASSWORD" } } } ``` This approach provides centralized secret management and works consistently across all deployment targets. See [Managing secrets](#managing-secrets) for complete documentation. The `build:` directive is not supported. All services must reference pre-built container images in registries. If you need to build images, do so before publishing your compose file and reference the built images. Only named volumes are supported. Bind mounts, tmpfs volumes, and volume driver options are not supported. Use PersistentVolumeClaims for all persistent storage needs. Tensor9 supports Docker Compose file format v2.x and v3.x. Older v1 format and experimental features are not supported. ## Troubleshooting **Symptom**: Kubernetes pods show CrashLoopBackOff or are continuously restarting. **Cause**: Container image not found, incorrect environment variables, missing secrets, or application crashes on startup. **Solution**: * Verify all container images exist in registries * Check that environment variables and secrets are correctly configured * View pod logs: `kubectl logs ` * Describe the pod: `kubectl describe pod ` * Test containers locally: `docker-compose up` **Symptom**: One service cannot reach another service (connection refused, DNS resolution fails). **Cause**: Incorrect service names, missing expose directives, or network policies blocking traffic. **Solution**: * Verify service names match those in docker-compose.yml (DNS-safe: lowercase, no underscores) * Check that services have `expose:` or `ports:` directives * Verify services are in the same namespace: `kubectl get services -n ` * Test connectivity from within a pod: `kubectl exec -- curl http://:` **Symptom**: PVC status shows Pending and pods can't start. **Cause**: No storage class available, insufficient storage quota, or cloud provider permissions issues. **Solution**: * Check PVC status: `kubectl get pvc` * Describe the PVC: `kubectl describe pvc ` * Verify storage class exists: `kubectl get storageclass` * Check cloud provider quota and permissions for creating volumes * Review events: `kubectl get events --sort-by='.lastTimestamp'` **Symptom**: Pods fail with "secret not found" errors. **Cause**: Kubernetes Secrets referenced in compose file don't exist in the namespace. **Solution**: * List secrets in namespace: `kubectl get secrets -n ` * Create missing secrets: `kubectl create secret generic --from-literal=key=value` * Verify secret names match those in docker-compose.yml * Check that secrets are marked as `external: true` in compose file **Symptom**: Cannot access service through external load balancer. **Cause**: Load balancer not provisioned, security groups blocking traffic, or service not ready. **Solution**: * Check service status: `kubectl get service ` * Verify load balancer is provisioned (may take a few minutes) * Check that external-facing port in `ports:` matches your application's listening port * Verify cloud provider security groups/firewall rules allow inbound traffic * Check pod readiness: `kubectl get pods` - all replicas should be Running and Ready ## Related topics * [**Terraform**](/origin-stack/terraform): For custom Kubernetes configurations or advanced features * [**Kubernetes**](/origin-stack/kubernetes): For embedding Kubernetes resources in Terraform * [**Docker**](/origin-stack/docker): For single-container deployments * [**Deployments**](/fundamentals/deployments): How to create releases and deploy * [**Form Factors**](/fundamentals/key-concepts#form-factor): Understand different cloud environments # Kubernetes Source: https://docs.tensor9.com/origin-stack/kubernetes Kubernetes resources can be used with Tensor9 by embedding them within Terraform or CloudFormation origin stacks. Unlike other infrastructure-as-code formats that Tensor9 supports, Kubernetes manifests cannot be used as standalone origin stacks - they must be embedded within a parent origin stack. ## What is a Kubernetes origin stack? A Kubernetes origin stack consists of Kubernetes resources (Deployments, Services, ConfigMaps, etc.) defined within a Terraform or CloudFormation origin stack using the respective provider's Kubernetes resources. **Key characteristic**: Kubernetes resources are always embedded within another origin stack format. The parent origin stack (Terraform or CloudFormation) serves as the container, while Kubernetes manifests define the workload orchestration. Your Kubernetes resources should be part of your existing Terraform or CloudFormation configuration. Tensor9 is designed to work with the infrastructure-as-code you already have - you don't need to rewrite your Kubernetes deployments just for Tensor9. The goal is to maintain a single stack that works for both your cloud deployment and private customer deployments. ### Why embed Kubernetes? Kubernetes manifests define **how your application runs** (pods, deployments, services), but they don't provision the underlying **infrastructure** (clusters, networks, load balancers). By embedding Kubernetes within Terraform or CloudFormation, you get: * **Complete infrastructure**: The parent stack provisions the cluster (EKS, GKE, AKS) and supporting infrastructure * **Unified deployment**: One release process deploys both infrastructure and workloads * **Automatic artifact handling**: Tensor9 automatically copies container images to customer environments * **Form factor adaptation**: Kubernetes workloads adapt to different cloud environments seamlessly ## How Kubernetes origin stacks work In your Terraform or CloudFormation origin stack, use the Kubernetes provider to define your Kubernetes resources. For Terraform, this typically means using `kubernetes_manifest` or `kubernetes_deployment` resources: **Terraform example:** ```hcl theme={null} resource "kubernetes_manifest" "my_app_deployment" { manifest = { apiVersion = "apps/v1" kind = "Deployment" metadata = { name = "my-app" namespace = "default" } spec = { replicas = 3 selector = { matchLabels = { app = "my-app" } } template = { metadata = { labels = { app = "my-app" } } spec = { containers = [ { name = "my-app" image = "myregistry.io/my-app:v1.0.0" ports = [ { containerPort = 8080 } ] } ] } } } } } ``` Publish your parent origin stack (Terraform or CloudFormation) to your control plane. When you create a release for an appliance, your control plane: 1. **Finds Kubernetes resources**: Scans the origin stack for `kubernetes_manifest`, `kubernetes_deployment`, and other Kubernetes provider resources 2. **Extracts container images**: Identifies all container image references in Kubernetes specs 3. **Prepares image copying**: Configures the deployment stack to copy images to the appliance's container registry (specific to the cloud provider) 4. **Rewrites image references**: Updates container image fields to point to the locally-copied images 5. **Compiles the deployment stack**: Generates a ready-to-deploy stack with all Kubernetes resources intact The result is a **deployment stack** that includes both your infrastructure and Kubernetes workloads. When deployed, the deployment stack will copy the container images into the customer's appliance. Deploy the compiled deployment stack using the parent stack's tooling: **For Terraform deployment stacks:** ```bash theme={null} cd acme-corp-production tofu init tofu apply ``` **For CloudFormation deployment stacks:** Your control plane automatically creates the CloudFormation stack in your control plane's AWS account. Monitor deployment using: ```bash theme={null} tensor9 report -customerName acme-corp ``` During deployment, the Kubernetes resources are applied to the customer's cluster with all container images pointing to the locally-copied versions. ## Supported Kubernetes resources Tensor9 automatically handles artifact copying for these Kubernetes resource types: ### kubernetes\_manifest (Terraform) The `kubernetes_manifest` resource accepts any Kubernetes manifest as a map. Tensor9 automatically detects and processes: * **Deployments** (`kind: "Deployment"`): Extracts container images from `spec.template.spec.containers[].image` * **Other workload types**: Support for additional resource types is expanding **Example:** ```hcl theme={null} resource "kubernetes_manifest" "nginx" { manifest = { apiVersion = "apps/v1" kind = "Deployment" metadata = { name = "nginx" } spec = { template = { spec = { containers = [ { name = "nginx" image = "docker.io/nginx:1.21" # Automatically copied } ] } } } } } ``` ### kubernetes\_deployment (Terraform) The `kubernetes_deployment` resource provides typed Kubernetes Deployment support. Tensor9 extracts container images from the deployment spec. **Example:** ```hcl theme={null} resource "kubernetes_deployment" "app" { metadata { name = "my-app" } spec { template { spec { container { name = "app" image = "ghcr.io/myorg/app:v1.0.0" # Automatically copied } } } } } ``` ### Other Kubernetes provider resources Tensor9 supports other Kubernetes provider resources (Services, ConfigMaps, Secrets, etc.). These resources pass through compilation unchanged, as they typically don't reference external artifacts. ## Helm charts Helm charts can be deployed using the Terraform Helm provider. Helm is a package manager for Kubernetes that bundles multiple Kubernetes resources into a single deployable unit called a "chart." Include the Helm provider in your `required_providers` block: ```hcl theme={null} terraform { required_providers { helm = { source = "hashicorp/helm" version = "~> 3.0" } kubernetes = { source = "hashicorp/kubernetes" version = "~> 2.38" } } } ``` Configure the Helm provider to use your cluster's endpoint and credentials: ```hcl theme={null} provider "helm" { kubernetes { host = aws_eks_cluster.main.endpoint cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data) token = data.aws_eks_cluster_auth.main.token } } ``` Define `helm_release` resources to deploy Helm charts: ```hcl theme={null} resource "helm_release" "nginx_ingress" { name = "nginx-ingress" repository = "https://kubernetes.github.io/ingress-nginx" chart = "ingress-nginx" namespace = "default" version = "4.10.1" set { name = "controller.service.type" value = "LoadBalancer" } depends_on = [aws_eks_cluster.main] } ``` ### How Helm charts are compiled The `helm_release` resource is included in the deployment stack unchanged. When you run `tofu apply` on the deployment stack, Terraform installs the Helm chart into the cluster. Container images referenced within Helm charts are pulled from their original registries at deployment time. Currently, Tensor9 does not automatically copy container images referenced within Helm charts to the appliance's container registry. This means Helm charts must be able to pull images from their original registries (e.g., Docker Hub, GitHub Container Registry). If your appliance cannot reach external registries, consider using Kubernetes manifests directly instead of Helm charts, or pre-pull images and reference them from a local registry. ### Best practices for Helm charts * **Pin chart versions**: Always specify a `version` in your `helm_release` to ensure consistent deployments * **Use accessible registries**: Ensure Helm charts reference images from registries the appliance can reach * **Test chart deployments**: Validate Helm charts in test appliances before deploying to customer appliances * **Configure chart values**: Use `set` or `values` blocks to customize chart behavior for each environment ## Container image handling Tensor9 automatically handles container images referenced in Kubernetes resources: ### Automatic image copying When Tensor9 finds a container image in a Kubernetes resource, it: 1. **Validates the image reference**: Ensures the image has a registry (e.g., `docker.io`, `ghcr.io`, `myregistry.io`) 2. **Configures image copying**: Prepares the deployment stack to copy the image to the appliance's container registry 3. **Rewrites the reference**: Updates the Kubernetes manifest to point to the locally-copied image **Before compilation (origin stack):** ```hcl theme={null} spec = { containers = [ { image = "docker.io/nginx:1.21" } ] } ``` **After compilation (deployment stack):** ```hcl theme={null} spec = { containers = [ { # Now points to the customer's ECR or appliance registry image = "123456789.dkr.ecr.us-west-2.amazonaws.com/t9-app-images:nginx-1.21-abc123" } ] } ``` ### Image registry requirements For Tensor9 to copy a container image, it must include a registry in the image reference: * ✅ **Supported**: `docker.io/nginx:latest`, `ghcr.io/myorg/app:v1.0`, `myregistry.io/image:tag` * ⚠️ **Skipped**: `nginx:latest` (no registry - assumed to be publicly available in the appliance) Images without a registry are assumed to be publicly available from Docker Hub and are not copied. If your appliance cannot reach Docker Hub, make sure to include the registry prefix: `docker.io/nginx:latest` instead of `nginx:latest`. ### Where images are stored Copied container images are stored in the appliance's container registry. The storage location is determined by the appliance's form factor and is handled automatically by Tensor9: | Appliance Environment | Container Registry | | --------------------- | ---------------------------------- | | AWS | Amazon ECR | | Google Cloud | Google Artifact Registry | | Azure | Azure Container Registry | | Private Kubernetes | Appliance-local container registry | ## Prerequisites Before using Kubernetes resources in your origin stack: ### For Terraform origin stacks * **Kubernetes provider configured**: Include the Kubernetes Terraform provider in your `required_providers` * **Cluster access configured**: The Kubernetes provider must be configured to connect to your cluster (typically via EKS, GKE, or AKS data sources) * **Valid Kubernetes manifests**: Your Kubernetes resources must be valid according to the Kubernetes API **Example Terraform configuration:** ```hcl theme={null} terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } kubernetes = { source = "hashicorp/kubernetes" version = "~> 2.38" } } } # Configure Kubernetes provider using EKS cluster data "aws_eks_cluster" "cluster" { name = aws_eks_cluster.main.name } data "aws_eks_cluster_auth" "cluster" { name = aws_eks_cluster.main.name } provider "kubernetes" { host = data.aws_eks_cluster.cluster.endpoint cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority[0].data) token = data.aws_eks_cluster_auth.cluster.token } ``` ### For CloudFormation origin stacks Support for Kubernetes resources in CloudFormation origin stacks is under development. Currently, Terraform is the recommended approach for embedding Kubernetes resources. If you have a CloudFormation + Kubernetes use case, please reach out to [support@tensor9.com](mailto:support@tensor9.com) to discuss your requirements. ## Example: Complete Terraform + Kubernetes origin stack This example shows a complete Terraform origin stack that provisions an EKS cluster and deploys a Kubernetes application: ```hcl theme={null} # Configure providers terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } kubernetes = { source = "hashicorp/kubernetes" version = "~> 2.38" } } } provider "aws" { region = "us-west-2" } # Create EKS cluster and supporting infrastructure # (VPC, subnets, IAM roles, etc. - see AWS documentation) resource "aws_eks_cluster" "main" { name = "my-app-cluster" role_arn = aws_iam_role.eks_cluster.arn vpc_config { subnet_ids = [aws_subnet.private_1.id, aws_subnet.private_2.id] } } resource "aws_eks_node_group" "main" { cluster_name = aws_eks_cluster.main.name node_group_name = "main-nodes" node_role_arn = aws_iam_role.eks_nodes.arn subnet_ids = [aws_subnet.private_1.id, aws_subnet.private_2.id] scaling_config { desired_size = 2 max_size = 4 min_size = 1 } } # Configure Kubernetes provider data "aws_eks_cluster" "main" { name = aws_eks_cluster.main.name } data "aws_eks_cluster_auth" "main" { name = aws_eks_cluster.main.name } provider "kubernetes" { host = data.aws_eks_cluster.main.endpoint cluster_ca_certificate = base64decode(data.aws_eks_cluster.main.certificate_authority[0].data) token = data.aws_eks_cluster_auth.main.token } # Deploy application to Kubernetes resource "kubernetes_manifest" "app_deployment" { manifest = { apiVersion = "apps/v1" kind = "Deployment" metadata = { name = "my-app" namespace = "default" } spec = { replicas = 3 selector = { matchLabels = { app = "my-app" } } template = { metadata = { labels = { app = "my-app" } } spec = { containers = [ { name = "app" # This image will be automatically copied to the customer's ECR image = "ghcr.io/myorg/my-app:v1.0.0" ports = [ { containerPort = 8080 } ] env = [ { name = "DATABASE_URL" value = "postgresql://..." } ] } ] } } } } } resource "kubernetes_manifest" "app_service" { manifest = { apiVersion = "v1" kind = "Service" metadata = { name = "my-app" namespace = "default" } spec = { type = "LoadBalancer" selector = { app = "my-app" } ports = [ { port = 80 targetPort = 8080 } ] } } } ``` ## Publishing and deploying Publishing and deploying a Kubernetes origin stack follows the same workflow as any Terraform origin stack: ### 1. Publish your origin stack ```bash theme={null} tensor9 stack publish \ -stackType TerraformWorkspace \ -stackS3Key my-kubernetes-app \ -dir /path/to/terraform ``` This uploads your entire Terraform workspace (including Kubernetes resources) to your control plane. ### 2. Bind the stack to your app ```bash theme={null} tensor9 stack bind \ -appName my-app \ -nativeStackId "s3://bucket/my-kubernetes-app.tf.tgz" ``` Only needed once per app. ### 3. Create a release ```bash theme={null} tensor9 stack release create \ -appName my-app \ -testApplianceName my-test-appliance \ -vendorVersion "1.0.0" ``` Your control plane compiles the origin stack, automatically copying container images and rewriting references. ### 4. Deploy the deployment stack ```bash theme={null} cd my-test-appliance tofu init tofu apply ``` This creates the EKS cluster, node groups, and deploys your Kubernetes workloads. The deployment stack will copy container images into the customer's appliance. ## Best practices Include the full registry in your container image references (`docker.io/nginx:latest` instead of `nginx:latest`). This ensures Tensor9 can copy the images to customer environments. Avoid using `:latest` tags in production. Use specific version tags (`v1.0.0`, `sha-abc123`) to ensure consistent deployments across customer appliances. When configuring the Kubernetes provider to connect to your cluster, use data sources (like `aws_eks_cluster`) rather than hardcoded values. This ensures the provider configuration adapts to each appliance's cluster. For startup values, declare secrets in your origin stack and pass their values into the pod's environment as shown below. Applications that need runtime reads can instead keep using supported AWS SDK calls through the configured [Secrets Manager service adapter](/service-adapters/aws/security-identity/secrets-manager). Review its target-specific limits and configure application access before deployment. ```terraform theme={null} # Define secret in AWS Secrets Manager resource "aws_secretsmanager_secret" "db_password" { name = "prod/db/password" } # Reference in Kubernetes Deployment as environment variable resource "kubernetes_deployment" "app" { spec { template { spec { container { env { name = "DB_PASSWORD" value_from { secret_key_ref { name = aws_secretsmanager_secret.db_password.name key = "password" } } } } } } } } ``` For non-sensitive configuration, Kubernetes ConfigMaps are appropriate. Always specify resource requests and limits for containers. This ensures proper scheduling and prevents resource contention in customer clusters. ## Limitations and considerations Kubernetes YAML files cannot be used as standalone origin stacks. They must be embedded within Terraform or CloudFormation. This is by design - Kubernetes defines workloads, not the underlying infrastructure. The parent origin stack (Terraform/CloudFormation) must provision or reference the Kubernetes cluster before defining Kubernetes resources. Use proper dependency management (`depends_on` in Terraform) to ensure correct ordering. Ensure your Kubernetes provider version is compatible with your target cluster version. Different Kubernetes versions may have different API schemas for resources. If your container images require authentication to pull, you'll need to configure image pull secrets in your Kubernetes manifests. Tensor9 copies the images but doesn't automatically create pull secrets. ## Troubleshooting **Symptom**: Deployment fails because container image cannot be pulled from the original registry. **Cause**: Image reference doesn't include a registry, or Tensor9 couldn't detect the image reference. **Solution**: * Ensure your image reference includes the full registry: `docker.io/nginx:latest` * Check that the image is referenced in a supported field (`spec.containers[].image`) * Review compilation logs for warnings about skipped images **Symptom**: Terraform apply fails with "unable to connect to Kubernetes cluster" error. **Cause**: Kubernetes provider is not correctly configured to connect to the cluster. **Solution**: * Verify the cluster exists before applying Kubernetes resources * Use data sources to dynamically configure the provider * Check that IAM roles/permissions allow cluster access **Symptom**: Kubernetes deployment is created but pods fail to start with `ImagePullBackOff`. **Cause**: Pods cannot pull the container image from the appliance registry. **Solution**: * Verify that the deployment stack shows rewritten image references * Check that the node group has permissions to pull from ECR (for AWS) * Ensure the image was successfully copied during compilation ## Related topics * [**Terraform/OpenTofu**](/origin-stack/terraform): Learn about Terraform origin stacks * [**CloudFormation**](/origin-stack/cloudformation): Learn about CloudFormation origin stacks * [**Deployments**](/fundamentals/deployments): Understand the deployment workflow * [**Form Factors**](/fundamentals/key-concepts#form-factor): How Kubernetes workloads adapt to different environments # Terraform/OpenTofu Source: https://docs.tensor9.com/origin-stack/terraform Terraform and OpenTofu are the most common infrastructure-as-code tools used with Tensor9. A Terraform origin stack is a standard Terraform workspace that Tensor9 compiles into customer-specific deployment stacks for each appliance. ## What is a Terraform origin stack? A Terraform origin stack is your existing Terraform configuration - the `.tf` files that define your application's infrastructure. Tensor9 uses this as the blueprint to generate deployment stacks tailored to each customer's environment. When you publish a Terraform origin stack to Tensor9, your control plane: 1. Archives your Terraform workspace into a `.tf.tgz` file 2. Uploads it to your control plane's S3 bucket 3. Uses it as the template for generating deployment stacks for each appliance The key difference from standard Terraform usage: **you maintain one origin stack** that Tensor9 compiles into many deployment stacks - one per customer appliance. Your origin stack should be your existing Terraform configuration. Tensor9 is designed to work with the infrastructure-as-code you already have - you don't need to write a new stack just for Tensor9. The goal is to maintain a single stack that works for both your cloud deployment and private customer deployments. ## How Terraform origin stacks work Using Terraform with Tensor9 follows a straightforward workflow: You publish your Terraform workspace to your control plane using `tensor9 stack publish`. This uploads your `.tf` files as a compressed archive to your control plane's S3 bucket. When you want to deploy to an appliance, you create a release using `tensor9 stack release create`. During release creation, your control plane **compiles** your origin stack into a **deployment stack** tailored to that specific appliance. The compilation process: * Translates cloud-specific resources to match the appliance's target environment (e.g., AWS RDS → Google Cloud SQL) * Resolves the `@namespace` annotation to a per-install value, ensuring resource uniqueness * Instruments the stack for observability (logs, metrics, traces) * Rewrites artifact references to point to appliance-local locations The result is a **deployment stack** - a new Terraform workspace ready to deploy to that specific appliance. Your control plane downloads the compiled deployment stack into a directory named after your appliance. This deployment stack is itself a complete Terraform workspace. You deploy it using standard Terraform commands: **For a test appliance:** ```bash theme={null} cd my-test-appliance tofu init tofu apply ``` **For a customer appliance:** ```bash theme={null} cd acme-corp-production tofu init tofu apply ``` This creates all the infrastructure resources in the appliance environment. You write and maintain **one origin stack**. Tensor9 compiles it into **many deployment stacks** (one per appliance), each customized for that appliance's target environment. You then deploy each deployment stack using standard `tofu apply`. ## Prerequisites Before using Terraform as an origin stack, ensure you have: * **Terraform or OpenTofu installed**: Version 1.0+ recommended * **Valid Terraform configuration**: Your configuration must pass `tofu validate` * **Tensor9 CLI installed**: For publishing your origin stack to your control plane * **Tensor9 API key configured**: Set as `T9_API_KEY` environment variable This guide uses the `tofu` CLI in all examples. If you're using Terraform instead of OpenTofu, simply replace `tofu` with `terraform` in all commands - they work identically. ## Structure of a Terraform origin stack Your Terraform origin stack should follow standard Terraform conventions: ``` my-app/ ├── main.tf # Main resource definitions ├── variables.tf # Variable declarations ├── outputs.tf # Output definitions ├── versions.tf # Provider version constraints ├── backend.tf # (Optional) Backend configuration └── modules/ # (Optional) Local modules └── networking/ ├── main.tf └── variables.tf ``` Tensor9 will archive this entire directory structure when you publish. ## Publishing your Terraform origin stack To make your Terraform configuration available to Tensor9, publish it to your control plane: ```bash theme={null} tensor9 stack publish \ -stackType TerraformWorkspace \ -stackS3Key my-stack \ -dir /path/to/terraform ``` ### What gets published The `tensor9 stack publish` command: 1. Creates a `.tf.tgz` archive of all `.tf` files in the specified directory 2. Uploads the archive to your control plane's S3 bucket 3. Returns a **native stack ID** you'll use to bind the stack to your app **Example output:** ``` Creating archive of .tf files in /path/to/your/terraform Uploading /tmp/my-stack.tf.tgz to s3://t9-ctrl-000001/terraform-stacks/origins/my-stack.tf.tgz Successfully uploaded stack. The native stack ID is s3://t9-ctrl-000001/terraform-stacks/origins/my-stack.tf.tgz ``` ### Publishing updates When you make changes to your Terraform configuration, publish a new version: ```bash theme={null} # Update your .tf files # Then publish the new version tensor9 stack publish \ -stackType TerraformWorkspace \ -stackS3Key my-stack \ -dir /path/to/terraform ``` The new version becomes available for creating releases. Previously deployed appliances continue running their current version until you create and deploy a new release. ## Binding your origin stack to an app After publishing for the first time, bind your origin stack to your app: ```bash theme={null} tensor9 stack bind \ -appName my-app \ -stackType TerraformWorkspace \ -nativeStackId s3://t9-ctrl-000001/terraform-stacks/origins/my-stack.tf.tgz ``` **Important**: You only need to bind once. Future publishes of the same stack don't require re-binding. ## Parameterization Parameterization is the process of making your origin stack capable of being deployed to multiple appliances without resource naming conflicts. This is the most critical requirement for a Terraform origin stack in Tensor9. ### The @namespace annotation Declare a variable and annotate it with `@namespace`. At compile time, Tensor9 replaces the variable's default with a deterministic namespace derived from your app name, the customer's name, and the appliance's ID: ```terraform theme={null} #@namespace(max_size=32, delimiter='-') variable "namespace" { type = string description = "Prefix that keeps globally-unique resource names distinct per install" default = "" } ``` `max_size` is required and must be `16`, `32`, or `64`. It caps the length of the generated value so that names built from it stay inside the limits of the tightest namespace you use (S3 bucket names, for example, cap at 63 characters). `delimiter` is optional and defaults to an empty string; it is appended to the generated value so you can write `"${var.namespace}myapp-data"` without a separator of your own. The empty default keeps the stack deployable outside Tensor9. When you run `tofu apply` against your own account, `var.namespace` stays `""` and your resource names are unchanged. Tensor9 fills the value in only during compilation, so you never set it by hand. ### Using the namespace for resource naming Reference the variable as a prefix on every name that has to be unique across all deployments: ```terraform theme={null} # ✓ CORRECT: Unique per install resource "aws_s3_bucket" "data" { bucket = "${var.namespace}myapp-data" } resource "aws_db_instance" "postgres" { identifier = "${var.namespace}myapp-db" } resource "aws_lambda_function" "api" { function_name = "${var.namespace}myapp-api" } # ✗ INCORRECT: Will cause conflicts across installs resource "aws_s3_bucket" "data" { bucket = "myapp-data" # Multiple installs will try to create the same bucket } ``` ### What to parameterize Prefix with `var.namespace` every name that has to be distinct, whatever its scope: * **Globally unique names**: S3 bucket names are unique across all of AWS * **Resource identifiers**: RDS identifiers, Lambda function names, EKS cluster names * **IAM resources**: Role names, policy names * **Networking**: VPC names, subnet tags, security group names * **Logging**: CloudWatch log group names * **Secret paths**: Secrets Manager secret names **Don't assume the account boundary makes a name safe.** It's tempting to reason that only globally unique names such as S3 buckets need the prefix, because account-scoped and region-scoped names can't collide when each install has its own account. Nothing guarantees that: a customer can put two installs in one account, and dev and staging environments routinely share one. Parameterize account-scoped and region-scoped names too. This matters more than a failed `apply` would suggest. Several AWS creates are really upserts - `PutRule`, `PutDashboard`, `PutMetricAlarm`, `PutRolePolicy`, `CreateCluster`, `RegisterTaskDefinition` - so a collision doesn't error. The second install silently takes over the first one's resource, and tearing it down deletes it. **DNS names are managed automatically**: Tensor9 automatically generates DNS names for your appliances using either your vendor vanity domain or the customer's vanity domain (if they specified one). You don't need to include the namespace in DNS records. See [Endpoints and DNS](/fundamentals/endpoints) for details. Without proper parameterization, attempting to deploy to multiple appliances will result in resource creation failures as Terraform tries to create duplicate resources. ## Complete example origin stack Here's a complete Terraform origin stack for a typical application: ### main.tf ```terraform theme={null} # Lambda function for API resource "aws_lambda_function" "api" { function_name = "${var.namespace}myapp-api" handler = "index.handler" runtime = "nodejs18.x" role = aws_iam_role.api_role.arn image_uri = var.api_image environment { variables = { DB_HOST = aws_db_instance.postgres.endpoint DB_NAME = aws_db_instance.postgres.db_name DB_USER = aws_db_instance.postgres.username BUCKET_NAME = aws_s3_bucket.data.id NAMESPACE = var.namespace } } } # PostgreSQL database resource "aws_db_instance" "postgres" { identifier = "${var.namespace}myapp-db" engine = "postgres" engine_version = "15.3" instance_class = "db.t3.micro" allocated_storage = 20 db_name = "myapp" username = "admin" password = var.db_password } # S3 bucket for application data resource "aws_s3_bucket" "data" { bucket = "${var.namespace}myapp-data" } # CloudWatch log group resource "aws_cloudwatch_log_group" "api_logs" { name = "/aws/lambda/${var.namespace}myapp-api" retention_in_days = 7 } # IAM role for Lambda resource "aws_iam_role" "api_role" { name = "${var.namespace}myapp-api-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "lambda.amazonaws.com" } }] }) } # IAM policy for Lambda resource "aws_iam_role_policy" "api_policy" { name = "${var.namespace}myapp-api-policy" role = aws_iam_role.api_role.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:GetObject", "s3:PutObject" ] Resource = "${aws_s3_bucket.data.arn}/*" }, { Effect = "Allow" Action = [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ] Resource = "arn:aws:logs:*:*:*" } ] }) } ``` ### variables.tf ```terraform theme={null} #@namespace(max_size=32, delimiter='-') variable "namespace" { type = string description = "Prefix that keeps globally-unique resource names distinct per install" default = "" } variable "api_image" { type = string description = "Container image for the API Lambda function" } variable "db_password" { type = string description = "Database password" sensitive = true } ``` ### outputs.tf ```terraform theme={null} output "api_function_arn" { description = "ARN of the API Lambda function" value = aws_lambda_function.api.arn } output "database_endpoint" { description = "Endpoint of the PostgreSQL database" value = aws_db_instance.postgres.endpoint } output "data_bucket" { description = "Name of the S3 data bucket" value = aws_s3_bucket.data.id } ``` ### versions.tf ```terraform theme={null} terraform { required_version = ">= 1.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } provider "aws" { region = "us-west-2" } ``` ## Tagging resources You don't need to tag resources for Tensor9's benefit. When Tensor9 compiles your origin stack, it stamps its own tags onto every resource whose provider schema supports them: | Tag | Value | | -------------------- | ---------------------------------------------- | | `t9-app-name` | Your app's name | | `t9-app-id` | Your app's ID | | `t9-buyer-name` | The customer's name | | `t9-appliance-id` | The appliance the resource belongs to | | `t9-projection-id` | The install the resource belongs to | | `t9-release-id` | The release that deployed the resource | | `t9-release-version` | Your vendor version for that release, when set | Where you already set tags of your own, Tensor9 merges its tags in rather than replacing yours. These tags allow: * **Steady-state permissions** to filter telemetry by appliance * **Cost tracking** for customers to monitor spending per appliance * **Resource discovery** by Tensor9 controllers ## Backend configuration Tensor9 **does not modify backend configuration** in your origin stack. You have full control over Terraform state management. ### Option 1: Include backend in origin stack ```terraform theme={null} # backend.tf terraform { backend "s3" { bucket = "my-terraform-state" key = "appliances/terraform.tfstate" region = "us-west-2" dynamodb_table = "terraform-locks" } } ``` **Backend blocks don't support variable interpolation**: Terraform backend configuration cannot use `${var.namespace}` or other variable references. If you include a backend in your origin stack, use a fixed key path. Tensor9 recommends using Option 2 or 3 below to provide instance-specific state paths at deployment time. ### Option 2: Provide backend at deployment time Don't include `backend.tf` in your origin stack. Instead, provide backend configuration when deploying: **For a test appliance:** ```bash theme={null} cd my-test-appliance tofu init \ -backend-config="bucket=my-terraform-state" \ -backend-config="key=appliances/test-aws-us-west-2/terraform.tfstate" \ -backend-config="region=us-west-2" tofu apply ``` **For a customer appliance:** ```bash theme={null} cd acme-corp-production tofu init \ -backend-config="bucket=my-terraform-state" \ -backend-config="key=appliances/acme-corp-production/terraform.tfstate" \ -backend-config="region=us-west-2" tofu apply ``` ### Option 3: Add backend after compilation Create `backend.tf` in the compiled deployment stack directory before running `tofu init`: **For a test appliance:** ```bash theme={null} cd my-test-appliance cat > backend.tf < backend.tf < data_bucket = "myapp-data-000000000000007e" ``` Outputs are also visible in `tensor9 report`: ```bash theme={null} tensor9 report ``` **Example from tensor9 report:** ``` Customer Appliance: acme-corp-production [id: 000000000000007e]: ... Installs: Acme Software/my-app → Acme Corp ... Outputs: api_endpoint: https://api.acme-corp-production.my-app.customer.com data_bucket: myapp-data-000000000000007e ``` ## Service equivalents When you create a release for an appliance, Tensor9 compiles your origin stack by replacing AWS-specific resources with their equivalents in the target environment. **Example: AWS to Google Cloud** Origin stack (AWS): ```terraform theme={null} resource "aws_db_instance" "postgres" { identifier = "${var.namespace}myapp-db" engine = "postgres" instance_class = "db.t3.micro" } resource "aws_s3_bucket" "data" { bucket = "${var.namespace}myapp-data" } ``` Deployment stack (compiled for Google Cloud): ```terraform theme={null} resource "google_sql_database_instance" "postgres" { name = "${var.namespace}myapp-db" database_version = "POSTGRES_15" tier = "db-f1-micro" } resource "google_storage_bucket" "data" { name = "${var.namespace}myapp-data" location = "US" } ``` See [Service Equivalents](/service-adapters/overview) for details on which services are supported and how they're mapped. ## Best practices Every resource that has a name, identifier, or globally unique value should be prefixed with `${var.namespace}`: ```terraform theme={null} # ✓ CORRECT resource "aws_s3_bucket" "data" { bucket = "${var.namespace}myapp-data" } resource "aws_iam_role" "api" { name = "${var.namespace}myapp-api" } # ✗ INCORRECT - Will cause collisions resource "aws_s3_bucket" "data" { bucket = "myapp-data" } ``` Without the namespace prefix, deploying to multiple appliances will fail due to resource naming conflicts. You don't need to add appliance-identifying tags yourself. Tensor9 stamps `t9-appliance-id`, `t9-buyer-name`, `t9-app-name`, and related tags onto every resource whose provider schema supports tags, merging them with your own. This enables: * Observability permissions scoping * Cost tracking per appliance * Resource discovery Define outputs for values that operators or other systems need to access: ```terraform theme={null} output "api_endpoint" { value = aws_lambda_function_url.api.function_url } ``` These appear in `tensor9 report` and `tofu output`. Always validate your Terraform configuration before publishing: ```bash theme={null} cd /path/to/terraform tofu init tofu validate ``` This catches syntax errors and missing variables early. Never deploy directly to customer appliances without testing: 1. Publish your origin stack 2. Create a release for a test appliance 3. Deploy and validate 4. Then create releases for customer appliances See [Testing](/fundamentals/testing) for details. Pin provider versions to avoid unexpected changes: ```terraform theme={null} terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } ``` This ensures consistent behavior across deployments. For large applications, use modules to organize resources: ``` my-app/ ├── main.tf ├── variables.tf ├── outputs.tf └── modules/ ├── api/ ├── database/ └── networking/ ``` This improves maintainability and reusability. ## Troubleshooting **Symptom**: `tensor9 stack publish` fails with validation errors. **Solutions**: * Run `tofu validate` locally to identify syntax errors * Ensure all required variables are declared * Check that all referenced resources exist * Verify provider versions are compatible **Symptom**: Release creation fails because a resource type isn't in the service adapter registry. **Solutions**: * Check if the resource is supported in your target form factor * Use a more generic resource type if available * Contact Tensor9 support to request support for the resource type **Symptom**: `tofu apply` fails with "resource already exists" errors. **Solutions**: * Ensure all resource names are prefixed with `${var.namespace}` * Check that you're not hard-coding any globally unique identifiers * Verify the `@namespace` annotated variable is declared in `variables.tf` **Symptom**: `tofu init` fails with backend errors or state is not found. **Solutions**: * Verify backend configuration is correct * Ensure state bucket exists and is accessible * Check that each appliance is given its own backend state path at deployment time * See [Backend Configuration](/fundamentals/deployments#backend-configuration) ## Next steps Now that you understand Terraform origin stacks, explore these topics: * [**Quick Start: Terraform**](/getting-started/quick-start-terraform): Step-by-step guide to your first deployment * [**Deployments**](/fundamentals/deployments): How to create releases and deploy * [**Service adapters**](/service-adapters/overview): How Terraform resources are mapped across clouds * [**Testing**](/fundamentals/testing): Validate your origin stack in test appliances # Frequently Asked Questions Source: https://docs.tensor9.com/overview/faq ## What is Tensor9? Tensor9 is an enterprise any-prem platform. We enable software vendors, like you, to unlock hard enterprise customers that can't share sensitive data. To do this, we help you convert your existing product for delivery inside the customer's cloud or datacenter, so that sensitive data stays with the customer. You can learn more [here](/overview/what-is-tensor9). ## What problem does Tensor9 solve? Selling to enterprise customers is challenging due to concerns about data leaving the enterprise, driven by regulatory requirements, heavy/siloed datasets, strict access control, and the need for operational resilience. Solving these challenges in-house requires costly engineering, maintenance, and support investments - culminating in you being forced to build, maintain and support multiple products. Tensor9 eliminates this friction by enabling you to deliver your existing product directly into your customer's environment, reducing costs and expanding access to sensitive/regulated enterprise markets. With Tensor9 you build, maintain, and support a single product, and deliver it to any enterprise customer. ## How does Tensor9 work? Tensor9 works by compiling the vendor's existing stack as defined via infrastructure as code for deployment into customer-owned environments. Tensor9 leverages your existing Terraform, CloudFormation, or Docker configurations to deploy and manage your applications within a customer's dedicated appliance. This ensures your infrastructure-as-code (IaC) is compiled securely for the customer environment while preserving your existing workflows and deployment tools (like Atlantis, Spacelift, or HCP Terraform). ## Can my customers configure their own deployment? Yes. Each customer can pick how their end users reach the application (public, allowlisted, Tailscale, or fully private), whether the install uses default-shipped managed services or their own equivalents (for example, the customer's own managed Temporal), and how the appliance reaches your control plane (public internet, AWS PrivateLink, or Tailscale). You maintain one origin stack; the compiler emits a deployment stack shaped to each customer's choices. See [Auto-Customizations](/customizations/overview) for the full picture. ## What is your roadmap and pricing model? Reach out to us at [hello@tensor9.com](mailto:hello@tensor9.com) or [contact us](https://site.tensor9.com/contact-us) to learn more. ## How many appliances do I need? Each of your end customers will typically need a single appliance. If an end customer needs multiple isolated instances of your application, or has multiple geographically separated regions, then that customer will need multiple appliances. ## Who manages the appliances? You (the vendor) manage your end customers' appliances, including their deployed software and auto-scaling. Tensor9 provides support. ## What managed services can I deploy into a customer's AWS account? We support any managed service definable via infrastructure as code. ## Which infrastructure-as-code tooling do you support? Tensor9 supports Terraform, CloudFormation, and Docker. Please reach out if you are interested in support for Pulumi. ## Who pays for the appliance cloud costs? The end customer does; the appliance runs in their cloud account. ## What cloud account permissions does the customer need to set up their appliance? A customer needs admin rights to the cloud account that will host the appliance. We're adding the ability to restrict this to a scoped set of permissions. ## What customer data do I have access to? All logs configured to be emitted by your projected resources will be sent back to your log sink. It is up to you to make sure those logs do not externalize sensitive customer data. ## Can I deploy a code change to a subset of customer appliances? Yes. You specify customer names during a release. ## How do I shut down customer appliances? An appliance can be shut down by performing tofu/terraform destroy, which removes the software from the customer's appliance (which they can then delete on their own). ## Can I enable a customer to run multiple versions of my app? Yes. That customer should have multiple appliances. Each appliance will have its own private endpoint. You can release different versions of your software to each appliance independently. ## How does my customer manage the capacity of their appliance? You manage capacity for them. All resources within the appliance are tagged. Your customer can create budget alerts based on that tag to monitor their costs. We recommend that your customer use an isolated cloud account with spending limits to explicitly control costs. ## Does my customer need to have a business relationship with Tensor9? No. You manage your relationship with your own customer. Tensor9 white-labels our product, and will act as part of your team if you ever would like us to interact with your customer. ## What customer data does Tensor9 have access to? Tensor9 only receives metadata from customer environments. This can include: * The versions of Tensor9 software running in your and your customers' environments. * The number of Tensor9 controllers in each environment. * The memory/cpu/network capacity of each machine. * The uptime of each machine (i.e., how long since the last software restart or hardware restart). * The cloud (i.e., AWS, Azure, Google Cloud, on-prem) and the region where an instance is located. * Time series data about the memory/cpu/network utilization of each machine in every environment. * Logs about errors occurring in each machine in your environment. These are logs about Tensor9 software operation, not about your software. For example, how long it took to prepare a deployment of your software to a customer environment, including to which customer environments (identified by an opaque identifier for that customer). * Logs about errors occurring in each machine in the customer's environment. These are logs about Tensor9 software operation, not about the operation of your software. For example, how long it took to apply a specific version of your software (identified by the version number of your choosing) on each machine in your customer's environment. In all cases, we are happy to share a full-fidelity copy of all data sent to Tensor9 from your and customers' environments. ## I need help! Please reach out to us here: [https://site.tensor9.com/contact-us](https://site.tensor9.com/contact-us). # What is Tensor9? Source: https://docs.tensor9.com/overview/what-is-tensor9 There is a strong and growing need for privacy and security in enterprise software. To meet these requirements, software/AI vendors often need to deliver software into **BYOC** and **on-prem** environments. Delivering an existing product into different environments is fundamentally challenging due to unique configurations, limited access, and security constraints. This often leads vendors to build and manage separate products, adding significant expense and distraction from delivering their core value proposition. Tensor9 solves for **any-prem** deployments: Enabling software/AI vendors to deliver their existing products directly into customer-owned environments, including BYOC and on-prem. This unlocks new markets for software/AI vendors without burdening the vendor with complex bespoke engineering and operations. ## What problems does Tensor9 solve? * **Deployment** of software across customers: Tensor9 compiles a vendor's existing stack (defined in infrastructure-as-code) into an installable customer-hosted appliance suited to that customer’s environment (e.g. AWS, Azure, GCP, on-prem). Vendors can hook into their existing continuous deployment tooling to continuously deploy updates to customer appliances, ensuring consistency across customers. * **Portability:** Tensor9 enables vendors to continue to use managed services and deploy cross-cloud, 3rd party, and OSS service equivalents per customer environment; where equivalents don’t exist, vendors can use Tensor9 to build for a subset of multi-cloud APIs and ensure portability from the get-go. * **Observability & operations:** Tensor9 mirrors the deployment and operational state of a customer's stack, allowing vendors to observe customer environments by synchronizing logs, metrics, and hardware failures - enabling vendors to observe, debug, and support customers as if they were using a cloud product. * **Customer control:** Tensor9 enables end customers to control how interactions are managed from their vendor, including maintenance windows, audit logs, and vendor access to private resources. ## What makes our approach unique? * **Vendors keep their existing stack and tooling:** Tensor9 enables vendors to leverage their existing stack without extensive rewrites; vendors don’t need to rearchitect their stack for Kubernetes or rewrite their Terraform, and Tensor9 provides practical paths to managed service equivalents or alternatives. * **Practical and thoughtful approach to simplification:** Tensor9 enables vendors to surface versus hide customer constraints, so vendors can make informed trade-offs when there are incompatibilities between their stack and a customer’s environment. * **Operations and support are tactile:** Tensor9 makes operating BYOC and on-prem as easy as cloud wherever possible by mirroring the operational state of a customer’s environment so vendors are never in the dark when issues occur. * **Self-serve and easy to get started:** Tensor9 enables fast-moving software/AI companies to try out our solution on their own time, and provides a simple path to onboard new customers who don’t have infrastructure expertise. Your customers set up their appliance through a guided install wizard, and manage it day-to-day through a Customer Portal, both white-labeled with your branding. On your side, the [Vendor Portal](/fundamentals/key-concepts#vendor-portal) gives you a dashboard to manage apps, appliances, and operations alongside the CLI. ## Per-customer configuration without forks Tensor9 compiles the same origin stack into different deployment stacks for different customers, based on each customer's configuration. The customer picks how their end users reach the application (public, allowlisted, Tailscale, or fully private), whether the install uses default-shipped managed services or their own equivalents, and how the appliance reaches your control plane. You maintain one stack; each customer gets a build shaped to their environment. See [Auto-Customizations](/customizations/overview) for the full picture. ## What scenarios is Tensor9 great for? * **From cloud to BYOC:** A vendor has built their multi-tenant cloud solution for AWS, and has leveraged infrastructure as code to ensure consistency in deployments. Prospective customers want to run the product on their own AWS accounts, and the vendor wants to serve these customers as quickly as possible. * **From Kubernetes-only to using cloud services:** A vendor has built on Kubernetes to enable multi-cloud portability, but as they grew, started to use AWS services to fill in the gaps for capabilities such as databases and queues. Now a prospective customer wants to run it on their own Google Cloud or Azure account (which happens to be their cloud of choice), due to security controls. * **Multi-cloud portability:** A vendor has built their multi-tenant or single-tenant cloud solution for AWS, and has leveraged infrastructure as code to ensure consistency in deployments. Prospective customers want to run the product on their own Google Cloud or Azure account, and the vendor wants to serve these customers as quickly as possible. # Bedrock (LLM inference) Source: https://docs.tensor9.com/service-adapters/aws/ai-machine-learning/bedrock-llm-inference AWS Bedrock (LLM inference). Provides API access to foundation models from several vendors, billed by token on demand or through provisioned throughput. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [On Google Cloud](#on-google-cloud) * [Via Vertex AI - Gemini](#via-vertex-ai-gemini) * [Via Vertex AI - Claude](#via-vertex-ai-claude) * [Via Vertex AI - Llama](#via-vertex-ai-llama) * [On Google Cloud, Azure, OCI, and Private Kubernetes](#on-google-cloud-azure-oci-and-private-kubernetes) * [Via vLLM / Ollama](#via-vllm-/-ollama) * [On Azure](#on-azure) * [Via Azure RAI Content Filter Policy](#via-azure-rai-content-filter-policy) * [On OCI](#on-oci) * [Via OCI Generative AI](#via-oci-generative-ai) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of Bedrock (LLM inference) with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | Bedrock (LLM inference) | Google Cloud · Vertex AI - Gemini | Google Cloud · Vertex AI - Claude | Google Cloud · Vertex AI - Llama | Google Cloud, Azure, OCI, and Private Kubernetes · vLLM / Ollama | Azure · Azure RAI Content Filter Policy | OCI · OCI Generative AI | | ------------ | ----------------------- | --------------------------------- | --------------------------------- | -------------------------------- | ---------------------------------------------------------------- | --------------------------------------- | ----------------------- | | API coverage | full | high | high | high | high | partial | high | ## On Google Cloud ### Via Vertex AI - Gemini | Capability | Area | Support | Required tier | Operations | Notes | | ----------------- | ---------------------- | ------------ | ------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------- | | InvokeInlineAgent | Inline agent | Partial | - | - | One chat turn per call with return-control. A bedrock-agent-runtime operation, distinct from the bedrock-runtime surface. | | Bedrock agents | Managed (out of scope) | Out of scope | - | - | Managed Bedrock feature; the adapter returns a real Bedrock error rather than partially serving it. | | Knowledge Bases | Managed (out of scope) | Out of scope | - | - | Managed Bedrock feature; the adapter returns a real Bedrock error rather than partially serving it. | | Operation | Area | Support | Depth | Notes | | ----------------------------- | ----------------------------- | --------- | ---------- | ------------------------------------------------------ | | InvokeModel | Native model body | Partial | Most usage | Claude-native request bodies only. | | InvokeModelWithResponseStream | Native model body (streaming) | Partial | Most usage | Claude-native request bodies only. | | CountTokens | Tokenization | Partial | Most usage | Uses Vertex countTokens for the selected Gemini model. | | Converse | Unified chat | Supported | Common | Chat, streaming, and tool use. | | ConverseStream | Unified chat (streaming) | Supported | Common | Chat, streaming, and tool use. | #### How it works Tensor9 runs an **adapter** alongside your application and points the AWS SDK's Bedrock endpoint to it. The adapter translates Bedrock requests for **Google Vertex AI Gemini**. Your application keeps its code and SDK, including Bedrock request formats, streaming events, and error codes. Gemini runs in the customer's Google Cloud project. The adapter authenticates with the appliance's Google Cloud identity; it needs no API key.
Before: on AWS the application's Bedrock SDK calls Amazon Bedrock. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the Bedrock API from Gemini. Before: on AWS the application's Bedrock SDK calls Amazon Bedrock. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the Bedrock API from Gemini.

The SDK calls the Tensor9 adapter, which sends requests to the configured model service.

#### How a conversation maps A Bedrock chat request is a list of turns, an optional system instruction, and inference settings. The adapter maps all of it across: every turn, the system instruction, and the token limit, temperature, top-p, top-k, and stop settings are preserved. Gemini's request and response shape differs from Bedrock's more than most targets do. The service adapter absorbs those differences so your application still sees a normal Bedrock conversation. The model your application names is not necessarily the model that runs; the customer chooses which Gemini model serves the traffic when the appliance is deployed. Because a Gemini model produces every response, the generated content (its wording, its reasoning, and its model-specific strengths) is Gemini's, and will differ from the Bedrock model your application named. The Bedrock interface around it stays identical: the request shapes, the streaming events, and the error codes are unchanged.
The adapter translates a Bedrock conversation and its supported settings into a request for Gemini, then converts the reply to Bedrock format. The adapter translates a Bedrock conversation and its supported settings into a request for Gemini, then converts the reply to Bedrock format.

The adapter sends the conversation to the model selected at deployment. Supported inference settings are listed in the text.

#### Tool use and streaming If your application uses Bedrock tool use (letting the model call functions you declare), the service adapter supports the full round trip: the model can request a tool call, your application runs it, and the result feeds back into the next turn. Gemini identifies a tool call by the function's name rather than by a call id, so the adapter keeps the calls straight when the model calls the same tool more than once in a turn. Streaming behaves the same as on Bedrock. As the model produces output, the adapter delivers the same incremental events your SDK already expects, including tool calls.
The tool-use round trip: your app declares tools, the model asks to call one, your app runs it and returns the result, and the model gives its final answer. The tool-use round trip: your app declares tools, the model asks to call one, your app runs it and returns the result, and the model gives its final answer.

The application executes tool calls and returns their results. During streaming, the adapter sends these as Bedrock events.

#### Limitations △ Where Bedrock and Vertex Gemini differ * **Reasoning uses part of the token budget.** Gemini 2.5 spends part of the token limit on internal reasoning before it produces visible text, so allow enough tokens for both reasoning and visible output. * **The organization must allow access to generative models.** Some Google Cloud organizations allow generative models only through a dedicated service identity, not an interactive user login. An appliance uses its own service identity, which must have the required access before deployment. * **Text inputs only.** Image and document content are not served on this path. * **Managed Bedrock features are not served.** Agents, Knowledge Bases, and guardrails configured as a service return a clear error rather than a silent or partial result. #### Other considerations **Migration.** Bedrock inference stores no conversation state in the service. Changing the model service requires configuration changes, with no stored inference data to transfer. **Operations.** The customer owns the Vertex AI service in their Google Cloud: which models are enabled, the quotas, and the regions. **Capacity and cost.** Token pricing and throughput are Vertex AI's, not Bedrock's. ### Via Vertex AI - Claude | Capability | Area | Support | Required tier | Operations | Notes | | ----------------- | ---------------------- | ------------ | ------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------- | | InvokeInlineAgent | Inline agent | Partial | - | - | One chat turn per call with return-control. A bedrock-agent-runtime operation, distinct from the bedrock-runtime surface. | | Bedrock agents | Managed (out of scope) | Out of scope | - | - | Managed Bedrock feature; the adapter returns a real Bedrock error rather than partially serving it. | | Knowledge Bases | Managed (out of scope) | Out of scope | - | - | Managed Bedrock feature; the adapter returns a real Bedrock error rather than partially serving it. | | Operation | Area | Support | Depth | Notes | | ----------------------------- | ----------------------------- | --------- | ---------- | --------------------------------------------------------------------------------------------------- | | InvokeModel | Native model body | Partial | Most usage | Claude-native request bodies only. | | InvokeModelWithResponseStream | Native model body (streaming) | Partial | Most usage | Claude-native request bodies only. | | CountTokens | Tokenization | Partial | Most usage | Uses the selected Anthropic endpoint's token-count operation; model and hosting restrictions apply. | | Converse | Unified chat | Supported | Common | Chat, streaming, and tool use. | | ConverseStream | Unified chat (streaming) | Supported | Common | Chat, streaming, and tool use. | #### How it works Tensor9 runs an **adapter** alongside your application and points the AWS SDK's Bedrock endpoint to it. The adapter translates Bedrock requests for **Anthropic's Claude on Google Vertex AI**. Your application keeps its code and SDK, including Bedrock request formats, streaming events, and error codes. This runs Claude inside Google Cloud, with no API key: the appliance authenticates with its Google Cloud identity. Bedrock and Claude use similar chat APIs, so the adapter preserves the conversation and settings described below. Prompts stay in Google Cloud.
Before: on AWS the application's Bedrock SDK calls Amazon Bedrock. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the Bedrock API from Claude. Before: on AWS the application's Bedrock SDK calls Amazon Bedrock. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the Bedrock API from Claude.

The SDK calls the Tensor9 adapter, which sends requests to the configured model service.

#### How a conversation maps A Bedrock chat request is a list of turns, an optional system instruction, and inference settings. Because the two chat APIs share a shape, the adapter forwards all of it almost unchanged: every turn, the system instruction, and the token limit, temperature, top-p, stop, and top-k settings are preserved. The model your application names is not necessarily the model that runs. The customer chooses which Claude model on Vertex serves the traffic when the appliance is deployed, and the adapter routes every request to it.
The adapter translates a Bedrock conversation and its supported settings into a request for Claude on Vertex, then converts the reply to Bedrock format. The adapter translates a Bedrock conversation and its supported settings into a request for Claude on Vertex, then converts the reply to Bedrock format.

The adapter sends the conversation to the model selected at deployment. Supported inference settings are listed in the text.

#### Tool use and streaming If your application uses Bedrock tool use (letting the model call functions you declare), the service adapter handles the full round trip: the model can request a tool call, your application runs it, and the result feeds back into the next turn. Because the two chat APIs represent tool calls the same way, this maps directly. Streaming behaves the same as on Bedrock. As the model produces output, the adapter delivers the same incremental events your SDK already expects, including partial tool calls.
The tool-use round trip: your app declares tools, the model asks to call one, your app runs it and returns the result, and the model gives its final answer. The tool-use round trip: your app declares tools, the model asks to call one, your app runs it and returns the result, and the model gives its final answer.

The application executes tool calls and returns their results. During streaming, the adapter sends these as Bedrock events.

#### Limitations △ Where Bedrock and Claude on Vertex differ * **Claude must be enabled in the project.** Claude on Vertex AI is offered in specific Google Cloud regions and must be turned on for the project before it can serve traffic. This is a one-time setup when the appliance is deployed. * **Text inputs only.** Image and document content are not served on this path. * **Managed Bedrock features are not served.** Agents, Knowledge Bases, and guardrails configured as a service return a clear error rather than a silent or partial result. #### Other considerations **Migration.** Bedrock inference stores no conversation state in the service. Changing the model service requires configuration changes, with no stored inference data to transfer. **Operations.** The customer owns the Vertex AI service in their Google Cloud, including enabling Claude, the regions, and the quotas. **Capacity and cost.** Token pricing and throughput are Vertex AI's, not Bedrock's. ### Via Vertex AI - Llama | Capability | Area | Support | Required tier | Operations | Notes | | ----------------- | ---------------------- | ------------ | ------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------- | | InvokeInlineAgent | Inline agent | Partial | - | - | One chat turn per call with return-control. A bedrock-agent-runtime operation, distinct from the bedrock-runtime surface. | | Bedrock agents | Managed (out of scope) | Out of scope | - | - | Managed Bedrock feature; the adapter returns a real Bedrock error rather than partially serving it. | | Knowledge Bases | Managed (out of scope) | Out of scope | - | - | Managed Bedrock feature; the adapter returns a real Bedrock error rather than partially serving it. | | Operation | Area | Support | Depth | Notes | | ----------------------------- | ----------------------------- | --------- | ---------- | -------------------------------------------------------------------------------------------------- | | InvokeModel | Native model body | Partial | Most usage | Claude-native request bodies only. | | InvokeModelWithResponseStream | Native model body (streaming) | Partial | Most usage | Claude-native request bodies only. | | CountTokens | Tokenization | Partial | Most usage | Counts locally with tiktoken; the estimate may differ from the selected model's billing tokenizer. | | Converse | Unified chat | Supported | Common | Chat, streaming, and tool use. | | ConverseStream | Unified chat (streaming) | Supported | Common | Chat, streaming, and tool use. | #### How it works Tensor9 runs an **adapter** alongside your application and points the AWS SDK's Bedrock endpoint to it. The adapter translates Bedrock requests for **open models on Google Vertex AI**, including Llama. Your application keeps its code and SDK, including Bedrock request formats, streaming events, and error codes. This runs in Google Cloud, with no API key: the appliance authenticates with its Google Cloud identity. Vertex offers these open models through an OpenAI-style interface, so the adapter maps the conversation the same way it does for any OpenAI-compatible service.
Before: on AWS the application's Bedrock SDK calls Amazon Bedrock. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the Bedrock API from Open LLMs. Before: on AWS the application's Bedrock SDK calls Amazon Bedrock. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the Bedrock API from Open LLMs.

The SDK calls the Tensor9 adapter, which sends requests to the configured model service.

#### How a conversation maps A Bedrock chat request is a list of turns, an optional system instruction, and inference settings. The adapter preserves every turn, the system instruction, and the token limit, temperature, top-p, and stop settings. One Bedrock setting, top-k, has no equivalent in the OpenAI-style interface these models use, so it is dropped rather than treated as an error. The model your application names is not necessarily the model that runs. The customer chooses which Vertex open model serves the traffic when the appliance is deployed.
The adapter translates a Bedrock conversation and its supported settings into a request for Open model, then converts the reply to Bedrock format. The adapter translates a Bedrock conversation and its supported settings into a request for Open model, then converts the reply to Bedrock format.

The adapter sends the conversation to the model selected at deployment. Supported inference settings are listed in the text.

#### Tool use and streaming If your application uses Bedrock tool use (letting the model call functions you declare), the service adapter handles the full round trip where the served model supports it: the model can request a tool call, your application runs it, and the result feeds back into the next turn. The adapter translates between the Bedrock and OpenAI-style representations of tool calls so your application does not have to. Streaming behaves the same as on Bedrock. As the model produces output, the adapter delivers the same incremental events your SDK already expects.
The tool-use round trip: your app declares tools, the model asks to call one, your app runs it and returns the result, and the model gives its final answer. The tool-use round trip: your app declares tools, the model asks to call one, your app runs it and returns the result, and the model gives its final answer.

The application executes tool calls and returns their results. During streaming, the adapter sends these as Bedrock events.

#### Limitations △ Where Bedrock and Vertex open models differ * **The top-k setting is dropped.** The OpenAI-style interface has no equivalent, so a request that sets it still succeeds, without that control applied. * **Capabilities vary by model.** Tool use is available only for models that support it; a plain chat model serves text conversations without it. * **Model availability is set by Vertex.** Which open models are offered, and in which regions, is decided by Google Vertex AI. The customer picks one that is available where the appliance runs. * **Text inputs only.** Image and document content are not served on this path. * **Managed Bedrock features are not served.** Agents, Knowledge Bases, and guardrails configured as a service return a clear error rather than a silent or partial result. #### Other considerations **Migration.** Bedrock inference stores no conversation state in the service. Changing the model service requires configuration changes, with no stored inference data to transfer. **Operations.** The customer owns the Vertex AI service in their Google Cloud, including which models are enabled and the quotas. **Capacity and cost.** Token pricing and throughput are Vertex AI's, not Bedrock's. ## On Google Cloud, Azure, OCI, and Private Kubernetes ### Via vLLM / Ollama | Capability | Area | Support | Required tier | Operations | Notes | | ----------------- | ---------------------- | ------------ | ------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | InvokeInlineAgent | Inline agent | Partial | - | - | One chat turn per call; tool execution returns to the application. A bedrock-agent-runtime operation, distinct from the bedrock-runtime surface. | | Bedrock agents | Managed (out of scope) | Out of scope | - | - | Managed Bedrock feature; the adapter returns a real Bedrock error rather than partially serving it. | | Knowledge Bases | Managed (out of scope) | Out of scope | - | - | Managed Bedrock feature; the adapter returns a real Bedrock error rather than partially serving it. | | Operation | Area | Support | Depth | Notes | | ----------------------------- | ----------------------------- | --------- | ---------- | -------------------------------------------------------------------------------------------------- | | InvokeModel | Native model body | Partial | Most usage | Claude-native request bodies only, translated to the target model API. | | InvokeModelWithResponseStream | Native model body (streaming) | Partial | Most usage | Claude-native request bodies only, translated to the target model API. | | CountTokens | Tokenization | Partial | Most usage | Counts locally with tiktoken; the estimate may differ from the selected model's billing tokenizer. | | Converse | Unified chat | Supported | Common | Chat and streaming. | | ConverseStream | Unified chat (streaming) | Supported | Common | Chat and streaming. | #### How it works Tensor9 runs an **adapter** alongside your application and points the AWS SDK's Bedrock endpoint to it. The adapter translates Bedrock requests for a **model running in the customer's cluster**. Your application keeps its code and SDK, including Bedrock request formats, streaming events, and error codes. The model runs on the customer's hardware, served by a standard open model server in their Kubernetes cluster. There is no external model service, no API key, and no prompt or response ever leaves the cluster. The server speaks an OpenAI-style interface, so the adapter maps the conversation the same way it does for any OpenAI-compatible service.
Before: on AWS the application's Bedrock SDK calls Amazon Bedrock. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the Bedrock API from Self-hosted. Before: on AWS the application's Bedrock SDK calls Amazon Bedrock. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the Bedrock API from Self-hosted.

The SDK calls the Tensor9 adapter, which sends requests to the configured model service.

#### How a conversation maps A Bedrock chat request is a list of turns, an optional system instruction, and inference settings. The adapter preserves every turn, the system instruction, and the token limit, temperature, top-p, and stop settings. One Bedrock setting, top-k, has no equivalent in the OpenAI-style interface, so it is dropped rather than treated as an error. The model your application names is not necessarily the model that runs. The customer chooses which model the in-cluster server hosts when the appliance is deployed, and the adapter routes every request to it.
The adapter translates a Bedrock conversation and its supported settings into a request for In-cluster model, then converts the reply to Bedrock format. The adapter translates a Bedrock conversation and its supported settings into a request for In-cluster model, then converts the reply to Bedrock format.

The adapter sends the conversation to the model selected at deployment. Supported inference settings are listed in the text.

#### Tool use and streaming If your application uses Bedrock tool use (letting the model call functions you declare), the service adapter handles the full round trip where the served model supports it: the model can request a tool call, your application runs it, and the result feeds back into the next turn. The adapter translates between the Bedrock and OpenAI-style representations of tool calls so your application does not have to. Streaming behaves the same as on Bedrock. As the model produces output, the adapter delivers the same incremental events your SDK already expects.
The tool-use round trip: your app declares tools, the model asks to call one, your app runs it and returns the result, and the model gives its final answer. The tool-use round trip: your app declares tools, the model asks to call one, your app runs it and returns the result, and the model gives its final answer.

The application executes tool calls and returns their results. During streaming, the adapter sends these as Bedrock events.

#### Limitations △ Where Bedrock and a self-hosted model differ * **The top-k setting is dropped.** The OpenAI-style interface has no equivalent, so a request that sets it still succeeds, without that control applied. * **Capabilities depend on the model you run.** Tool use, context length, and quality are set by the open model the customer hosts, not by Bedrock. Choose a model that supports what your application needs. * **Capacity is the customer's to provide.** Throughput is bounded by the GPUs the customer allocates to the server; there is no elastic managed backend behind it. * **Text inputs only.** Image and document content are not served on this path. * **Managed Bedrock features are not served.** Agents, Knowledge Bases, and guardrails configured as a service return a clear error rather than a silent or partial result. #### Other considerations **Migration.** Bedrock inference stores no conversation state in the service. Changing the model service requires configuration changes, with no stored inference data to transfer. **Operations.** The customer runs the model server: choosing the model, provisioning the GPUs, and keeping it available. Tensor9 injects the adapter and points it at that server. **Capacity and cost.** Throughput and cost are the customer's own hardware, sized for the workload. ## On Azure ### Via Azure RAI Content Filter Policy #### Inference through an Azure model deployment The Max inference path uses the Tensor9 OpenAI-compatible backend to serve supported Bedrock text-chat requests from an Azure model deployment. Configure the deployment endpoint, API version and API-key or Entra bearer authentication. The backend translates messages, tool calls, stop reasons and streaming fragments between Bedrock and Chat Completions. The application's Bedrock SDK keeps its supported request and response formats. Model behavior still differs: test prompts, tool schemas, refusals and partial-stream failures against the selected model. The OpenAI path has no `top_k` field; that setting is omitted. Native model-body support remains limited to the body formats listed in the inference profile. #### Content filtering and retrieval A declared guardrail maps to an Azure responsible-AI content policy on the cognitive account. Attach the policy to the deployment that serves requests. Azure categories and thresholds differ from Bedrock's, so compare allow/block outcomes using representative content. A knowledge base maps to Azure AI Search infrastructure. Load the corpus, configure ingestion, chunking and embeddings, and connect retrieval results to model requests. Creating the search service does not populate an index or provide the Bedrock Retrieve API. #### Deployment and validation Configure the model endpoint and its credentials separately from the content policy and search service. Check network access, model deployment availability and permissions for inference and retrieval. Test the complete application path, including a denied request, a tool call and cancellation of a stream. Managed Bedrock agents are outside this mapping. An application that depends on an agent runtime needs an explicit target implementation; neither a content policy nor a search index supplies that runtime. ## On OCI ### Via OCI Generative AI | Capability | Area | Support | Required tier | Operations | Notes | | ----------------- | ---------------------- | ------------ | ------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------- | | InvokeInlineAgent | Inline agent | Partial | - | - | One chat turn per call with return-control. A bedrock-agent-runtime operation, distinct from the bedrock-runtime surface. | | Bedrock agents | Managed (out of scope) | Out of scope | - | - | Managed Bedrock feature; the adapter returns a real Bedrock error rather than partially serving it. | | Knowledge Bases | Managed (out of scope) | Out of scope | - | - | Managed Bedrock feature; the adapter returns a real Bedrock error rather than partially serving it. | | Operation | Area | Support | Depth | Notes | | ----------------------------- | ----------------------------- | --------- | ---------- | --------------------------------------------------------------------------------------------------------------- | | InvokeModel | Native model body | Partial | Most usage | Claude-native request bodies only. | | InvokeModelWithResponseStream | Native model body (streaming) | Partial | Most usage | Claude-native request bodies only. | | CountTokens | Tokenization | Partial | Most usage | Token-count fidelity depends on the selected model and tokenizer; compare estimates with target-reported usage. | | Converse | Unified chat | Supported | Common | Chat, streaming, and tool use. | | ConverseStream | Unified chat (streaming) | Supported | Common | Chat, streaming, and tool use. | #### How it works Tensor9 runs an **adapter** alongside your application and points the AWS SDK's Bedrock endpoint to it. The adapter translates Bedrock requests for **OCI Generative AI**. Your application keeps its code and SDK, including Bedrock request formats, streaming events, and error codes. OCI Generative AI hosts two families of chat models: an OpenAI-style family (Llama, Grok, Gemini, and the gpt-oss models) and the Cohere Command family. Each family expects a slightly different request shape. The adapter picks the right shape for whichever model the customer has configured, passes the conversation through, and translates the reply back into a normal Bedrock response.
Before: on AWS the application's Bedrock SDK calls Amazon Bedrock. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the Bedrock API from OCI GenAI. Before: on AWS the application's Bedrock SDK calls Amazon Bedrock. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the Bedrock API from OCI GenAI.

The SDK calls the Tensor9 adapter, which sends requests to the configured model service.

#### How a conversation maps A Bedrock chat request is a list of turns, an optional system instruction, and inference settings. The adapter forwards all of it: every turn, the system instruction, and the token limit, temperature, top-p, top-k, and stop settings are preserved, so the model sees the same conversation it would on AWS. The model your application names in the request is not necessarily the model that runs. The customer chooses which OCI model serves the traffic when the appliance is deployed, and the adapter routes every request to it. Authentication is keyless. The adapter signs each request with the appliance's own OCI identity, so there is no API key or static secret to manage. The account that is billed is fixed when the appliance is deployed and cannot be changed by a request.
The adapter translates a Bedrock conversation and its supported settings into a request for OCI GenAI, then converts the reply to Bedrock format. The adapter translates a Bedrock conversation and its supported settings into a request for OCI GenAI, then converts the reply to Bedrock format.

The adapter sends the conversation to the model selected at deployment. Supported inference settings are listed in the text.

#### Tool use and streaming If your application uses Bedrock tool use (letting the model call functions you declare), the service adapter handles the full round trip for the OpenAI-style model family: the model can request a tool call, your application runs it, and the result feeds back into the next turn. Streaming behaves the same as on Bedrock. As the model produces output, the adapter delivers the same incremental events your SDK already expects, including partial tool calls. If the OCI model service reports an error partway through a stream, the adapter surfaces it as a normal error rather than a truncated response.
The tool-use round trip: your app declares tools, the model asks to call one, your app runs it and returns the result, and the model gives its final answer. The tool-use round trip: your app declares tools, the model asks to call one, your app runs it and returns the result, and the model gives its final answer.

The application executes tool calls and returns their results. During streaming, the adapter sends these as Bedrock events.

#### Limitations △ Where Bedrock and OCI Generative AI differ * **Tool use is limited to the OpenAI-style family.** Tool use with the Cohere Command family is not supported. * **Text inputs only.** Image and document inputs are not supported; the adapter serves text conversations and tool use. * **Model availability is region-specific.** The Cohere Command family is offered on demand in a different OCI region than the OpenAI-style family, and a given model must be currently offered on demand in the appliance's region. Choose a model that is available where the appliance runs. * **Managed Bedrock features are not served.** Agents, Knowledge Bases, and guardrails configured as a service return a clear error rather than a silent or partial result. #### Other considerations **Migration.** Bedrock inference stores no conversation state in the service. Changing the model service requires configuration changes, with no stored inference data to transfer. **Operations.** The customer owns the OCI Generative AI service: which models are enabled, the quotas, and the availability in each region. **Capacity and cost.** Token pricing and throughput are the OCI model's, not Bedrock's. Size the model choice for the workload the same way you would size any inference deployment. [Service Catalog](/service-adapters/catalog). # Amplify Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/amplify AWS Amplify. Hosts web applications with branch deployments, build workflows and custom domains, and connects them to application backends. Amplify is available on AWS only. [Service Catalog](/service-adapters/catalog). # AppSync Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/appsync AWS AppSync. Serves a managed GraphQL endpoint, resolving each field onto a data source through resolvers written in its own template language or JavaScript. AppSync is available on AWS only. [Service Catalog](/service-adapters/catalog). # Athena Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/athena AWS Athena. Queries data files in S3 with SQL, using table definitions from the Glue Data Catalog. Athena is available on AWS only. [Service Catalog](/service-adapters/catalog). # AWS Backup Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/aws-backup AWS Backup. Centralizes scheduled snapshots of EBS, RDS, DynamoDB, EFS and other resources into vaults with retention rules and restore jobs. AWS Backup is available on AWS only. [Service Catalog](/service-adapters/catalog). # Batch Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/batch AWS Batch. Queues container jobs and runs them on EC2 or Fargate capacity it scales up and down, with job priorities and array jobs. Batch is available on AWS only. [Service Catalog](/service-adapters/catalog). # Cloud Map (Service Discovery) Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/cloud-map-service-discovery AWS Cloud Map (Service Discovery). A service registry mapping logical service names to current endpoints, resolvable by DNS or API, with optional health-status filtering. Cloud Map (Service Discovery) is available on AWS only. [Service Catalog](/service-adapters/catalog). # CloudFormation Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/cloudformation Provisions AWS resources from a declarative template as a stack, applying updates through change sets and rolling back failed deployments. CloudFormation is available on AWS only. [Service Catalog](/service-adapters/catalog). # CloudWatch Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/cloudwatch Metrics, logs, alarms and dashboards for AWS resources and your own code, collected per region and account. CloudWatch is available on AWS only. [Service Catalog](/service-adapters/catalog). # CloudWatch Alarms Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/cloudwatch-alarms AWS CloudWatch Alarms. Evaluates metrics, metric expressions and combinations of alarm states, triggering configured actions. CloudWatch Alarms is available on AWS only. [Service Catalog](/service-adapters/catalog). # CloudWatch Dashboards Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/cloudwatch-dashboards AWS CloudWatch Dashboards. Arranges metric graphs, numbers, text and log widgets on a saved page defined by a JSON document. CloudWatch Dashboards is available on AWS only. [Service Catalog](/service-adapters/catalog). # CloudWatch Logs Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/cloudwatch-logs AWS CloudWatch Logs. Collects log events into groups and streams with retention settings, searchable with Logs Insights and forwardable through subscription filters. CloudWatch Logs is available on AWS only. [Service Catalog](/service-adapters/catalog). # CloudWatch Metrics Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/cloudwatch-metrics Stores numeric time series identified by namespace and dimensions, published by AWS services or your own code, and rolled up into statistics. CloudWatch Metrics is available on AWS only. [Service Catalog](/service-adapters/catalog). # CodeBuild Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/codebuild AWS CodeBuild. Executes build and test commands from a buildspec inside a managed container, then uploads the artifacts it produces. CodeBuild is available on AWS only. [Service Catalog](/service-adapters/catalog). # CodePipeline Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/codepipeline AWS CodePipeline. Moves a change through ordered release stages, source, build, approval and deploy, with configurable triggers for source changes. CodePipeline is available on AWS only. [Service Catalog](/service-adapters/catalog). # Cognito Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/cognito Covers both halves of application identity: a directory that signs end users in, and a broker that turns logins into AWS credentials. Cognito is available on AWS only. [Service Catalog](/service-adapters/catalog). # Cognito Identity Pools Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/cognito-identity-pools Exchanges a login from a user pool or an external provider for temporary AWS credentials under an IAM role, guests included. Cognito Identity Pools is available on AWS only. [Service Catalog](/service-adapters/catalog). # DMS (Database Migration) Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/dms-database-migration AWS DMS (Database Migration). Copies data between database engines with a full load followed by ongoing change capture, keeping the source online during a migration. DMS (Database Migration) is available on AWS only. [Service Catalog](/service-adapters/catalog). # Glue Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/glue AWS Glue. A catalog of table and schema metadata, crawlers that discover schemas, and serverless Spark and Python jobs that transform and load data between stores. Glue is available on AWS only. [Service Catalog](/service-adapters/catalog). # IAM Identity Center (SSO) Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/iam-identity-center-sso Manages workforce access to AWS accounts through permission sets and account assignments. IAM Identity Center (SSO) is available on AWS only. [Service Catalog](/service-adapters/catalog). # IoT Core Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/iot-core AWS IoT Core. Connects devices to cloud applications through messaging, device shadows and rules that route device data to other services. IoT Core is available on AWS only. [Service Catalog](/service-adapters/catalog). # Lake Formation Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/lake-formation AWS Lake Formation. Grants and enforces database, table, column and row level permissions over Glue catalog data in S3 for engines like Athena and Redshift. Lake Formation is available on AWS only. [Service Catalog](/service-adapters/catalog). # Neptune (graph) Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/neptune-graph AWS Neptune (graph). A graph database that stores property graphs queried with Gremlin or openCypher and RDF triples queried with SPARQL. Neptune (graph) is available on AWS only. [Service Catalog](/service-adapters/catalog). # Network Manager Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/network-manager AWS Network Manager. Manages and monitors global networks, including Cloud WAN core-network policies and attachments connecting cloud and on-premises networks. Network Manager is available on AWS only. [Service Catalog](/service-adapters/catalog). # Organizations Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/organizations Groups multiple AWS accounts under one management account, with organizational units, consolidated billing and service control policies that bound member permissions. Organizations is available on AWS only. [Service Catalog](/service-adapters/catalog). # Pinpoint Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/pinpoint Customer engagement campaigns over email, SMS and push with endpoint analytics; AWS has set its end of support for 30 October 2026. Pinpoint is available on AWS only. [Service Catalog](/service-adapters/catalog). # Redshift Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/redshift AWS Redshift. A columnar data warehouse for SQL analytics, distributing tables across compute nodes and querying data left in S3 through Redshift Spectrum. Redshift is available on AWS only. [Service Catalog](/service-adapters/catalog). # Resource Access Manager Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/resource-access-manager Shares individual resources, such as subnets, Transit Gateways and Route 53 rules, with other AWS accounts without duplicating them. Resource Access Manager is available on AWS only. [Service Catalog](/service-adapters/catalog). # Resource Groups Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/resource-groups Groups AWS resources using tag or CloudFormation-stack queries so they can be viewed and managed together. Resource Groups is available on AWS only. [Service Catalog](/service-adapters/catalog). # S3 Tables Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/s3-tables AWS S3 Tables. A bucket type dedicated to Apache Iceberg tables, adding table-level permissions and automatic compaction, snapshot expiry and unreferenced file removal. S3 Tables is available on AWS only. [Service Catalog](/service-adapters/catalog). # S3 Vectors Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/s3-vectors AWS S3 Vectors. Stores vectors in dedicated vector buckets and indexes, with similarity queries and metadata filtering. S3 Vectors is available on AWS only. [Service Catalog](/service-adapters/catalog). # SageMaker AI (Training and Notebooks) Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/sagemaker-ai-training-and-notebooks AWS SageMaker AI (Training and Notebooks). The notebook and training capabilities of SageMaker AI: interactive model development, managed training jobs and hyperparameter tuning. Inference is listed separately. SageMaker AI (Training and Notebooks) is available on AWS only. [Service Catalog](/service-adapters/catalog). # Verified Permissions Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/verified-permissions AWS Verified Permissions. Evaluates application authorization requests against Cedar policies stored in managed policy stores. Verified Permissions is available on AWS only. [Service Catalog](/service-adapters/catalog). # VPC Lattice Source: https://docs.tensor9.com/service-adapters/aws/available-on-aws-only/vpc-lattice AWS VPC Lattice. Connects services and resources through service networks, with routing, access policies and associations across VPCs and accounts. VPC Lattice is available on AWS only. [Service Catalog](/service-adapters/catalog). # EC2 Source: https://docs.tensor9.com/service-adapters/aws/compute-containers/ec2 AWS EC2. A virtual machine booted from an AMI onto a chosen instance type, running inside a subnet with attached volumes and security groups. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of EC2 with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | EC2 | Google Cloud | Azure | OCI | | ---------------------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------- | -------------------------------------------------- | | General purpose · M-series | m7i / m6i / m8g | n2 / n2d / n4 / c4 | Dsv5 / Dsv6 (Intel) · Dasv5 (AMD) | VM.Standard3.Flex / E5.Flex | | Burstable · T-series | t3 / t4g | e2 | B-series (Bsv2) | - (A1.Flex, no credit-burst) | | Compute optimized · C-series | c7i / c6i / c7g | c2 / c2d / c3 / c3d / h3 | Fsv2 / FX | VM.Optimized3.Flex | | Memory optimized · R-series | r7i / r6i / r8g | m1 / m3 / n2-highmem | Esv5 / Easv5 / Esv6 | E5.Flex at high GB/OCPU | | High-memory · X / U-series (multi-TB) | x2iedn / u-\* | m3-ultramem / x4 (to 32 TB) | M-series / Mv3 (to \~30 TB) | BM.Standard.E5 / extended-memory | | Storage optimized · I / D-series | i4i / i7ie / d3 | z3 (local SSD) | Lsv3 (local NVMe) | VM.DenseIO.E5 (local NVMe) | | Accelerated: GPU · P / G-series | p5 / p4d / g6 | a3 (H100) / a2 (A100) / g2 (L4) | ND H100 v5 / NC A100 v4 / NV (A10) | BM.GPU.H100 / GPU4 (A100) / VM.GPU.A10 / MI300X | | Accelerated: ML ASIC · Inf / Trn | inf2 / trn1 | Cloud TPU v5e / v6 (separate product) | - (Maia not GA) | - | | HPC · Hpc-series | hpc7g / hpc6a | h3 / h4d | HBv4 / HBv3 / HC | BM.Optimized3 / BM.HPC.E5 | | Arm / Graviton · cross-class | m7g / c7g / r8g | c4a (Axion) | Dpsv6 (Cobalt 100) | A1.Flex (Ampere) | | Provisioning model · IaC and runtime lifecycle | RunInstances + infrastructure-as-code | Google Compute Engine native resources | Azure Virtual Machines native resources | OCI Compute native resources | | Instance discovery · EC2 response format | DescribeInstances / DescribeVpcs / DescribeSubnets / DescribeSecurityGroups | managed resource state | managed resource state | managed resource state | | Instance identity · management and metadata | describe APIs + instance metadata | the same managed instance | the same managed instance | the same managed instance | | Discovery scope · adapter-managed resources | fleet / cross-account describe | managed account and fleet | managed account and fleet | managed account and fleet | | Network throughput + latency | depends on EC2 instance type, size and placement | depends on Compute Engine machine type, size and placement | - | - | | API coverage | full | high | high | high | | Block + local storage | EBS gp3 / io2, instance store | - | Premium / Ultra SSD, Lsv3 local NVMe | - | | OCPU sizing model · x86: 1 OCPU = 2 vCPU | 1 vCPU = 1 hyperthread | - | - | x86: 1 OCPU = 2 vCPU; Flex configures CPU + memory | ### Infrastructure-only adaptation | Capability | EC2 | Google Cloud | Azure | OCI | Private Kubernetes | | ---------------------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------- | -------------------------------------------------- | ------------------------------------------------------ | | General purpose · M-series | m7i / m6i / m8g | n2 / n2d / n4 / c4 | Dsv5 / Dsv6 (Intel) · Dasv5 (AMD) | VM.Standard3.Flex / E5.Flex | VM on a general-purpose node pool | | Burstable · T-series | t3 / t4g | e2 | B-series (Bsv2) | - (A1.Flex, no credit-burst) | VM with CPU requests below limits; no EC2 credit model | | Compute optimized · C-series | c7i / c6i / c7g | c2 / c2d / c3 / c3d / h3 | Fsv2 / FX | VM.Optimized3.Flex | VM on a high-clock node pool | | Memory optimized · R-series | r7i / r6i / r8g | m1 / m3 / n2-highmem | Esv5 / Easv5 / Esv6 | E5.Flex at high GB/OCPU | VM on a high-memory node pool | | High-memory · X / U-series (multi-TB) | x2iedn / u-\* | m3-ultramem / x4 (to 32 TB) | M-series / Mv3 (to \~30 TB) | BM.Standard.E5 / extended-memory | VM on a large-memory / bare-metal node | | Storage optimized · I / D-series | i4i / i7ie / d3 | z3 (local SSD) | Lsv3 (local NVMe) | VM.DenseIO.E5 (local NVMe) | VM on a node with local NVMe + a PV | | Accelerated: GPU · P / G-series | p5 / p4d / g6 | a3 (H100) / a2 (A100) / g2 (L4) | ND H100 v5 / NC A100 v4 / NV (A10) | BM.GPU.H100 / GPU4 (A100) / VM.GPU.A10 / MI300X | VM with GPU passthrough / vGPU | | Accelerated: ML ASIC · Inf / Trn | inf2 / trn1 | Cloud TPU v5e / v6 (separate product) | - (Maia not GA) | - | - (no ASIC passthrough analog) | | HPC · Hpc-series | hpc7g / hpc6a | h3 / h4d | HBv4 / HBv3 / HC | BM.Optimized3 / BM.HPC.E5 | VM on an HPC node pool (RDMA / SR-IOV) | | Arm / Graviton · cross-class | m7g / c7g / r8g | c4a (Axion) | Dpsv6 (Cobalt 100) | A1.Flex (Ampere) | VM on an arm64 node pool | | Provisioning model · IaC and runtime lifecycle | RunInstances + infrastructure-as-code | Google Compute Engine native resources | Azure Virtual Machines native resources | OCI Compute native resources | KubeVirt infrastructure configuration | | Instance discovery · EC2 response format | DescribeInstances / DescribeVpcs / DescribeSubnets / DescribeSecurityGroups | managed resource state | managed resource state | managed resource state | local instance and attached resources | | Instance identity · management and metadata | describe APIs + instance metadata | the same managed instance | the same managed instance | the same managed instance | the same local VM | | Discovery scope · adapter-managed resources | fleet / cross-account describe | managed account and fleet | managed account and fleet | managed account and fleet | local resources only | | Network throughput + latency | depends on EC2 instance type, size and placement | depends on Compute Engine machine type, size and placement | - | - | - | | API coverage | full | high | high | high | high | | Block + local storage | EBS gp3 / io2, instance store | - | Premium / Ultra SSD, Lsv3 local NVMe | - | - | | OCPU sizing model · x86: 1 OCPU = 2 vCPU | 1 vCPU = 1 hyperthread | - | - | x86: 1 OCPU = 2 vCPU; Flex configures CPU + memory | - | | CPU per-core · host-relative | cloud instance | - | - | - | host node minus a small virtualization overhead | | Cost · no separate SKU | per-instance on-demand | - | - | - | the underlying node | | Availability · cluster-provided | 99.5% single-instance SLA | - | - | - | the cluster's SLA | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------------------------------- | -------------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Fleet / cross-resource discovery | Control plane | Partial | Full surface | The management API lists instances in the caller's adapter-managed account, with supported filters and pagination. It does not enumerate unrelated cloud accounts. The separate in-guest self-discovery endpoint remains limited to local resources. | | RunInstances / TerminateInstances / ModifyInstanceAttribute / CreateTags | Control plane | Partial | Full surface | Maximum adaptation supports bounded instance launch, termination and tagging for adapter-managed resources. ModifyInstanceAttribute covers security-group changes; other attributes and unsupported launch settings are rejected. Creation completes asynchronously on the selected target. | | Instance provisioning | Instance lifecycle | Supported | Common | your aws\_instance / launch template compiles to a native Google Compute Engine instance of the equivalent size | | Start / Stop / Reboot / Terminate | Instance lifecycle | Supported | Common | Infrastructure configuration maps to native VM lifecycle controls; this row does not establish runtime EC2 API support. | | DescribeAvailabilityZones | Metadata + discovery | Supported | Most usage | Returns the deployment's available zones in EC2 format for applications that choose placement by zone. | | DescribeSecurityGroups | Metadata + discovery | Supported | Common | Returns the caller's security-group IDs, names, and inbound and outbound rules. | | DescribeVolumes / DescribeNetworkInterfaces / DescribeTags (the caller's own) | Metadata + discovery | Supported | Most usage | Returns the caller's attached volumes, network interfaces, and tags in EC2 format, consistent with its instance information. | | DescribeVpcs / DescribeSubnets | Metadata + discovery | Supported | Common | Returns the caller's VPC and subnet in EC2 format, including IDs, CIDR blocks, and zones, consistent with instance metadata. | | Instance metadata (IMDS) + DescribeInstances (the running instance) | Metadata + discovery | Supported | Common | Instance metadata and DescribeInstances return the caller's instance in EC2 format: instance id, type, image, VPC, subnet, security groups, availability zone, and IPs. Values come from the live deployment. | | Attached data volumes (EBS volumes) | Storage | Out of scope | Common | a SECONDARY data volume -- an ebs\_block\_device on the instance, or an aws\_ebs\_volume attached to it -- STOPS THE BUILD under the v1 stateless-only contract on every cloud, because a destroy-and-recreate migration would silently lose its data. (This row said 'EBS volumes map to the target's native block storage' until 2026-09-14; the shared VM arm refuses them.) | | Root volume | Storage | Supported | Common | the instance's ROOT volume maps to the target's native block storage (size and, where the target has one, provisioned IOPS) | #### How it works On AWS your workload runs on an EC2 instance. Tensor9 compiles the AWS Terraform into a **Compute Engine VM** using a compatible target image containing the application software. Your process runs directly on the VM. A Tensor9 metadata service runs inside the VM. It answers requests to `169.254.169.254` for the instance ID, region, and IAM credentials your workload expects from EC2.
Before: on AWS your workload runs on an Amazon EC2 instance. After: in the target cloud the application workload runs on a native Compute Engine VM, with an in-guest metadata responder. Before: on AWS your workload runs on an Amazon EC2 instance. After: in the target cloud the application workload runs on a native Compute Engine VM, with an in-guest metadata responder.

The workload runs on a native Compute Engine VM, with an in-guest metadata responder.

#### Machine Images Tensor9 translates the image and VM size. A **stock image is looked up:** a `data aws_ami` lookup for a current Ubuntu LTS resolves to the equivalent Compute Engine image family (Ubuntu 22.04 on x86 → `ubuntu-os-cloud/ubuntu-2204-lts`), kept at the resolved version so a change to "latest" does not trigger a rebuild. A **custom image is compiled into the target image format:** your own built AMI becomes a native Compute Engine image containing your software, so both paths land a native image. The `instance_type` maps to the nearest Compute Engine machine type in the same hardware class, and the root EBS volume becomes a persistent disk. An input with no faithful target (an Arm origin where no matching image family exists, or an image that can't be built) is caught at build rather than booted as the wrong machine. * **AMI → Compute Engine image:** a `data aws_ami` stock lookup resolves to the equivalent image family at a fixed version; a custom AMI is compiled into a native Compute Engine image built from your software. Both produce an image for the target cloud. * **instance\_type → machine\_type:** general → `n2 / n4`, compute → `c2 / c3`, memory → `m3`, storage → `z3`, GPU → `a3 / a2 / g2`. The Arm class has its analog in `c4a` (Axion), gated today on Arm image-family coverage, so an Arm origin is caught at build rather than landed on an x86 host. * **The root disk:** it boots the translated image, keeping its size and type; attached data volumes require target disks, guest mounts and a separate data transfer. * **Cloud-managed.** Google operates the hypervisor, host, and availability; your workload runs unchanged in the guest. #### IMDSv2 The responder implements **Instance Metadata Service v2 (IMDSv2)**, the token-protected mode AWS defaults to: `PUT /latest/api/token` returns a short-lived session token, which the client sends back in the `X-aws-ec2-metadata-token` header on each `GET /latest/meta-data/…`. Token-less IMDSv1 reads work too, unless the origin instance set `http_tokens=required`. The AWS SDK and most bootstrap scripts read this metadata from the fixed link-local address `169.254.169.254`. Compute Engine uses that address for its own metadata API. The adapter serves the AWS metadata paths in the guest: the address is redirected to a loopback responder that returns an EC2-shaped identity, read-only, and never leaving the VM. A field it can't resolve truthfully returns an error, so unresolved fields fail rather than returning a fabricated identity. Native Google metadata uses `/computeMetadata/v1/` paths and the required `Metadata-Flavor: Google` request header. The responder forwards those native requests to Compute Engine metadata, preserving the path, query and header; it returns the provider's response. Only the AWS `/latest/` paths use the EC2 responder and its IMDSv2 token handshake. The shared link-local address therefore does not turn a native Google metadata request into an AWS request. the IMDSv2 handshake, unchanged, on the migrated GCE VM ```bash theme={null} # 1. mint a session token (IMDSv2) $ TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \ -H "X-aws-ec2-metadata-token-ttl-seconds: 60") # 2. read the instance-id with the token $ curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \ "http://169.254.169.254/latest/meta-data/instance-id" i-0f5a2e9e516da3151 # an EC2-shaped id, served in-guest on GCP ```
Inside the Compute Engine VM: your process reads the link-local metadata address 169.254.169.254, which is redirected to an in-guest Tensor9 responder that speaks IMDSv2, read-only, and returns an EC2-shaped instance identity. Inside the Compute Engine VM: your process reads the link-local metadata address 169.254.169.254, which is redirected to an in-guest Tensor9 responder that speaks IMDSv2, read-only, and returns an EC2-shaped instance identity.

The link-local metadata address is redirected to an in-guest responder that speaks IMDSv2 and returns an EC2-shaped identity.

#### Launch templates and Auto Scaling fleets A **launch template** defines instance settings. An **Auto Scaling group** adds the member count, placement and scaling policy. The fleet mapping combines them into the target's instance template and managed group. Review the Auto Scaling page for scaling-policy and orchestration limits. **Launch templates.** A launch template is a blueprint for an instance: the same fields an `aws_instance` sets (image, machine type, key, user-data, disks, IAM role), declared once for reuse. An instance that references one is read as a single effective configuration: the template supplies the base and the instance's own inline arguments override it, field by field, exactly as AWS resolves them. That one effective machine is translated the same way a standalone instance is, so a launch-template-defined VM gets the same image, machine type, disks, network, identity, and in-guest metadata as any other. **Auto Scaling groups.** An ASG is a fleet of identical instances behind one size-and-scaling policy, and Google's native equivalent is a **regional managed instance group** driven by a `google_compute_instance_template`. `desired_capacity` becomes the group's target size and `min_size`/`max_size` become the regional autoscaler's bounds. The launch template's effective configuration becomes the instance template every member boots from, so each replica is a full VM with the same disks, network, identity, and read-only metadata API as a standalone instance, spread across the region's zones. * **Launch template → one effective machine:** template base + the instance's inline overrides resolve to a single configuration, translated like any standalone VM. * **ASG → regional managed instance group.** `desired_capacity` → target size; `min_size`/`max_size` → the regional autoscaler's bounds. * **Every replica is a full VM:** the blueprint becomes the `google_compute_instance_template` each member boots from, giving each member the same identity and metadata as a single instance. #### When the instance ID changes An EC2 instance-id (`i-…`) is stable for the life of the instance and changes only when the instance is replaced. The responder reproduces that by deriving the instance-id from the GCE VM's own Google-assigned id (hashed together with the install id). While the VM exists the id is stable, including across reboots and across Tensor9 redeploys of the surrounding stack. It changes when the VM is replaced. Tensor9 ties the VM's replacement to the AWS attributes it models as requiring replacement (AMI, subnet, key pair, availability zone, CPU options, placement), so replacing any of them replaces the VM and rotates the id. Target replacement rules can differ from EC2; review image, shape and network changes before applying them. * **Stable across reboots and redeploys:** the id is derived from the persistent GCE VM id, not from boot state. * **Rotates on a modeled force-new change:** a change to a modeled attribute (AMI, subnet, key pair, AZ, CPU options, placement) replaces the VM and rotates the id. * **Never the raw VM id:** the Google id is hashed into an EC2-shaped `i-…`; it is not served or logged verbatim. #### Limitations The compute path is native and the metadata handshake matches EC2. Review these metadata, storage and network differences: Known limits * **The metadata API is read-only:** it serves identity and metadata, not a control plane: there is no EC2 create or terminate against it; the VM is managed as a Compute Engine resource. * **The signed instance-identity document isn't served:** its signature can't be reproduced, so it is withheld; the plaintext identity document is served, with a reconciled account id and region. Workloads that verify the IMDS signature won't run. * **Discovery uses the configured EC2 adapter endpoint.** Describe operations return supported instances and related resources in the managed account, including the instance. Filters and pagination follow the supported API scope. The metadata endpoint inside the guest remains a separate service; unrelated real cloud accounts are outside this discovery scope. * **The IAM credentials from the metadata service are Tensor9-scoped placeholders.** AWS calls still succeed (they travel the Tensor9 egress path, which substitutes the real caller credential), but the placeholder credentials don't work against an AWS endpoint reached directly. * **The VM runs as the origin instance's IAM role:** that role is mapped to a Google service account and the VM runs as it (not GCP's default Compute Engine service account), so the origin's declared IAM intent is what the VM can actually do. A role with no literal name (a `name_prefix` or computed name) can't be resolved, so it is caught at compile. * **The machine type is the nearest Compute Engine analog, not identical hardware:** performance tracks Compute Engine rather than the specific EC2 instance, close on most shapes, with older Intel generations trailing on single-core. The performance tab has the published comparison. * **Security groups map to target network controls.** Use the Compute Engine firewall rules for the VM's translated rules. Preserve default-deny ingress and the declared egress permissions; do not rely on the network's default reachability. Review target rule scope, protocol support and address ranges when validating connectivity. * **A per-instance public IP is caught at build:** an instance that requests one (`associate_public_ip_address`) is refused because this target does not create a per-instance public IP. * **Explicit disk-performance settings fail during the build:** an instance that sets `iops` or `throughput` independently (AWS `gp3`/`io1`/`io2`) asks for something a Compute Engine `pd-*` boot disk cannot honor independently, so the build stops rather than quietly giving the disk a different performance profile than the one you declared. * **Disks use the target storage service.** The root volume uses a Compute Engine persistent disk; attached data volumes need corresponding target disks and guest mounts. Match size, performance class, encryption and attachment limits to the workload. Provisioning a disk does not copy the data from an existing EBS volume. * **Launch templates and Auto Scaling groups use the fleet mapping.** A launch template or launch configuration defines each member, and the group maps to a regional managed instance group. Review the Auto Scaling page for the target's scaling, health-check and rolling-update limits; support for individual instances does not establish every fleet policy. #### Other considerations Plan for the following operating requirements. * **Native execution:** the workload runs directly on the Compute Engine VM; Tensor9 operates the separate in-guest metadata service. * **Google operates the machine:** the hypervisor, host, and hardware availability are Google's to run, as AWS does for EC2. * **Tensor9 operates the read-only metadata API:** the in-guest responder answers EC2 identity and IAM credentials on 169.254.169.254, read-only, and accepts no create or terminate against the VM. * **The machine type is the nearest analog, not identical hardware:** the workload lands on the closest Compute Engine machine type in its class, so plan for performance that tracks that type rather than the specific EC2 instance. * **The instance-id follows the VM's lifecycle:** it is stable across reboots and redeploys while the VM exists, and changes when the VM is replaced. Review target replacement rules before changing size or image. #### Runtime lifecycle and instance metadata Maximum adaptation separates the EC2 management API from the metadata endpoint inside each VM. Supported lifecycle requests record the requested instance state; a reconciler creates or changes the target VM and its associated resources. A successful request can return a pending state while the target is still creating the machine. Use the describe operations to check readiness. RunInstances, termination and tagging operate on resources managed by this adapter-managed account. Attribute updates and launch shapes remain bounded by the supported request and target settings. Describe requests can list managed instances; they do not grant access to unrelated cloud accounts. The in-guest metadata service supplies the workload's identity and credentials independently of those lifecycle operations. Metadata and management responses must refer to the same managed instance. ## On Azure | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------------------------------- | -------------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Fleet / cross-resource discovery | Control plane | Partial | Full surface | The management API lists instances in the caller's adapter-managed account, with supported filters and pagination. It does not enumerate unrelated cloud accounts. The separate in-guest self-discovery endpoint remains limited to local resources. | | RunInstances / TerminateInstances / ModifyInstanceAttribute / CreateTags | Control plane | Partial | Full surface | Maximum adaptation supports bounded instance launch, termination and tagging for adapter-managed resources. ModifyInstanceAttribute covers security-group changes; other attributes and unsupported launch settings are rejected. Creation completes asynchronously on the selected target. | | Instance provisioning | Instance lifecycle | Supported | Common | your aws\_instance / launch template compiles to a native Azure Virtual Machine of the equivalent size | | Start / Stop / Reboot / Terminate | Instance lifecycle | Supported | Common | Infrastructure configuration maps to native VM lifecycle controls; this row does not establish runtime EC2 API support. | | DescribeAvailabilityZones | Metadata + discovery | Supported | Most usage | Returns the deployment's available zones in EC2 format for applications that choose placement by zone. | | DescribeSecurityGroups | Metadata + discovery | Supported | Common | Returns the caller's security-group IDs, names, and inbound and outbound rules. | | DescribeVolumes / DescribeNetworkInterfaces / DescribeTags (the caller's own) | Metadata + discovery | Supported | Most usage | Returns the caller's attached volumes, network interfaces, and tags in EC2 format, consistent with its instance information. | | DescribeVpcs / DescribeSubnets | Metadata + discovery | Supported | Common | Returns the caller's VPC and subnet in EC2 format, including IDs, CIDR blocks, and zones, consistent with instance metadata. | | Instance metadata (IMDS) + DescribeInstances (the running instance) | Metadata + discovery | Supported | Common | Instance metadata and DescribeInstances return the caller's instance in EC2 format: instance id, type, image, VPC, subnet, security groups, availability zone, and IPs. Values come from the live deployment. | | Attached data volumes (EBS volumes) | Storage | Out of scope | Common | a SECONDARY data volume -- an ebs\_block\_device on the instance, or an aws\_ebs\_volume attached to it -- STOPS THE BUILD under the v1 stateless-only contract on every cloud, because a destroy-and-recreate migration would silently lose its data. (This row said 'EBS volumes map to the target's native block storage' until 2026-09-14; the shared VM arm refuses them.) | | Root volume | Storage | Supported | Common | the instance's ROOT volume maps to the target's native block storage (size and, where the target has one, provisioned IOPS) | #### How it works On AWS your workload runs on an EC2 instance. Tensor9 compiles the AWS Terraform into an **Azure Linux Virtual Machine** using a compatible target image containing the application software. Your process runs directly on the VM. A Tensor9 metadata service runs inside the VM. It answers requests to `169.254.169.254` for the instance ID, region, and IAM credentials your workload expects from EC2.
Before: on AWS your workload runs on an Amazon EC2 instance. After: in the target cloud the application workload runs on a native Azure Linux VM, with an in-guest metadata responder. Before: on AWS your workload runs on an Amazon EC2 instance. After: in the target cloud the application workload runs on a native Azure Linux VM, with an in-guest metadata responder.

The workload runs on a native Azure Linux VM, with an in-guest metadata responder.

#### Machine Images Tensor9 translates the VM size and image. The `instance_type` maps to the nearest Azure VM size in the same hardware family (general purpose → `D`-series, compute → `F`, memory → `E`, burstable → `B`), and the root EBS volume becomes an Azure managed disk. **The image.** A stock image is looked up: a `data aws_ami` lookup for a current Ubuntu LTS resolves to the equivalent **Canonical image in the Azure Marketplace** (the `Canonical` publisher's `source_image_reference`), kept at the resolved version so a change to "latest" does not trigger a rebuild. A **custom image is compiled into the target image format:** your own built AMI becomes a native Azure managed image containing your software, so both paths boot a native Azure image. An input with no faithful target (an Arm origin with no matching image, or an image that can't be built) is caught at build rather than booted as the wrong machine. * **Stock image → Azure Marketplace:** a `data aws_ami` Ubuntu-LTS lookup resolves to the matching Canonical Marketplace image, kept at the resolved version. * **instance\_type → VM size:** general → `D`, compute → `F`, memory → `E`, burstable → `B`, GPU → `N`-series, Arm → `Dpds/Epsv`. * **The OS disk:** it boots the resolved image, keeping its size and type; attached data volumes require target disks, guest mounts and a separate data transfer. * **Custom image → compiled Azure image:** a non-Canonical AMI is compiled into a native Azure managed image built from your software; both stock and custom images use the target cloud's format. * **Cloud-managed.** Azure operates the hypervisor, host, and availability; your workload runs unchanged in the guest. #### IMDSv2 The in-guest responder implements **Instance Metadata Service v2 (IMDSv2)**, the token-protected mode AWS defaults to: `PUT /latest/api/token` returns a short-lived session token, which the client sends back in the `X-aws-ec2-metadata-token` header on each `GET /latest/meta-data/…`. Token-less IMDSv1 reads work too, unless the origin instance set `http_tokens=required`. An Azure VM already answers on `169.254.169.254`: that is Azure's own Instance Metadata Service. Tensor9 does not take it over: the EC2 metadata paths (`/latest/…`) are answered in the guest by the EC2-shaped responder, while Azure's own metadata paths (`/metadata/…`) are passed straight through to Azure untouched, so managed-identity and platform tooling that read Azure's metadata keep working. A field the responder can't resolve truthfully returns an error, so unresolved fields fail rather than returning a fabricated identity. the IMDSv2 handshake, unchanged, on the migrated Azure VM ```bash theme={null} # 1. mint a session token (IMDSv2) $ TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \ -H "X-aws-ec2-metadata-token-ttl-seconds: 60") # 2. read the instance-id with the token $ curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \ "http://169.254.169.254/latest/meta-data/instance-id" i-0f5a2e9e516da3151 # an EC2-shaped id, served in-guest on Azure ```
Inside the Azure VM, the link-local metadata address 169.254.169.254 is shared: a request for the EC2 metadata path is answered by an in-guest EC2-shaped responder, while a request for Azure's own metadata path is passed straight through to Azure's native metadata service. Inside the Azure VM, the link-local metadata address 169.254.169.254 is shared: a request for the EC2 metadata path is answered by an in-guest EC2-shaped responder, while a request for Azure's own metadata path is passed straight through to Azure's native metadata service.

The link-local metadata address is shared by path: the EC2 metadata path gets an EC2-shaped answer in the guest; Azure's own metadata path is passed straight through, untouched.

#### Launch templates and Auto Scaling fleets A **launch template** defines instance settings. An **Auto Scaling group** adds the member count, placement and scaling policy. The fleet mapping combines them into the target's instance template and managed group. Review the Auto Scaling page for scaling-policy and orchestration limits. **Launch templates.** A launch template is a blueprint for an instance: the same fields an `aws_instance` sets (image, size, key, user-data, disks, IAM role), declared once for reuse. An instance that references one is read as a single effective configuration: the template supplies the base and the instance's own inline arguments override it, field by field, exactly as AWS resolves them. That one effective machine is then translated the same way a standalone instance is, so a launch-template-defined VM gets the same image, size, disks, network, identity, and in-guest metadata as any other. **Auto Scaling groups.** An ASG is a fleet of identical instances behind one size-and-scaling policy, and Azure's native equivalent is a **Virtual Machine Scale Set**. `desired_capacity` becomes the scale set's desired size; running and ready capacity depend on allocation, startup and health checks. `min_size`/`max_size` become desired-capacity autoscale bounds. On Azure the scale set declares its machine blueprint inline (there is no separate template resource), so the launch template's effective configuration becomes the scale set's VM profile, and every replica is a full VM with the same disks, network, identity, and read-only metadata API as a standalone instance. * **Launch template → one effective machine:** template base + the instance's inline overrides resolve to a single configuration, translated like any standalone VM. * **ASG → Virtual Machine Scale Set.** `desired_capacity` → desired size, with readiness following allocation and health; `min_size`/`max_size` → autoscale bounds. * **Every replica is a full VM:** the blueprint becomes the scale set's inline VM profile, so each replica gets the same identity and metadata as a single instance. #### When the instance ID changes An EC2 instance-id (`i-…`) is stable for the life of the instance and changes only when the instance is replaced. The responder reproduces that by deriving the instance-id from the Azure VM's own `vmId`. While the VM exists the id is stable, including across reboots and across Tensor9 redeploys of the surrounding stack, and it is served consistently from first boot, not regenerated on a restart. It changes when the VM is replaced. Tensor9 ties the VM's replacement to the AWS attributes it models as requiring replacement (image, subnet, key pair, availability zone, CPU options, placement), so replacing any of them replaces the VM and rotates the id. Target replacement rules can differ from EC2; review image, shape and network changes before applying them. * **Stable across reboots and redeploys:** the id is derived from the persistent Azure `vmId`, and served consistently across a responder restart. * **Rotates on a modeled force-new change:** a change to a modeled attribute (image, subnet, key pair, zone, CPU options, placement) replaces the VM and rotates the id. * **Never the raw VM id:** the Azure `vmId` is hashed into an EC2-shaped `i-…`; it is not served or logged verbatim. #### Limitations The compute path is native and the metadata handshake matches EC2. Review these metadata, storage and network differences: Known limits * **The metadata API is read-only:** it serves identity and metadata, not a control plane: there is no EC2 create or terminate against it; the VM is managed as an Azure resource. * **The signed instance-identity document isn't served:** its signature can't be reproduced, so it is withheld; the plaintext identity document is served, with a reconciled account id and region. Workloads that verify the IMDS signature won't run. * **Discovery uses the configured EC2 adapter endpoint.** Describe operations return supported instances and related resources in the managed account, including the VM. Filters and pagination follow the supported API scope. The metadata endpoint inside the guest remains a separate service; unrelated real cloud accounts are outside this discovery scope. * **Security groups map to target network controls.** Use the Azure network security group for the VM's translated rules. Preserve default-deny ingress and the declared egress permissions; do not rely on Azure's default intra-network allow. Review target rule scope, protocol support and address ranges when validating connectivity. * **A per-instance public IP is caught at build:** an instance that requests one (`associate_public_ip_address`) is refused because this target does not create a per-instance public IP. * **Explicit disk-performance settings fail during the build:** an instance that sets `iops` or `throughput` independently (AWS `gp3`/`io1`/`io2`) asks for something an Azure managed OS disk cannot honor (independent IOPS needs a different disk type entirely), so the build stops rather than quietly giving the disk a different performance profile than the one you declared. * **Disks use the target storage service.** The root volume uses an Azure managed disk; attached data volumes need corresponding target disks and guest mounts. Match size, performance class, encryption and attachment limits to the workload. Provisioning a disk does not copy the data from an existing EBS volume. * **Launch templates and Auto Scaling groups use the fleet mapping.** A launch template or launch configuration defines each member, and the group maps to a Virtual Machine Scale Set. Review the Auto Scaling page for the target's scaling, health-check and rolling-update limits; support for individual instances does not establish every fleet policy. * **The VM runs as the origin instance's IAM role, mapped to an Azure managed identity:** the origin's declared IAM intent is what the VM can actually do; a role with no literal name (a `name_prefix` or a pre-existing external profile that can't be resolved) is caught at compile rather than binding a wrong or default identity. * **The VM size is the nearest Azure analog, not identical hardware:** performance tracks the Azure size rather than the specific EC2 instance. The performance tab has the published comparison. * **Burstable and Spot instances have limitations on Azure:** a burstable T-instance maps to a B-series size; the AWS unlimited-burst mode has no B-series equivalent and is caught rather than silently throttled. Spot maps to an Azure Spot VM (stop → deallocate, terminate → delete); a hibernate-on-interruption policy is caught at compile. #### Other considerations Plan for the following operating requirements. * **Native execution:** the workload runs directly on the Azure Linux VM; Tensor9 operates the separate in-guest metadata service. * **Azure operates the machine:** the hypervisor, host, and hardware availability are Azure's to run, as AWS does for EC2. * **Tensor9 operates the read-only metadata API:** the in-guest responder serves EC2 identity and IAM credentials but accepts no create or terminate against the VM, and Azure's own instance metadata on the same address is passed through untouched. * **The VM size is the nearest analog, not identical hardware:** the workload lands on the closest Azure size in its family (D/F/E/B), so plan for performance that tracks that size rather than the specific EC2 instance. * **The instance-id follows the VM's lifecycle:** it is stable across reboots and redeploys while the VM exists, and changes when the VM is replaced. Review target replacement rules before changing size or image. #### Runtime management on Azure The [EC2 lifecycle and metadata model](/service-adapters/aws/compute-containers/ec2#runtime-lifecycle-and-instance-metadata) also applies to Azure VMs. Supported runtime requests reconcile onto Azure resources; acceptance precedes readiness. The Azure metadata endpoint remains separate from managed-account discovery. The operation rows above bound supported launch and attribute changes. ## On OCI | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------------------------------- | -------------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Fleet / cross-resource discovery | Control plane | Partial | Full surface | The management API lists instances in the caller's adapter-managed account, with supported filters and pagination. It does not enumerate unrelated cloud accounts. The separate in-guest self-discovery endpoint remains limited to local resources. | | RunInstances / TerminateInstances / ModifyInstanceAttribute / CreateTags | Control plane | Partial | Full surface | Maximum adaptation supports bounded instance launch, termination and tagging for adapter-managed resources. ModifyInstanceAttribute covers security-group changes; other attributes and unsupported launch settings are rejected. Creation completes asynchronously on the selected target. | | Instance provisioning | Instance lifecycle | Supported | Common | your aws\_instance / launch template compiles to a native OCI Compute instance of the equivalent size | | Start / Stop / Reboot / Terminate | Instance lifecycle | Supported | Common | Infrastructure configuration maps to native VM lifecycle controls; this row does not establish runtime EC2 API support. | | DescribeAvailabilityZones | Metadata + discovery | Supported | Most usage | Returns the deployment's available zones in EC2 format for applications that choose placement by zone. | | DescribeSecurityGroups | Metadata + discovery | Supported | Common | Returns the caller's security-group IDs, names, and inbound and outbound rules. | | DescribeVolumes / DescribeNetworkInterfaces / DescribeTags (the caller's own) | Metadata + discovery | Supported | Most usage | Returns the caller's attached volumes, network interfaces, and tags in EC2 format, consistent with its instance information. | | DescribeVpcs / DescribeSubnets | Metadata + discovery | Supported | Common | Returns the caller's VPC and subnet in EC2 format, including IDs, CIDR blocks, and zones, consistent with instance metadata. | | Instance metadata (IMDS) + DescribeInstances (the running instance) | Metadata + discovery | Supported | Common | Instance metadata and DescribeInstances return the caller's instance in EC2 format: instance id, type, image, VPC, subnet, security groups, availability zone, and IPs. Values come from the live deployment. | | Attached data volumes (EBS volumes) | Storage | Out of scope | Common | a SECONDARY data volume -- an ebs\_block\_device on the instance, or an aws\_ebs\_volume attached to it -- STOPS THE BUILD under the v1 stateless-only contract on every cloud, because a destroy-and-recreate migration would silently lose its data. (This row said 'EBS volumes map to the target's native block storage' until 2026-09-14; the shared VM arm refuses them.) | | Root volume | Storage | Supported | Common | the instance's ROOT volume maps to the target's native block storage (size and, where the target has one, provisioned IOPS) | #### How it works On AWS your workload runs on an EC2 instance. Tensor9 compiles the AWS Terraform into an **OCI Compute instance** (`oci_core_instance`) using a compatible target image containing the application software. Your process runs directly on the instance. A Tensor9 metadata service runs inside the VM. It answers requests to `169.254.169.254` for the instance ID, region, and IAM credentials your workload expects from EC2.
Before: on AWS your workload runs on an Amazon EC2 instance. After: in the target cloud the application workload runs on a native OCI Compute instance, with an in-guest metadata responder. Before: on AWS your workload runs on an Amazon EC2 instance. After: in the target cloud the application workload runs on a native OCI Compute instance, with an in-guest metadata responder.

The workload runs on a native OCI Compute instance, with an in-guest metadata responder.

#### Machine Images Tensor9 translates the VM size and image. The `instance_type` maps to the nearest **OCI shape**, a `VM.Standard` flexible shape, where the origin's vCPU count sets the shape's OCPUs and its memory sizing; the root EBS volume becomes an OCI boot volume. **The image.** A stock image is looked up: a `data aws_ami` lookup for a current Ubuntu LTS resolves to the equivalent **Canonical Ubuntu image** through a `data oci_core_images` find-latest lookup, kept at the resolved version so a change to "latest" does not trigger a rebuild. A **custom image is compiled into the target image format:** your own built AMI becomes a native OCI custom image containing your software, so both paths boot a native OCI image. An input with no faithful target (an Arm origin with no matching image, or an image that can't be built) is caught at build rather than booted as the wrong machine. * **Stock image → Canonical Ubuntu:** a `data aws_ami` Ubuntu-LTS lookup resolves through `data oci_core_images` to the matching Canonical image (x86 today; an Arm origin is caught at build rather than booted on the wrong architecture). * **instance\_type → OCI shape:** mapped to the nearest `VM.Standard` flexible shape; the vCPU count sets the shape's OCPUs, and `cpu_options.core_count` maps onto the shape's OCPU config. * **The boot volume:** it runs the resolved image, keeping its size and type; attached data volumes use separately provisioned volumes and guest mounts. * **Custom image → compiled OCI image:** a non-Canonical AMI is compiled into a native OCI custom image that contains your software; both stock and custom images use the target cloud's format. * **Cloud-managed.** OCI operates the hypervisor, host, and availability domain; your workload runs unchanged in the guest. #### IMDSv2 The in-guest responder implements **Instance Metadata Service v2 (IMDSv2)**, the token-protected mode AWS defaults to: `PUT /latest/api/token` returns a short-lived session token, which the client sends back in the `X-aws-ec2-metadata-token` header on each `GET /latest/meta-data/…`. Token-less IMDSv1 reads work too, unless the origin instance set `http_tokens=required`. An OCI Compute instance already answers on `169.254.169.254`: that is OCI's own Instance Metadata Service, which serves under `/opc/…` paths. Tensor9 does not take it over: the EC2 metadata paths (`/latest/…`) are answered in the guest by the EC2-shaped responder, while OCI's own metadata paths (`/opc/…`) are passed straight through to OCI untouched, so OCI instance-principal and platform tooling that read `/opc` keep working. A field the responder can't resolve truthfully returns an error, so unresolved fields fail rather than returning a fabricated identity. the IMDSv2 handshake, unchanged, on the migrated OCI Compute instance ```bash theme={null} # 1. mint a session token (IMDSv2) $ TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \ -H "X-aws-ec2-metadata-token-ttl-seconds: 60") # 2. read the instance-id with the token $ curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \ "http://169.254.169.254/latest/meta-data/instance-id" i-0f5a2e9e516da3151 # an EC2-shaped id, served in-guest on OCI ```
Inside the OCI Compute instance, the link-local metadata address 169.254.169.254 is shared: a request for the EC2 metadata path is answered by an in-guest EC2-shaped responder, while a request for OCI's own metadata path is passed straight through to OCI's native metadata service. Inside the OCI Compute instance, the link-local metadata address 169.254.169.254 is shared: a request for the EC2 metadata path is answered by an in-guest EC2-shaped responder, while a request for OCI's own metadata path is passed straight through to OCI's native metadata service.

The link-local metadata address is shared by path: the EC2 metadata path gets an EC2-shaped answer in the guest; OCI's own /opc metadata path is passed straight through, untouched.

#### Launch templates and Auto Scaling fleets A **launch template** defines instance settings. An **Auto Scaling group** adds the member count, placement and scaling policy. The fleet mapping combines them into the target's instance template and managed group. Review the Auto Scaling page for scaling-policy and orchestration limits. **Launch templates.** A launch template is a blueprint for an instance: the same fields an `aws_instance` sets (image, size, key, user-data, disks, IAM role), declared once for reuse. An instance that references one is read as a single effective configuration: the template supplies the base and the instance's own inline arguments override it, field by field, exactly as AWS resolves them. That one effective machine is then translated the same way a standalone instance is, so a launch-template-defined instance gets the same image, shape, disks, network, identity, and in-guest metadata as any other. **Auto Scaling groups.** An ASG is a fleet of identical instances behind one size-and-scaling policy, and OCI's native equivalent is an **instance pool**. Unlike Azure's inline scale set, OCI keeps the machine blueprint in a separate `oci_core_instance_configuration` resource, which the pool references; `desired_capacity` becomes the pool's desired size. Running and ready capacity depend on allocation, startup and health checks. `min_size`/`max_size` become its desired-capacity autoscale bounds. The launch template's effective configuration becomes that configuration's launch details, so every replica is a full instance with the same disks, network, scoped identity, and read-only metadata API as a standalone instance. * **Launch template → one effective machine:** template base + the instance's inline overrides resolve to a single configuration, translated like any standalone instance. * **ASG → instance pool + instance configuration.** `desired_capacity` → desired size, with readiness following allocation and health; `min_size`/`max_size` → autoscale bounds; the blueprint lives in a separate `oci_core_instance_configuration`, not inline. * **Every replica is a full instance:** the blueprint becomes the configuration's launch details, so each pool member has the same scoped identity and metadata as a single instance. #### When the instance ID changes An EC2 instance-id (`i-…`) is stable for the life of the instance and changes only when the instance is replaced. The responder reproduces that by deriving the instance-id from the OCI instance's own **OCID**. While the instance exists the id is stable, including across reboots and across Tensor9 redeploys of the surrounding stack, and it is served consistently from first boot, not regenerated on a restart. It changes when the instance is replaced. Tensor9 ties the instance's replacement to the AWS attributes it models as requiring replacement (image, subnet, SSH key pair, availability domain, CPU options, placement), so replacing any of them replaces the instance and rotates the id. Target replacement rules can differ from EC2. * **Stable across reboots and redeploys:** the id is derived from the persistent OCI instance `OCID`, and served consistently across a responder restart. * **Rotates on a modeled force-new change:** a change to a modeled attribute (image, subnet, SSH key, availability domain, CPU options, placement) replaces the instance and rotates the id. * **Never the raw OCID:** the OCI `OCID` is hashed into an EC2-shaped `i-…`; it is not served or logged verbatim. #### Limitations The compute path is native and the metadata handshake matches EC2. Review these metadata, storage and network differences: Known limits * **The metadata API is read-only:** it serves identity and metadata, not a control plane: there is no EC2 create or terminate against it; the instance is managed as an OCI resource. * **The signed instance-identity document isn't served:** its signature can't be reproduced, so it is withheld; the plaintext identity document is served, with a reconciled account id and region. Workloads that verify the IMDS signature won't run. * **Discovery uses the configured EC2 adapter endpoint.** Describe operations return supported instances and related resources in the managed account, including the instance. Filters and pagination follow the supported API scope. The metadata endpoint inside the guest remains a separate service; unrelated real cloud accounts are outside this discovery scope. * **Security groups map to target network controls.** Use the OCI network security list for the instance's translated rules. Preserve default-deny ingress and the declared egress permissions; do not rely on OCI's more permissive default. Review target rule scope, protocol support and address ranges when validating connectivity. * **A per-instance public IP is caught at build:** an instance that requests one (`associate_public_ip_address`) is refused because this target does not create a per-instance public IP. * **Launch templates and Auto Scaling groups use the fleet mapping.** A launch template or launch configuration defines each member, and the group maps to an OCI instance pool. Review the Auto Scaling page for the target's scaling, health-check and rolling-update limits; support for individual instances does not establish every fleet policy. * **The instance runs as the origin's IAM role, mapped to an OCI dynamic group and instance principal, scoped to this one instance:** every OCI instance is automatically an instance principal; the origin's declared IAM intent becomes a dynamic group whose membership rule and policy use a per-install tag to match only this instance, so a grant never reaches sibling VMs. A role that can't be resolved to a literal identity (a `name_prefix`, or an external profile that can't be resolved) is caught at compile, and an identity that can't be scoped to the installed instance fails rather than granting access to other instances. * **The OCI shape is the nearest analog, not identical hardware:** performance tracks the OCI shape rather than the specific EC2 instance type. The performance tab has the published comparison. * **Disks use the target storage service.** The root volume uses an OCI boot volume; attached data volumes need corresponding target disks and guest mounts. Match size, performance class, encryption and attachment limits to the workload. Provisioning a disk does not copy the data from an existing EBS volume. * **Boot-volume performance is one coupled knob, not two.** AWS `gp3` sets `iops` and `throughput` independently; an OCI boot volume sets both together from a single per-GB performance setting. The volume class comes across (a general-purpose origin lands on the balanced setting), but explicit `iops` or `throughput` numbers do not, so an instance that sets either is caught at build rather than having the two values silently coerced into one. * **Availability-zone placement isn't reproduced one-to-one:** an AWS availability zone has no faithful OCI availability domain, so the instance is placed in the compartment's availability domain rather than a mapped zone, which changes its placement. * **Burstable and Spot instances have limitations on OCI:** a burstable T-instance maps to an OCI shape with a reduced baseline OCPU utilization; the AWS unlimited-burst mode has no OCI equivalent and is caught rather than silently throttled. Spot maps to an OCI preemptible instance (stop → the preemption action); a hibernate-on-interruption policy is caught at compile. #### Other considerations Plan for the following operating requirements. * **Native execution:** the workload runs directly on the OCI Compute instance; Tensor9 operates the separate in-guest metadata service. * **OCI operates the machine:** the hypervisor, host, and availability domain are OCI's to run, as AWS does for EC2. * **Tensor9 operates the read-only metadata API:** the in-guest responder answers the EC2 `/latest` paths on 169.254.169.254 while OCI's own `/opc` metadata is passed through, and it accepts no create or terminate against the instance. * **The OCI shape is the nearest analog, not identical hardware:** the workload lands on the closest `VM.Standard` flexible shape, so plan for performance that tracks that shape rather than the specific EC2 instance type. * **The instance-id follows the instance's lifecycle:** it is derived from the instance's OCID, remains stable while the instance exists, and changes when the instance is replaced. Review target replacement rules before changing size or image. #### Runtime management on OCI OCI instances use the same bounded [EC2 lifecycle and metadata model](/service-adapters/aws/compute-containers/ec2#runtime-lifecycle-and-instance-metadata). Runtime changes finish asynchronously on OCI; describe operations expose observed readiness. In-guest discovery remains local, while the separate management API lists only the caller's managed resources. ## On Private Kubernetes | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------------------------------- | -------------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Fleet / cross-resource discovery | Control plane | Out of scope | Full surface | The KubeVirt mapping provides local discovery of the calling VM and attached resources. It does not establish managed fleet enumeration through the EC2 runtime API. | | RunInstances / TerminateInstances / ModifyInstanceAttribute / CreateTags | Control plane | Out of scope | Full surface | KubeVirt maps VM infrastructure and local metadata on an existing cluster, not managed lifecycle through these EC2 runtime APIs. Manage the VM through its infrastructure configuration and native cluster controls. | | Instance provisioning | Instance lifecycle | Supported | Common | your aws\_instance / launch template compiles to a native KubeVirt VirtualMachine of the equivalent size | | Start / Stop / Reboot / Terminate | Instance lifecycle | Supported | Common | Infrastructure configuration maps to native VM lifecycle controls; this row does not establish runtime EC2 API support. | | DescribeAvailabilityZones | Metadata + discovery | Supported | Most usage | Returns the deployment's available zones in EC2 format for applications that choose placement by zone. | | DescribeSecurityGroups | Metadata + discovery | Supported | Common | Returns the caller's security-group IDs, names, and inbound and outbound rules. | | DescribeVolumes / DescribeNetworkInterfaces / DescribeTags (the caller's own) | Metadata + discovery | Supported | Most usage | Returns the caller's attached volumes, network interfaces, and tags in EC2 format, consistent with its instance information. | | DescribeVpcs / DescribeSubnets | Metadata + discovery | Supported | Common | Returns the caller's VPC and subnet in EC2 format, including IDs, CIDR blocks, and zones, consistent with instance metadata. | | Instance metadata (IMDS) + DescribeInstances (the running instance) | Metadata + discovery | Supported | Common | Instance metadata and DescribeInstances return the caller's instance in EC2 format: instance id, type, image, VPC, subnet, security groups, availability zone, and IPs. Values come from the live deployment. | | Attached data volumes (EBS volumes) | Storage | Out of scope | Common | a SECONDARY data volume -- an ebs\_block\_device on the instance, or an aws\_ebs\_volume attached to it -- STOPS THE BUILD under the v1 stateless-only contract on every cloud, because a destroy-and-recreate migration would silently lose its data. (This row said 'EBS volumes map to the target's native block storage' until 2026-09-14; the shared VM arm refuses them.) | | Root volume | Storage | Supported | Common | the instance's ROOT volume maps to the target's native block storage (size and, where the target has one, provisioned IOPS) | #### How it works On AWS your workload runs on an EC2 instance. Tensor9 compiles the AWS Terraform into a **KubeVirt VirtualMachine** using a compatible target image containing the application software, running under KVM on the Kubernetes cluster you operate. Your process runs directly inside the VM. A Tensor9 metadata service runs inside the VM. It answers requests to `169.254.169.254` for the instance ID, region, and IAM credentials your workload expects from EC2.
Before: on AWS your workload runs on an Amazon EC2 instance. After: in the target cluster the application workload runs on a KubeVirt VirtualMachine under KVM on Kubernetes nodes, with an in-guest metadata responder. Before: on AWS your workload runs on an Amazon EC2 instance. After: in the target cluster the application workload runs on a KubeVirt VirtualMachine under KVM on Kubernetes nodes, with an in-guest metadata responder.

The workload runs on a KubeVirt VirtualMachine under KVM on the target cluster, with an in-guest metadata responder.

#### Machine Images Tensor9 translates the VM size and image. The `instance_type` becomes the VM's guest resources (its **domain CPU and memory**) read straight from the instance's vCPU and RAM rather than mapped through a fixed shape table. The VM boots the resolved image directly as its disk, sized by that image, so an origin that sets its own root-volume size is caught at build rather than booted smaller than it asked for. **The image.** A stock image is looked up: a `data aws_ami` lookup for a current Ubuntu LTS resolves to the equivalent **Canonical Ubuntu containerDisk** (a bootable cloud image with cloud-init) that the VM boots directly. A custom image is compiled into a bootable volume: your own built AMI is compiled into a bootable persistent volume by the **Containerized Data Importer**, so the VM boots a native disk built from your software. That path, like the VM primitive itself, needs the KubeVirt operator and the Data Importer installed on the cluster, so a compile that references a custom image is caught at build with a clear message when they are absent, rather than booting the wrong image. * **Stock image → Canonical containerDisk:** a `data aws_ami` Ubuntu-LTS lookup resolves to a Canonical Ubuntu containerDisk (a bootable cloud image with cloud-init) booted directly. * **instance\_type → domain CPU and memory:** the guest's vCPU and RAM are read straight from the instance shape, not mapped through a fixed size table. * **The boot disk:** it is created from the resolved image and sized by it; additional capacity and data disks require appropriately sized persistent volumes and guest mounts. * **Custom image → compiled by the Data Importer:** a non-Canonical AMI is compiled into a bootable persistent volume containing your software; the KubeVirt operator and Data Importer must be installed, and a compile that needs them when absent is caught at build rather than booted wrong. * **Self-managed:** the VM runs under KVM on the nodes you operate: you run the hypervisor and the cluster, not a cloud provider; your workload runs unchanged in the guest. #### IMDSv2 The in-guest responder implements **Instance Metadata Service v2 (IMDSv2)**, the token-protected mode AWS defaults to: `PUT /latest/api/token` returns a short-lived session token, which the client sends back in the `X-aws-ec2-metadata-token` header on each `GET /latest/meta-data/…`. Token-less IMDSv1 reads work too, unless the origin instance set `http_tokens=required`. The AWS SDK and most bootstrap scripts read this metadata from the fixed link-local address `169.254.169.254`. A KubeVirt guest boots a real cloud image with cloud-init and, unlike a cloud VM, has no native metadata service sitting on that address to coexist with. So the in-guest responder **owns `169.254.169.254` outright**: it answers there directly, read-only, never leaving the VM, with nothing to pass through. A field it can't resolve truthfully returns an error, so a wrong value never reaches the workload. the IMDSv2 handshake, unchanged, on the migrated KubeVirt VM ```bash theme={null} # 1. mint a session token (IMDSv2) $ TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \ -H "X-aws-ec2-metadata-token-ttl-seconds: 60") # 2. read the instance-id with the token $ curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \ "http://169.254.169.254/latest/meta-data/instance-id" i-0f5a2e9e516da3151 # an EC2-shaped id, served in-guest on KubeVirt ```
Inside the KubeVirt VM: your process reads the link-local metadata address 169.254.169.254, which the in-guest Tensor9 responder owns outright (there is no native metadata service to share it with) speaking IMDSv2, read-only, and returning an EC2-shaped instance identity. Inside the KubeVirt VM: your process reads the link-local metadata address 169.254.169.254, which the in-guest Tensor9 responder owns outright (there is no native metadata service to share it with) speaking IMDSv2, read-only, and returning an EC2-shaped instance identity.

The metadata address is owned outright in the guest (there is no native metadata service to share it with), and an in-guest responder speaks IMDSv2 and returns an EC2-shaped identity.

#### Launch templates and Auto Scaling fleets A **launch template** defines instance settings. An **Auto Scaling group** adds the member count, placement and scaling policy. The fleet mapping combines them into the target's instance template and managed group. Review the Auto Scaling page for scaling-policy and orchestration limits. **Launch templates.** A launch template is a blueprint for an instance: the same fields an `aws_instance` sets (image, size, key, user-data, disks, IAM role), declared once for reuse. An instance that references one is read as a single effective configuration: the template supplies the base and the instance's own inline arguments override it, field by field, exactly as AWS resolves them. That one effective machine is then translated the same way a standalone instance is, so a launch-template-defined VM gets the same image, guest resources, disks, network, identity, and in-guest metadata as any other. **Auto Scaling groups.** An ASG is a fleet of identical instances behind one size-and-scaling policy, and KubeVirt's native equivalent is a **VirtualMachinePool**. The launch template's effective configuration becomes the pool's embedded `virtualMachineTemplate`, and `desired_capacity` becomes the pool's `replicas`, every replica a full VM with the same disks, network, identity, and read-only metadata API as a standalone instance, scheduled onto your nodes. The pool exposes a standard `scale` subresource, so `min_size`/`max_size` become the bounds of a horizontal autoscaler you point at it; without one, the pool holds a fixed replica count. * **Launch template → one effective machine:** template base + the instance's inline overrides resolve to a single configuration, translated like any standalone VM. * **ASG → VirtualMachinePool.** `desired_capacity` → the pool's `replicas`; the blueprint becomes the pool's embedded `virtualMachineTemplate`. * **Scaling → a horizontal autoscaler on the scale subresource.** `min_size`/`max_size` become the bounds of a HorizontalPodAutoscaler you point at the pool; without one it holds a fixed count. * **Every replica is a full VM:** each replica has the same identity and metadata as a single instance, scheduled onto your own nodes. #### When the instance ID changes An EC2 instance-id (`i-…`) is stable for the life of the instance and changes only when the instance is replaced. The responder reproduces that by deriving the instance-id from the VM's own **firmware UUID**, the SMBIOS identifier the VM presents to its guest, set when the VM is created. While the VM exists the id is stable, including across reboots and across Tensor9 redeploys of the surrounding stack, and it is served consistently from first boot, not regenerated on a restart. It changes when the VM is replaced. Tensor9 ties the VM's firmware UUID to the AWS attributes it models as requiring replacement (image, subnet, key pair, availability zone, CPU options, placement), so replacing any of them replaces the VM and rotates the id. Target replacement rules can differ from EC2; review image, shape and network changes before applying them. In a pool, each replica gets its own firmware UUID, so every replica gets a distinct EC2-shaped id. * **Stable across reboots and redeploys:** the id is derived from the VM's persistent firmware UUID, and served consistently across a responder restart. * **Rotates on a modeled force-new change:** a change to a modeled attribute (image, subnet, key pair, zone, CPU options, placement) replaces the VM and rotates the id. * **Distinct per replica:** each VM in a pool has its own firmware UUID, so each is served its own EC2-shaped id. * **Never the raw UUID:** the firmware UUID is hashed into an EC2-shaped `i-…`; it is not served or logged verbatim. #### Limitations The compute path is native and the metadata handshake matches EC2. Review these metadata, storage and network differences: Known limits * **This is self-managed, not a cloud service:** you operate the Kubernetes cluster and the KVM nodes the VM runs on, so availability, live-migration, and rescheduling on node drain are the target cluster's (not a cloud provider's single-instance SLA), and performance tracks the node the VM lands on. * **The metadata API is read-only:** it serves identity and metadata, not a control plane: there is no EC2 create or terminate against it; the VM is managed as a Kubernetes resource on the target cluster. * **The VM runs as the origin instance's IAM role, mapped to a Kubernetes ServiceAccount:** the origin's declared IAM role becomes the ServiceAccount the VM runs as (its `serviceAccountName`), so the origin's declared identity is what the VM can actually do; a named role with no projected ServiceAccount on the cluster is caught at compile rather than the VM silently running as the namespace default. * **The signed instance-identity document isn't served:** its signature can't be reproduced, so it is withheld; the plaintext identity document is served, with a reconciled account id and region. Workloads that verify the IMDS signature won't run. * **Local discovery.** The configured EC2 adapter endpoint describes only the calling VM and its attached resources. It does not enumerate a managed account or fleet. The in-guest metadata endpoint remains a separate read-only service. * **No per-VM public IP:** a KubeVirt VM is single-homed on the target cluster network with no cloud-assigned public address; a request for a per-instance public IP is caught at compile, and an Elastic IP is reproduced as a LoadBalancer Service that fronts the VM rather than an address on the VM itself. * **Security groups map to target network controls.** Use a default-deny Kubernetes NetworkPolicy with explicit allows for the VM's translated rules. Preserve default-deny ingress and the declared egress permissions; do not rely on the cluster's default pod-to-pod reachability. Review target rule scope, protocol support and address ranges when validating connectivity. * **Persistent storage needs explicit sizing.** An image-backed boot disk and a persistent boot volume have different capacity and recovery behavior. Provision the required PersistentVolumes and guest mounts for root expansion or attached data disks; importing an image does not copy unrelated EBS volume contents. * **Launch templates and Auto Scaling groups use the fleet mapping.** A launch template or launch configuration defines each member, and the group maps to a VirtualMachinePool. Review the Auto Scaling page for the target's scaling, health-check and rolling-update limits; support for individual instances does not establish every fleet policy. * **Disk performance belongs to the target cluster, not the VM:** a volume's independent `iops`/`throughput` settings have no equivalent knob here (your StorageClass and CSI driver own disk performance), so an instance that sets either is caught at build rather than having the values silently ignored. * **The KubeVirt operator and the Data Importer must be installed on the cluster:** the VM primitive and the custom-image rebuild path depend on them; a compile that needs either is caught at build when they are absent, rather than emitting a manifest the cluster can't run. #### Other considerations Plan for the following operating requirements. * **Native execution:** the workload runs directly in the KubeVirt VM's guest; Tensor9 operates the separate in-guest metadata service. * **You operate the machine:** unlike the cloud arms, the VM runs under KVM on the Kubernetes cluster and nodes you run, so availability, live-migration, and rescheduling on node drain are the target cluster's, not a cloud provider's. * **Tensor9 operates the read-only metadata API:** the in-guest responder owns 169.254.169.254 outright and serves EC2 identity and IAM credentials, but accepts no create or terminate against the VM. * **Guest size is read directly, and performance tracks the node:** the instance's vCPU and RAM become the VM's domain resources rather than a mapped shape, so performance tracks the node the VM is scheduled onto, not the specific EC2 instance. * **The instance-id follows the VM's lifecycle:** it is derived from the VM's firmware UUID, remains stable while the VM exists, and changes when the VM is replaced. Review target replacement rules before changing size or image. #### Infrastructure and local discovery KubeVirt maps VM infrastructure and local metadata on the existing cluster, not managed lifecycle through the EC2 runtime API. Local DescribeInstances, VPC, subnet and security-group responses describe the calling VM and agree with its metadata. They do not provide managed fleet enumeration. Use the native cluster and infrastructure configuration to manage the VM. [Service Catalog](/service-adapters/catalog). # EC2 Auto Scaling Source: https://docs.tensor9.com/service-adapters/aws/compute-containers/ec2-auto-scaling AWS EC2 Auto Scaling. Keeps a set number of EC2 instances running across Availability Zones, replacing unhealthy ones and resizing the group on policy or schedule. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of EC2 Auto Scaling with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | EC2 Auto Scaling | OCI | Private Kubernetes | | ---------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | Fleet size · desired replicas | desired\_capacity | desired replica count; readiness follows allocation and health | desired replica count; readiness follows allocation and health | | Autoscaling bounds · min / max | min\_size / max\_size | the autoscaling configuration's bounds | the horizontal pod autoscaler's bounds | | Per-instance configuration · instance settings | launch template / launch configuration / mixed-instances policy | the standalone-instance mapping | the standalone-instance mapping | | Zonal spread · multi-AZ | vpc\_zone\_identifier | The ASG's subnets map to the fleet's selected target zones. Running capacity depends on allocation and health in those zones. | Zone spread requires nodes with zone topology labels, scheduling rules that use them, and schedulable capacity in each selected zone. | | Fleet orchestration · AWS orchestration APIs | lifecycle hooks / instance refresh / warm pools / on-demand-spot split | AWS orchestration APIs not reproduced; native provisioning rollout is target-specific | AWS orchestration APIs not reproduced; native provisioning rollout is target-specific | | API coverage | full | high | high | ### Infrastructure-only adaptation | Capability | EC2 Auto Scaling | Google Cloud | Azure | OCI | Private Kubernetes | | ---------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | Fleet size · desired replicas | desired\_capacity | desired replica count; readiness follows allocation and health | desired replica count; readiness follows allocation and health | desired replica count; readiness follows allocation and health | desired replica count; readiness follows allocation and health | | Autoscaling bounds · min / max | min\_size / max\_size | the regional autoscaler's bounds | the autoscale setting's bounds | the autoscaling configuration's bounds | the horizontal pod autoscaler's bounds | | Per-instance configuration · instance settings | launch template / launch configuration / mixed-instances policy | the standalone-instance mapping | the standalone-instance mapping | the standalone-instance mapping | the standalone-instance mapping | | Zonal spread · multi-AZ | vpc\_zone\_identifier | The ASG's subnets map to the fleet's selected target zones. Running capacity depends on allocation and health in those zones. | The ASG's subnets map to the fleet's selected target zones. Running capacity depends on allocation and health in those zones. | The ASG's subnets map to the fleet's selected target zones. Running capacity depends on allocation and health in those zones. | Zone spread requires nodes with zone topology labels, scheduling rules that use them, and schedulable capacity in each selected zone. | | Fleet orchestration · AWS orchestration APIs | lifecycle hooks / instance refresh / warm pools / on-demand-spot split | AWS orchestration APIs not reproduced; native provisioning rollout is target-specific | AWS orchestration APIs not reproduced; native provisioning rollout is target-specific | AWS orchestration APIs not reproduced; native provisioning rollout is target-specific | AWS orchestration APIs not reproduced; native provisioning rollout is target-specific | | API coverage | full | high | high | high | high | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------------ | ------------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Scaling policy (target-tracking / step) | Autoscaling | Partial | Most usage | A supported metric-driven policy maps to the regional autoscaler's utilization targets or threshold rules. Review translated thresholds: metric definitions, evaluation windows, cooldowns and scaling timing follow the target autoscaler. Custom or ASG-specific metrics need a supported target trigger; size bounds alone do not create a scaling policy. | | min\_size / max\_size | Autoscaling | Supported | Common | Map to the regional autoscaler's bounds on desired capacity; min==max fixes the desired count. These bounds do not guarantee ready capacity or cap temporary replicas during replacement. | | SetDesiredCapacity / runtime scaling-policy mutations | Control plane | Out of scope | Full surface | SetDesiredCapacity and runtime scaling-policy changes are outside this mapping. Set size and bounds through infrastructure-as-code or the owning service's supported lifecycle configuration. The runtime group API separately supports discovery and deletion. | | Fleet provisioning (aws\_autoscaling\_group) | Fleet lifecycle | Supported | Common | your aws\_autoscaling\_group compiles to a Google Compute Engine regional managed instance group, one managed group whose instances use the same configuration | | Lifecycle hooks / instance refresh / warm pools / mixed-instances distribution | Fleet orchestration | Out of scope | Full surface | The AWS StartInstanceRefresh API, lifecycle hooks, warm pools and on-demand/Spot distribution are outside this mapping. Target-native rolling replacement during provisioning is separate and follows the target's documented update policy where supported. | | desired\_capacity | Fleet sizing | Supported | Common | Sets the desired number of replicas using the same launch configuration. Running and ready capacity converge as allocation, scheduling and health checks complete. | | Instance blueprint (launch template / launch configuration / mixed-instances policy) | Instance definition | Supported | Common | the ASG's instance definition (sourced from a launch template, a legacy launch configuration, or a mixed-instances policy) uses the same instance mapping as a standalone Compute Engine instance, so every replica inherits the disk, subnet, security groups, identity, and metadata of a single instance | | Instance metadata + identity on every replica | Metadata + identity | Supported | Most usage | each replica gets the same read-only instance-metadata (IMDS) API and workload identity as a standalone instance, so per-replica service discovery and credential lookups keep working | | Multi-AZ placement (vpc\_zone\_identifier) | Placement | Supported | Most usage | The ASG's subnets map to the fleet's selected target zones. Running capacity depends on allocation and health in those zones. | #### How it works On AWS an `aws_autoscaling_group` packs the fleet and its instance blueprint into one resource. On Google Cloud that same fleet compiles to **three resources**: a `google_compute_instance_template` (the immutable blueprint) feeds versions into a `google_compute_region_instance_group_manager`, the **regional MIG** that runs the replicas, and a separate `google_compute_region_autoscaler` holds the size bounds and the scaling metric and drives the MIG. `desired_capacity` becomes the MIG's desired `target_size`. Ready capacity follows allocation, startup and health checks. `min_size` and `max_size` become the autoscaler's `min_replicas`/`max_replicas`. Because the MIG is regional, it spreads its replicas across the zones of the region on its own and recreates any replica that fails a health check, so the multi-AZ resilience the ASG was deployed for is preserved by the MIG itself.
On Google Cloud the Auto Scaling group maps to an immutable instance template, a regional managed instance group with a desired replica count, and a separate autoscaler for a declared scaling policy. Running and ready replicas depend on capacity allocation and health checks across the selected zones. On Google Cloud the Auto Scaling group maps to an immutable instance template, a regional managed instance group with a desired replica count, and a separate autoscaler for a declared scaling policy. Running and ready replicas depend on capacity allocation and health checks across the selected zones.

On Google Cloud the ASG splits into three resources: a standalone instance template, the regional MIG that runs the replicas, and a separate regional autoscaler that drives the MIG's size.

#### The instance template Google Cloud stores the instance settings in a **separate, immutable template**. The ASG's instance definition (from a launch template, a legacy launch configuration, or a mixed-instances policy) compiles to a standalone `google_compute_instance_template` that defines the machine type, the translated boot image, disks, network placement, and the workload identity (service account). The MIG references it by version. Because instance templates are immutable, a change to the blueprint is a new template plus a rolling replacement driven by the MIG's update policy, never an in-place edit of a running instance. Every replica the MIG brings up boots from that template through the same instance mapping as a standalone Compute Engine VM, so each is a full instance with its own disks, subnet, security rules, identity, and read-only metadata surface.
The ASG's instance definition (from a launch template, a legacy launch configuration, or a mixed-instances policy) maps to one immutable google_compute_instance_template holding the machine type, boot image, disks, network, and service-account identity. The regional MIG references a template version and stamps every replica from it; because the template is immutable, changing the blueprint is a new template version plus a rolling replacement, never an in-place edit. The ASG's instance definition (from a launch template, a legacy launch configuration, or a mixed-instances policy) maps to one immutable google_compute_instance_template holding the machine type, boot image, disks, network, and service-account identity. The regional MIG references a template version and stamps every replica from it; because the template is immutable, changing the blueprint is a new template version plus a rolling replacement, never an in-place edit.

The ASG's instance definition maps to one standalone, immutable instance template; the MIG references a version of it and stamps every replica from that version.

#### The regional autoscaler Scaling lives on a separate `google_compute_region_autoscaler` that targets the MIG; it is not a field on the fleet. Its `autoscaling_policy` holds the `min_replicas`/`max_replicas` bounds and the trigger. A CPU- or load-balancing-utilization target-tracking policy retains its utilization target; evaluation windows and scale-out and scale-in timing follow Google's autoscaler. A fixed `min_size == max_size` becomes a fixed-size MIG the autoscaler never resizes. * **Separate autoscaler resource:** `min_size`/`max_size` live on the region autoscaler that targets the MIG, not on the fleet body. * **CPU / LB utilization maps over:** the utilization target is preserved; scaling timing follows Google's autoscaler. * **Custom-metric autoscaling:** the autoscaler also supports a custom Cloud Monitoring metric, so a policy on a supported custom metric maps directly; an ASG-specific metric with no equivalent narrows to the nearest supported trigger. △ Auto Scaling differences * **Some custom metrics use a different trigger.** CPU and load-balancing utilization become target-tracking; a policy on a custom metric maps where the autoscaler has an equivalent Cloud Monitoring metric, otherwise it is narrowed to the nearest supported trigger rather than reproduced exactly.
Scaling lives on a separate google_compute_region_autoscaler that targets the MIG. Its autoscaling_policy holds the min and max replica bounds and the trigger (a CPU-utilization or load-balancing-utilization target, or a custom metric), and it resizes the MIG's target_size between the bounds. It is not a field on the fleet. Scaling lives on a separate google_compute_region_autoscaler that targets the MIG. Its autoscaling_policy holds the min and max replica bounds and the trigger (a CPU-utilization or load-balancing-utilization target, or a custom metric), and it resizes the MIG's target_size between the bounds. It is not a field on the fleet.

Scaling is a separate regional autoscaler that targets the MIG: it holds the min/max bounds and the trigger and resizes the MIG, rather than being a field on the fleet.

#### Placement, health checks, and self-healing Because the MIG is **regional**, it spreads its replicas across the region's zones on its own: the ASG's multi-subnet `vpc_zone_identifier` maps to the MIG's `distribution_policy_zones`, so replicas are distributed across the selected target zones. Self-healing comes across too. The ASG's health-check-driven replacement maps to the MIG's `auto_healing_policies`: a health check plus an `initial_delay_sec`, and a replica that fails the check is recreated from the current template version. An application health check (an ELB or target-group health check on the ASG) maps to a Compute Engine health check the MIG uses the same way: it replaces a replica that is running but failing its check, not only one that has stopped. * **Regional zonal spread:** `vpc_zone_identifier` maps to `distribution_policy_zones`; the MIG spreads replicas across the region's zones. * **Autohealing on a health check:** `auto_healing_policies` recreates a replica that fails its health check, the same intent as the ASG's health-check replacement. * **Application health survives:** an ELB/target-group health check maps to a Compute Engine health check the MIG autohealing acts on, so a running-but-unhealthy replica is replaced, and not only a stopped one. #### Updates, lifecycle hooks, and warm pools The MIG supports rolling replacement. Other Auto Scaling group features have the following limits. * **Rolling replacement holds:** a template update can use the MIG's `update_policy` for proactive rolling replacement. This provisioning behavior does not implement the AWS StartInstanceRefresh API. * **Spot is a template field, not a mix:** a replica can run as a Spot VM through the template's provisioning model, but the ASG's mixed-instances distribution (multiple instance types with an on-demand/spot allocation strategy) has no MIG analog; a MIG runs one machine type. * **Lifecycle hooks: no analog:** the ASG's launch/terminate lifecycle hooks (pause an instance for a custom action) have no MIG equivalent. * **Warm pools: no analog:** there is no pre-initialized, stopped-replica pool on the MIG. △ Auto Scaling differences * **Lifecycle hooks and warm pools are not reproduced:** these ASG orchestration mechanisms have no MIG analog; the fleet's size, autoscaling, and rolling replacement are preserved, that orchestration is not. * **Mixed-instances distribution narrows to one machine type.** Spot is set per replica, but the on-demand/spot allocation across multiple instance types has no equivalent; size the template for the machine type the fleet should run. #### Limitations The fleet shape, the template, the regional zonal spread, and health-check autohealing map directly. Two considerations frame how the running fleet behaves after cutover. △ Auto Scaling differences * **Declare scaling changes in infrastructure code:** runtime `SetDesiredCapacity` and scaling-policy mutations are outside this mapping. Unsupported infrastructure settings can be reported during the build; unsupported SDK requests return an error when called. * **Sizing decisions belong to the autoscaler.** Tensor9 provisions the MIG, its bounds, and its scaling policy at build time; when the platform actually adds or removes a replica is the region autoscaler's decision, evaluated on Google Cloud's own metrics and cadence, not something Tensor9 sizes at runtime. #### Other considerations Plan persistent storage, instance costs, and updates before deployment. * **Instances are replaced, not repaired:** health-check autohealing recreates an unhealthy VM, so any state that must survive lives on attached persistent disks or an object store, not the instance's boot disk. * **Runtime scaling is Google Cloud's decision:** the build provisions the MIG, its bounds, and its policy; the region autoscaler decides when to add or remove a replica on Google Cloud's own metrics and cadence. * **Costs depend on the running instances.** VM charges follow actual running replicas, including temporary replacement capacity, plus storage, networking and other target charges. Desired-capacity bounds are not an enforced spending cap. * **A template change uses the target update policy:** a new instance-template version can roll replacement replicas through the MIG's update policy. Batch sizes and health gates follow Google Cloud's controls, not the AWS StartInstanceRefresh API. #### Fleet lifecycle and runtime management A launch template or launch configuration describes each fleet member. The Auto Scaling group supplies desired, minimum and maximum counts; scaling and health policies determine when those members change. The fleet mapping creates the target template, group and relevant autoscaler, instead of treating the template as a standalone VM. Maximum adaptation exposes DescribeAutoScalingGroups, DescribeAutoScalingInstances and DeleteAutoScalingGroup over managed group state. Desired capacity and scaling-policy changes use the owning service's lifecycle configuration or infrastructure-as-code. When a higher-level service such as EKS owns the physical node pool, its reconciler remains responsible for that pool: the Auto Scaling adapter reads and changes the logical group without introducing a second controller for the same physical resource. The describe calls report the adapter's managed groups and members. InService and Pending describe whether a member's configuration has converged. HealthStatus is reported as Healthy; it does not report native probe results or establish application health. Check the owning service and the target's health checks before relying on a member to serve traffic. DeleteAutoScalingGroup records a deletion request. A group with nonzero desired capacity or members requires ForceDelete=true; physical teardown completes asynchronously through its owner. Rolling replacement, application health and zone distribution still depend on the selected target. ## On Azure | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------------ | ------------------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Scaling policy (target-tracking / step) | Autoscaling | Partial | Most usage | A supported metric-driven policy maps to the autoscale setting's utilization targets or threshold rules. Review translated thresholds: metric definitions, evaluation windows, cooldowns and scaling timing follow the target autoscaler. Custom or ASG-specific metrics need a supported target trigger; size bounds alone do not create a scaling policy. | | min\_size / max\_size | Autoscaling | Supported | Common | Map to the autoscale setting's bounds on desired capacity; min==max fixes the desired count. These bounds do not guarantee ready capacity or cap temporary replicas during replacement. | | SetDesiredCapacity / runtime scaling-policy mutations | Control plane | Out of scope | Full surface | SetDesiredCapacity and runtime scaling-policy changes are outside this mapping. Set size and bounds through infrastructure-as-code or the owning service's supported lifecycle configuration. The runtime group API separately supports discovery and deletion. | | Fleet provisioning (aws\_autoscaling\_group) | Fleet lifecycle | Supported | Common | your aws\_autoscaling\_group compiles to an Azure Virtual Machine Scale Set, one managed group whose instances use the same configuration | | Lifecycle hooks / instance refresh / warm pools / mixed-instances distribution | Fleet orchestration | Out of scope | Full surface | The AWS StartInstanceRefresh API, lifecycle hooks, warm pools and on-demand/Spot distribution are outside this mapping. Target-native rolling replacement during provisioning is separate and follows the target's documented update policy where supported. | | desired\_capacity | Fleet sizing | Supported | Common | Sets the desired number of replicas using the same launch configuration. Running and ready capacity converge as allocation, scheduling and health checks complete. | | Instance blueprint (launch template / launch configuration / mixed-instances policy) | Instance definition | Supported | Common | the ASG's instance definition (sourced from a launch template, a legacy launch configuration, or a mixed-instances policy) uses the same instance mapping as a standalone Azure Virtual Machine, so every replica inherits the disk, subnet, security groups, identity, and metadata of a single instance | | Instance metadata + identity on every replica | Metadata + identity | Supported | Most usage | each replica gets the same read-only instance-metadata (IMDS) API and workload identity as a standalone instance, so per-replica service discovery and credential lookups keep working | | Multi-AZ placement (vpc\_zone\_identifier) | Placement | Supported | Most usage | The ASG's subnets map to the fleet's selected target zones. Running capacity depends on allocation and health in those zones. | #### How it works On Azure, Tensor9 translates the Auto Scaling group into a **Virtual Machine Scale Set**. The scale set contains the VM configuration: the `sku` (machine size), the source image, the OS and data disks, the network interface, and the managed identity, alongside the `instances` count. `desired_capacity` becomes the scale set's desired instance count. Ready capacity follows allocation, startup and health checks. The ASG's zonal spread lands as the scale set's `zones`. Scaling is handled by a separate `azurerm_monitor_autoscale_setting` that targets the scale set; Azure keeps the sizing policy in its own resource rather than on the fleet.
On Azure the Auto Scaling group maps to a Virtual Machine Scale Set with an inline VM configuration and a desired instance count across selected zones. A declared scaling policy uses a separate autoscale setting with metric-driven rules. Running and ready instances depend on allocation and health checks. On Azure the Auto Scaling group maps to a Virtual Machine Scale Set with an inline VM configuration and a desired instance count across selected zones. A declared scaling policy uses a separate autoscale setting with metric-driven rules. Running and ready instances depend on allocation and health checks.

On Azure the blueprint is not a separate resource; it lives inline in the scale set body. Scaling is a separate autoscale setting with metric rules that targets the scale set.

#### The VM configuration The ASG's instance definition (from a launch template, a legacy launch configuration, or a mixed-instances policy) is folded directly into the scale set's own VM profile. There is no separate blueprint artifact to reference; the machine size, translated boot image, disks, subnet and security rules, and identity are fields on the scale set itself, and every instance the scale set brings up boots from that inline model through the same instance mapping as a standalone Azure VM. Each instance is a VM with its own disks, network placement, identity, and read-only metadata surface: a workload that reads its own identity or discovers its topology on startup runs unchanged on every instance. * **Desired instance count:** `desired_capacity` sets the requested count; allocation and health checks determine ready capacity. * **Zonal spread is preserved:** the ASG's subnets map to the scale set's `zones` across availability zones. △ Auto Scaling differences * **Blueprint and fleet are edited together:** because the VM model is inline, the blueprint and the scale set are one resource; a model change updates existing instances through the scale set's upgrade policy rather than versioning a standalone artifact.
The ASG's instance definition (from a launch template, a legacy launch configuration, or a mixed-instances policy) folds directly into the scale set's own VM profile. There is no separate blueprint artifact: the sku, image, disks, network interface, and identity are fields inside the scale set body, and every instance boots from that inline model. The ASG's instance definition (from a launch template, a legacy launch configuration, or a mixed-instances policy) folds directly into the scale set's own VM profile. There is no separate blueprint artifact: the sku, image, disks, network interface, and identity are fields inside the scale set body, and every instance boots from that inline model.

There is no separate template resource on Azure: the ASG's instance definition folds into the scale set's own inline VM model, and every instance boots from it.

#### The autoscale setting Scaling lives on a separate `azurerm_monitor_autoscale_setting` whose `target_resource_id` is the scale set. Its `profile` sets the capacity `minimum`/`maximum` (from `min_size`/`max_size`) and a set of metric **rules**. This is the Azure-specific shape: rather than a single target value, a CPU-utilization target-tracking policy maps to a **scale-out rule and a scale-in rule** on a metric such as Percentage CPU. The translated rules retain the utilization-metric intent; evaluation windows, cooldowns and resulting scale timing follow Azure's autoscaler. A utilization target alone does not determine both thresholds: review the scale-out threshold, scale-in threshold and cooldown for the workload. Diagram thresholds are illustrative. * **Separate autoscale setting:** capacity bounds and rules live on the monitor autoscale setting that targets the scale set. * **Target-tracking → a rule pair:** a CPU/utilization policy maps to a scale-out and a scale-in rule on the same metric. * **Custom metrics can require a different trigger:** a policy on a custom or ASG-specific metric maps to the nearest supported metric trigger where there is no exact equivalent. △ Auto Scaling differences * **Target-tracking becomes rule-based:** the autoscale setting expresses scaling as scale-out / scale-in rules on a metric rather than a single continuous target value; a CPU/utilization policy becomes an equivalent rule pair, but the rule model is Azure's, not the ASG's.
Scaling lives on a separate azurerm_monitor_autoscale_setting whose target is the scale set. Its profile sets the capacity minimum, maximum, and default, and a target-tracking policy maps to a rule pair: a scale-out rule that adds instances when a metric such as Percentage CPU rises above a threshold, and a scale-in rule that removes instances when it falls below one. Scaling lives on a separate azurerm_monitor_autoscale_setting whose target is the scale set. Its profile sets the capacity minimum, maximum, and default, and a target-tracking policy maps to a rule pair: a scale-out rule that adds instances when a metric such as Percentage CPU rises above a threshold, and a scale-in rule that removes instances when it falls below one.

Scaling is a separate autoscale setting: capacity bounds plus a rule pair (a scale-out rule and a scale-in rule on a metric) that expresses the target-tracking intent in Azure's rule model.

#### Placement, health checks, and self-healing The ASG's multi-subnet `vpc_zone_identifier` maps to the scale set's `zones`, and the scale set spreads its instances across those **availability zones** and, within a zone, across **fault domains**. The across-zone footprint the ASG was deployed for holds. Self-healing maps to the scale set's **automatic instance repair** (`automatic_instance_repair`): with an application health probe configured, an instance the probe marks unhealthy is repaired (reimaged or replaced) after a grace period, the same intent as the ASG's health-check replacement. Which instance is removed on scale-in is governed by the scale set's `scale_in` rule (`Default` / `NewestVM` / `OldestVM`), a partial analog to the ASG's termination policies. * **Zones plus fault domains:** the ASG's subnets map to the scale set's `zones`; instances spread across availability zones and fault domains. * **Automatic instance repair:** with a health probe configured, an unhealthy instance is repaired after a grace period, the ASG's health-check replacement in Azure's terms. * **Scale-in ordering only partly survives:** the scale set's `scale_in` rule chooses which instance to remove, a partial analog to the ASG's termination policies. △ Auto Scaling differences * **Autorepair needs a health probe:** automatic instance repair only replaces an unhealthy instance when an application health probe (or the health extension) is configured; without one, the scale set maintains its count but does not act on application health. #### Updates, lifecycle hooks, and warm pools The scale set supports rolling upgrades. Other Auto Scaling group features have the following limits. * **Rolling replacement holds:** a model update can use the scale set's `rolling_upgrade_policy` to replace instances in batches. This provisioning behavior does not implement the AWS StartInstanceRefresh API. * **Spot works; the mix narrows:** instances can run at Spot priority, but the ASG's mixed-instances distribution across several instance types with an allocation strategy has no direct scale-set analog. * **Lifecycle hooks: no direct analog:** the ASG's pause-and-continue launch/terminate hooks have no scale-set equivalent (scheduled-event / terminate notifications are close but not the same pause-for-a-custom-action mechanism). * **Warm pools: no analog:** there is no pre-initialized, stopped-instance pool on the scale set. △ Auto Scaling differences * **Lifecycle hooks and warm pools are not reproduced:** these ASG mechanisms have no scale-set analog; the fleet's size, autoscaling, and rolling upgrade are preserved, that orchestration is not. #### Limitations The scale set, its inline model, its zonal spread, and automatic instance repair map directly. Two considerations frame how the running fleet behaves after cutover. △ Auto Scaling differences * **Declare scaling changes in infrastructure code:** runtime `SetDesiredCapacity` and scaling-policy mutations are outside this mapping. Unsupported infrastructure settings can be reported during the build; unsupported SDK requests return an error when called. * **Sizing decisions belong to the autoscale setting.** Tensor9 provisions the scale set, its capacity bounds, and its metric rules at build time; when Azure Monitor actually adds or removes an instance is the autoscale setting's decision, evaluated on Azure's own metrics and cadence, not something Tensor9 sizes at runtime. #### Other considerations Plan persistent storage, instance costs, and updates before deployment. * **Instances are replaced, not repaired:** automatic instance repair recreates an unhealthy VM, so surviving state lives on attached managed disks or a storage account, not the instance's OS disk. * **Runtime scaling is Azure's decision:** the build provisions the scale set, its capacity bounds, and its metric rules; the Azure Monitor autoscale setting decides when to add or remove an instance on Azure's own metrics and cadence. * **Costs depend on the running instances.** VM charges follow actual running instances, including temporary replacement capacity, plus storage, networking and other target charges. Desired-capacity bounds are not an enforced spending cap. * **A model change is a rolling upgrade:** the launch template maps to the scale set's inline VM model, and a change rolls the instances, the same immutable-fleet update model the Auto Scaling group used. #### Fleet management scope The fleet runs as an Azure Virtual Machine Scale Set with the autoscale setting's sizing controls. See [fleet lifecycle and runtime management](/service-adapters/aws/compute-containers/ec2-auto-scaling#fleet-lifecycle-and-runtime-management) for logical group discovery/deletion and higher-level ownership. Desired-capacity and policy changes use infrastructure-as-code or the owning service; native readiness, placement and replacement follow this target's controls above. ## On OCI | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------------ | ------------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Scaling policy (target-tracking / step) | Autoscaling | Partial | Most usage | A supported metric-driven policy maps to the autoscaling configuration's utilization targets or threshold rules. Review translated thresholds: metric definitions, evaluation windows, cooldowns and scaling timing follow the target autoscaler. Custom or ASG-specific metrics need a supported target trigger; size bounds alone do not create a scaling policy. | | min\_size / max\_size | Autoscaling | Supported | Common | Map to the autoscaling configuration's bounds on desired capacity; min==max fixes the desired count. These bounds do not guarantee ready capacity or cap temporary replicas during replacement. | | SetDesiredCapacity / runtime scaling-policy mutations | Control plane | Out of scope | Full surface | SetDesiredCapacity and runtime scaling-policy changes are outside this mapping. Set size and bounds through infrastructure-as-code or the owning service's supported lifecycle configuration. The runtime group API separately supports discovery and deletion. | | Fleet provisioning (aws\_autoscaling\_group) | Fleet lifecycle | Supported | Common | your aws\_autoscaling\_group compiles to an OCI Instance Pool, one managed group whose instances use the same configuration | | Lifecycle hooks / instance refresh / warm pools / mixed-instances distribution | Fleet orchestration | Out of scope | Full surface | The AWS StartInstanceRefresh API, lifecycle hooks, warm pools and on-demand/Spot distribution are outside this mapping. Target-native rolling replacement during provisioning is separate and follows the target's documented update policy where supported. | | desired\_capacity | Fleet sizing | Supported | Common | Sets the desired number of replicas using the same launch configuration. Running and ready capacity converge as allocation, scheduling and health checks complete. | | Instance blueprint (launch template / launch configuration / mixed-instances policy) | Instance definition | Supported | Common | the ASG's instance definition (sourced from a launch template, a legacy launch configuration, or a mixed-instances policy) uses the same instance mapping as a standalone OCI Compute instance, so every replica inherits the disk, subnet, security groups, identity, and metadata of a single instance | | Instance metadata + identity on every replica | Metadata + identity | Supported | Most usage | each replica gets the same read-only instance-metadata (IMDS) API and workload identity as a standalone instance, so per-replica service discovery and credential lookups keep working | | Multi-AZ placement (vpc\_zone\_identifier) | Placement | Supported | Most usage | The ASG's subnets map to the fleet's selected target zones. Running capacity depends on allocation and health in those zones. | #### How it works On OCI, Tensor9 translates the Auto Scaling group into **three resources**. An `oci_core_instance_configuration` (the immutable blueprint) is referenced by an `oci_core_instance_pool` that runs the instances, and a separate `oci_autoscaling_auto_scaling_configuration` attaches to the pool to drive its size. `desired_capacity` becomes the pool's desired `size`. Ready capacity follows allocation, startup and health checks. The ASG's zonal spread lands as the pool's `placement_configurations` across the region's availability domains and fault domains . `min_size`/`max_size` become the autoscaling configuration's bounds.
On OCI the Auto Scaling group maps to an instance configuration and an instance pool with a desired count across selected availability and fault domains. A separate autoscaling configuration applies CPU or memory threshold rules. Running and ready instances depend on allocation and health checks. On OCI the Auto Scaling group maps to an instance configuration and an instance pool with a desired count across selected availability and fault domains. A separate autoscaling configuration applies CPU or memory threshold rules. Running and ready instances depend on allocation and health checks.

On OCI the ASG splits into three resources: a standalone instance configuration, the instance pool that runs the instances, and a separate autoscaling configuration attached to the pool.

#### The instance configuration Like the GCP mapping, OCI keeps the blueprint in a separate, immutable resource, but here it is an `oci_core_instance_configuration`, a saved launch specification. The ASG's instance definition (from a launch template, a legacy launch configuration, or a mixed-instances policy) compiles to that configuration, capturing the compute shape, the translated image, the VNIC and subnet, and the instance metadata. The pool references it by id. Because the instance configuration is a fixed snapshot of the launch spec, changing the blueprint means a new configuration the pool is pointed at, not an in-place edit. Every instance the pool launches boots from it through the same instance mapping as a standalone OCI Compute instance, so each is a full instance with its own block volumes, VNIC, identity, and read-only metadata surface. * **Placement across AD + FD:** the pool spreads instances across availability domains and fault domains for resilience. △ Auto Scaling differences * **Blueprint changes point at a new configuration:** an instance configuration is a fixed snapshot, so editing the blueprint provisions a new configuration and re-points the pool; existing instances are not rewritten in place.
The ASG's instance definition (from a launch template, a legacy launch configuration, or a mixed-instances policy) maps to one standalone oci_core_instance_configuration, a saved launch specification holding the compute shape, translated image, VNIC, subnet, and metadata. The instance pool references it by id and launches every instance from it; because the configuration is a fixed snapshot, changing the blueprint means a new configuration the pool is pointed at. The ASG's instance definition (from a launch template, a legacy launch configuration, or a mixed-instances policy) maps to one standalone oci_core_instance_configuration, a saved launch specification holding the compute shape, translated image, VNIC, subnet, and metadata. The instance pool references it by id and launches every instance from it; because the configuration is a fixed snapshot, changing the blueprint means a new configuration the pool is pointed at.

The ASG's instance definition maps to one standalone, immutable instance configuration (a saved launch spec) which the pool references by id and launches every instance from.

#### The autoscaling configuration Scaling lives on a separate `oci_autoscaling_auto_scaling_configuration` attached to the pool. Its policy sets the `min_size`/`max_size` bounds. The OCI-specific shape is that its scaling is **threshold-based**: a CPU- or memory-utilization target maps to threshold rules (scale out above a high threshold, scale in below a low one) with a cooldown, rather than a single continuous target-tracking value. Review both thresholds and the cooldown; one utilization target does not uniquely determine the rule pair. OCI evaluates the configured metrics using its own timing. A fixed `min_size == max_size` fixes desired capacity, not the number of ready instances. * **Separate autoscaling configuration:** bounds and rules live on the autoscaling configuration attached to the pool. * **Threshold rules + cooldown:** a CPU/memory target maps to scale-out / scale-in thresholds, not a single continuous target. * **Schedule-based scaling too:** the autoscaling configuration also supports a scheduled policy, so a time-of-day capacity plan maps directly where the ASG used scheduled actions. △ Auto Scaling differences * **Scaling is threshold-based, not continuous target-tracking:** a CPU/utilization policy becomes high/low threshold rules with a cooldown; the model is OCI's step-threshold one, so the exact response curve differs from the ASG's target-tracking.
Scaling lives on a separate oci_autoscaling_auto_scaling_configuration attached to the instance pool. It sets the min and max bounds and threshold rules: scale out when CPU or memory utilization rises above a high threshold, scale in when it falls below a low one, with a cooldown between steps. This is a step-threshold model rather than a single continuous target-tracking value. Scaling lives on a separate oci_autoscaling_auto_scaling_configuration attached to the instance pool. It sets the min and max bounds and threshold rules: scale out when CPU or memory utilization rises above a high threshold, scale in when it falls below a low one, with a cooldown between steps. This is a step-threshold model rather than a single continuous target-tracking value.

Scaling is a separate autoscaling configuration attached to the pool: min/max bounds plus high/low threshold rules and a cooldown, OCI's step model rather than continuous target-tracking.

#### Placement, health checks, and self-healing The ASG's zonal spread maps well: the pool's `placement_configurations` spread instances across the region's **availability domains** and, within each, across fault domains , and the pool can attach to a load balancer's backend set so new instances are registered as they come up. Where OCI diverges from the ASG is **health-driven replacement**. An instance pool maintains its declared size, restoring the count when an instance is terminated or stops, but it does not replace an instance that is running yet failing an application health check the way an ASG with ELB/target-group health checks does. Autoscaling here reacts to metrics, not to health. * **Placement across AD + FD.** `placement_configurations` spread instances across availability domains and fault domains. * **Load-balancer attachment:** the pool can register instances into a load balancer backend set as it scales. * **Size is maintained:** a terminated or stopped instance is replaced to hold the pool's declared size. △ Auto Scaling differences * **No application-health autohealing:** the pool restores count when an instance is terminated, but a running-but-unhealthy instance is not automatically replaced the way an ASG's ELB/target-group health check would; application-health replacement is not an instance-pool feature. #### Updates, lifecycle hooks, and warm pools Instance pools do not provide the following Auto Scaling group features. * **No managed rolling replacement:** updating the instance configuration affects instances launched afterward; there is no built-in instance-refresh that rolls the existing pool to the new configuration in batches. * **Lifecycle hooks: no analog:** the ASG's launch/terminate lifecycle hooks have no instance-pool equivalent. * **Warm pools: no analog:** there is no pre-initialized, stopped-instance pool. * **Mixed-instances distribution: no analog:** the pool launches one instance configuration; the ASG's multi-type on-demand/spot allocation strategy is not reproduced. △ Auto Scaling differences * **ASG-proprietary orchestration is not reproduced:** instance refresh, lifecycle hooks, warm pools, and the mixed-instances distribution have no instance-pool analog; the fleet's size, placement, and metric/schedule autoscaling are preserved, that orchestration is not. #### Limitations The three resources, the instance configuration, and the AD/FD placement map directly. Two considerations frame how the running fleet behaves after cutover. △ Auto Scaling differences * **Declare scaling changes in infrastructure code:** runtime `SetDesiredCapacity` and scaling-policy mutations are outside this mapping. Unsupported infrastructure settings can be reported during the build; unsupported SDK requests return an error when called. * **Sizing decisions belong to the autoscaling configuration.** Tensor9 provisions the pool, its bounds, and its threshold policy at build time; when OCI actually adds or removes an instance is the autoscaling configuration's decision, evaluated on OCI's own metrics and cooldowns, not something Tensor9 sizes at runtime. #### Other considerations Plan persistent storage, instance costs, and updates before deployment. * **Instances are replaced, not repaired:** the pool replaces a terminated or stopped instance, so surviving state lives on attached block volumes or Object Storage, not the instance's boot volume. * **Runtime scaling is OCI's decision:** the build provisions the pool, its bounds, and its threshold policy; the autoscaling configuration decides when to add or remove an instance on OCI's own metrics and cooldowns. * **Costs depend on the running instances.** Compute charges follow actual running instances, including replacement capacity, plus storage, networking and other target charges. Desired-capacity bounds are not an enforced spending cap. * **Update existing instances separately.** A new instance configuration applies to instances launched afterward. The pool has no managed rolling replacement for existing instances. #### Fleet management scope The fleet runs as an OCI Instance Pool with the autoscaling configuration's sizing controls. See [fleet lifecycle and runtime management](/service-adapters/aws/compute-containers/ec2-auto-scaling#fleet-lifecycle-and-runtime-management) for logical group discovery/deletion and higher-level ownership. Desired-capacity and policy changes use infrastructure-as-code or the owning service; native readiness, placement and replacement follow this target's controls above. ## On Private Kubernetes | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------------ | ------------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Scaling policy (target-tracking / step) | Autoscaling | Partial | Most usage | A supported metric-driven policy maps to the horizontal pod autoscaler's utilization targets or threshold rules. Review translated thresholds: metric definitions, evaluation windows, cooldowns and scaling timing follow the target autoscaler. Custom or ASG-specific metrics need a supported target trigger; size bounds alone do not create a scaling policy. | | min\_size / max\_size | Autoscaling | Supported | Common | Map to the horizontal pod autoscaler's bounds on desired capacity; min==max fixes the desired count. These bounds do not guarantee ready capacity or cap temporary replicas during replacement. | | SetDesiredCapacity / runtime scaling-policy mutations | Control plane | Out of scope | Full surface | SetDesiredCapacity and runtime scaling-policy changes are outside this mapping. Set size and bounds through infrastructure-as-code or the owning service's supported lifecycle configuration. The runtime group API separately supports discovery and deletion. | | Fleet provisioning (aws\_autoscaling\_group) | Fleet lifecycle | Supported | Common | your aws\_autoscaling\_group compiles to a KubeVirt VirtualMachinePool, one managed group whose instances use the same configuration | | Lifecycle hooks / instance refresh / warm pools / mixed-instances distribution | Fleet orchestration | Out of scope | Full surface | The AWS StartInstanceRefresh API, lifecycle hooks, warm pools and on-demand/Spot distribution are outside this mapping. Target-native rolling replacement during provisioning is separate and follows the target's documented update policy where supported. | | desired\_capacity | Fleet sizing | Supported | Common | Sets the desired number of replicas using the same launch configuration. Running and ready capacity converge as allocation, scheduling and health checks complete. | | Instance blueprint (launch template / launch configuration / mixed-instances policy) | Instance definition | Supported | Common | the ASG's instance definition (sourced from a launch template, a legacy launch configuration, or a mixed-instances policy) uses the same instance mapping as a standalone KubeVirt VirtualMachine, so every replica inherits the disk, subnet, security groups, identity, and metadata of a single instance | | Instance metadata + identity on every replica | Metadata + identity | Supported | Most usage | each replica gets the same read-only instance-metadata (IMDS) API and workload identity as a standalone instance, so per-replica service discovery and credential lookups keep working | | Multi-AZ placement (vpc\_zone\_identifier) | Placement | Supported | Most usage | Zone spread requires nodes with zone topology labels, scheduling rules that use them, and schedulable capacity in each selected zone. | #### How it works On KubeVirt the ASG compiles to a **VirtualMachinePool** that runs on the Kubernetes cluster you operate. Unlike the cloud targets, it is self-managed: you run the cluster and its nodes yourself. There is no cloud fleet control plane; the pool reconciles its replicas as full virtual machines scheduled onto your nodes, and live-migration and rescheduling on node drain are the target cluster's, not a cloud SLA. `desired_capacity` becomes the pool's desired `replicas` count. Scheduling, node capacity and guest health determine how many replicas are ready. Each replica is a full KubeVirt `VirtualMachine` (a VMI backed by KVM on a node). Autoscaling is **optional**: the pool exposes a standard `scale` subresource, so a Kubernetes `HorizontalPodAutoscaler` can target it to adjust the desired replica count between bounds. Without an HPA the desired count stays fixed.
On Kubernetes the Auto Scaling group maps to a KubeVirt VirtualMachinePool with an embedded VM template and a desired replica count. Scheduling and health determine ready capacity on your nodes. An optional HorizontalPodAutoscaler adjusts the desired count through the pool's scale subresource. On Kubernetes the Auto Scaling group maps to a KubeVirt VirtualMachinePool with an embedded VM template and a desired replica count. Scheduling and health determine ready capacity on your nodes. An optional HorizontalPodAutoscaler adjusts the desired count through the pool's scale subresource.

On KubeVirt the pool runs on the target cluster: the blueprint is embedded in the pool spec, and autoscaling is an optional standard HorizontalPodAutoscaler on the pool's scale subresource.

#### The virtualMachineTemplate The blueprint is **embedded** in the pool spec as its `virtualMachineTemplate`: a full VirtualMachine spec nested inside the pool, not a separate resource and not a cloud template artifact. The ASG's instance definition (from a launch template, a legacy launch configuration, or a mixed-instances policy) compiles into that template: the domain (CPU and memory), disks and volumes, networks, and the boot image. Every replica the pool reconciles is stamped from it. Each replica is a VM: it gets its disks, its cluster networking, and the read-only metadata surface a workload expects, running under KVM on one of your nodes. A workload that reads its own identity or discovers its topology on startup runs unchanged on every replica. * **Desired replicas:** `desired_capacity` becomes the pool's requested `replicas` count; readiness follows scheduling and health checks. * **Self-managed availability:** live-migration and rescheduling on node drain are the target cluster's, not a cloud single-instance SLA.
The ASG's instance definition (from a launch template, a legacy launch configuration, or a mixed-instances policy) compiles into the pool's embedded virtualMachineTemplate, a full VirtualMachine spec nested inside the pool spec holding the domain (CPU and memory), disks and volumes, networks, and boot image. Every replica the pool reconciles is stamped from that embedded template as a full VM. The ASG's instance definition (from a launch template, a legacy launch configuration, or a mixed-instances policy) compiles into the pool's embedded virtualMachineTemplate, a full VirtualMachine spec nested inside the pool spec holding the domain (CPU and memory), disks and volumes, networks, and boot image. Every replica the pool reconciles is stamped from that embedded template as a full VM.

The blueprint is embedded in the pool spec as its virtualMachineTemplate, a full VirtualMachine spec nested inside the pool, and every replica is stamped from it as a full VM.

#### Autoscaling with an HPA Unlike the cloud targets, there is **no dedicated autoscaler resource**. The pool exposes a `scale` subresource, and scaling is done (if you want it) with a standard Kubernetes `HorizontalPodAutoscaler` whose `scaleTargetRef` points at the pool. `min_size`/`max_size` become the HPA's `minReplicas`/`maxReplicas`, and a CPU/utilization target maps to the HPA's CPU or memory metric (served by metrics-server on your cluster). Metric definitions and evaluation timing follow Kubernetes. If no HPA is declared, the desired `replicas` count stays fixed; it does not guarantee that every replica is ready. * **Optional HPA on the scale subresource:** a standard HorizontalPodAutoscaler targets the pool; without one the desired replica count stays fixed. * **Bounds → HPA min/max.** `min_size`/`max_size` become the HPA's `minReplicas`/`maxReplicas`. * **Pod-level metrics.** CPU/memory target-tracking is served by metrics-server on your self-managed cluster, not by a cloud autoscaler. △ Auto Scaling differences * **Autoscaling is opt-in and self-operated:** scaling requires a declared HorizontalPodAutoscaler and pod-level metrics on the target cluster. Without an HPA the desired count stays fixed. Running replicas still depend on available capacity and health.
On KubeVirt an optional HorizontalPodAutoscaler adjusts the VirtualMachinePool's desired replica count through its scale subresource. The HPA uses configured bounds and pod metrics from the target cluster. Without an HPA the desired count stays fixed; scheduling, capacity and health determine ready replicas. On KubeVirt an optional HorizontalPodAutoscaler adjusts the VirtualMachinePool's desired replica count through its scale subresource. The HPA uses configured bounds and pod metrics from the target cluster. Without an HPA the desired count stays fixed; scheduling, capacity and health determine ready replicas.

An optional HorizontalPodAutoscaler adjusts the pool's desired count through its scale subresource. Without one, that desired count stays fixed; ready capacity still depends on scheduling and health.

#### Placement, health, and self-healing Placement is the Kubernetes scheduler's job, not a cloud fleet's. The pool's VMs are scheduled onto your nodes, and across-zone or across-node spread comes from the scheduling rules you set: pod anti-affinity or topology-spread constraints against your nodes' zone or hostname labels. There is no cloud availability zone unless your nodes span zones and are labeled for it. Each selected zone also needs enough schedulable capacity for the requested VMs. The pool **reconciles toward its desired replica count**: a VM that is deleted or whose node is lost needs replacement capacity. The number of running or ready VMs can lag behind `replicas`. Liveness checks identify failed guests for recovery; readiness checks remove unready guests from service. All of this runs on the cluster you operate, so the resilience is the target cluster's, not a cloud SLA. * **Scheduler-driven spread:** across-zone/node spread comes from pod anti-affinity or topology-spread constraints against your nodes' labels, not from a cloud fleet's zonal distribution. * **Desired capacity is reconciled:** replacing a deleted or node-lost VM requires schedulable capacity; check running and ready counts separately. * **Health via probes:** liveness checks identify failed guests for recovery; readiness checks remove unready guests from service. △ Auto Scaling differences * **Spread and availability are the target cluster's:** multi-zone resilience exists only if your nodes span zones and your scheduling rules place replicas across them; there is no cloud AZ guarantee, and node-drain rescheduling is the cluster's behavior, not a cloud SLA. #### Updates, lifecycle hooks, and warm pools VirtualMachinePool does not provide the following Auto Scaling group features. * **No managed rolling replacement:** the pool is not a Deployment; a template change applies to reconciled replicas but there is no built-in batched rolling-upgrade policy like the ASG's instance refresh. * **Lifecycle hooks: no analog:** the ASG's launch/terminate lifecycle hooks have no VirtualMachinePool equivalent. * **Warm pools: no analog:** there is no pre-initialized, stopped-VM pool. * **Mixed-instances / Spot: not applicable:** there is no cloud instance market; a replica's size is the template's, on your own nodes. △ Auto Scaling differences * **ASG-proprietary orchestration is not reproduced:** instance refresh, lifecycle hooks, warm pools, and mixed-instances/Spot assume a cloud fleet control plane the pool does not have; the fleet's size and optional HPA scaling are preserved, that orchestration is not. #### Limitations The pool, its embedded template, and optional HPA scaling map directly on the cluster you operate. Two considerations frame how the running fleet behaves after cutover. △ Auto Scaling differences * **Declare scaling changes in infrastructure code:** runtime `SetDesiredCapacity` and scaling-policy mutations are outside this mapping. Unsupported infrastructure settings can be reported during the build; unsupported SDK requests return an error when called. * **Sizing and operation are yours.** Tensor9 provisions the pool and, if declared, the HPA at build time; the actual scaling decisions run against metrics-server on the cluster you operate, so both the sizing cadence and the cluster's health are your responsibility rather than a cloud platform's. #### Other considerations Plan persistent storage, instance costs, and updates before deployment. * **Instances are replaced, not repaired:** the pool recreates an unhealthy VM, so surviving state lives on PersistentVolumes or external storage, not the VM's ephemeral disk. * **Scaling and operation run on the cluster you own:** the build provisions the pool and, if declared, an HPA; scaling decisions run against metrics-server on your cluster, so both the cadence and the cluster's health are yours to operate. * **The pool consumes cluster capacity:** the VM pool consumes your cluster's own compute, so the fleet's size is bounded by the capacity you run rather than a cloud autoscaling budget. * **Plan replacement of existing VMs.** The pool applies virtualMachineTemplate changes when it creates replicas; it has no built-in batched rolling-upgrade policy. #### Fleet management scope The fleet runs as a KubeVirt VirtualMachinePool with the horizontal pod autoscaler's sizing controls. See [fleet lifecycle and runtime management](/service-adapters/aws/compute-containers/ec2-auto-scaling#fleet-lifecycle-and-runtime-management) for logical group discovery/deletion and higher-level ownership. Desired-capacity and policy changes use infrastructure-as-code or the owning service; native readiness, placement and replacement follow this target's controls above. [Service Catalog](/service-adapters/catalog). # ECR Source: https://docs.tensor9.com/service-adapters/aws/compute-containers/ecr AWS ECR. A private registry for container images, with per repository IAM permissions, image scanning and lifecycle rules that expire old tags. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) * [Via Zot Registry](#via-zot-registry) * [Via Distribution Registry](#via-distribution-registry) * [Via Harbor Registry](#via-harbor-registry) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of ECR with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | ECR | Google Cloud | Azure | OCI | Private Kubernetes · Zot Registry | Private Kubernetes · Distribution Registry | Private Kubernetes · Harbor Registry | | --------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | Repository topology · repository organization | ECR: repositories within an account-and-region registry | Artifact Registry: one Docker-format repository per ECR repository (N to N), topology preserved | ACR: one provisioned registry contains distinct repository paths; image references change | OCIR: one repository per ECR repository (N to N), repository organization preserved | in-cluster: the ECR repository set becomes the registry's repositories/namespaces (N to N) | in-cluster: the ECR repository set becomes the registry's repositories/namespaces (N to N) | in-cluster: the ECR repository set becomes the registry's repositories/namespaces (N to N) | | Tag immutability | Yes - image\_tag\_mutability | Yes - IMMUTABLE\_TAGS setting | - | Yes - is\_immutable per-repo flag | Partial - Zot has no per-repo immutable-tag lock | No - Distribution has no immutable-tag control | Yes - Harbor immutable-tag rules (per-repo) | | Lifecycle / retention · scope | ECR: per-repository expiry rules | per-repository keep/delete rules; the bounded single-image-package count/age mapping does not preserve the full ECR policy | ACR: registry-wide retention (Premium tier), a scope change that is surfaced | OCIR: image retention policies, region-wide by default and overridable per repository, surfaced at the build | Zot retention policies (registry-level) | Distribution offers blob garbage-collection only, not tag/age retention | Harbor tag-retention is per-repository, the closest in-cluster analog to ECR lifecycle rules | | Vulnerability scanning | Yes - ECR enhanced (Inspector) | Partial - Container Analysis: on-push + continuous | Partial - Microsoft Defender for Containers: native scanner, different coverage | Partial - OCI Vulnerability Scanning: native scanner, different coverage | Partial - Zot exposes CVE search via an optional Trivy-DB extension (not built-in scan-on-push) | No - CNCF Distribution has no built-in vulnerability scanner | Partial - Harbor ships a built-in Trivy integration (scan-on-push) | | API coverage | full | high | high | high | high | high | high | | Tag immutability | Yes - per-repo image\_tag\_mutability | - | Partial - image and repository write locks; different from ECR tag immutability | - | - | - | - | | Data path · managed vs in-cluster | ECR: AWS-managed registry | - | - | - | in-cluster registry starts empty: deploy + load (via the appliance) + rewrite refs; no pull-through to AWS | in-cluster registry starts empty: deploy + load (via the appliance) + rewrite refs; no pull-through to AWS | in-cluster registry starts empty: deploy + load (via the appliance) + rewrite refs; no pull-through to AWS | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | ------------------------ | ------------ | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Registry authentication | Auth | Supported | Common | Native registry credentials: the GKE node service account needs Artifact Registry Reader; application workload federation is separate | | docker push / pull | Image path | Supported | Common | byte-identical OCI Distribution v2; the host changes to \-docker.pkg.dev | | Image tag policy mapping | Immutability | Supported | Most usage | IMMUTABLE\_TAGS repository setting | | Lifecycle policy mapping | Lifecycle | Partial | Most usage | Per-repository cleanup policies provide count or age retention for one image package. Keeping ten versions combines delete and keep rules. Tag selectors, ECR rule priority, age clocks and immutable tags require separate policy decisions; the full ECR rule language is not preserved. | | Replication config | Replication | Out of scope | Full surface | ECR destination push replication is not mapped. Multi-region storage, pull-through caches and virtual repositories do not create the specified regional or account replicas; deliver required image copies through your pipeline. | | Repository provisioning | Repositories | Supported | Common | one Docker-format repository (format = DOCKER) per ECR repository (N to N) | | Scan-on-push findings | Scanning | Partial | Most usage | Container Analysis / Artifact Analysis (on-push + continuous) | #### How it works On AWS your build pushes container images to **Amazon ECR** and your workloads pull from it. **Google Artifact Registry** is Google Cloud's unified artifact service: one system that hosts many package formats, governs access with per-repository IAM, and integrates with VPC Service Controls and customer-managed encryption. At the build, Tensor9 reads your ECR repositories and provisions a Docker-format Artifact Registry repository for each one; afterward your own `docker push` and `docker pull` reach it directly, without routing image traffic through Tensor9. The image itself never changes. ECR and Artifact Registry both speak the **OCI Distribution v2** protocol, so your image manifest, its layer blobs, and their content digests are byte-identical on both sides; docker and containerd are untouched. The target reference includes the `-docker.pkg.dev` host, project, repository and image path. This mapping provisions a target registry and updates image references. Docker and containerd use the target's registry protocol and credentials. It does not provide the AWS ECR management or authorization-token API. Applications that call ECR directly, including `GetAuthorizationToken`, need a separate integration.
Before: on AWS your build pushes images to Amazon ECR and your workloads pull from it. After: on the target cloud the same docker push and pull reach Google Artifact Registry, provisioned at the build. Only the registry address is rewritten; the image is unchanged and Tensor9 is not in the pull path. Before: on AWS your build pushes images to Amazon ECR and your workloads pull from it. After: on the target cloud the same docker push and pull reach Google Artifact Registry, provisioned at the build. Only the registry address is rewritten; the image is unchanged and Tensor9 is not in the pull path.

Your ECR repositories are provisioned as Docker-format Artifact Registry repositories at the build; afterward your push and pull reach them directly and the image is unchanged.

#### Artifact Registry features Artifact Registry hosts Docker/OCI images alongside Maven, npm, Python, Apt, Yum, and Go artifacts, so a container registry and a language-package registry are one system. Your ECR repositories map to **Docker-format** repositories, one per ECR repository, so the per-repository topology is preserved. Access is **per-repository IAM**, with read and write roles bound at the repository level rather than a single registry-wide policy. GKE pulls images using the [node pool's IAM service account](https://docs.cloud.google.com/kubernetes-engine/security/configure-node-service-accounts), including when Workload Identity Federation is enabled for applications. Grant that node identity **Artifact Registry Reader** on the target repository. **Remote repositories** act as a pull-through cache of an upstream, and **virtual repositories** aggregate several repositories behind one endpoint. VPC Service Controls place the registry inside a service perimeter, and **customer-managed encryption keys (CMEK)** encrypt contents with your own Cloud KMS keys. **Cleanup policies** apply keep/delete rules per repository. For a repository containing one image package, an unfiltered count policy keeps the ten newest versions by combining a delete rule for all versions with a keep rule for the ten most recent. Keep rules take precedence. This count mapping requires mutable tags, one count rule, and a version population whose target creation order matches its source push order. A single unfiltered age rule maps to a delete rule only when source and target age clocks agree. Additional image packages, tag selectors and overlapping ECR rules need a separate policy review; the target cleanup rules do not encode every ECR rule.
Google Artifact Registry control plane Google Artifact Registry control plane

Artifact Registry package formats, authentication, and retention.

#### Limitations △ Where ECR and Artifact Registry stay different * **Access control is Google IAM.** Image pulls use the node identity or separately configured registry credentials; the permission model is per-repository IAM rather than ECR resource policies. * **Scanning moves to Artifact Analysis.** On-push and continuous analysis replace ECR's enhanced scanning, with its own coverage and cadence. * **Lifecycle rule languages differ.** The count/age mapping covers one image package with no tag filter or overlapping priorities. ECR requires all listed tag selectors to match; Google prefix lists match any prefix. Google recent-count rules cannot also select tagged or untagged versions. Keep wins over delete, and immutable tagged versions cannot be deleted. Target age starts at creation in Artifact Registry; source image ages and archive transitions are not preserved. * **ECR destination replication is not mapped.** Multi-region storage does not select ECR destination regions or accounts. Remote repositories cache on the first pull and need their upstream for uncached content. Virtual repositories aggregate upstreams in the same region or multi-region; neither mechanism supplies destination push replication. Deliver required regional copies through your pipeline. #### Other considerations Plan image delivery, credentials, and storage costs for the target registry. * **Push images from your build pipeline:** the build creates one Docker-format Artifact Registry repository per ECR repository; your pipeline repoints to `-docker.pkg.dev` and pushes, since the image data path stays yours. * **Authorize the image-pulling identity.** Grant the GKE node service account read access to the target repository, including for cross-project pulls. Application workload federation does not grant this node permission. * **Review retention before deletion.** Use Artifact Registry's cleanup dry run to inspect the versions selected by the keep/delete policy. Preserve deployment and rollback images, and review policies outside the single-image-package count/age mapping separately. * **Deliver regional image copies explicitly.** Push the required digests to each selected target repository. A multi-region location or an uncached remote repository does not replace the ECR destination rule set. * **Storage and network egress are billed.** Artifact Registry bills for stored bytes and network egress rather than an ECR-style model, and CMEK ties contents to your Cloud KMS keys if you enable it. ## On Azure | Operation | Area | Support | Depth | Notes | | ------------------------ | ------------ | --------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | Registry authentication | Auth | Supported | Common | the AcrPull managed identity takes over; the registry admin user is disabled | | docker push / pull | Image path | Supported | Common | byte-identical OCI Distribution v2; the host changes to \.azurecr.io | | Image tag policy mapping | Immutability | Partial | Most usage | ACR image and repository write locks differ from ECR tag immutability; configure the required locks explicitly | | Lifecycle policy mapping | Lifecycle | Partial | Most usage | registry-wide retention (Premium); a scope change from per-repo, surfaced at the build | | Replication config | Replication | Partial | Full surface | Azure geo-replication (Premium-only); not emitted on the Standard default | | Repository provisioning | Repositories | Supported | Common | Provisions one ACR registry with a distinct path for each ECR repository. The registry name must be globally unique, alphanumeric, and 5-50 characters. | | Scan-on-push findings | Scanning | Partial | Most usage | scanning moves to Microsoft Defender for Containers | #### How it works On AWS your build pushes container images to **Amazon ECR** and your workloads pull from it. **Azure Container Registry** is Microsoft's managed OCI registry, with a tiered service model (Basic, Standard, Premium), Microsoft Entra identity, and, on Premium, geo-replication, content trust, and private networking. At the build, Tensor9 reads your ECR repositories and provisions an Azure Container Registry to match; afterward your own `docker push` and `docker pull` reach it directly, without routing image traffic through Tensor9. The image itself never changes. ECR and Azure Container Registry both speak the **OCI Distribution v2** protocol, so your image manifest, its layer blobs, and their content digests are byte-identical on both sides; docker and containerd are untouched. The target reference uses `.azurecr.io` and the mapped repository path. Registry administration and policy differences are described below. This mapping provisions a target registry and updates image references. Docker and containerd use the target's registry protocol and credentials. It does not provide the AWS ECR management or authorization-token API. Applications that call ECR directly, including `GetAuthorizationToken`, need a separate integration.
Before: on AWS your build pushes images to Amazon ECR and your workloads pull from it. After: on the target cloud the same docker push and pull reach Azure Container Registry, provisioned at the build. Only the registry address is rewritten; the image is unchanged and Tensor9 is not in the pull path. Before: on AWS your build pushes images to Amazon ECR and your workloads pull from it. After: on the target cloud the same docker push and pull reach Azure Container Registry, provisioned at the build. Only the registry address is rewritten; the image is unchanged and Tensor9 is not in the pull path.

Your ECR repositories are provisioned as an Azure Container Registry at the build; afterward your push and pull reach it directly and the image is unchanged.

#### Azure Container Registry features Azure Container Registry offers **Basic, Standard, and Premium tiers**. The Basic, Standard, and Premium SKUs scale storage, throughput, and features, with Premium providing additional features: **geo-replication** turns one registry into a set of regional replicas behind a single login server, so each region pulls locally; **content trust** lets you push and enforce signed images; and **ACR Tasks** build and patch images inside the registry, including automatic rebuilds when a base image changes. For AKS image pulls, grant the [kubelet managed identity](https://learn.microsoft.com/en-us/azure/container-registry/authenticate-kubernetes-options) access to the registry. Use **AcrPull** for a registry using the conventional RBAC model, or the repository-reader role required by an ABAC-enabled registry. Other clusters can use an `imagePullSecret`. The application's workload identity is separate from the identity that pulls its image. **Private Link** keeps the registry on a private endpoint. A single Azure Container Registry holds many repositories as paths beneath one host. ECR also groups repositories within an account-and-region registry. Tensor9 provisions one ACR registry for the repository set and rewrites each image reference to its target repository path.
Azure Container Registry control plane Azure Container Registry control plane

Azure Container Registry tiers, authentication, and optional features.

#### Limitations △ Where ECR and Azure Container Registry stay different * **Repositories share one registry.** Tensor9 provisions one ACR registry for the ECR repository set. Repository paths remain distinct; review the rewritten image references and shared registry settings. * **Tag locks and retention use different controls.** ACR can lock individual images or entire repositories against writes. These locks differ from ECR's immutable-tag setting: locking a repository also blocks new pushes. Configure the needed locks separately. Registry-wide retention requires Premium. * **Scanning moves to Microsoft Defender for Containers.** It has its own coverage and cadence, different from ECR's enhanced scanning. * **Geo-replication and private networking need Premium.** On the Basic and Standard tiers those capabilities are not available. #### Other considerations Plan image delivery, credentials, and storage costs for the target registry. * **Push images from your build pipeline:** the build stands up the Azure Container Registry to match your ECR repositories; your own pipeline repoints to `.azurecr.io` and pushes images, since the image data path is yours, not the appliance's. * **Configure Microsoft Entra pull credentials:** a workload that pulled under ECR's caller IAM now pulls with an AcrPull managed identity (or an imagePullSecret), so the pull credential is established on Azure rather than inherited from AWS. * **Microsoft operates the registry; you choose the tier:** availability and the control plane are Azure's, and the SKU you pick (Basic, Standard, Premium) sets storage, throughput, and which features are available. * **Cost depends on the selected tier.** Azure Container Registry bills per tier plus storage, and geo-replication and Private Link require Premium, so the feature set you rely on drives the tier and its cost. ## On OCI | Operation | Area | Support | Depth | Notes | | ------------------------ | ------------ | ------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Registry authentication | Auth | Supported | Common | handled by the OCI instance-principal credential helper, in-tenancy; no static secret in the pull path | | docker push / pull | Image path | Supported | Common | byte-identical OCI Distribution v2; manifest, layers and SHA-256 digest unchanged; host and repository references are rewritten | | Image tag policy mapping | Immutability | Supported | Most usage | per-repository immutability, set from your ECR tag-mutability setting | | Lifecycle policy mapping | Lifecycle | Partial | Most usage | ECR per-repo lifecycle rules are surfaced at the build; OCIR provides its own image retention policies (region-wide by default, overridable for specific repositories) that you configure separately | | Replication config | Replication | Out of scope | Full surface | OCIR replication is configured separately and is not translated by default | | Repository provisioning | Repositories | Supported | Common | one OCI repository per ECR repository (N to N); the repository name is normalized to lowercase with slash-nesting and no underscore | | Scan-on-push findings | Scanning | Partial | Most usage | served by OCI Vulnerability Scanning, a native scanner with different coverage than ECR enhanced | #### How it works On AWS your build pushes container images to **Amazon ECR** and your workloads pull from it. **OCI Container Registry** is Oracle Cloud's managed registry, organized around your tenancy: repositories live under your tenancy's namespace, are scoped to compartments, and authenticate through OCI IAM. At the build, Tensor9 reads your ECR repositories and provisions one OCI repository for each; afterward your own `docker push` and `docker pull` reach it directly, without routing image traffic through Tensor9. The image itself never changes. ECR and OCI Container Registry both speak the **OCI Distribution v2** protocol, so your image manifest, its layer blobs, and their content digests are byte-identical on both sides; docker and containerd are untouched. The target image reference uses `.ocir.io//`, with the mapped repository path. Registry administration and policy differences are described below. This mapping provisions a target registry and updates image references. Docker and containerd use the target's registry protocol and credentials. It does not provide the AWS ECR management or authorization-token API. Applications that call ECR directly, including `GetAuthorizationToken`, need a separate integration.
Before: on AWS your build pushes images to Amazon ECR and your workloads pull from it. After: on the target cloud the same docker push and pull reach OCI Container Registry, provisioned at the build. Only the registry address is rewritten; the image is unchanged and Tensor9 is not in the pull path. Before: on AWS your build pushes images to Amazon ECR and your workloads pull from it. After: on the target cloud the same docker push and pull reach OCI Container Registry, provisioned at the build. Only the registry address is rewritten; the image is unchanged and Tensor9 is not in the pull path.

Your ECR repositories are provisioned as tenancy-namespaced OCI repositories at the build; afterward your push and pull reach them directly and the image is unchanged.

#### OCI Container Registry features OCI Container Registry is organized around your **tenancy**. Every repository name is prefixed by your tenancy's object-storage namespace (`/`, with slash-nested paths), and repositories are scoped to **compartments** for access control and billing. Your ECR repositories map **one-to-one** to OCI repositories, so the per-repository topology is preserved, and each keeps an **immutability** flag matching your ECR tag-mutability setting. Authentication is **OCI IAM**: a workload pulls with an auth token, or with an instance or workload principal through the configured credential helper. An auth token is a credential that you must store and rotate; the principal-based helper avoids a static registry token. The registry is **regional**, with cross-region image copy for multi-region estates, and **OCI Vulnerability Scanning** provides native image scanning. Image retention policies apply region-wide by default and can be overridden with custom policies for specific repositories, deleting images that have not been pulled or versioned within a set number of days.
OCI Container Registry control plane OCI Container Registry control plane

OCI Container Registry's control plane, organized around your tenancy and compartments.

#### Limitations △ Where ECR and OCI Container Registry stay different * **Image retention is configured separately.** ECR's per-repository lifecycle rule has no direct analog; OCIR applies a region-wide retention policy by default that you can override for specific repositories. * **Scanning moves to OCI Vulnerability Scanning.** A native scanner with its own coverage, different from ECR's enhanced scanning. * **Cross-region replication is a separate feature.** OCIR replication is configured on its own and is not translated on the default configuration. #### Other considerations Plan image delivery, credentials, and storage costs for the target registry. * **Push images from your build pipeline:** the build creates one tenancy-namespaced OCI repository per ECR repository; your pipeline repoints to `.ocir.io` and pushes, since the image data path stays yours. * **Pull identity moves to OCI IAM:** a workload pulls with an auth token or an in-tenancy instance or workload principal, replacing the ECR caller-IAM pull; the pull credential is established on OCI. * **Repositories live under your tenancy namespace and compartments.** Oracle operates the registry, and the compartment placement that scopes access and billing is set on OCI rather than inherited from AWS. * **Image storage is billed to the tenancy.** OCIR bills stored image data under your tenancy, retention applies region-wide by default, and cross-region copy is configured separately when a multi-region estate needs it. ## On Private Kubernetes ### Via Zot Registry | Operation | Area | Support | Depth | Notes | | ------------------------ | ------------ | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | Registry authentication | Auth | Supported | Common | in-cluster imagePullSecret; Harbor also supports robot accounts | | Reference rewrite | Image path | Supported | Common | every consumer's ECR image ref is rewritten to the in-cluster host; the imagePullSecret is injected and the ECR auth helper dropped | | docker push / pull | Image path | Supported | Common | byte-identical OCI Distribution v2; the host changes to registry.\.svc | | Lifecycle policy mapping | Lifecycle | Partial | Most usage | Zot retention policies (registry-level) | | Image load (bootstrap) | Migration | Supported | Common | The appliance loads images before workloads pull them. Images are identified by content, so repeating the load does not duplicate them. | | Cross-region replication | Replication | Out of scope | Full surface | an in-cluster registry has no cross-region replica; the pull-through cache to ECR is deliberately not used, so there is no outbound AWS dependency | | Repository provisioning | Repositories | Supported | Common | the registry is deployed in-cluster and the ECR repository set becomes its repositories/namespaces (N to N) | | Scan-on-push findings | Scanning | Partial | Most usage | Zot exposes CVE search via an optional Trivy-DB extension (not built-in scan-on-push) | #### How it works On AWS your build pushes container images to **Amazon ECR** and your workloads pull from it. When the target is your own **Kubernetes cluster**, the registry runs inside the cluster: an open-source registry you operate, with no cloud registry service behind it. Harbor is the recommended registry; Zot is a smaller alternative; Distribution provides basic image storage and delivery. At the build, Tensor9 provisions the registry, loads your images into it through the appliance, and rewrites every image reference to the in-cluster host; afterward your workloads pull directly from it. The image itself never changes. ECR and the in-cluster registry both speak the **OCI Distribution v2** protocol, so your image manifest, its layer blobs, and their content digests are byte-identical; docker and containerd are untouched. The one thing rewritten at the build is the registry address: your ECR host becomes `registry..svc`. You operate the registry and configure its policies. This mapping provisions a target registry and updates image references. Docker and containerd use the target's registry protocol and credentials. It does not provide the AWS ECR management or authorization-token API. Applications that call ECR directly, including `GetAuthorizationToken`, need a separate integration.
Before: on AWS your build pushes images to Amazon ECR and your workloads pull from it. After: on the target cloud the same docker push and pull reach in-cluster registry, provisioned at the build. Only the registry address is rewritten; the image is unchanged and Tensor9 is not in the pull path. Before: on AWS your build pushes images to Amazon ECR and your workloads pull from it. After: on the target cloud the same docker push and pull reach in-cluster registry, provisioned at the build. Only the registry address is rewritten; the image is unchanged and Tensor9 is not in the pull path.

Your images are loaded into an in-cluster registry through the appliance at the build; afterward your workloads pull from it directly and the image is unchanged.

#### Registry features and operation The customer operates the registry and configures the features provided by the selected software. The registry is deployed empty : your images are loaded into it through the appliance's image delivery service before any workload pulls, and there is deliberately **no pull-through cache** back to ECR, which removes the outbound AWS dependency entirely. **Harbor**, the recommended registry, ships built-in Trivy scan-on-push, robot accounts, per-repository retention, and immutable-tag rules, the closest match to ECR's registry-management features. **Zot** is lightweight and OCI-native, with vulnerability (CVE) search available through an optional extension. **Distribution** is the bare reference registry: it stores and serves images and offers blob garbage-collection, but no scanning or tag controls. Whichever you run, your ECR repositories map one-to-one to the registry's repositories, and a workload pulls with an injected `imagePullSecret`; Harbor's robot accounts provide the richer, scoped path.
In-cluster registry model In-cluster registry model

Choose the registry features your application needs: Harbor, Zot, or Distribution.

#### Limitations △ Where ECR and an in-cluster registry stay different * **The registry starts empty.** Your images are loaded through the appliance before any workload pulls; there is deliberately no pull-through cache back to ECR. * **Scanning, immutability, and retention depend on which registry you run.** Harbor supports most ECR management features; Zot covers it partially; Distribution is a bare registry with none of it built in. * **There is no managed cross-region replica.** An in-cluster registry serves the cluster it runs in; images are seeded through the appliance rather than replicated across regions. #### Other considerations Plan image delivery, credentials, and storage costs for the target registry. * **Images are seeded through the appliance, then served locally:** the registry is deployed empty and your images are loaded through the appliance before any workload pulls, with no pull-through cache back to ECR, so the running cluster has no outbound AWS dependency. * **You operate the registry:** there is no cloud control plane; Harbor, Zot, or Distribution is yours to run, and its availability, upgrades, and access model are your responsibility. * **Back up the registry's persistent volume:** the registry's blobs live on a PersistentVolume in the cluster, so backup and retention of the image store are yours, not a managed registry's. * **Budget for cluster compute and storage:** the registry consumes the cluster's own compute and storage rather than a managed per-tier or per-byte charge, and the feature depth (scanning, immutability, retention) follows which registry you chose. ### Via Distribution Registry | Operation | Area | Support | Depth | Notes | | ------------------------ | ------------ | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | Registry authentication | Auth | Supported | Common | in-cluster imagePullSecret; Harbor also supports robot accounts | | Reference rewrite | Image path | Supported | Common | every consumer's ECR image ref is rewritten to the in-cluster host; the imagePullSecret is injected and the ECR auth helper dropped | | docker push / pull | Image path | Supported | Common | byte-identical OCI Distribution v2; the host changes to registry.\.svc | | Lifecycle policy mapping | Lifecycle | Partial | Most usage | Distribution offers blob garbage-collection only, not tag/age retention | | Image load (bootstrap) | Migration | Supported | Common | The appliance loads images before workloads pull them. Images are identified by content, so repeating the load does not duplicate them. | | Cross-region replication | Replication | Out of scope | Full surface | an in-cluster registry has no cross-region replica; the pull-through cache to ECR is deliberately not used, so there is no outbound AWS dependency | | Repository provisioning | Repositories | Supported | Common | the registry is deployed in-cluster and the ECR repository set becomes its repositories/namespaces (N to N) | | Scan-on-push findings | Scanning | Out of scope | Most usage | CNCF Distribution has no built-in vulnerability scanner | #### How it works On AWS your build pushes container images to **Amazon ECR** and your workloads pull from it. When the target is your own **Kubernetes cluster**, the registry runs inside the cluster: an open-source registry you operate, with no cloud registry service behind it. Harbor is the recommended registry; Zot is a smaller alternative; Distribution provides basic image storage and delivery. At the build, Tensor9 provisions the registry, loads your images into it through the appliance, and rewrites every image reference to the in-cluster host; afterward your workloads pull directly from it. The image itself never changes. ECR and the in-cluster registry both speak the **OCI Distribution v2** protocol, so your image manifest, its layer blobs, and their content digests are byte-identical; docker and containerd are untouched. The one thing rewritten at the build is the registry address: your ECR host becomes `registry..svc`. You operate the registry and configure its policies. This mapping provisions a target registry and updates image references. Docker and containerd use the target's registry protocol and credentials. It does not provide the AWS ECR management or authorization-token API. Applications that call ECR directly, including `GetAuthorizationToken`, need a separate integration.
Before: on AWS your build pushes images to Amazon ECR and your workloads pull from it. After: on the target cloud the same docker push and pull reach in-cluster registry, provisioned at the build. Only the registry address is rewritten; the image is unchanged and Tensor9 is not in the pull path. Before: on AWS your build pushes images to Amazon ECR and your workloads pull from it. After: on the target cloud the same docker push and pull reach in-cluster registry, provisioned at the build. Only the registry address is rewritten; the image is unchanged and Tensor9 is not in the pull path.

Your images are loaded into an in-cluster registry through the appliance at the build; afterward your workloads pull from it directly and the image is unchanged.

#### Registry features and operation The customer operates the registry and configures the features provided by the selected software. The registry is deployed empty : your images are loaded into it through the appliance's image delivery service before any workload pulls, and there is deliberately **no pull-through cache** back to ECR, which removes the outbound AWS dependency entirely. **Harbor**, the recommended registry, ships built-in Trivy scan-on-push, robot accounts, per-repository retention, and immutable-tag rules, the closest match to ECR's registry-management features. **Zot** is lightweight and OCI-native, with vulnerability (CVE) search available through an optional extension. **Distribution** is the bare reference registry: it stores and serves images and offers blob garbage-collection, but no scanning or tag controls. Whichever you run, your ECR repositories map one-to-one to the registry's repositories, and a workload pulls with an injected `imagePullSecret`; Harbor's robot accounts provide the richer, scoped path.
In-cluster registry model In-cluster registry model

Choose the registry features your application needs: Harbor, Zot, or Distribution.

#### Limitations △ Where ECR and an in-cluster registry stay different * **The registry starts empty.** Your images are loaded through the appliance before any workload pulls; there is deliberately no pull-through cache back to ECR. * **Scanning, immutability, and retention depend on which registry you run.** Harbor supports most ECR management features; Zot covers it partially; Distribution is a bare registry with none of it built in. * **There is no managed cross-region replica.** An in-cluster registry serves the cluster it runs in; images are seeded through the appliance rather than replicated across regions. #### Other considerations Plan image delivery, credentials, and storage costs for the target registry. * **Images are seeded through the appliance, then served locally:** the registry is deployed empty and your images are loaded through the appliance before any workload pulls, with no pull-through cache back to ECR, so the running cluster has no outbound AWS dependency. * **You operate the registry:** there is no cloud control plane; Harbor, Zot, or Distribution is yours to run, and its availability, upgrades, and access model are your responsibility. * **Back up the registry's persistent volume:** the registry's blobs live on a PersistentVolume in the cluster, so backup and retention of the image store are yours, not a managed registry's. * **Budget for cluster compute and storage:** the registry consumes the cluster's own compute and storage rather than a managed per-tier or per-byte charge, and the feature depth (scanning, immutability, retention) follows which registry you chose. ### Via Harbor Registry | Operation | Area | Support | Depth | Notes | | ------------------------ | ------------ | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | Registry authentication | Auth | Supported | Common | in-cluster imagePullSecret; Harbor also supports robot accounts | | Reference rewrite | Image path | Supported | Common | every consumer's ECR image ref is rewritten to the in-cluster host; the imagePullSecret is injected and the ECR auth helper dropped | | docker push / pull | Image path | Supported | Common | byte-identical OCI Distribution v2; the host changes to registry.\.svc | | Lifecycle policy mapping | Lifecycle | Partial | Most usage | Harbor tag-retention is per-repository, the closest in-cluster analog to ECR lifecycle rules | | Image load (bootstrap) | Migration | Supported | Common | The appliance loads images before workloads pull them. Images are identified by content, so repeating the load does not duplicate them. | | Cross-region replication | Replication | Out of scope | Full surface | an in-cluster registry has no cross-region replica; the pull-through cache to ECR is deliberately not used, so there is no outbound AWS dependency | | Repository provisioning | Repositories | Supported | Common | the registry is deployed in-cluster and the ECR repository set becomes its repositories/namespaces (N to N) | | Scan-on-push findings | Scanning | Partial | Most usage | Harbor ships a built-in Trivy integration (scan-on-push) | #### How it works On AWS your build pushes container images to **Amazon ECR** and your workloads pull from it. When the target is your own **Kubernetes cluster**, the registry runs inside the cluster: an open-source registry you operate, with no cloud registry service behind it. Harbor is the recommended registry; Zot is a smaller alternative; Distribution provides basic image storage and delivery. At the build, Tensor9 provisions the registry, loads your images into it through the appliance, and rewrites every image reference to the in-cluster host; afterward your workloads pull directly from it. The image itself never changes. ECR and the in-cluster registry both speak the **OCI Distribution v2** protocol, so your image manifest, its layer blobs, and their content digests are byte-identical; docker and containerd are untouched. The one thing rewritten at the build is the registry address: your ECR host becomes `registry..svc`. You operate the registry and configure its policies. This mapping provisions a target registry and updates image references. Docker and containerd use the target's registry protocol and credentials. It does not provide the AWS ECR management or authorization-token API. Applications that call ECR directly, including `GetAuthorizationToken`, need a separate integration.
Before: on AWS your build pushes images to Amazon ECR and your workloads pull from it. After: on the target cloud the same docker push and pull reach in-cluster registry, provisioned at the build. Only the registry address is rewritten; the image is unchanged and Tensor9 is not in the pull path. Before: on AWS your build pushes images to Amazon ECR and your workloads pull from it. After: on the target cloud the same docker push and pull reach in-cluster registry, provisioned at the build. Only the registry address is rewritten; the image is unchanged and Tensor9 is not in the pull path.

Your images are loaded into an in-cluster registry through the appliance at the build; afterward your workloads pull from it directly and the image is unchanged.

#### Registry features and operation The customer operates the registry and configures the features provided by the selected software. The registry is deployed empty : your images are loaded into it through the appliance's image delivery service before any workload pulls, and there is deliberately **no pull-through cache** back to ECR, which removes the outbound AWS dependency entirely. **Harbor**, the recommended registry, ships built-in Trivy scan-on-push, robot accounts, per-repository retention, and immutable-tag rules, the closest match to ECR's registry-management features. **Zot** is lightweight and OCI-native, with vulnerability (CVE) search available through an optional extension. **Distribution** is the bare reference registry: it stores and serves images and offers blob garbage-collection, but no scanning or tag controls. Whichever you run, your ECR repositories map one-to-one to the registry's repositories, and a workload pulls with an injected `imagePullSecret`; Harbor's robot accounts provide the richer, scoped path.
In-cluster registry model In-cluster registry model

Choose the registry features your application needs: Harbor, Zot, or Distribution.

#### Limitations △ Where ECR and an in-cluster registry stay different * **The registry starts empty.** Your images are loaded through the appliance before any workload pulls; there is deliberately no pull-through cache back to ECR. * **Scanning, immutability, and retention depend on which registry you run.** Harbor supports most ECR management features; Zot covers it partially; Distribution is a bare registry with none of it built in. * **There is no managed cross-region replica.** An in-cluster registry serves the cluster it runs in; images are seeded through the appliance rather than replicated across regions. #### Other considerations Plan image delivery, credentials, and storage costs for the target registry. * **Images are seeded through the appliance, then served locally:** the registry is deployed empty and your images are loaded through the appliance before any workload pulls, with no pull-through cache back to ECR, so the running cluster has no outbound AWS dependency. * **You operate the registry:** there is no cloud control plane; Harbor, Zot, or Distribution is yours to run, and its availability, upgrades, and access model are your responsibility. * **Back up the registry's persistent volume:** the registry's blobs live on a PersistentVolume in the cluster, so backup and retention of the image store are yours, not a managed registry's. * **Budget for cluster compute and storage:** the registry consumes the cluster's own compute and storage rather than a managed per-tier or per-byte charge, and the feature depth (scanning, immutability, retention) follows which registry you chose. [Service Catalog](/service-adapters/catalog). # ECR Public Source: https://docs.tensor9.com/service-adapters/aws/compute-containers/ecr-public AWS ECR Public. Hosts container images that anyone can pull without credentials, published under public.ecr.aws and listed in the ECR Public Gallery. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) * [Via Zot](#via-zot) * [Via Distribution](#via-distribution) * [Via Harbor](#via-harbor) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of ECR Public with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | ECR Public | Google Cloud | Azure | OCI | Private Kubernetes · Zot | Private Kubernetes · Distribution | Private Kubernetes · Harbor | | -------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | Adaptation mechanism | AWS public registry and management APIs | Infrastructure only: native public registry | Infrastructure only: native public registry | Infrastructure only: native public registry | Infrastructure only: native public registry | Infrastructure only: native public registry | Infrastructure only: native public registry | | Anonymous image pull | Yes | Yes - Selected images at the new public HTTPS URL | Yes - Selected images at the new public HTTPS URL | Yes - Selected images at the new public HTTPS URL | Yes - Selected images at the new public HTTPS URL | Yes - Selected images at the new public HTTPS URL | Yes - Selected images at the new public HTTPS URL | | Publisher authentication | AWS credentials and ECR Public authorization | Native principal; repository writer | Native repository-scoped credentials | Native credentials; selected-repository permissions | Named users: read/create/update | docker\_auth: exact pull/push scopes | Project robot with pull and push | | Public and private isolation | Separate AWS public and private registries | Dedicated public repositories; private repositories separate | Separate public-only registry; all its repositories public | Explicitly public repositories; private repositories separate | Exact anonymous-read ACLs; separate public deployment | Exact pull-only public ACL; separate registry and trust | Dedicated public project; private deployment separate | | Repository topology | Public repositories under the AWS registry alias | One standard Docker repository per source repository | One dedicated registry with selected repository paths | Public repository per source repository in selected region | Dedicated Zot registry with selected repository paths | Public Distribution registry plus token issuer | Public project containing selected repository paths | | Image and index digests | Content-addressed manifests, indexes and layers | Selected supported content copied without digest changes; fail if preservation is impossible | Selected supported content copied without digest changes; fail if preservation is impossible | Selected supported content copied without digest changes; fail if preservation is impossible | Selected supported content copied without digest changes; fail if preservation is impossible | Selected supported content copied without digest changes; fail if preservation is impossible | Selected supported content copied without digest changes; fail if preservation is impossible | | AWS management and token APIs | Yes | No - Native registry APIs; no AWS request adapter | No - Native registry APIs; no AWS request adapter | No - Native registry APIs; no AWS request adapter | No - Native registry APIs; no AWS request adapter | No - Native registry APIs; no AWS request adapter | No - Native registry APIs; no AWS request adapter | | AWS Gallery and registry aliases | Yes | No - New public registry URL | No - New public registry URL | No - New public registry URL | No - New public registry URL | No - New public registry URL | No - New public registry URL | | Retention, scanning and recovery | AWS service configuration and operating model | Native cleanup/scanning; no AWS policy transfer | Native cleanup/scanning; no AWS policy transfer | Native retention/scanning; regional copies planned separately | Zot retention/extensions configured separately | Public delete disabled; operator cleanup and recovery | Harbor retention/scanning configured separately | | API coverage | full | partial | partial | partial | partial | partial | partial | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | ------------------------------------------- | ------------------ | ------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | AWS ECR Public management and token APIs | AWS API boundary | Out of scope | Full surface | Native registry access does not serve AWS CreateRepository, DescribeRepositories, PutImage, layer-upload calls or GetAuthorizationToken; no STS GetServiceBearerToken operation is added. | | Anonymous image pull | Image distribution | Supported | Common | Readers pull selected images from the new public HTTPS registry URL without an AWS identity or target credential. | | Authenticated image publication | Image distribution | Supported | Common | Grant Artifact Registry Reader to allUsers on each selected repository. Publishers authenticate with a target principal granted write access to that repository; a public reader receives no write grant. | | Continuous replication from AWS | Migration | Out of scope | Full surface | The initial selected image transfer does not continuously copy later changes from the AWS public registry. | | Historical images and attached artifacts | Migration | Partial | Most usage | The selected images do not include an unbounded registry history or continued replication of later AWS changes. Signatures, attestations, referrers, SBOMs and scanning history need their own transfer scope. Foreign layers fetched from external URLs are outside the initial self-contained copy guarantee. Preserving an image digest does not preserve repository identity or signature trust. | | Selected image and digest transfer | Migration | Supported | Common | Select the source images and tags to transfer and freeze each selected tag to its source digest. Copy supported Docker schema-2 or OCI manifests and layers without changing their bytes. A retained multi-platform index includes all its referenced platform images. Verify the destination digests; a copy that requires media-type conversion or cannot preserve a required digest fails. Update only the image references the mapping controls. External users must change their old AWS image URLs. | | Native retention and scanning configuration | Operations | Partial | Most usage | Configure Artifact Registry cleanup separately and retain releases needed for rollback. This mapping does not translate AWS repository policies or import scanning findings. Optional native scanning is a separate target configuration. | | AWS Gallery and registry aliases | Public identity | Out of scope | Full surface | The target registry has a different hostname and repository path; the Public Gallery and public.ecr.aws aliases are not preserved. | | Public repository provisioning | Repositories | Supported | Common | Each selected ECR Public repository becomes a dedicated standard Docker repository in the target project and location. The image URL includes the location, project, repository and image path. | #### Public images on Artifact Registry Each selected ECR Public repository becomes a dedicated standard Docker repository in the target project and location. The image URL includes the location, project, repository and image path. Clients use the target registry directly at Infrastructure-only adaptation. AWS ECR Public management and authorization-token APIs, the Public Gallery and public.ecr.aws registry aliases are outside this mapping. Native push and pull do not implement AWS PutImage, layer-upload APIs or GetAuthorizationToken. No Max request adapter or additional STS operation is implied.
Anonymous readers pull from Artifact Registry; authenticated publishers write selected public repositories. Anonymous readers pull from Artifact Registry; authenticated publishers write selected public repositories.
#### Anonymous reads and authenticated publication Grant Artifact Registry Reader to allUsers on each selected repository. Publishers authenticate with a target principal granted write access to that repository; a public reader receives no write grant. Public access is repository-scoped. Keep private ECR images in their separate repositories and never grant project-wide public access to satisfy this mapping. Organization policy must permit anonymous readers. #### Selected images and changed references Select the source images and tags to transfer and freeze each selected tag to its source digest. Copy supported Docker schema-2 or OCI manifests and layers without changing their bytes. A retained multi-platform index includes all its referenced platform images. Verify the destination digests; a copy that requires media-type conversion or cannot preserve a required digest fails. Update only the image references the mapping controls. External users must change their old AWS image URLs. The selected images do not include an unbounded registry history or continued replication of later AWS changes. Signatures, attestations, referrers, SBOMs and scanning history need their own transfer scope. Foreign layers fetched from external URLs are outside the initial self-contained copy guarantee. Preserving an image digest does not preserve repository identity or signature trust. #### Operating the public registry Google operates the registry. You own its repository permissions, publisher identity, image retention and public traffic costs. Set request quotas appropriate for anonymous clients; a quota or provider outage can interrupt new pulls. Configure Artifact Registry cleanup separately and retain releases needed for rollback. This mapping does not translate AWS repository policies or import scanning findings. Optional native scanning is a separate target configuration. #### Limits of this mapping * The source AWS registry alias and ECR Public Gallery listing do not follow the image to its new URL. * A remote or virtual repository is not a substitute for the dedicated repository containing the selected copied images. * A project policy that forbids allUsers makes this public choice unavailable; a private repository is not an equivalent public result. * Public distribution requires reachable public HTTPS and permissions for anonymous readers. A policy that forbids that access must reject this choice, not silently produce a private registry. ## On Azure | Operation | Area | Support | Depth | Notes | | ------------------------------------------- | ------------------ | ------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | AWS ECR Public management and token APIs | AWS API boundary | Out of scope | Full surface | Native registry access does not serve AWS CreateRepository, DescribeRepositories, PutImage, layer-upload calls or GetAuthorizationToken; no STS GetServiceBearerToken operation is added. | | Anonymous image pull | Image distribution | Supported | Common | Readers pull selected images from the new public HTTPS registry URL without an AWS identity or target credential. | | Authenticated image publication | Image distribution | Supported | Common | Enable anonymous pull for the public registry. Publishers use authenticated native credentials scoped to read and write their selected repository paths; public access does not grant publishing rights. | | Continuous replication from AWS | Migration | Out of scope | Full surface | The initial selected image transfer does not continuously copy later changes from the AWS public registry. | | Historical images and attached artifacts | Migration | Partial | Most usage | The selected images do not include an unbounded registry history or continued replication of later AWS changes. Signatures, attestations, referrers, SBOMs and scanning history need their own transfer scope. Foreign layers fetched from external URLs are outside the initial self-contained copy guarantee. Preserving an image digest does not preserve repository identity or signature trust. | | Selected image and digest transfer | Migration | Supported | Common | Select the source images and tags to transfer and freeze each selected tag to its source digest. Copy supported Docker schema-2 or OCI manifests and layers without changing their bytes. A retained multi-platform index includes all its referenced platform images. Verify the destination digests; a copy that requires media-type conversion or cannot preserve a required digest fails. Update only the image references the mapping controls. External users must change their old AWS image URLs. | | Native retention and scanning configuration | Operations | Partial | Most usage | Configure native cleanup and any optional scanning independently. AWS repository policies, scan results and continuous replication are not imported. Retain current releases and rollback digests before deleting old content. | | AWS Gallery and registry aliases | Public identity | Out of scope | Full surface | The target registry has a different hostname and repository path; the Public Gallery and public.ecr.aws aliases are not preserved. | | Public repository provisioning | Repositories | Supported | Common | Use a dedicated public-only Standard or Premium Azure Container Registry. Selected public repositories become distinct image paths under its azurecr.io hostname. | #### Public images on Azure Container Registry Use a dedicated public-only Standard or Premium Azure Container Registry. Selected public repositories become distinct image paths under its azurecr.io hostname. Clients use the target registry directly at Infrastructure-only adaptation. AWS ECR Public management and authorization-token APIs, the Public Gallery and public.ecr.aws registry aliases are outside this mapping. Native push and pull do not implement AWS PutImage, layer-upload APIs or GetAuthorizationToken. No Max request adapter or additional STS operation is implied.
Anonymous readers pull from Azure Container Registry; authenticated publishers write selected public repositories. Anonymous readers pull from Azure Container Registry; authenticated publishers write selected public repositories.
#### Anonymous reads and authenticated publication Enable anonymous pull for the public registry. Publishers use authenticated native credentials scoped to read and write their selected repository paths; public access does not grant publishing rights. Anonymous pull exposes every repository in this registry, including repositories with separate permission settings. Never reuse the private ECR registry or turn it public. Private ECR images must remain in a different registry. #### Selected images and changed references Select the source images and tags to transfer and freeze each selected tag to its source digest. Copy supported Docker schema-2 or OCI manifests and layers without changing their bytes. A retained multi-platform index includes all its referenced platform images. Verify the destination digests; a copy that requires media-type conversion or cannot preserve a required digest fails. Update only the image references the mapping controls. External users must change their old AWS image URLs. The selected images do not include an unbounded registry history or continued replication of later AWS changes. Signatures, attestations, referrers, SBOMs and scanning history need their own transfer scope. Foreign layers fetched from external URLs are outside the initial self-contained copy guarantee. Preserving an image digest does not preserve repository identity or signature trust. #### Operating the public registry Azure operates the registry. You own its SKU, public endpoint, publisher scopes, credentials and storage/egress costs. A Private Link-only or otherwise private-only endpoint cannot supply anonymous internet distribution. Configure native cleanup and any optional scanning independently. AWS repository policies, scan results and continuous replication are not imported. Retain current releases and rollback digests before deleting old content. #### Limits of this mapping * Anonymous pull requires the Standard or Premium service tier; it is not a Basic-tier public option. * The public-access switch is registry-wide, not an anonymous exception for one selected repository. * Public traffic can be throttled. Network restrictions must still allow the readers this mapping is intended to serve. * Public distribution requires reachable public HTTPS and permissions for anonymous readers. A policy that forbids that access must reject this choice, not silently produce a private registry. ## On OCI | Operation | Area | Support | Depth | Notes | | ------------------------------------------- | ------------------ | ------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | AWS ECR Public management and token APIs | AWS API boundary | Out of scope | Full surface | Native registry access does not serve AWS CreateRepository, DescribeRepositories, PutImage, layer-upload calls or GetAuthorizationToken; no STS GetServiceBearerToken operation is added. | | Anonymous image pull | Image distribution | Supported | Common | Readers pull selected images from the new public HTTPS registry URL without an AWS identity or target credential. | | Authenticated image publication | Image distribution | Supported | Common | Anyone who can reach the public URL can pull the selected images without credentials. Publishers authenticate with native credentials and permissions to read and update the selected repositories. | | Continuous replication from AWS | Migration | Out of scope | Full surface | The initial selected image transfer does not continuously copy later changes from the AWS public registry. | | Historical images and attached artifacts | Migration | Partial | Most usage | The selected images do not include an unbounded registry history or continued replication of later AWS changes. Signatures, attestations, referrers, SBOMs and scanning history need their own transfer scope. Foreign layers fetched from external URLs are outside the initial self-contained copy guarantee. Preserving an image digest does not preserve repository identity or signature trust. | | Selected image and digest transfer | Migration | Supported | Common | Select the source images and tags to transfer and freeze each selected tag to its source digest. Copy supported Docker schema-2 or OCI manifests and layers without changing their bytes. A retained multi-platform index includes all its referenced platform images. Verify the destination digests; a copy that requires media-type conversion or cannot preserve a required digest fails. Update only the image references the mapping controls. External users must change their old AWS image URLs. | | Native retention and scanning configuration | Operations | Partial | Most usage | Use OCIR retention and optional scanning as separate target controls. The mapping does not transfer AWS scanning history or repository policy semantics. Additional regional copies require an explicit image-delivery plan. | | AWS Gallery and registry aliases | Public identity | Out of scope | Full surface | The target registry has a different hostname and repository path; the Public Gallery and public.ecr.aws aliases are not preserved. | | Public repository provisioning | Repositories | Supported | Common | Create an explicitly public OCIR repository for each selected public source repository. Consumers use the target region, tenancy namespace and mapped repository path. | #### Public images on OCI Container Registry Create an explicitly public OCIR repository for each selected public source repository. Consumers use the target region, tenancy namespace and mapped repository path. Clients use the target registry directly at Infrastructure-only adaptation. AWS ECR Public management and authorization-token APIs, the Public Gallery and public.ecr.aws registry aliases are outside this mapping. Native push and pull do not implement AWS PutImage, layer-upload APIs or GetAuthorizationToken. No Max request adapter or additional STS operation is implied.
Anonymous readers pull from OCI Container Registry; authenticated publishers write selected public repositories. Anonymous readers pull from OCI Container Registry; authenticated publishers write selected public repositories.
#### Anonymous reads and authenticated publication Anyone who can reach the public URL can pull the selected images without credentials. Publishers authenticate with native credentials and permissions to read and update the selected repositories. Publicity is set on each selected repository. Keep private repositories private and separate. Scope publisher policy to the selected repositories rather than granting blanket registry administration across the tenancy. #### Selected images and changed references Select the source images and tags to transfer and freeze each selected tag to its source digest. Copy supported Docker schema-2 or OCI manifests and layers without changing their bytes. A retained multi-platform index includes all its referenced platform images. Verify the destination digests; a copy that requires media-type conversion or cannot preserve a required digest fails. Update only the image references the mapping controls. External users must change their old AWS image URLs. The selected images do not include an unbounded registry history or continued replication of later AWS changes. Signatures, attestations, referrers, SBOMs and scanning history need their own transfer scope. Foreign layers fetched from external URLs are outside the initial self-contained copy guarantee. Preserving an image digest does not preserve repository identity or signature trust. #### Operating the public registry Oracle operates the regional registry. You own the public repository settings, publisher credentials, cleanup and traffic costs. Copy required release and rollback images to the selected region before directing clients there. Use OCIR retention and optional scanning as separate target controls. The mapping does not transfer AWS scanning history or repository policy semantics. Additional regional copies require an explicit image-delivery plan. #### Limits of this mapping * Region and tenancy namespace are part of the image reference. Replace AWS account and registry-alias URLs with the target image reference. * A private service-gateway path does not replace the public URL required by external anonymous readers. * A selected regional copy does not establish continuous cross-region or AWS-to-OCI replication. * Public distribution requires reachable public HTTPS and permissions for anonymous readers. A policy that forbids that access must reject this choice, not silently produce a private registry. ## On Private Kubernetes ### Via Zot | Operation | Area | Support | Depth | Notes | | ------------------------------------------- | ------------------ | ------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | AWS ECR Public management and token APIs | AWS API boundary | Out of scope | Full surface | Native registry access does not serve AWS CreateRepository, DescribeRepositories, PutImage, layer-upload calls or GetAuthorizationToken; no STS GetServiceBearerToken operation is added. | | Anonymous image pull | Image distribution | Supported | Common | Readers pull selected images from the new public HTTPS registry URL without an AWS identity or target credential. | | Authenticated image publication | Image distribution | Supported | Common | For each selected path, grant anonymousPolicy read only. Named authenticated publishers receive read, create and update actions. Deny mutation to other users and anonymous clients; do not install a global create/update or administrator grant as a default. | | Continuous replication from AWS | Migration | Out of scope | Full surface | The initial selected image transfer does not continuously copy later changes from the AWS public registry. | | Historical images and attached artifacts | Migration | Partial | Most usage | The selected images do not include an unbounded registry history or continued replication of later AWS changes. Signatures, attestations, referrers, SBOMs and scanning history need their own transfer scope. Foreign layers fetched from external URLs are outside the initial self-contained copy guarantee. Preserving an image digest does not preserve repository identity or signature trust. | | Selected image and digest transfer | Migration | Supported | Common | Select the source images and tags to transfer and freeze each selected tag to its source digest. Copy supported Docker schema-2 or OCI manifests and layers without changing their bytes. A retained multi-platform index includes all its referenced platform images. Verify the destination digests; a copy that requires media-type conversion or cannot preserve a required digest fails. Update only the image references the mapping controls. External users must change their old AWS image URLs. | | Native retention and scanning configuration | Operations | Partial | Most usage | Configure Zot retention independently of AWS policies. Optional native extensions do not import ECR Public scanning results or history. Retain manifests and layers referenced by current releases and rollback images. | | AWS Gallery and registry aliases | Public identity | Out of scope | Full surface | The target registry has a different hostname and repository path; the Public Gallery and public.ecr.aws aliases are not preserved. | | Public repository provisioning | Repositories | Supported | Common | Use a dedicated public Zot deployment with persistent image storage and a trusted public HTTPS endpoint. Keep selected repository paths explicit instead of exposing the private registry deployment. | #### Public images on Zot Use a dedicated public Zot deployment with persistent image storage and a trusted public HTTPS endpoint. Keep selected repository paths explicit instead of exposing the private registry deployment. Clients use the target registry directly at Infrastructure-only adaptation. AWS ECR Public management and authorization-token APIs, the Public Gallery and public.ecr.aws registry aliases are outside this mapping. Native push and pull do not implement AWS PutImage, layer-upload APIs or GetAuthorizationToken. No Max request adapter or additional STS operation is implied.
Anonymous readers pull from Zot; authenticated publishers write selected public repositories. Anonymous readers pull from Zot; authenticated publishers write selected public repositories.
#### Anonymous reads and authenticated publication For each selected path, grant anonymousPolicy read only. Named authenticated publishers receive read, create and update actions. Deny mutation to other users and anonymous clients; do not install a global create/update or administrator grant as a default. Keep private images in the separate private deployment. Use the configuration schema for the deployed Zot release and verify exact repository matching and more-specific path precedence before exposing the endpoint. #### Selected images and changed references Select the source images and tags to transfer and freeze each selected tag to its source digest. Copy supported Docker schema-2 or OCI manifests and layers without changing their bytes. A retained multi-platform index includes all its referenced platform images. Verify the destination digests; a copy that requires media-type conversion or cannot preserve a required digest fails. Update only the image references the mapping controls. External users must change their old AWS image URLs. The selected images do not include an unbounded registry history or continued replication of later AWS changes. Signatures, attestations, referrers, SBOMs and scanning history need their own transfer scope. Foreign layers fetched from external URLs are outside the initial self-contained copy guarantee. Preserving an image digest does not preserve repository identity or signature trust. #### Operating the public registry You operate storage capacity, backups, publisher authentication, public ingress and TLS renewal. A fresh anonymous client must pull without a cached credential or layer. Restore and credential-rotation procedures must retain the read/write boundary. Configure Zot retention independently of AWS policies. Optional native extensions do not import ECR Public scanning results or history. Retain manifests and layers referenced by current releases and rollback images. #### Limits of this mapping * The public endpoint must be reachable by intended internet readers; cluster-local DNS alone is insufficient. * Anonymous read and authenticated publication are separate ACLs. A broad authenticated default write rule would defeat publisher isolation. * The mapping does not promise AWS lifecycle-policy translation, AWS scanning equivalence or continuous replication. * Public distribution requires reachable public HTTPS and permissions for anonymous readers. A policy that forbids that access must reject this choice, not silently produce a private registry. ### Via Distribution | Operation | Area | Support | Depth | Notes | | ------------------------------------------- | ------------------ | ------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | AWS ECR Public management and token APIs | AWS API boundary | Out of scope | Full surface | Native registry access does not serve AWS CreateRepository, DescribeRepositories, PutImage, layer-upload calls or GetAuthorizationToken; no STS GetServiceBearerToken operation is added. | | Anonymous image pull | Image distribution | Supported | Common | Readers pull selected images from the new public HTTPS registry URL without an AWS identity or target credential. | | Authenticated image publication | Image distribution | Supported | Common | The token issuer grants anonymous clients pull only for the exact public repositories. Authenticated publishers receive pull and push only for their assigned repositories. Requested scopes are intersected with the ordered ACL; asking for push cannot enlarge anonymous rights. | | Continuous replication from AWS | Migration | Out of scope | Full surface | The initial selected image transfer does not continuously copy later changes from the AWS public registry. | | Historical images and attached artifacts | Migration | Partial | Most usage | The selected images do not include an unbounded registry history or continued replication of later AWS changes. Signatures, attestations, referrers, SBOMs and scanning history need their own transfer scope. Foreign layers fetched from external URLs are outside the initial self-contained copy guarantee. Preserving an image digest does not preserve repository identity or signature trust. | | Selected image and digest transfer | Migration | Supported | Common | Select the source images and tags to transfer and freeze each selected tag to its source digest. Copy supported Docker schema-2 or OCI manifests and layers without changing their bytes. A retained multi-platform index includes all its referenced platform images. Verify the destination digests; a copy that requires media-type conversion or cannot preserve a required digest fails. Update only the image references the mapping controls. External users must change their old AWS image URLs. | | Native retention and scanning configuration | Operations | Partial | Most usage | Public deletion is disabled by default. Retirement and garbage collection are separate operator procedures with writes stopped or read-only as required by the selected release. Back up storage, ACL configuration and signing material. AWS cleanup policies, scanning findings and automatic replication are outside this mapping. | | AWS Gallery and registry aliases | Public identity | Out of scope | Full surface | The target registry has a different hostname and repository path; the Public Gallery and public.ecr.aws aliases are not preserved. | | Public repository provisioning | Repositories | Supported | Common | Deploy a dedicated public Distribution registry and a separate Cesanta docker\_auth token issuer. Both have public trusted HTTPS endpoints; the registry keeps its own persistent image storage. | #### Public images on Distribution Deploy a dedicated public Distribution registry and a separate Cesanta docker\_auth token issuer. Both have public trusted HTTPS endpoints; the registry keeps its own persistent image storage. Clients use the target registry directly at Infrastructure-only adaptation. AWS ECR Public management and authorization-token APIs, the Public Gallery and public.ecr.aws registry aliases are outside this mapping. Native push and pull do not implement AWS PutImage, layer-upload APIs or GetAuthorizationToken. No Max request adapter or additional STS operation is implied.
Anonymous readers pull from Distribution; authenticated publishers write selected public repositories. Anonymous readers pull from Distribution; authenticated publishers write selected public repositories.
#### Anonymous reads and authenticated publication The token issuer grants anonymous clients pull only for the exact public repositories. Authenticated publishers receive pull and push only for their assigned repositories. Requested scopes are intersected with the ordered ACL; asking for push cannot enlarge anonymous rights. Use an exact publisher/repository rule followed by the exact public-read rule and deny everything else. Keep the private registry and its signing trust separate. Do not copy example localhost, bridge-network or administrator wildcard grants into the public ACL. #### Selected images and changed references Select the source images and tags to transfer and freeze each selected tag to its source digest. Copy supported Docker schema-2 or OCI manifests and layers without changing their bytes. A retained multi-platform index includes all its referenced platform images. Verify the destination digests; a copy that requires media-type conversion or cannot preserve a required digest fails. Update only the image references the mapping controls. External users must change their old AWS image URLs. The selected images do not include an unbounded registry history or continued replication of later AWS changes. Signatures, attestations, referrers, SBOMs and scanning history need their own transfer scope. Foreign layers fetched from external URLs are outside the initial self-contained copy guarantee. Preserving an image digest does not preserve repository identity or signature trust. #### Operating the public registry You operate the registry, token issuer, TLS certificates, protected publisher credentials and token-signing trust. Use a reviewed docker\_auth release and compatible Distribution image. Match issuer and audience and configure trusted signing material. Rotate keys with a bounded verification overlap; new token issuance depends on issuer availability. Public deletion is disabled by default. Retirement and garbage collection are separate operator procedures with writes stopped or read-only as required by the selected release. Back up storage, ACL configuration and signing material. AWS cleanup policies, scanning findings and automatic replication are outside this mapping. #### Limits of this mapping * An anonymous bearer token is native registry access machinery, not an AWS ECR Public authorization token or an AWS identity. * Publisher pull/push grants do not include deletion, other repository namespaces or token-service administration. * Operating the separate token issuer is part of this choice. Existing valid tokens do not establish availability of future token requests. * Public distribution requires reachable public HTTPS and permissions for anonymous readers. A policy that forbids that access must reject this choice, not silently produce a private registry. ### Via Harbor | Operation | Area | Support | Depth | Notes | | ------------------------------------------- | ------------------ | ------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | AWS ECR Public management and token APIs | AWS API boundary | Out of scope | Full surface | Native registry access does not serve AWS CreateRepository, DescribeRepositories, PutImage, layer-upload calls or GetAuthorizationToken; no STS GetServiceBearerToken operation is added. | | Anonymous image pull | Image distribution | Supported | Common | Readers pull selected images from the new public HTTPS registry URL without an AWS identity or target credential. | | Authenticated image publication | Image distribution | Supported | Common | Anonymous clients can pull from the public project. A project robot authenticates publication with pull and push permissions. Do not give anonymous clients or publisher robots project administration or deletion permissions. | | Continuous replication from AWS | Migration | Out of scope | Full surface | The initial selected image transfer does not continuously copy later changes from the AWS public registry. | | Historical images and attached artifacts | Migration | Partial | Most usage | The selected images do not include an unbounded registry history or continued replication of later AWS changes. Signatures, attestations, referrers, SBOMs and scanning history need their own transfer scope. Foreign layers fetched from external URLs are outside the initial self-contained copy guarantee. Preserving an image digest does not preserve repository identity or signature trust. | | Selected image and digest transfer | Migration | Supported | Common | Select the source images and tags to transfer and freeze each selected tag to its source digest. Copy supported Docker schema-2 or OCI manifests and layers without changing their bytes. A retained multi-platform index includes all its referenced platform images. Verify the destination digests; a copy that requires media-type conversion or cannot preserve a required digest fails. Update only the image references the mapping controls. External users must change their old AWS image URLs. | | Native retention and scanning configuration | Operations | Partial | Most usage | Harbor offers its own retention and scanning controls, which need separate configuration. This mapping does not import AWS policy rules, findings or Gallery metadata. Ensure cleanup retains the image digests used by running deployments and rollback plans. | | AWS Gallery and registry aliases | Public identity | Out of scope | Full surface | The target registry has a different hostname and repository path; the Public Gallery and public.ecr.aws aliases are not preserved. | | Public repository provisioning | Repositories | Supported | Common | Deploy Harbor behind a public DNS name and trusted HTTPS endpoint. Place the selected images in a dedicated public project; repository names remain distinct within that project. | #### Public images on Harbor Deploy Harbor behind a public DNS name and trusted HTTPS endpoint. Place the selected images in a dedicated public project; repository names remain distinct within that project. Clients use the target registry directly at Infrastructure-only adaptation. AWS ECR Public management and authorization-token APIs, the Public Gallery and public.ecr.aws registry aliases are outside this mapping. Native push and pull do not implement AWS PutImage, layer-upload APIs or GetAuthorizationToken. No Max request adapter or additional STS operation is implied.
Anonymous readers pull from Harbor; authenticated publishers write selected public repositories. Anonymous readers pull from Harbor; authenticated publishers write selected public repositories.
#### Anonymous reads and authenticated publication Anonymous clients can pull from the public project. A project robot authenticates publication with pull and push permissions. Do not give anonymous clients or publisher robots project administration or deletion permissions. Every repository placed in the public project is public. Do not flip an existing private project to public or load private ECR images into it. Keep the private registry deployment and its image population separate. #### Selected images and changed references Select the source images and tags to transfer and freeze each selected tag to its source digest. Copy supported Docker schema-2 or OCI manifests and layers without changing their bytes. A retained multi-platform index includes all its referenced platform images. Verify the destination digests; a copy that requires media-type conversion or cannot preserve a required digest fails. Update only the image references the mapping controls. External users must change their old AWS image URLs. The selected images do not include an unbounded registry history or continued replication of later AWS changes. Signatures, attestations, referrers, SBOMs and scanning history need their own transfer scope. Foreign layers fetched from external URLs are outside the initial self-contained copy guarantee. Preserving an image digest does not preserve repository identity or signature trust. #### Operating the public registry You operate Harbor, its registry storage and backing state, the public ingress and certificate renewal. Preserve robot secrets when issued, rotate them through the operator workflow and account for credential expiry. Back up configuration and stored content together and verify recovery. Harbor offers its own retention and scanning controls, which need separate configuration. This mapping does not import AWS policy rules, findings or Gallery metadata. Ensure cleanup retains the image digests used by running deployments and rollback plans. #### Limits of this mapping * Private Kubernetes describes the hosting environment; anonymous internet distribution still needs a deliberately public endpoint. * Project publicity is broader than a single image tag; all repositories added to the public project share that exposure. * Native retention, scanning and replication capabilities do not imply equivalent AWS policies, results or ongoing replication. * Public distribution requires reachable public HTTPS and permissions for anonymous readers. A policy that forbids that access must reject this choice, not silently produce a private registry. [Service Catalog](/service-adapters/catalog). # ECS Source: https://docs.tensor9.com/service-adapters/aws/compute-containers/ecs AWS ECS. Schedules containers defined by task definitions onto EC2 or Fargate capacity, grouped in a cluster where each service holds a desired task count. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of ECS with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | ECS | Google Cloud | Azure | OCI | Private Kubernetes | | ---------------------------------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | Cluster · ECS cluster → GKE cluster / namespace | ECS cluster + capacity providers | a regional GKE Standard cluster or a namespace; capacity providers → the cluster's node pools / cluster-autoscaler (Google operates the control plane and nodes) | - | - | - | | Workload configuration · desired count → replicas | always-on ECS service | Deployment replicas = desiredCount, continuously running; the DAEMON strategy → a DaemonSet | Deployment replicas = desiredCount, continuously running; the DAEMON strategy → a DaemonSet | Deployment replicas = desiredCount, continuously running; the DAEMON strategy → a DaemonSet | Deployment replicas = desiredCount, continuously running (no scale-to-zero unless KEDA / Knative) | | Compute sizing · Fargate cpu/mem → requests/limits | Fargate \{0.25 … 16 vCPU} with tied memory bands | Pod requests / limits: values within node capacity and quota, without Fargate fixed-size combinations | Pod requests / limits: values within node capacity and quota, without Fargate fixed-size combinations | Pod requests / limits: values within node capacity and quota, without Fargate fixed-size combinations | - | | Networking (awsvpc) · per-task ENI → per-Pod IP | awsvpc: a routable IP + security group per task | a per-Pod IP (VPC-native) + NetworkPolicy: individually addressable, with network isolation between pods | a per-Pod IP (Azure CNI) + NetworkPolicy: individually addressable, with network isolation between pods | a per-Pod IP (VCN-native, OCI\_VCN\_IP\_NATIVE) + NetworkPolicy: individually addressable, with network isolation between pods | - | | Service Connect mesh · namespace mesh + metrics | Yes - injected proxy, aliases, metrics | Partial - a Service gives discovery; a service mesh (Istio / Linkerd) gives the proxy and metrics | Partial - a Service gives discovery; a service mesh (Istio / Linkerd) gives the proxy and metrics | Partial - a Service gives discovery; a service mesh (Istio / Linkerd) gives the proxy and metrics | Partial - a Service gives discovery; a service mesh (Istio / Linkerd) gives the proxy and metrics | | DAEMON strategy · one copy per host | Yes - DAEMON scheduling strategy | Yes - a DaemonSet, one Pod per node | Yes - a DaemonSet, one Pod per node | Yes - a DaemonSet, one Pod per node | Yes - a DaemonSet, one Pod per node | | ECS Exec · shell into a task | Yes - SSM interactive shell | Yes - kubectl exec into the Pod | Yes - kubectl exec into the Pod | Yes - kubectl exec into the Pod | Yes - kubectl exec into the Pod | | Workload identity (task role → WI) · keyless binding | Yes - taskRoleArn | Yes - GKE Workload Identity: keyless, a compile-time service-account binding | Yes - Azure Workload Identity, keyless | - | - | | Load balancing · service LB block → Service + Ingress | ALB / NLB target group | a Service (LoadBalancer) plus Ingress / Gateway (see the load-balancer service adapter pages) | a Service (LoadBalancer) plus Ingress / Application Gateway (see the load-balancer service adapter pages) | a Service (LoadBalancer) plus Ingress / Gateway (see the load-balancer service adapter pages) | a Service (LoadBalancer / ClusterIP) plus Ingress (see the load-balancer service adapter pages) | | Secrets / logging · secrets → Secret Manager · awslogs → Cloud Logging | secrets, awslogs | Google Secret Manager / External Secrets, Cloud Logging | - | - | - | | API coverage | full | high | high | high | high | | Cluster · ECS cluster → AKS cluster / namespace | ECS cluster + capacity providers | - | an AKS cluster or a namespace; capacity providers → the cluster's node pools / cluster-autoscaler (Microsoft operates the control plane and nodes) | - | - | | Secrets / logging · secrets → Key Vault · awslogs → Log Analytics | secrets, awslogs | - | Azure Key Vault / External Secrets, Azure Monitor / Log Analytics | - | - | | Cluster · ECS cluster → OKE cluster / namespace | ECS cluster + capacity providers | - | - | an enhanced OKE cluster or a namespace; capacity providers → the cluster's node pools / cluster-autoscaler (Oracle operates the control plane and nodes) | - | | Workload identity (task role → WI) · OCI-native analog | Yes - taskRoleArn | - | - | Partial - OKE Workload Identity, the OCI-native analog: IAM policies selecting a workload principal by cluster, namespace and service account | - | | Secrets / logging · secrets → OCI Vault · awslogs → OCI Logging | secrets, awslogs | - | - | OCI Vault / External Secrets, OCI Logging | - | | Cluster · ECS cluster → k8s cluster / namespace | ECS cluster + capacity providers | - | - | - | the cluster or a namespace; capacity providers → the cluster's node pools / cluster-autoscaler (the target cluster owns the nodes) | | Compute sizing · Fargate cpu/mem → requests/limits | Fargate \{0.25 … 16 vCPU} | - | - | - | Pod requests / limits: values within node capacity and quota, without Fargate fixed-size combinations | | Autoscaling · Application Auto Scaling → HPA | target-tracking on CPU / memory / custom | - | - | - | HorizontalPodAutoscaler on CPU / memory / custom, the closest faithful analog | | Identity / secrets / logging · task role → ServiceAccount + RBAC | task role, secrets, awslogs | - | - | - | native Kubernetes ServiceAccount + RBAC (you own identity; no cloud workload-identity binding), k8s Secrets / External Secrets, the cluster's log stack | ### Infrastructure-only adaptation | Capability | ECS | Google Cloud | Azure | OCI | | ---------------------------------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | Cluster · ECS cluster → GKE cluster / namespace | ECS cluster + capacity providers | a regional GKE Standard cluster or a namespace; capacity providers → the cluster's node pools / cluster-autoscaler (Google operates the control plane and nodes) | - | - | | Workload configuration · desired count → replicas | always-on ECS service | Deployment replicas = desiredCount, continuously running; the DAEMON strategy → a DaemonSet | Deployment replicas = desiredCount, continuously running; the DAEMON strategy → a DaemonSet | Deployment replicas = desiredCount, continuously running; the DAEMON strategy → a DaemonSet | | Compute sizing · Fargate cpu/mem → requests/limits | Fargate \{0.25 … 16 vCPU} with tied memory bands | Pod requests / limits: values within node capacity and quota, without Fargate fixed-size combinations | Pod requests / limits: values within node capacity and quota, without Fargate fixed-size combinations | Pod requests / limits: values within node capacity and quota, without Fargate fixed-size combinations | | Networking (awsvpc) · per-task ENI → per-Pod IP | awsvpc: a routable IP + security group per task | a per-Pod IP (VPC-native) + NetworkPolicy: individually addressable, with network isolation between pods | a per-Pod IP (Azure CNI) + NetworkPolicy: individually addressable, with network isolation between pods | a per-Pod IP (VCN-native, OCI\_VCN\_IP\_NATIVE) + NetworkPolicy: individually addressable, with network isolation between pods | | Service Connect mesh · namespace mesh + metrics | Yes - injected proxy, aliases, metrics | Partial - a Service gives discovery; a service mesh (Istio / Linkerd) gives the proxy and metrics | Partial - a Service gives discovery; a service mesh (Istio / Linkerd) gives the proxy and metrics | Partial - a Service gives discovery; a service mesh (Istio / Linkerd) gives the proxy and metrics | | DAEMON strategy · one copy per host | Yes - DAEMON scheduling strategy | Yes - a DaemonSet, one Pod per node | Yes - a DaemonSet, one Pod per node | Yes - a DaemonSet, one Pod per node | | ECS Exec · shell into a task | Yes - SSM interactive shell | Yes - kubectl exec into the Pod | Yes - kubectl exec into the Pod | Yes - kubectl exec into the Pod | | Workload identity (task role → WI) · keyless binding | Yes - taskRoleArn | Yes - GKE Workload Identity: keyless, a compile-time service-account binding | Yes - Azure Workload Identity, keyless | - | | Load balancing · service LB block → Service + Ingress | ALB / NLB target group | a Service (LoadBalancer) plus Ingress / Gateway (see the load-balancer service adapter pages) | a Service (LoadBalancer) plus Ingress / Application Gateway (see the load-balancer service adapter pages) | a Service (LoadBalancer) plus Ingress / Gateway (see the load-balancer service adapter pages) | | Secrets / logging · secrets → Secret Manager · awslogs → Cloud Logging | secrets, awslogs | Google Secret Manager / External Secrets, Cloud Logging | - | - | | API coverage | full | high | high | high | | Cluster · ECS cluster → AKS cluster / namespace | ECS cluster + capacity providers | - | an AKS cluster or a namespace; capacity providers → the cluster's node pools / cluster-autoscaler (Microsoft operates the control plane and nodes) | - | | Secrets / logging · secrets → Key Vault · awslogs → Log Analytics | secrets, awslogs | - | Azure Key Vault / External Secrets, Azure Monitor / Log Analytics | - | | Cluster · ECS cluster → OKE cluster / namespace | ECS cluster + capacity providers | - | - | an enhanced OKE cluster or a namespace; capacity providers → the cluster's node pools / cluster-autoscaler (Oracle operates the control plane and nodes) | | Workload identity (task role → WI) · OCI-native analog | Yes - taskRoleArn | - | - | Partial - OKE Workload Identity, the OCI-native analog: IAM policies selecting a workload principal by cluster, namespace and service account | | Secrets / logging · secrets → OCI Vault · awslogs → OCI Logging | secrets, awslogs | - | - | OCI Vault / External Secrets, OCI Logging | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | ------------------------------------------------- | --------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Capacity providers / FARGATE\_SPOT | Cluster | Out of scope | Full surface | the cluster owns capacity via its node pools; FARGATE\_SPOT maps to a spot / preemptible node pool | | Identity: task role | Identity | Supported | Common | taskRoleArn → GKE Workload Identity, a keyless compile-time service-account binding; the execution role is not needed | | Secrets and environment | Identity | Supported | Common | environment variables are set as-is; secrets map to Google Secret Manager | | Load balancer: service LB block | Load balancer | Supported | Common | ALB / NLB target group → a Service (type=LoadBalancer) plus Ingress / Gateway; See the load-balancer adapter pages for individual routing-rule support. | | Logging: awslogs | Logging | Supported | Common | awslogs → Cloud Logging; a FireLens sidecar stays a logging sidecar | | Networking: awsvpc (per-task IP + security group) | Networking | Supported | Common | per-task ENI + security group → per-Pod IP (VPC-native) + NetworkPolicy: individually addressable, with network isolation between pods | | Networking: service discovery / Service Connect | Networking | Partial | Most usage | a Service + cluster DNS gives discovery natively; the injected mesh proxy and its metrics need an added service mesh (Istio / Linkerd) | | ECS Exec | Runtime | Supported | Most usage | ECS Exec's interactive shell → kubectl exec into the Pod | | Task-metadata endpoint (runtime) | Runtime | Supported | Common | 169.254.170.2 / ECS\_CONTAINER\_METADATA\_URI\_V4 served read-only, the ECS task-metadata API | | Scheduling: DAEMON strategy | Service | Supported | Full surface | one copy per host → a DaemonSet, one Pod per node | | Service: autoscaling | Service | Supported | Most usage | Application Auto Scaling → a HorizontalPodAutoscaler on CPU / memory / custom | | Service: deployment controller | Service | Partial | Most usage | rolling update is native (maxSurge / maxUnavailable); weighted blue-green / canary requires release-management tooling (Gateway API / Argo Rollouts / Flagger) | | Service: desired count | Service | Supported | Common | desiredCount → Deployment desired replicas; ready count depends on scheduling and health | | Task definition: containers / sidecars | Task definition | Supported | Common | multi-container task → a Pod with a main container plus sidecars, native | | Task definition: cpu / memory | Task definition | Supported | Common | Fargate \{0.25 … 16 vCPU} with tied memory bands → Pod requests / limits: values within node capacity and quota, without Fargate fixed-size combinations | | Task definition: image | Task definition | Supported | Common | Copies the container image to your registry during the build. | #### How it works On AWS your service runs as an **ECS service**: a task definition describes the container image and its sizing, and the service keeps a desired number of copies running on Fargate behind a load balancer. Tensor9 reads that task definition and service and, at the build, turns them into native Kubernetes on **GKE**: a **Deployment** (or a **DaemonSet** for the DAEMON strategy) plus a **Service**, on a regional GKE Standard control plane that Google operates. Kubernetes schedules the same container image. After deployment, clients reach your container through the target load balancer or cluster DNS. Tensor9 translates the stack during the build and does not proxy application traffic.
Before: on AWS your container image runs as an ECS service on Fargate behind an Application Load Balancer. After: on Google Cloud the same image runs as a native GKE Deployment (or a DaemonSet) plus a Service, provisioned at the build and reached by Google's own load balancer. Tensor9 is not in the service's traffic path. Before: on AWS your container image runs as an ECS service on Fargate behind an Application Load Balancer. After: on Google Cloud the same image runs as a native GKE Deployment (or a DaemonSet) plus a Service, provisioned at the build and reached by Google's own load balancer. Tensor9 is not in the service's traffic path.

Your task definition and service are compiled into native Kubernetes on GKE at the build; afterward Google's own load balancer and runtime carry every request.

#### The task definition and service A **task definition** describes a main container and any helper containers (sidecars). Tensor9 translates them into a multi-container **Pod** and copies the image to your registry during the build. Fargate expresses compute as a CPU value in \{0.25 … 16} vCPU with tied memory bands; on GKE that becomes Pod `requests` and `limits`, which are not restricted to Fargate's fixed size combinations. Requested resources must fit node capacity, scheduler rules and quota. An ECS service keeps a **desired count** of tasks always on. That maps directly to a Deployment's `replicas`, the desired replica count, not a guaranteed number of ready pods; scale-to-zero is not the default and is added only if you want it. **Application Auto Scaling** becomes a **HorizontalPodAutoscaler** on CPU, memory, or a custom metric, and a rolling update maps to the Deployment's own `RollingUpdate` with `maxSurge` and `maxUnavailable`. For other ECS operations, the **DAEMON** strategy becomes a **DaemonSet** (one Pod per node), and **ECS Exec** becomes `kubectl exec` into the Pod.
A task definition with one or more containers, an image, and a cpu-and-memory sizing becomes a Kubernetes Pod spec, and an ECS service with a desired count becomes a Deployment with that many replicas. Fargate cpu and memory become Pod requests and limits within node capacity and quota. The DAEMON strategy becomes a DaemonSet and ECS Exec becomes kubectl exec. A task definition with one or more containers, an image, and a cpu-and-memory sizing becomes a Kubernetes Pod spec, and an ECS service with a desired count becomes a Deployment with that many replicas. Fargate cpu and memory become Pod requests and limits within node capacity and quota. The DAEMON strategy becomes a DaemonSet and ECS Exec becomes kubectl exec.

The task definition becomes a Pod spec and the service becomes a Deployment: containers and sizing land as requests and limits, desiredCount sets replicas, the DAEMON strategy becomes a DaemonSet, and ECS Exec becomes kubectl exec.

#### Networking, discovery, and load balancing ECS's **awsvpc** mode gives each task its own routable IP and security group. A **VPC-native** GKE cluster offers the same model: every Pod gets its own individually-addressable IP, and a per-task security group maps to a **NetworkPolicy** (enforced by GKE Dataplane V2) to preserve network isolation between tasks. **Service discovery** by name, whether Cloud Map or DNS, is repointed at the build to a Kubernetes **Service** with cluster DNS, which is exactly ECS's name-based model. The service's `loadBalancer` block (the ALB or NLB target group its tasks register into) becomes a Kubernetes **Service** of type LoadBalancer (which provisions Google Cloud Load Balancing) plus an **Ingress** or a Gateway for host and path routing, with the target-group health check becoming the Pod's readiness probe. For supported routing rules, weights and certificates, see the [Application Load Balancer](/service-adapters/aws/networking-traffic/application-load-balancer) and [Network Load Balancer](/service-adapters/aws/networking-traffic/network-load-balancer) articles. This page places the Pod fleet behind that load balancer. For **ECS Service Connect**, a Service gives you name-based discovery, but Service Connect's injected mesh proxy and its built-in metrics are partial and need an added service mesh (Istio or Linkerd), which you configure separately. #### The GKE control plane This mapping selects a regional **GKE Standard** cluster: Google operates the control plane, and you configure node pools. **Autopilot** is a native alternative, not the selected adapter mapping; Google sizes its nodes and its workload billing model differs. On Standard, configured **node auto-provisioning** creates and resizes node pools to fit pending Pod requests, so an ECS **capacity provider** maps to that node-pool and cluster-autoscaler layer, and a `FARGATE_SPOT` preference maps to a spot (preemptible) node pool. Node upgrades and control-plane patching are Google's to run. GKE offers **managed integrations** that you enable and configure for the translated ECS workload; they are not all installed by selecting this adapter. The managed **GKE Gateway controller** implements the Gateway API with weighted routes, so a weighted blue-green or canary split has a managed path rather than a hand-rolled one (automated canary analysis still uses Argo Rollouts or Flagger). Managed Service for Prometheus collects metrics, including a service mesh's when you add one for Service Connect; **Config Connector** lets the cluster manage Google Cloud resources as Kubernetes objects; **Backup for GKE** covers workload and volume backup; and the Compute Engine, Filestore, and Cloud Storage **CSI drivers** back any volumes your task declares. Identity uses GKE Workload Identity, covered in the next section.
The GKE control plane, operated by Google. Regional GKE Standard is selected for this mapping; you configure node pools. Native Autopilot is an alternative where Google sizes nodes, not the selected mapping. Node auto-provisioning and managed integrations require configuration: the GKE Gateway controller for Gateway API, Managed Service for Prometheus, Config Connector, Backup for GKE, and the Compute Engine, Filestore, and Cloud Storage CSI drivers. Capacity providers map to node pools and Fargate Spot to a spot node pool. The GKE control plane, operated by Google. Regional GKE Standard is selected for this mapping; you configure node pools. Native Autopilot is an alternative where Google sizes nodes, not the selected mapping. Node auto-provisioning and managed integrations require configuration: the GKE Gateway controller for Gateway API, Managed Service for Prometheus, Config Connector, Backup for GKE, and the Compute Engine, Filestore, and Cloud Storage CSI drivers. Capacity providers map to node pools and Fargate Spot to a spot node pool.

The selected mapping uses regional GKE Standard and configured node pools. Autopilot is a native alternative. Gateway routing, monitoring, resource controllers, backups and storage drivers require the corresponding integration settings.

#### Pod identity: the task role An ECS task's **task role** is the same idea as EKS's IRSA: the identity the app's own code assumes, delivered as scoped cloud credentials with no static secret. On Kubernetes that shape is a **ServiceAccount federated to a cloud identity through the cluster's OIDC provider**, and on GKE that provider is **GKE Workload Identity**, which binds a Kubernetes ServiceAccount to a **Google service account** through the cluster's workload-identity pool (an IAM binding plus the `iam.gke.io/gcp-service-account` annotation), with the Pod reading its credentials from the **GKE metadata server**, no key to mount or rotate. The AWS role and the Google identity stay consistent through a startup check: at boot the appliance fetches the Pod's real GKE workload identity and checks that the declared AWS task role is bound to it, failing closed if it does not. Inside the appliance the IAM surface then **reflects** that identity, so `aws sts get-caller-identity` and `iam:GetRole` answer as the task's own role, and calls to other AWS services use the Tensor9 adapters with that role. The **execution role** (the one ECS itself used to pull the image, fetch secrets, and write logs) is not needed, because GKE owns image pull and log delivery with its own identity.
An ECS task role is the same idea as EKS IRSA: a Kubernetes ServiceAccount federated to a cloud identity through the cluster's OIDC provider, so the pod gets scoped cloud credentials with no static secret. On GKE the ServiceAccount is bound to a Google service account through the cluster's workload-identity pool, and the pod reads its credentials from the GKE metadata server, keyless. An ECS task role is the same idea as EKS IRSA: a Kubernetes ServiceAccount federated to a cloud identity through the cluster's OIDC provider, so the pod gets scoped cloud credentials with no static secret. On GKE the ServiceAccount is bound to a Google service account through the cluster's workload-identity pool, and the pod reads its credentials from the GKE metadata server, keyless.

The task role is ECS's EKS-IRSA equivalent: a Kubernetes ServiceAccount federated to a Google service account through the cluster's workload-identity pool, so the pod gets scoped credentials from the metadata server with no static secret.

#### AWS request authorization The task role's AWS IAM policy supplies authorization rules for supported **AWS API** requests handled by Tensor9. The build prepares the declared rules, including supported action and resource wildcards. At runtime, the adapter evaluates the verified caller's role, requested action and resource; a denial overrides an allowance. Policy changes are delivered with integrity verification to the appliance. Native Google Cloud permissions are granted separately to the Google service account. GKE Workload Identity supplies that identity; selecting it does not grant access to every Google resource or replace the AWS task-role policy. #### Secrets and logging The container's `environment` variables are kept as-is, and its `secrets` references resolve from **Google Secret Manager** (often via the External Secrets Operator), the same store the SSM equivalence uses. The `awslogs` log driver becomes **Cloud Logging**, with stdout and stderr delivered automatically; a custom FireLens log router can run as a sidecar; configure its destination and credentials for the target logging service. #### Limitations Review the remaining service, deployment and networking differences below. △ Where it diverges * **ECS Service Connect's mesh is partial:** a Service handles name-based discovery, but the injected mesh proxy and its built-in metrics need an added service mesh (Istio / Linkerd); its metrics can then flow into Managed Service for Prometheus. * **Weighted blue-green or canary releases require extra tooling:** a plain rolling update is native; weighted traffic splitting is handled by the managed GKE Gateway controller (Gateway API), and automated canary analysis uses Argo Rollouts or Flagger. * **Capacity providers and Fargate Spot are the cluster's own:** the cluster owns capacity via its node pools, so a capacity-provider strategy maps to node pools / node auto-provisioning and a Spot preference to a spot (preemptible) node pool. * **The task-metadata endpoint is served read-only.** `169.254.170.2` (`ECS_CONTAINER_METADATA_URI_V4`), the ECS task-metadata API, is served read-only; other AWS calls use their corresponding adapters. #### Other considerations Plan persistent storage, cluster capacity, logging, and secret access before deployment. * **Migrate mounted volumes separately:** a task definition and service hold no data, and the image is already a container image pulled into your own registry at the build, so adoption is a redeploy; any volume a task mounts (EFS, EBS) moves under its own service card. * **Plan node-pool capacity.** ECS on Fargate exposed no nodes, but on Google Cloud the workload lands on a GKE cluster whose selected Standard node pools require capacity planning, while Google operates and patches the control plane. * **Costs include continuously running nodes.** `desiredCount` becomes always-on replicas billed as running nodes in the selected GKE Standard deployment, so capacity is sized for steady state rather than metered per Fargate task. * **Telemetry and secrets move to Google Cloud's stores:** logs land in Cloud Logging and `secrets` resolve from Google Secret Manager, so dashboards, alerts, and secret access follow the target services. #### Runtime task execution The runtime ECS adapter accepts task-definition and task-lifecycle calls from the AWS SDK. It translates supported task definitions into Kubernetes workload configuration and maps RunTask, DescribeTasks, ListTasks and StopTask to the corresponding task state and execution. Registering a definition does not start its containers; a task can be pending while images are pulled or capacity is unavailable. Check task status and container exit information separately from API acceptance. Service deployment, scaling, task execution and workload credentials are distinct concerns. The target cluster schedules and runs the containers. The selected identity integration must provide credentials for calls made by those containers; a Kubernetes ServiceAccount does not by itself supply AWS credentials. Review task and service features against their specific rows, including network policy, Service Connect, storage and execution settings. ## On Azure | Operation | Area | Support | Depth | Notes | | ------------------------------------------------- | --------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Capacity providers / FARGATE\_SPOT | Cluster | Out of scope | Full surface | the cluster owns capacity via its node pools; FARGATE\_SPOT maps to a Spot node pool | | Identity: task role | Identity | Supported | Common | taskRoleArn → Azure Workload Identity (a per-workload user-assigned managed identity + federated credential), keyless; the execution role is not needed | | Secrets and environment | Identity | Supported | Common | environment variables are set as-is; secrets map to Azure Key Vault | | Load balancer: service LB block | Load balancer | Supported | Common | ALB / NLB target group → a Service (type=LoadBalancer) plus Ingress / Gateway; See the load-balancer adapter pages for individual routing-rule support. | | Logging: awslogs | Logging | Supported | Common | awslogs → Azure Monitor / Log Analytics; a FireLens sidecar stays a logging sidecar | | Networking: awsvpc (per-task IP + security group) | Networking | Supported | Common | per-task ENI + security group → per-Pod IP (Azure CNI) + NetworkPolicy: individually addressable, with network isolation between pods | | Networking: service discovery / Service Connect | Networking | Partial | Most usage | a Service + cluster DNS gives discovery natively; the injected mesh proxy and its metrics need an added service mesh (Istio / Linkerd) | | ECS Exec | Runtime | Supported | Most usage | ECS Exec's interactive shell → kubectl exec into the Pod | | Task-metadata endpoint (runtime) | Runtime | Supported | Common | 169.254.170.2 / ECS\_CONTAINER\_METADATA\_URI\_V4 served read-only, the ECS task-metadata API | | Scheduling: DAEMON strategy | Service | Supported | Full surface | one copy per host → a DaemonSet, one Pod per node | | Service: autoscaling | Service | Supported | Most usage | Application Auto Scaling → a HorizontalPodAutoscaler on CPU / memory / custom | | Service: deployment controller | Service | Partial | Most usage | rolling update is native (maxSurge / maxUnavailable); weighted blue-green / canary requires release-management tooling (Gateway API / Argo Rollouts / Flagger) | | Service: desired count | Service | Supported | Common | desiredCount → Deployment desired replicas; ready count depends on scheduling and health | | Task definition: containers / sidecars | Task definition | Supported | Common | multi-container task → a Pod with a main container plus sidecars, native | | Task definition: cpu / memory | Task definition | Supported | Common | Fargate \{0.25 … 16 vCPU} with tied memory bands → Pod requests / limits: values within node capacity and quota, without Fargate fixed-size combinations | | Task definition: image | Task definition | Supported | Common | Copies the container image to your registry during the build. | #### How it works On AWS your service runs as an **ECS service**: a task definition describes the container image and its sizing, and the service keeps a desired number of copies running on Fargate behind a load balancer. Tensor9 reads that task definition and service and, at the build, turns them into native Kubernetes on **AKS**: a **Deployment** (or a **DaemonSet** for the DAEMON strategy) plus a **Service**, on a control plane that Microsoft operates. Kubernetes schedules the same container image. After deployment, clients reach your container through the target load balancer or cluster DNS. Tensor9 translates the stack during the build and does not proxy application traffic.
Before: on AWS your container image runs as an ECS service on Fargate behind an Application Load Balancer. After: on Azure the same image runs as a native AKS Deployment (or a DaemonSet) plus a Service, provisioned at the build and reached by Azure's own load balancer. Tensor9 is not in the service's traffic path. Before: on AWS your container image runs as an ECS service on Fargate behind an Application Load Balancer. After: on Azure the same image runs as a native AKS Deployment (or a DaemonSet) plus a Service, provisioned at the build and reached by Azure's own load balancer. Tensor9 is not in the service's traffic path.

Your task definition and service are compiled into native Kubernetes on AKS at the build; afterward Azure's own load balancer and runtime carry every request.

#### The task definition and service A **task definition** describes a main container and any helper containers (sidecars). Tensor9 translates them into a multi-container **Pod** and copies the image to your registry during the build. Fargate expresses compute as a CPU value in \{0.25 … 16} vCPU with tied memory bands; on AKS that becomes Pod `requests` and `limits`, which are not restricted to Fargate's fixed size combinations. Requested resources must fit node capacity, scheduler rules and quota. An ECS service keeps a **desired count** of tasks always on. That maps directly to a Deployment's `replicas`, the desired replica count, not a guaranteed number of ready pods; scale-to-zero is not the default and is added only if you want it (AKS ships a managed way to, covered below). **Application Auto Scaling** becomes a **HorizontalPodAutoscaler** on CPU, memory, or a custom metric, and a rolling update maps to the Deployment's own `RollingUpdate` with `maxSurge` and `maxUnavailable`. For other ECS operations, the **DAEMON** strategy becomes a **DaemonSet** (one Pod per node), and **ECS Exec** becomes `kubectl exec` into the Pod.
A task definition with one or more containers, an image, and a cpu-and-memory sizing becomes a Kubernetes Pod spec, and an ECS service with a desired count becomes a Deployment with that many replicas. Fargate cpu and memory become Pod requests and limits within node capacity and quota. The DAEMON strategy becomes a DaemonSet and ECS Exec becomes kubectl exec. A task definition with one or more containers, an image, and a cpu-and-memory sizing becomes a Kubernetes Pod spec, and an ECS service with a desired count becomes a Deployment with that many replicas. Fargate cpu and memory become Pod requests and limits within node capacity and quota. The DAEMON strategy becomes a DaemonSet and ECS Exec becomes kubectl exec.

The task definition becomes a Pod spec and the service becomes a Deployment: containers and sizing become requests and limits, desiredCount becomes replicas, the DAEMON strategy becomes a DaemonSet, and ECS Exec becomes kubectl exec.

#### Networking, discovery, and load balancing ECS's **awsvpc** mode gives each task its own routable IP and security group. On AKS **Azure CNI** provides the same natively: every Pod gets its own individually-addressable VNet IP, and a per-task security group maps to a **NetworkPolicy** to preserve network isolation between tasks. **Service discovery** by name, whether Cloud Map or DNS, is repointed at the build to a Kubernetes **Service** with cluster DNS, which is exactly ECS's name-based model. The service's `loadBalancer` block (the ALB or NLB target group its tasks register into) becomes a Kubernetes **Service** of type LoadBalancer (which provisions an Azure Load Balancer) plus an **Ingress** for host and path routing, with the target-group health check becoming the Pod's readiness probe. For supported routing rules, weights and certificates, see the [Application Load Balancer](/service-adapters/aws/networking-traffic/application-load-balancer) and [Network Load Balancer](/service-adapters/aws/networking-traffic/network-load-balancer) articles. This page places the Pod fleet behind that load balancer. For **ECS Service Connect**, a Service provides name-based discovery, but Service Connect's injected mesh proxy and its built-in metrics are partial and need an added service mesh (Istio or Linkerd), surfaced rather than silently skipped. #### The AKS control plane Microsoft operates the control plane, and nodes come as **system and user node pools** of different VM sizes with a cluster autoscaler; a capacity provider maps to that node-pool layer, and a `FARGATE_SPOT` preference maps to a **Spot node pool**. Node image upgrades and control-plane patching are Microsoft's to run. Beyond fixed nodes, AKS can burst Pods onto **virtual nodes** backed by **Azure Container Instances**, a serverless capacity tier with the same elastic, pay-per-pod character as the origin's own on-demand compute. AKS offers **managed add-ons and cluster extensions**. The managed KEDA addon adds scale-to-zero and event-driven autoscaling as a managed option; the **Application Routing** addon provides a managed NGINX ingress for the Service-plus-Ingress mapping; the **Azure Key Vault Provider for Secrets Store CSI Driver** mounts Key Vault secrets into Pods; and **Container Insights** is the managed Azure Monitor path for container logs and metrics. As cluster extensions, **Dapr**, **GitOps with Flux**, and Azure Policy are available too. Identity federates to Microsoft Entra Workload ID, covered in the next section.
The AKS control plane, operated by Microsoft. Nodes come as system and user node pools of different VM sizes, with a cluster autoscaler, and Pods can burst onto virtual nodes backed by Azure Container Instances. The control plane ships managed addons and cluster extensions: the managed KEDA addon for event-driven autoscaling, the Application Routing managed ingress, the Azure Key Vault Secrets Store CSI addon, Container Insights, Dapr, GitOps with Flux, and Azure Policy. Capacity providers map to node pools and Fargate Spot to a Spot node pool. The AKS control plane, operated by Microsoft. Nodes come as system and user node pools of different VM sizes, with a cluster autoscaler, and Pods can burst onto virtual nodes backed by Azure Container Instances. The control plane ships managed addons and cluster extensions: the managed KEDA addon for event-driven autoscaling, the Application Routing managed ingress, the Azure Key Vault Secrets Store CSI addon, Container Insights, Dapr, GitOps with Flux, and Azure Policy. Capacity providers map to node pools and Fargate Spot to a Spot node pool.

AKS runs the control plane over system and user node pools, with virtual-node burst to a serverless capacity tier, and ships managed addons the ECS workload can use: managed KEDA, Application Routing, the Key Vault CSI addon, Container Insights, Dapr, Flux, and Azure Policy.

#### Pod identity: the task role An ECS task's **task role** is the same idea as EKS's IRSA: the identity the app's own code assumes, delivered as scoped cloud credentials with no static secret. On Kubernetes that shape is a **ServiceAccount federated to a cloud identity through the cluster's OIDC provider**, and on AKS that provider is **Microsoft Entra Workload ID**: the cluster's **AKS OIDC issuer** and a **federated identity credential** federate a Kubernetes ServiceAccount to a **user-assigned managed identity** (or an Entra app registration), the mutating admission webhook injects a token into the Pod, and the app exchanges it for a scoped Entra token, with no key to mount or rotate. The AWS role and the Entra identity stay consistent through a startup check: at boot the appliance fetches the Pod's real AKS workload identity and checks that the declared AWS task role is bound to it, failing closed if it does not. Inside the appliance the IAM surface then **reflects** that identity, so `aws sts get-caller-identity` and `iam:GetRole` answer as the task's own role, and calls to other AWS services use the Tensor9 adapters with that role. The **execution role** (the one ECS itself used to pull the image, fetch secrets, and write logs) is not needed, because AKS owns image pull and log delivery with its own identity.
An ECS task role is the same idea as EKS IRSA: a Kubernetes ServiceAccount federated to a cloud identity through the cluster's OIDC provider, so the pod gets scoped cloud credentials with no static secret. On AKS the ServiceAccount is federated to a Microsoft Entra identity (a user-assigned managed identity or app registration) through the AKS OIDC issuer with a federated identity credential, and the pod exchanges its ServiceAccount token for an Entra token, keyless. An ECS task role is the same idea as EKS IRSA: a Kubernetes ServiceAccount federated to a cloud identity through the cluster's OIDC provider, so the pod gets scoped cloud credentials with no static secret. On AKS the ServiceAccount is federated to a Microsoft Entra identity (a user-assigned managed identity or app registration) through the AKS OIDC issuer with a federated identity credential, and the pod exchanges its ServiceAccount token for an Entra token, keyless.

The task role is ECS's EKS-IRSA equivalent: a Kubernetes ServiceAccount federated through the AKS OIDC issuer to a Microsoft Entra identity, so the pod exchanges its ServiceAccount token for a scoped Entra token with no static secret.

#### AWS request authorization For supported AWS API calls, Tensor9 evaluates the task role's IAM policy against the verified caller, action and resource; a denial overrides an allowance. See [AWS request authorization](/service-adapters/aws/compute-containers/ecs#aws-request-authorization) for policy preparation, wildcard support and verified policy delivery. Native Azure permissions are granted separately to the managed identity in Azure. Entra Workload ID supplies identity tokens, not the resource grants or an AWS role session. #### Secrets and logging The container's `environment` variables are set as-is, and its `secrets` references resolve from **Azure Key Vault** (through the managed Key Vault Secrets Store CSI addon or the External Secrets Operator), the same store the SSM equivalence uses. The `awslogs` log driver becomes **Azure Monitor / Log Analytics** (Container Insights), with stdout and stderr delivered automatically; a custom FireLens log router can run as a sidecar; configure its destination and credentials for the target logging service. #### Limitations Review the remaining service, deployment and networking differences below. △ Where it diverges * **ECS Service Connect's mesh is partial:** a Service provides name-based discovery, but the injected mesh proxy and its built-in metrics need an added service mesh (Istio / Linkerd). * **Weighted blue-green or canary releases require extra tooling:** a plain rolling update is native; a weighted traffic split uses Gateway API, Argo Rollouts, or Flagger. * **Capacity providers and Fargate Spot are the cluster's own:** the cluster owns capacity via its node pools, so a capacity-provider strategy maps to node pools (or virtual-node burst to Azure Container Instances) and a Spot preference to a Spot node pool. * **The task-metadata endpoint is served read-only.** `169.254.170.2` (`ECS_CONTAINER_METADATA_URI_V4`), the ECS task-metadata API, is served read-only; other AWS calls use their corresponding adapters. #### Other considerations Plan persistent storage, cluster capacity, logging, and secret access before deployment. * **Migrate mounted volumes separately:** a task definition and service hold no data, and the image is already a container image pulled into your own registry at the build, so adoption is a redeploy; any volume a task mounts (EFS, EBS) moves under its own service card. * **Plan node-pool capacity.** The origin exposed no nodes, but on AKS the workload lands on a cluster whose node pools (or a serverless virtual-node burst tier) are an operational surface, while Microsoft operates and patches the control plane. * **Costs include continuously running nodes.** `desiredCount` becomes always-on replicas billed as running nodes, so the node pool is sized for steady state rather than metered per Fargate task. * **Telemetry and secrets move to Azure's stores:** logs land in Container Insights and `secrets` resolve from Azure Key Vault, so dashboards, alerts, and secret access follow the target services. #### Runtime tasks on AKS AKS schedules the translated tasks on configured node pools. Task acceptance, container readiness and exit status remain separate; native Azure grants and AWS task-role credentials also remain separate. See [runtime task execution](/service-adapters/aws/compute-containers/ecs#runtime-task-execution) for supported task-definition and task-lifecycle calls. ## On OCI | Operation | Area | Support | Depth | Notes | | ------------------------------------------------- | --------------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Capacity providers / FARGATE\_SPOT | Cluster | Out of scope | Full surface | the cluster owns capacity via its node pools; FARGATE\_SPOT maps to a preemptible node pool | | Identity: task role | Identity | Partial | Common | taskRoleArn → OKE Workload Identity, the OCI-native analog: IAM policies selecting a workload principal by cluster, namespace and service account; the execution role is not needed | | Secrets and environment | Identity | Supported | Common | environment variables are set as-is; secrets map to OCI Vault | | Load balancer: service LB block | Load balancer | Supported | Common | ALB / NLB target group → a Service (type=LoadBalancer) plus Ingress / Gateway; See the load-balancer adapter pages for individual routing-rule support. | | Logging: awslogs | Logging | Supported | Common | awslogs → OCI Logging; a FireLens sidecar stays a logging sidecar | | Networking: awsvpc (per-task IP + security group) | Networking | Supported | Common | per-task ENI + security group → per-Pod IP (VCN-native) + NetworkPolicy: individually addressable, with network isolation between pods | | Networking: service discovery / Service Connect | Networking | Partial | Most usage | a Service + cluster DNS gives discovery natively; the injected mesh proxy and its metrics need an added service mesh (Istio / Linkerd) | | ECS Exec | Runtime | Supported | Most usage | ECS Exec's interactive shell → kubectl exec into the Pod | | Task-metadata endpoint (runtime) | Runtime | Supported | Common | 169.254.170.2 / ECS\_CONTAINER\_METADATA\_URI\_V4 served read-only, the ECS task-metadata API | | Scheduling: DAEMON strategy | Service | Supported | Full surface | one copy per host → a DaemonSet, one Pod per node | | Service: autoscaling | Service | Supported | Most usage | Application Auto Scaling → a HorizontalPodAutoscaler on CPU / memory / custom | | Service: deployment controller | Service | Partial | Most usage | rolling update is native (maxSurge / maxUnavailable); weighted blue-green / canary requires release-management tooling (Gateway API / Argo Rollouts / Flagger) | | Service: desired count | Service | Supported | Common | desiredCount → Deployment desired replicas; ready count depends on scheduling and health | | Task definition: containers / sidecars | Task definition | Supported | Common | multi-container task → a Pod with a main container plus sidecars, native | | Task definition: cpu / memory | Task definition | Supported | Common | Fargate \{0.25 … 16 vCPU} with tied memory bands → Pod requests / limits: values within node capacity and quota, without Fargate fixed-size combinations | | Task definition: image | Task definition | Supported | Common | Copies the container image to your registry during the build. | #### How it works On AWS your service runs as an **ECS service**: a task definition describes the container image and its sizing, and the service keeps a desired number of copies running on Fargate behind a load balancer. Tensor9 reads that task definition and service and, at the build, turns them into native Kubernetes on **OKE**: a **Deployment** (or a **DaemonSet** for the DAEMON strategy) plus a **Service**, on an enhanced OKE control plane that Oracle operates, with VCN-native pod networking. Kubernetes schedules the same container image. After deployment, clients reach your container through the target load balancer or cluster DNS. Tensor9 translates the stack during the build and does not proxy application traffic.
Before: on AWS your container image runs as an ECS service on Fargate behind an Application Load Balancer. After: on Oracle Cloud the same image runs as a native OKE Deployment (or a DaemonSet) plus a Service, provisioned at the build and reached by OCI's own load balancer. Tensor9 is not in the service's traffic path. Before: on AWS your container image runs as an ECS service on Fargate behind an Application Load Balancer. After: on Oracle Cloud the same image runs as a native OKE Deployment (or a DaemonSet) plus a Service, provisioned at the build and reached by OCI's own load balancer. Tensor9 is not in the service's traffic path.

Your task definition and service are compiled into native Kubernetes on OKE at the build; afterward OCI's own load balancer and runtime carry every request.

#### The task definition and service A **task definition** describes a main container and any helper containers (sidecars). Tensor9 translates them into a multi-container **Pod** and copies the image to your registry during the build. Fargate expresses compute as a CPU value in \{0.25 … 16} vCPU with tied memory bands; on OKE that becomes Pod `requests` and `limits`, which are not restricted to Fargate's fixed size combinations. Requested resources must fit node capacity, scheduler rules and quota. An ECS service keeps a **desired count** of tasks always on. That maps directly to a Deployment's `replicas`, the desired replica count, not a guaranteed number of ready pods; scale-to-zero is not the default and is added only if you want it. **Application Auto Scaling** becomes a **HorizontalPodAutoscaler** on CPU, memory, or a custom metric, and a rolling update maps to the Deployment's own `RollingUpdate` with `maxSurge` and `maxUnavailable`. For other ECS operations, the **DAEMON** strategy becomes a **DaemonSet** (one Pod per node), and **ECS Exec** becomes `kubectl exec` into the Pod.
A task definition with one or more containers, an image, and a cpu-and-memory sizing becomes a Kubernetes Pod spec, and an ECS service with a desired count becomes a Deployment with that many replicas. Fargate cpu and memory become Pod requests and limits within node capacity and quota. The DAEMON strategy becomes a DaemonSet and ECS Exec becomes kubectl exec. A task definition with one or more containers, an image, and a cpu-and-memory sizing becomes a Kubernetes Pod spec, and an ECS service with a desired count becomes a Deployment with that many replicas. Fargate cpu and memory become Pod requests and limits within node capacity and quota. The DAEMON strategy becomes a DaemonSet and ECS Exec becomes kubectl exec.

The task definition becomes a Pod spec and the service becomes a Deployment: containers and sizing land as requests and limits, desiredCount sets replicas, the DAEMON strategy becomes a DaemonSet, and ECS Exec becomes kubectl exec.

#### Networking, discovery, and load balancing ECS's **awsvpc** mode gives each task its own routable IP and security group. OKE offers the same model through **VCN-native pod networking** (`OCI_VCN_IP_NATIVE`): every Pod gets its own individually-addressable VCN IP, and a per-task security group maps to a **NetworkPolicy** to preserve network isolation between tasks. **Service discovery** by name, whether Cloud Map or DNS, is repointed at the build to a Kubernetes **Service** with cluster DNS, which is exactly ECS's name-based model. The service's `loadBalancer` block (the ALB or NLB target group its tasks register into) becomes a Kubernetes **Service** of type LoadBalancer (which provisions an OCI Load Balancer) plus an **Ingress** for host and path routing, with the target-group health check becoming the Pod's readiness probe. For supported routing rules, weights and certificates, see the [Application Load Balancer](/service-adapters/aws/networking-traffic/application-load-balancer) and [Network Load Balancer](/service-adapters/aws/networking-traffic/network-load-balancer) articles. This page places the Pod fleet behind that load balancer. For **ECS Service Connect**, a Service gives you name-based discovery, but Service Connect's injected mesh proxy and its built-in metrics are partial and need an added service mesh (Istio or Linkerd), which you configure separately. #### The OKE control plane Oracle operates an **enhanced cluster** control plane, and nodes come three ways: **managed node pools** (Oracle manages the node lifecycle on your compute), serverless **virtual node pools**, and **self-managed nodes**, with a cluster autoscaler. An ECS **capacity provider** maps to that node-pool layer, and a `FARGATE_SPOT` preference maps to a preemptible node pool . Control-plane patching is Oracle's to run. Oracle manages **cluster add-ons** through the OKE add-ons API: the **cluster autoscaler** scales node pools, the **OCI Native Ingress Controller** backs the Service-plus-Ingress mapping with an OCI Load Balancer, and the **VCN-native pod-networking CNI** is what gives each Pod its routable VCN IP. Enhanced clusters are also what include OKE Workload Identity, the identity binding covered in the next section. A weighted blue-green or canary split still uses a progressive-delivery layer you add (Argo Rollouts or Flagger); a plain rolling update is native.
The OKE control plane, operated by Oracle as an enhanced cluster. Nodes come as managed node pools, serverless virtual node pools, or self-managed nodes, with a cluster autoscaler. Oracle manages cluster addons through the OKE addons API: the cluster autoscaler, the OCI Native Ingress Controller, and the VCN-native pod-networking CNI. Capacity providers map to node pools and Fargate Spot to a preemptible node pool. The OKE control plane, operated by Oracle as an enhanced cluster. Nodes come as managed node pools, serverless virtual node pools, or self-managed nodes, with a cluster autoscaler. Oracle manages cluster addons through the OKE addons API: the cluster autoscaler, the OCI Native Ingress Controller, and the VCN-native pod-networking CNI. Capacity providers map to node pools and Fargate Spot to a preemptible node pool.

OKE runs an enhanced control plane over managed, virtual, or self-managed node pools, and manages cluster addons through its addons API: the cluster autoscaler, the OCI Native Ingress Controller, and the VCN-native pod-networking CNI.

#### Pod identity: the task role The ECS task role identifies the application's AWS API calls. The Max credential path supplies the corresponding AWS role session. Native OCI access uses [OKE workload identity](https://docs.oracle.com/en-us/iaas/Content/ContEng/Tasks/contenggrantingworkloadaccesstoresources.htm): IAM policies match a workload principal's cluster, namespace and service account. This is separate from dynamic groups used for compute-instance principals. The AWS role and the OCI identity still stay consistent through a startup check: at boot the appliance fetches the Pod's real OKE workload identity and proves the declared AWS task role binds to it, failing closed if it does not. Inside the appliance the IAM surface then **reflects** that identity, so `aws sts get-caller-identity` and `iam:GetRole` answer as the task's own role, and calls to other AWS services use the Tensor9 adapters with that role. The **execution role** (the one ECS itself used to pull the image, fetch secrets, and write logs) is not needed, because OKE owns image pull and log delivery with its own identity.
For native OCI access, IAM policies match the workload principal by cluster, namespace and service account. AWS adapter calls separately use the configured task role. For native OCI access, IAM policies match the workload principal by cluster, namespace and service account. AWS adapter calls separately use the configured task role.

Native OCI permissions use workload-principal policies; AWS adapter requests use the configured task role.

#### AWS request authorization For supported AWS API calls, Tensor9 evaluates the task role's IAM policy against the verified caller, action and resource; a denial overrides an allowance. See [AWS request authorization](/service-adapters/aws/compute-containers/ecs#aws-request-authorization) for policy preparation, wildcard support and verified policy delivery. Native OCI permissions are granted separately through OCI workload-principal policies. OKE workload identity selects the principal; it does not replace the AWS task-role policy or its credential exchange. #### Secrets and logging The container's `environment` variables are kept as-is, and its `secrets` references resolve from **OCI Vault** (often via the External Secrets Operator), the same store the SSM equivalence uses. The `awslogs` log driver becomes **OCI Logging**, with stdout and stderr delivered automatically; a custom FireLens log router remains a sidecar, with its destination and credentials configured for the target. #### Limitations Review the remaining service, deployment and networking differences below. △ Where it diverges * **Native OCI access uses workload-principal policies.** Scope those permissions to the cluster, namespace and service account separately from the AWS task-role policy. * **ECS Service Connect's mesh is partial:** a Service handles name-based discovery, but the injected mesh proxy and its built-in metrics need an added service mesh (Istio / Linkerd). * **Weighted blue-green or canary releases require extra tooling:** a plain rolling update is native; a weighted traffic split uses Argo Rollouts or Flagger. * **Capacity providers and Fargate Spot are the cluster's own:** the cluster owns capacity via its node pools, so a capacity-provider strategy maps to node pools and a Spot preference to a preemptible node pool. * **The task-metadata endpoint is served read-only.** `169.254.170.2` (`ECS_CONTAINER_METADATA_URI_V4`), the ECS task-metadata API, is served read-only; other AWS calls use their corresponding adapters. #### Other considerations Plan persistent storage, cluster capacity, logging, and secret access before deployment. * **Migrate mounted volumes separately:** a task definition and service hold no data, and the image is already a container image pulled into your own registry at the build, so adoption is a redeploy; any volume a task mounts (EFS, EBS) moves under its own service card. * **Plan node-pool capacity.** ECS on Fargate exposed no nodes, but on Oracle Cloud the workload lands on an OKE cluster whose managed, virtual, or self-managed node pools are an operational surface, while Oracle operates and patches the enhanced control plane. * **Costs include continuously running nodes.** `desiredCount` becomes always-on replicas billed as running nodes (or serverless virtual nodes), so capacity is sized for steady state rather than metered per Fargate task. * **Telemetry and secrets move to OCI's stores:** logs land in OCI Logging and `secrets` resolve from OCI Vault, so dashboards, alerts, and secret access follow the target services. #### Runtime tasks on OKE OKE schedules tasks on the selected node pools. OCI workload-principal policy controls native OCI access; it does not replace AWS task-role credentials or remove pending-image and capacity conditions. See [runtime task execution](/service-adapters/aws/compute-containers/ecs#runtime-task-execution) for supported task-definition and task-lifecycle calls. ## On Private Kubernetes | Operation | Area | Support | Depth | Notes | | ------------------------------------------------- | --------------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Capacity providers / FARGATE\_SPOT | Cluster | Out of scope | Full surface | the cluster owns capacity via its node pools; FARGATE\_SPOT maps to the cluster's spot node pools | | Identity: task role | Identity | Supported | Common | taskRoleArn → the target's workload identity, keyless; execution role is not needed | | Secrets and environment | Identity | Supported | Common | environment variables are set as-is; secrets map to the target's secret store | | Load balancer: service LB block | Load balancer | Supported | Common | ALB / NLB target group → the cluster's own LB backend; See the load-balancer adapter pages for individual routing-rule support. | | Logging: awslogs | Logging | Supported | Common | awslogs → the target's logging; a FireLens sidecar stays a logging sidecar | | Networking: Service Connect | Networking | Partial | Most usage | a Service gives discovery natively; the injected mesh proxy and its metrics need an added service mesh (Istio / Linkerd) | | Networking: awsvpc (per-task IP + security group) | Networking | Supported | Common | per-task ENI + security group → per-Pod IP + NetworkPolicy: individually addressable, with network isolation between pods; Cloud Map name → the cluster's own Service name | | ECS Exec | Runtime | Supported | Most usage | ECS Exec's interactive shell → kubectl exec into the Pod | | Task-metadata endpoint (runtime) | Runtime | Supported | Common | 169.254.170.2 / ECS\_CONTAINER\_METADATA\_URI\_V4 served read-only, the ECS task-metadata API | | DAEMON scheduling strategy | Service | Supported | Full surface | one copy per host → a DaemonSet, one Pod per node | | Service: autoscaling | Service | Supported | Most usage | Application Auto Scaling → a HorizontalPodAutoscaler on CPU / memory / custom | | Service: deployment controller | Service | Partial | Most usage | rolling update is native (maxSurge / maxUnavailable); weighted blue-green / canary requires release-management tooling (Gateway API / Argo Rollouts / Flagger / service mesh) | | Service: desired count | Service | Supported | Common | desiredCount → Deployment desired replicas; HPA minimum is configured separately | | Task definition: containers / sidecars | Task definition | Supported | Common | multi-container task → main container plus sidecars, native on every target | | Task definition: cpu / memory | Task definition | Supported | Common | Fargate \{0.25 … 16 vCPU} with tied memory bands → Pod requests / limits: values within node capacity and quota, without Fargate fixed-size combinations | | Task definition: image | Task definition | Supported | Common | Copies the container image to your registry during the build. | #### How it works On AWS your service runs as an **ECS service**: a task definition describes the container image and its sizing, and the service keeps a desired number of copies running on Fargate behind a load balancer. Tensor9 reads that task definition and service and, at the build, turns them into native Kubernetes on **a cluster you already operate**: a **Deployment** (or a **DaemonSet** for the DAEMON strategy) plus a **Service**. Kubernetes schedules the same container image. After deployment, clients reach your container through the target load balancer or cluster DNS. Tensor9 translates the stack during the build and does not proxy application traffic.
Before: on AWS your container image runs as an ECS service on Fargate behind an Application Load Balancer. After: on a Kubernetes cluster you already operate the same image runs as a native Deployment (or a DaemonSet) plus a Service, provisioned at the build and reached by the cluster's own load balancer. Tensor9 is not in the service's traffic path. Before: on AWS your container image runs as an ECS service on Fargate behind an Application Load Balancer. After: on a Kubernetes cluster you already operate the same image runs as a native Deployment (or a DaemonSet) plus a Service, provisioned at the build and reached by the cluster's own load balancer. Tensor9 is not in the service's traffic path.

Your task definition and service are compiled into native Kubernetes on a cluster you operate at the build; afterward the cluster's own load balancer and runtime carry every request.

#### The task definition and service A **task definition** describes a main container and any helper containers (sidecars). Tensor9 translates them into a multi-container **Pod** and copies the image to your registry during the build. Fargate expresses compute as a CPU value in \{0.25 … 16} vCPU with tied memory bands; on the target cluster that becomes Pod `requests` and `limits`, which are not restricted to Fargate's fixed size combinations. Requested resources must fit node capacity, scheduler rules and quota. An ECS service keeps a **desired count** of tasks always on. That maps directly to a Deployment's `replicas`, the desired replica count, not a guaranteed number of ready pods; scale-to-zero is not the default and is added only if you want it (KEDA or Knative). **Application Auto Scaling** becomes a **HorizontalPodAutoscaler** on CPU, memory, or a custom metric, and a rolling update maps to the Deployment's own `RollingUpdate` with `maxSurge` and `maxUnavailable`. For other ECS operations, the **DAEMON** strategy becomes a **DaemonSet** (one Pod per node), and **ECS Exec** becomes `kubectl exec` into the Pod.
A task definition with one or more containers, an image, and a cpu-and-memory sizing becomes a Kubernetes Pod spec, and an ECS service with a desired count becomes a Deployment with that many replicas. Fargate cpu and memory become Pod requests and limits within node capacity and quota. The DAEMON strategy becomes a DaemonSet and ECS Exec becomes kubectl exec. A task definition with one or more containers, an image, and a cpu-and-memory sizing becomes a Kubernetes Pod spec, and an ECS service with a desired count becomes a Deployment with that many replicas. Fargate cpu and memory become Pod requests and limits within node capacity and quota. The DAEMON strategy becomes a DaemonSet and ECS Exec becomes kubectl exec.

The task definition becomes a Pod spec and the service becomes a Deployment: containers and sizing become requests and limits, desiredCount becomes replicas, the DAEMON strategy becomes a DaemonSet, and ECS Exec becomes kubectl exec.

#### Networking, discovery, and load balancing ECS's **awsvpc** mode gives each task its own routable IP and security group. On the target cluster your CNI provides the same natively: every Pod gets its own individually-addressable IP, and a per-task security group maps to a **NetworkPolicy** to preserve network isolation between tasks. **Service discovery** by name, whether Cloud Map or DNS, is repointed at the build to a Kubernetes **Service** with cluster DNS, which is exactly ECS's name-based model; a ClusterIP Service plus DNS resolves the name the way Cloud Map did. The service's `loadBalancer` block (the ALB or NLB target group its tasks register into) becomes a Kubernetes **Service** (type LoadBalancer or ClusterIP) plus an **Ingress** for host and path routing, with the target-group health check becoming the Pod's readiness probe. For supported routing rules, weights and certificates, see the [Application Load Balancer](/service-adapters/aws/networking-traffic/application-load-balancer) and [Network Load Balancer](/service-adapters/aws/networking-traffic/network-load-balancer) articles. This page places the Pod fleet behind that load balancer. For ECS Service Connect : a Service provides name-based discovery, but Service Connect's injected mesh proxy and its built-in metrics are partial and need an added service mesh (Istio or Linkerd), which you configure separately. #### The cluster you operate The compiled Deployment and Service run on a cluster **you already operate** (kubeadm, k3s, or kops, or a managed cluster you bring), and it runs on any deployment target the appliance does, including fully disconnected environments. For a self-operated cluster, the customer maintains the **control plane and nodes**, including API server, etcd, upgrades and availability. For a customer-provided managed cluster, its provider retains those responsibilities, and an ECS **capacity provider** maps to your own node pools and cluster-autoscaler, with a `FARGATE_SPOT` preference mapping to your spot node pools. For a self-operated cluster, install and operate the required add-ons; for a customer-provided managed cluster, use its supported managed services. Required components include the ingress controller behind the Service-plus-Ingress mapping, the cluster autoscaler, the metrics or Prometheus stack, and the service mesh that hosts Service Connect's proxy. Confirm which components you operate and which are supplied by the cluster provider. Identity is native Kubernetes ServiceAccount and RBAC, covered next.
Use an existing self-operated or managed cluster. Confirm who operates its control plane, nodes, ingress, autoscaling, metrics, Gateway API and secrets integrations. Disconnected operation requires these dependencies to be available locally. Use an existing self-operated or managed cluster. Confirm who operates its control plane, nodes, ingress, autoscaling, metrics, Gateway API and secrets integrations. Disconnected operation requires these dependencies to be available locally.

Use the existing cluster and confirm who operates its control plane, nodes and add-ons.

#### Pod identity: the task role An ECS task's **task role** is the same idea as EKS's IRSA: the identity the app's own code assumes. On the managed targets that federates to a cloud workload identity through the cluster's OIDC provider, while an existing cluster uses its configured identity integrations. Cluster API identity is native **Kubernetes ServiceAccount and RBAC**, which you own: the ServiceAccount is the workload's identity inside the cluster, and RBAC governs what it may do against the Kubernetes API. Separately, AWS API calls handled by Tensor9 use the selected credential adapter and task-role policy. Calls to real external cloud services need credentials authorized by that cloud, using the cluster's federation or secret configuration. The Tensor9 appliance still **reflects** the task role from the Pod's injected identity, so `aws sts get-caller-identity` and `iam:GetRole` answer as the task's own role. The **execution role** (the one ECS itself used to pull the image, fetch secrets, and write logs) is not needed, because the cluster owns image pull and log delivery.
Kubernetes ServiceAccounts and RBAC govern cluster access. AWS adapter calls use task-role sessions. Real external cloud access uses separately configured credentials or federation. Kubernetes ServiceAccounts and RBAC govern cluster access. AWS adapter calls use task-role sessions. Real external cloud access uses separately configured credentials or federation.

Kubernetes permissions, AWS adapter sessions and real external-cloud credentials have separate scopes.

#### AWS request authorization For supported AWS API calls, Tensor9 evaluates the task role's IAM policy against the verified caller, action and resource; a denial overrides an allowance. See [AWS request authorization](/service-adapters/aws/compute-containers/ecs#aws-request-authorization) for policy preparation, wildcard support and verified policy delivery. Native external cloud permissions are granted separately, using credentials or federation authorized by that provider. Kubernetes RBAC separately governs the cluster API; AWS adapter policy evaluation remains local and can operate without a cloud IAM service. #### Secrets and logging The container's `environment` variables are set as-is, and its `secrets` references resolve from the cluster's own secret store, **Kubernetes Secrets** (often through the External Secrets Operator), the same store the SSM equivalence uses here. The `awslogs` log driver becomes **the target cluster's log stack**, with stdout and stderr delivered by the node's logging; a custom FireLens log router can run as a sidecar; configure its destination and credentials for the target logging service. #### Limitations Review the remaining service, deployment and networking differences below. △ Where it diverges * **ECS Service Connect's mesh is partial:** a Service provides name-based discovery, but the injected mesh proxy and its built-in metrics need an added service mesh (Istio / Linkerd) that you run. * **Weighted blue-green or canary releases require extra tooling:** a plain rolling update is native; a weighted traffic split uses Gateway API, Argo Rollouts, or Flagger. * **Capacity providers and Fargate Spot are the target cluster's own:** the cluster owns capacity via its node pools, so a capacity-provider strategy maps to your node pools and a Spot preference to your spot node pools. * **Configure each credential path.** Kubernetes RBAC governs cluster access. AWS adapters use the task role; real external services require credentials or federation authorized by their provider. * **The task-metadata endpoint is served read-only.** `169.254.170.2` (`ECS_CONTAINER_METADATA_URI_V4`), the ECS task-metadata API, is served read-only; other AWS calls use their corresponding adapters. #### Other considerations Plan persistent storage, cluster capacity, logging, and secret access before deployment. * **Migrate mounted volumes separately:** a task definition and service hold no data, and the image is already a container image pulled into your own registry at the build, so adoption is a redeploy; any volume a task mounts (EFS, EBS) moves under its own service card. * **Confirm cluster operating responsibilities.** For a self-operated cluster you maintain the control plane, nodes and add-ons. A customer-provided managed cluster retains its provider's control-plane and add-on services. * **Cost is your own infrastructure.** `desiredCount` becomes always-on replicas running on nodes you provision, sized by your node pools rather than metered per Fargate task. * **Telemetry and secrets are the cluster's own:** logs flow through the node's log stack and `secrets` resolve from Kubernetes Secrets (often via External Secrets), so there is no CloudWatch or Secrets Manager in the path; observability is whatever the cluster runs. #### Runtime tasks on your cluster Your cluster supplies scheduling, capacity and task execution. Kubernetes RBAC controls its API; selected credentials separately authorize AWS adapter calls. Check task readiness and container exits rather than treating request acceptance as successful execution. See [runtime task execution](/service-adapters/aws/compute-containers/ecs#runtime-task-execution) for supported task-definition and task-lifecycle calls. [Service Catalog](/service-adapters/catalog). # EKS Source: https://docs.tensor9.com/service-adapters/aws/compute-containers/eks Managed Kubernetes, where AWS runs and patches the control plane while worker nodes run in your VPC. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of EKS with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | EKS | Google Cloud | Azure | OCI | | ----------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Control plane · who operates it | EKS managed control plane | GKE Standard, regional (control plane across multiple zones), Google-operated | azurerm\_kubernetes\_cluster, Microsoft-operated; SystemAssigned cluster identity | oci\_containerengine\_cluster ENHANCED\_CLUSTER, Oracle-operated | | Kubernetes version · version selection | requested minor version (e.g. 1.29) | release channel (STABLE); the build reports the selected minor version, which may differ from the requested version | - | - | | Node groups → node pools · machine mapping | aws\_eks\_node\_group (instance\_types) | one node pool per group; same hardware class (m5→n2/e2, c5→c2, arm→t2a, GPU→g2/L4; default e2-medium); region-wide autoscaling | - | - | | Workload identity (IRSA → WI) | Yes - IRSA (OIDC + AssumeRoleWithWebIdentity) | Yes - GKE Workload Identity | - | Partial - OKE Workload Identity, the OCI-native analog: workload-principal policies scoped to cluster, namespace and service account | | Private cluster endpoint | Yes | Partial - public control-plane endpoint; a private endpoint is not part of this equivalence, surfaced | Partial - public control-plane endpoint; a private endpoint is not part of this equivalence, surfaced | Yes - private in this mapping; a public-access origin gets a network-access warning, not an apply failure | | Add-ons · core vs custom | EKS add-ons (vpc-cni/coredns/kube-proxy/ebs-csi) | GKE platform components (managed for you); a non-core add-on with no analog is dropped and surfaced | - | - | | API coverage | full | high | high | high | | Kubernetes version · version selection | requested minor version | - | AKS recommended default; the build reports the selected minor version, which may differ from the requested version | a supported patch of your requested minor version; exact patch chosen at plan time, surfaced | | Node groups → node pools · machine mapping | aws\_eks\_node\_group | - | default\_node\_pool + one node pool per additional group; VM size in the same hardware class, default Standard\_D2s\_v5 | - | | Workload identity (IRSA → WI) | Yes - IRSA | - | Yes - Azure Workload Identity: OIDC issuer + UAMI + federated identity credential | - | | Add-ons · core vs custom | EKS add-ons | - | AKS platform components (managed for you); non-core add-on with no analog dropped and surfaced | - | | Node groups → node pools · shapes + autoscaling | aws\_eks\_node\_group (scaling\_config min/max) | - | - | one node pool per group; flex shapes specify ocpus + memory; the desired size is applied, min/max bounds are unsupported on the node-pool resource; the build reports their omission | | Pod networking · CNI | VPC-CNI | - | - | VCN-native pod IPs (OCI\_VCN\_IP\_NATIVE) | ### Infrastructure-only adaptation | Capability | EKS | Google Cloud | Azure | OCI | Private Kubernetes | | ----------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | | Control plane · who operates it | EKS managed control plane | GKE Standard, regional (control plane across multiple zones), Google-operated | azurerm\_kubernetes\_cluster, Microsoft-operated; SystemAssigned cluster identity | oci\_containerengine\_cluster ENHANCED\_CLUSTER, Oracle-operated | your existing cluster; you operate the API server, and it is not provisioned | | Kubernetes version · version selection | requested minor version (e.g. 1.29) | release channel (STABLE); the build reports the selected minor version, which may differ from the requested version | - | - | - | | Node groups → node pools · machine mapping | aws\_eks\_node\_group (instance\_types) | one node pool per group; same hardware class (m5→n2/e2, c5→c2, arm→t2a, GPU→g2/L4; default e2-medium); region-wide autoscaling | - | - | - | | Workload identity (IRSA → WI) | Yes - IRSA (OIDC + AssumeRoleWithWebIdentity) | Yes - GKE Workload Identity | - | Partial - OKE Workload Identity, the OCI-native analog: workload-principal policies scoped to cluster, namespace and service account | - | | Private cluster endpoint | Yes | Partial - public control-plane endpoint; a private endpoint is not part of this equivalence, surfaced | Partial - public control-plane endpoint; a private endpoint is not part of this equivalence, surfaced | Yes - private in this mapping; a public-access origin gets a network-access warning, not an apply failure | - | | Add-ons · core vs custom | EKS add-ons (vpc-cni/coredns/kube-proxy/ebs-csi) | GKE platform components (managed for you); a non-core add-on with no analog is dropped and surfaced | - | - | - | | API coverage | full | high | high | high | high | | Kubernetes version · version selection | requested minor version | - | AKS recommended default; the build reports the selected minor version, which may differ from the requested version | a supported patch of your requested minor version; exact patch chosen at plan time, surfaced | - | | Node groups → node pools · machine mapping | aws\_eks\_node\_group | - | default\_node\_pool + one node pool per additional group; VM size in the same hardware class, default Standard\_D2s\_v5 | - | - | | Workload identity (IRSA → WI) | Yes - IRSA | - | Yes - Azure Workload Identity: OIDC issuer + UAMI + federated identity credential | - | - | | Add-ons · core vs custom | EKS add-ons | - | AKS platform components (managed for you); non-core add-on with no analog dropped and surfaced | - | - | | Node groups → node pools · shapes + autoscaling | aws\_eks\_node\_group (scaling\_config min/max) | - | - | one node pool per group; flex shapes specify ocpus + memory; the desired size is applied, min/max bounds are unsupported on the node-pool resource; the build reports their omission | - | | Pod networking · CNI | VPC-CNI | - | - | VCN-native pod IPs (OCI\_VCN\_IP\_NATIVE) | - | | Node groups · who owns the nodes | aws\_eks\_node\_group | - | - | - | your nodes; node-group provisioning is owned by you | | Workload identity (IRSA) | Yes - IRSA (cloud-federated) | - | - | - | Partial - Kubernetes RBAC for cluster access; selected STS adapter for AWS role sessions; no native cloud binding provisioned | | Cluster authentication · how Tensor9 reaches it | data.aws\_eks\_cluster\_auth token | - | - | - | the client cert / token you supply with the existing cluster | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------- | ------------------ | ------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DescribeCluster / ListClusters | EKS management API | Supported | Common | if a workload calls the EKS management API at runtime, it is translated to the target's managed-Kubernetes API | | DescribeNodegroup / ListNodegroups | EKS management API | Supported | Most usage | answered against the compiled GKE node pools | | CreateCluster / DeleteCluster / UpdateClusterVersion | Provisioning | Supported | Most usage | Maximum adaptation accepts supported cluster lifecycle requests and reconciles them onto GKE. Creation and deletion complete asynchronously; describes expose observed readiness. | | Fargate profile | Serverless nodes | Out of scope | Full surface | no GKE-Standard analog; fargate-selected pods reschedule onto real node pools, and a Fargate-only cluster stops the build | | In-cluster Kubernetes API (client-go / in-cluster config) | Workload | Supported | Full surface | native GKE API server: standard workload verbs and objects; cloud-specific annotations and controllers need target configuration | #### The GKE control plane Tensor9 translates your EKS cluster definition into a **regional GKE Standard** cluster. Google operates the Kubernetes control plane: its API server, controllers, upgrades, availability, and scaling. The regional control plane spans multiple zones, preserving EKS's availability across zones. Two GKE choices matter here. GKE can run in **Autopilot** mode, where Google also owns the nodes and you never size a pool, or **Standard** mode, where you keep node pools and their machine types. Tensor9 compiles to **Standard**, so your EKS node-group sizing maps across one-to-one rather than being handed to a fully-managed node fleet. The second choice is the version: EKS lets you select an exact minor, while GKE keeps the control plane on a **release channel** and holds a supported version near your requested version. You get a channel-managed version, not the exact number, and Tensor9 surfaces that at the build rather than letting you assume an exact match.
The EKS API server, AWS-managed with a requested minor version, maps to a regional GKE Standard control plane that Google operates and that uses a release channel. Both are conformant Kubernetes. The EKS API server, AWS-managed with a requested minor version, maps to a regional GKE Standard control plane that Google operates and that uses a release channel. Both are conformant Kubernetes.

Tensor9 provisions a Google-operated, regional GKE Standard cluster; the exact minor version uses a release channel, surfaced at the build.

#### The managed platform: add-ons and APIs GKE offers managed services alongside Kubernetes: capabilities Google operates that go beyond plain Kubernetes. Its control plane exposes the **Gateway API** through Google's built-in GKE Gateway controller, and Config Connector lets a cluster manage Google Cloud resources as Kubernetes objects. On the operations side, Google ships **Managed Service for Prometheus**, **Backup for GKE**, and the managed persistent-disk and Filestore **CSI drivers** as first-party add-ons, and the cluster autoscaler can grow the fleet through **node auto-provisioning**. These are what GKE makes available; Tensor9 does not switch them on for you. What the translation actually compiles from your EKS definition is the cluster, its node pools, Workload Identity, and VPC-native networking, with the **core EKS add-ons** you rely on (the CNI, CoreDNS, kube-proxy, and the CSI driver) re-provided as GKE's own platform components. A non-core EKS add-on with no GKE analog is dropped and surfaced at the build rather than silently discarded. #### Kubernetes workloads and cloud integrations GKE supports the Kubernetes workload API. Deployments, Services and other standard resources remain usable; review AWS-specific identity annotations, storage classes, ingress controllers and add-ons in manifests and Helm values. #### Nodes and scaling Each EKS node group becomes one GKE **node pool**, and the instance type maps to a Google machine type by a rule that preserves the hardware class: general-purpose (m5) becomes n2 or e2, compute-optimized (c5) becomes c2, Graviton (ARM) becomes t2a, and a GPU node becomes g2 with an L4 accelerator, defaulting to e2-medium when nothing more specific fits. The workload class you chose on AWS is preserved, so a compute-optimized group never quietly lands on a general-purpose machine. Your node-group scaling settings become GKE's **cluster autoscaler** with region-wide minimum and maximum bounds, and Spot or preemptible nodes remain interruptible. Unsupported accelerator requirements fail during the build: an oversized accelerator such as the A100 or H100 classes has no clean machine analog, so the build stops with a clear error rather than substituting a smaller GPU; a lesser GPU swap that does have an analog still reaches you as a surfaced warning, so a change in accelerator class is never silent. #### Identity and authorization (IRSA → GKE Workload Identity) On EKS, **IRSA** (IAM Roles for Service Accounts) does two jobs at once: it gives a pod an AWS **identity** (a role it runs as) and it governs that role's **permissions**. Tensor9 preserves the role identity and evaluates its permissions separately. **Role identity.** Your workload's identity reads (`aws sts get-caller-identity`, `iam:GetRole`) are answered **in-appliance** from the identity injected into the pod, so they return your own role and account (the same account the instance metadata serves) without contacting AWS. On GKE that reflected identity is **bound to a real cloud identity**: Kubernetes ServiceAccount (KSA) is tied to a Google service account (GSA) through GKE **Workload Identity** (the KSA holds the `iam.gke.io/gcp-service-account` annotation), and at boot the appliance **verifies** the pod really runs as that GSA before it serves an identity; an unbound pod fails closed and is pulled from service rather than presenting a role it cannot prove. A migrated EKS-plus-IRSA pod has its `get-caller-identity` answered on real GKE with the pod's own role. **AWS request authorization.** Tensor9 prepares the supported AWS IAM policy rules at build time and evaluates the verified role, requested action and resource inside the appliance. A denial overrides an allowance; policy changes arrive through verified policy delivery. Native Google Cloud permissions are granted separately to the Google service account. Those grants do not replace the AWS policy or supply its role session. The selected STS or EKS Auth adapter separately supplies AWS credentials for supported AWS API calls.
IRSA includes an identity and permissions. Identity: your IRSA role is reflected in-appliance and bound to GKE Workload Identity, so get-caller-identity answers your own role. Authorization: Tensor9 evaluates supported AWS IAM policy rules for AWS adapter requests; a denial overrides an allowance. Native provider permissions are granted separately. IRSA includes an identity and permissions. Identity: your IRSA role is reflected in-appliance and bound to GKE Workload Identity, so get-caller-identity answers your own role. Authorization: Tensor9 evaluates supported AWS IAM policy rules for AWS adapter requests; a denial overrides an allowance. Native provider permissions are granted separately.

IRSA identity and permissions: your identity is reflected in-appliance and bound to GKE Workload Identity; your AWS IAM policy authorizes supported adapter requests, with deny precedence. Native provider grants remain separate.

#### Limitations Almost everything maps onto the GKE cluster: the Kubernetes workload, the managed control plane, node pools, VPC-native pod networking (alias-IP, with your pod and service secondary ranges), and workload identity bound to a real cloud identity. What has no counterpart is named here and surfaced at the build, never dropped silently. △ Where EKS and GKE diverge * **Fargate serverless nodes have no GKE-Standard analog.** Fargate-selected pods reschedule onto real node pools, and a Fargate-only cluster stops the build rather than pretending to have serverless capacity. * **The Kubernetes version may change.** GKE uses a release channel, so you get a supported version near your requested minor version, surfaced at the build. * **The control-plane endpoint is public.** A private GKE control-plane endpoint is not part of this equivalence; the public endpoint is surfaced rather than assumed. * **The AWS cluster endpoint, ARN, and token have no equivalent.** References are repointed to GKE's own endpoint and certificate authority, or the build stops rather than binding a value that points at nothing. #### Other considerations Account for the following operating requirements. * **Review cloud integrations in manifests.** Standard Kubernetes workload resources remain usable; adapt identity annotations, storage classes and controllers to the selected target. * **Google operates the control plane; you still run the nodes:** upgrades, API-server availability, and control-plane scaling become Google's, while node-pool sizing, capacity, and cost stay yours to set on Google Cloud. * **Migrate persistent data separately:** a PersistentVolume rebinds to GKE's persistent-disk or Filestore CSI, but the durable data behind it is migrated or re-provisioned separately; the cluster otherwise comes up empty of it. * **Account for cluster and node charges:** a regional GKE Standard control plane has an hourly management charge, and you pay for the node pools you size rather than a serverless node fleet, so set the autoscaler bounds for the load you expect. #### Runtime cluster management and credentials With maximum adaptation, the EKS management API handles supported cluster lifecycle requests at runtime on managed Kubernetes targets. A self-managed target continues to use the existing cluster described in its target-specific configuration. The adapter stores accepted cluster, node-group, add-on and pod-identity-association requests. Reconciliation applies the corresponding configuration to the target Kubernetes service. Creation and deletion remain asynchronous: a returned cluster record is not proof that its endpoint is ready, and describes reflect the observed target state. Kubernetes workload requests and AWS credential exchanges are separate from cluster management. Native target workload identity authorizes access to that cloud. An unchanged AWS SDK instead needs the selected STS or EKS Auth adapter to obtain an AWS session for the configured IAM role. Creating a cloud identity alone does not perform that exchange. Confirm the cluster issuer, service-account binding and role trust together, then test the application's actual API calls. ## On Azure | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------- | ------------------ | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------- | | DescribeCluster / ListClusters | EKS management API | Supported | Common | if a workload calls the EKS management API at runtime, it is translated to the target's managed-Kubernetes API | | CreateCluster / DeleteCluster / node pools | Provisioning | Supported | Most usage | Max lifecycle requests reconcile an AKS cluster and node pools; readiness is asynchronous and target settings apply | | Fargate profile | Serverless nodes | Out of scope | Full surface | no AKS analog; pods reschedule onto node pools; Fargate-only cluster stops the build | | In-cluster Kubernetes API (client-go / in-cluster config) | Workload | Supported | Full surface | native AKS API server: standard workload verbs and objects; cloud-specific integrations need target configuration | #### The AKS control plane Tensor9 translates your EKS cluster definition into an **AKS** cluster. Microsoft operates its Kubernetes control plane, including upgrades, availability, and API-server scaling. The cluster uses a system-assigned **managed identity** in Microsoft Entra ID to access Azure resources. Two AKS traits shape this equivalent. Upgrades are **managed**: AKS drives control-plane upgrades and holds you on a supported version, so rather than selecting an exact Kubernetes minor as EKS lets you, the cluster rides AKS's **recommended default** near your requested version, surfaced at the build. And AKS structures nodes as a required **system node pool** (which runs the cluster's own system pods) plus any number of **user node pools** for your workloads, which receive your translated EKS node groups.
The EKS API server, AWS-managed with a requested minor version, maps to an AKS control plane that Microsoft operates with a Microsoft Entra managed identity. AKS drives control-plane upgrades and rides its recommended default version. Both are conformant Kubernetes. The EKS API server, AWS-managed with a requested minor version, maps to an AKS control plane that Microsoft operates with a Microsoft Entra managed identity. AKS drives control-plane upgrades and rides its recommended default version. Both are conformant Kubernetes.

Tensor9 provisions a Microsoft-operated AKS cluster with a managed Entra identity; upgrades are managed and the version uses AKS's recommended default, surfaced at the build.

#### The managed platform: add-ons and APIs AKS offers managed add-ons and cluster extensions : first-party capabilities Microsoft operates on top of the control plane. Event-driven autoscaling ships as the managed **KEDA** add-on; **Dapr** and GitOps with Flux are managed cluster extensions; **Azure Policy** enforces guardrails on the cluster; **Container Insights** feeds metrics and logs to Azure Monitor; the Key Vault Secrets Store CSI add-on mounts secrets from Azure Key Vault; and the **application-routing** add-on runs a managed NGINX ingress. AKS also offers a **virtual-node** option that schedules pods onto Azure Container Instances through a virtual kubelet. These are what AKS makes available; Tensor9 does not switch them on for you. What the translation compiles from your EKS definition is the cluster, its system and user node pools, Azure Workload Identity, and native pod networking, with your **core EKS add-ons** (the CNI, CoreDNS, kube-proxy, and the CSI driver) re-provided as AKS's platform components. A non-core EKS add-on with no AKS analog is dropped and surfaced at the build. #### Kubernetes workloads and cloud integrations The target exposes the Kubernetes workload API for Deployments, Services, ConfigMaps, StatefulSets and other supported resources. Review AWS-specific identity annotations, storage classes, ingress controllers and add-ons when adapting manifests and Helm values. Kubernetes conformance does not make those cloud integrations interchangeable. #### Nodes and scaling Each EKS node group becomes an AKS **node pool** (the first is the required system pool, the rest are user pools) and the instance type maps to an Azure VM size by a rule that preserves the hardware class, defaulting to `Standard_D2s_v5` when nothing more specific fits. Your node-group scaling settings become the AKS **cluster autoscaler**, and a spot posture comes across. The **virtual-node** option above is a distinct model, not a drop-in for EKS Fargate: Fargate profiles have no AKS analog and their pods reschedule onto real node pools. An oversized accelerator with no clean VM analog stops the build rather than downgrading silently, so a change in machine class is never silent. #### Identity and authorization (IRSA → Microsoft Entra Workload ID) On EKS, **IRSA** (IAM Roles for Service Accounts) does two jobs at once: it gives a pod an AWS **identity** (a role it runs as) and it governs that role's **permissions**. Tensor9 preserves the role identity and evaluates its permissions separately. **Role identity.** Your workload's identity reads (`aws sts get-caller-identity`, `iam:GetRole`) are answered **in-appliance** from the identity injected into the pod, so they return your own role and account (the same account the instance metadata serves) without contacting AWS. On AKS that reflected identity is **bound to a real cloud identity** through Microsoft Entra Workload ID : the cluster's OIDC issuer is enabled and Kubernetes ServiceAccount is federated to a **user-assigned managed identity** in Entra ID by a federated identity credential (with the `azure.workload.identity` annotations marking the pod), so the pod exchanges its ServiceAccount token for Entra tokens with no long-lived secret. **AWS request authorization.** Tensor9 prepares the supported AWS IAM policy rules at build time and evaluates the verified role, requested action and resource inside the appliance. A denial overrides an allowance; policy changes arrive through verified policy delivery. Native Azure permissions are granted separately to the managed identity. Those grants do not replace the AWS policy or supply its role session. The selected STS or EKS Auth adapter separately supplies AWS credentials for supported AWS API calls.
IRSA includes an identity and permissions. Identity: your IRSA role is reflected in-appliance and bound to Microsoft Entra Workload ID, so get-caller-identity answers your own role. Authorization: Tensor9 evaluates supported AWS IAM policy rules for AWS adapter requests; a denial overrides an allowance. Native provider permissions are granted separately. IRSA includes an identity and permissions. Identity: your IRSA role is reflected in-appliance and bound to Microsoft Entra Workload ID, so get-caller-identity answers your own role. Authorization: Tensor9 evaluates supported AWS IAM policy rules for AWS adapter requests; a denial overrides an allowance. Native provider permissions are granted separately.

IRSA identity and permissions: your identity is reflected in-appliance and bound to Microsoft Entra Workload ID; your AWS IAM policy authorizes supported adapter requests, with deny precedence. Native provider grants remain separate.

#### Limitations Almost everything maps onto the AKS cluster: the Kubernetes workload, the managed control plane, system and user node pools, native pod networking, and workload identity bound per workload. What has no counterpart is named here and surfaced at the build, never dropped silently. △ Where EKS and AKS diverge * **Fargate serverless nodes have no AKS analog.** Fargate-selected pods reschedule onto real node pools, and a Fargate-only cluster stops the build rather than pretending to have serverless capacity. * **The Kubernetes version may change.** AKS rides its recommended default, so you get a supported version near your requested minor version, surfaced at the build. * **The control-plane endpoint is public.** A private AKS control-plane endpoint is not part of this equivalence; the public endpoint is surfaced rather than assumed. * **The AWS cluster endpoint, ARN, and token have no equivalent.** References are repointed to the AKS endpoint and certificate authority, or the build stops rather than binding a value that points at nothing. #### Other considerations Account for the following operating requirements. * **Review cloud integrations in manifests.** Standard Kubernetes workload resources remain usable; adapt identity annotations, storage classes and controllers to the selected target. * **Microsoft operates the control plane, and a system node pool is now required.** AKS mandates a system pool for the cluster's own pods alongside your user pools: capacity and cost you did not size on EKS. * **Workload identity is set up per workload:** each ServiceAccount is federated to a user-assigned managed identity in Entra ID, so a migrated pod gets the workload-identity annotations rather than an IRSA role annotation. * **Account for cluster and node charges:** the AKS control plane is free on the base tier (the Uptime-SLA tier is paid), and you pay for the node pools you run including the required system pool; size the autoscaler bounds for expected load. #### Management and credentials on AKS Supported cluster, node-group, add-on and association changes reconcile onto AKS asynchronously. Entra Workload ID grants access only through separately configured Azure permissions; AWS SDK calls need the selected AWS credential exchange. See [cluster management and credential separation](/service-adapters/aws/compute-containers/eks#runtime-cluster-management-and-credentials) for the common model and its managed-target restriction. ## On OCI | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------- | ------------------ | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------- | | DescribeCluster / ListClusters | EKS management API | Supported | Common | if a workload calls the EKS management API at runtime, it is translated to the target's managed-Kubernetes API | | Node-group autoscaling bounds | Node pools | Partial | Most usage | the desired size is applied; cluster-autoscaler min/max is unsupported on the node-pool resource; the build reports its omission | | CreateCluster / DeleteCluster / node pools | Provisioning | Supported | Most usage | Max lifecycle requests reconcile an enhanced OKE cluster and node pools; readiness is asynchronous and target settings apply | | Fargate profile | Serverless nodes | Out of scope | Full surface | no OKE analog; pods reschedule onto node pools; Fargate-only cluster stops the build | | In-cluster Kubernetes API (client-go / in-cluster config) | Workload | Supported | Full surface | native OKE API server: standard workload verbs and objects; cloud-specific integrations need target configuration | #### The OKE control plane On Oracle Cloud the control plane is run by **OKE**, and Tensor9 provisions it as an **enhanced cluster**, OKE's fuller tier, the one that includes the cluster autoscaler, workload identity, and higher node ceilings. Oracle operates the control plane, and its API-server endpoint is **private in this mapping**, so the cluster is reached over your own VCN by default rather than a public address. OKE gives you more than one way to run nodes under that control plane: **managed node pools**, where OKE manages the Kubernetes components on nodes in your tenancy; fully OKE-run virtual nodes , where Oracle runs the node capacity for you; and self-managed nodes. Tensor9 compiles your EKS node groups into **managed node pools**, so the sizing you chose maps directly. On the version, EKS lets you pin an exact minor; OKE selects a **supported patch** of that requested minor version, chosen at plan time and surfaced.
The EKS API server, AWS-managed with a requested minor version, maps to an enhanced OKE control plane that Oracle operates with a private endpoint in this mapping. OKE selects a supported patch of your requested minor version. Both are conformant Kubernetes. The EKS API server, AWS-managed with a requested minor version, maps to an enhanced OKE control plane that Oracle operates with a private endpoint in this mapping. OKE selects a supported patch of your requested minor version. Both are conformant Kubernetes.

Tensor9 provisions an Oracle-operated enhanced OKE cluster with a private endpoint in this mapping; the version selects a supported patch of your requested minor version, surfaced at the build.

#### The managed platform: add-ons and APIs Tensor9 provisions OKE's **enhanced-cluster** tier. It includes managed cluster add-ons that Oracle operates and version-manages (the cluster autoscaler among them) along with **OKE Workload Identity** and higher node-count ceilings. Under the control plane, OKE lets you run nodes as managed node pools, fully OKE-run virtual nodes, or self-managed nodes, and its API-server endpoint is private in this mapping, reached over your own VCN. These are what the enhanced OKE cluster makes available; what Tensor9 compiles from your EKS definition is the enhanced cluster, its managed node pools on flexible shapes, the OCI-native workload-identity binding, and VCN-native pod networking, with your **core EKS add-ons** re-provided as OKE's platform components. A non-core EKS add-on with no OKE analog is dropped and surfaced at the build. #### Kubernetes workloads and cloud integrations The target exposes the Kubernetes workload API for Deployments, Services, ConfigMaps, StatefulSets and other supported resources. Review AWS-specific identity annotations, storage classes, ingress controllers and add-ons when adapting manifests and Helm values. Kubernetes conformance does not make those cloud integrations interchangeable. #### Nodes and scaling Each EKS node group becomes one OKE **node pool** on an OCI **flexible shape**, which takes your core (OCPU) and memory sizing directly rather than rounding to a fixed instance size. Pod networking is **VCN-native** (pods get IPs from your VCN) matching the routable-pod-IP model EKS gives you. There is one node-pool limit specific to OKE: its cluster-autoscaler minimum and maximum bounds have no node-pool-resource analog, so the desired size is provisioned and the min/max bounds are dropped and surfaced. An oversized accelerator with no clean shape analog stops the build rather than downgrading silently. #### Identity and authorization (IRSA → OCI workload identity) On EKS, **IRSA** (IAM Roles for Service Accounts) does two jobs at once: it gives a pod an AWS **identity** (a role it runs as) and it governs that role's **permissions**. Tensor9 preserves the role identity and evaluates its permissions separately. **Role identity.** Your workload's identity reads (`aws sts get-caller-identity`, `iam:GetRole`) are answered **in-appliance** from the identity injected into the pod, so they return your own role and account (the same account the instance metadata serves) without contacting AWS. On OKE that reflected identity is **bound to a real cloud identity** through OCI workload identity . OCI IAM policies select the workload principal by cluster, namespace and service account. These native grants are separate from the AWS role policy and credential exchange. **AWS request authorization.** Tensor9 prepares the supported AWS IAM policy rules at build time and evaluates the verified role, requested action and resource inside the appliance. A denial overrides an allowance; policy changes arrive through verified policy delivery. Native OCI permissions are granted separately to the workload principal. Those grants do not replace the AWS policy or supply its role session. The selected STS or EKS Auth adapter separately supplies AWS credentials for supported AWS API calls.
IRSA includes an identity and permissions. Identity: your IRSA role is reflected in-appliance and bound to OCI workload identity, so get-caller-identity answers your own role. Authorization: Tensor9 evaluates supported AWS IAM policy rules for AWS adapter requests; a denial overrides an allowance. Native provider permissions are granted separately. IRSA includes an identity and permissions. Identity: your IRSA role is reflected in-appliance and bound to OCI workload identity, so get-caller-identity answers your own role. Authorization: Tensor9 evaluates supported AWS IAM policy rules for AWS adapter requests; a denial overrides an allowance. Native provider permissions are granted separately.

IRSA identity and permissions: your identity is reflected in-appliance and bound to OCI workload identity; your AWS IAM policy authorizes supported adapter requests, with deny precedence. Native provider grants remain separate.

#### Limitations Almost everything maps onto the OKE cluster: the Kubernetes workload, the enhanced managed control plane, node pools on flexible shapes, VCN-native pod networking, and a private endpoint in this mapping. What has no counterpart is named here and surfaced at the build, never dropped silently. △ Where EKS and OKE diverge * **Fargate serverless nodes have no OKE analog.** Fargate-selected pods reschedule onto real node pools, and a Fargate-only cluster stops the build rather than pretending to have serverless capacity. * **The Kubernetes version may change.** OKE selects a supported patch of your requested minor version, chosen at plan time and surfaced at the build. * **Cluster-autoscaler bounds have no node-pool analog.** The desired size is kept; the autoscaler minimum and maximum are dropped and surfaced. * **The AWS cluster endpoint, ARN, and token have no equivalent.** References are repointed to the OKE endpoint and certificate authority, or the build stops rather than binding a value that points at nothing. #### Other considerations Account for the following operating requirements. * **Deploy workloads onto the enhanced OKE cluster.** Review OCI identity bindings, storage classes and VCN networking alongside Kubernetes manifests. * **This mapping selects a private control-plane endpoint.** OKE reaches the API server over your own VCN, so cluster administration needs in-VCN reachability (a bastion or private access) rather than a public endpoint. * **Autoscaler bounds have no counterpart:** the desired node count is provisioned, but the cluster-autoscaler minimum and maximum have no node-pool analog, so scaling limits are re-established on OCI after cutover. * **Account for cluster and node charges.** OKE's enhanced tier has a per-cluster charge and you pay for the managed node pools you size on flexible shapes (OCPU and memory directly), rather than an EKS control-plane fee plus fixed instance types. #### Management and credentials on OKE Supported cluster and node-pool changes reconcile onto OKE asynchronously. OCI workload identity and its native grants remain separate from STS or EKS Auth role sessions used for AWS adapter calls. See [cluster management and credential separation](/service-adapters/aws/compute-containers/eks#runtime-cluster-management-and-credentials) for the common model and its managed-target restriction. ## On Private Kubernetes | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------- | ---------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Workload identity (IRSA) | Identity | Partial | Full surface | Kubernetes ServiceAccounts and RBAC govern cluster access. The selected STS service adapter supplies supported AWS role sessions; this mapping does not provision a native cloud identity binding. Real external-cloud access requires separately authorized credentials or federation. | | Cluster provisioning (control plane / node groups) | Provisioning | Out of scope | Full surface | not provisioned: you bring the API server and the nodes; Tensor9 emits a reference to your existing cluster's endpoint and auth | | Fargate profile | Serverless nodes | Out of scope | Full surface | no analog; you run the nodes | | In-cluster Kubernetes API (client-go / in-cluster config) | Workload | Supported | Full surface | your existing cluster's API server: standard workload verbs and objects; cloud-specific integrations need target configuration | #### The control plane you run Tensor9 uses an existing Kubernetes cluster in the customer's environment, such as kubeadm, k3s, or kops. It generates references to that cluster's endpoint and authentication; it creates no control plane or node pools. The customer operates the API server, maintains control-plane availability, upgrades Kubernetes, and provisions and replaces nodes. They also supply the capacity your workloads need.
Unlike the managed targets, nothing is provisioned here. The EKS control plane is repointed at the build onto a Kubernetes cluster you already operate (kubeadm, k3s, or kops) that you run yourself. Tensor9 references that cluster's endpoint and authentication. Unlike the managed targets, nothing is provisioned here. The EKS control plane is repointed at the build onto a Kubernetes cluster you already operate (kubeadm, k3s, or kops) that you run yourself. Tensor9 references that cluster's endpoint and authentication.

Nothing is provisioned here: the EKS cluster is redirected onto a Kubernetes cluster you already operate, and Tensor9 references its endpoint and authentication.

#### Cluster add-ons The customer installs and operates the cluster's ingress controller, autoscaler, monitoring, secrets driver, and backup service. Tensor9 deploys workloads onto the existing cluster without adding those services. The API server, scheduler, controller-manager, and existing networking (CNI), DNS, and storage (CSI) components stay in place. #### Kubernetes workloads and cloud integrations Deploy the workload onto the existing cluster's Kubernetes API. Match API versions, identity annotations, storage classes and installed controllers to that cluster; AWS-specific integrations may need different manifests or Helm values. #### Node capacity The nodes are yours. There is no machine-type mapping to a cloud instance family and no cloud autoscaler wired for you, because **you own node provisioning and lifecycle**: you decide how nodes are added, sized, and retired, and you run an autoscaler (cluster-autoscaler, Karpenter, or none) if you want one. Tensor9 does not size or scale the fleet; it schedules your workloads onto the capacity you already run. #### Cluster access and AWS credentials **Kubernetes API access.** ServiceAccounts and RBAC govern what workloads may do against the existing cluster's API. They do not by themselves supply AWS credentials. **AWS adapter credentials.** With IRSA (IAM Roles for Service Accounts), the AWS SDK sends a projected service-account token to the selected STS adapter through `AssumeRoleWithWebIdentity`. That exchange supplies a supported AWS role session for calls handled by Tensor9. Pod Identity uses the selected EKS Auth adapter's `AssumeRoleForPodIdentity` exchange. Configure the cluster issuer, service-account binding and IAM role trust for the selected exchange. **Real external-cloud access.** Calls to real external-cloud services need credentials or federation authorized by that cloud. This mapping does not provision a native cloud identity binding; configure that access using the cluster's identity integration or mounted credentials. **AWS adapter permissions.** Tensor9 evaluates the role's AWS policies for API calls handled by its service adapters. An explicit deny overrides an allow. Kubernetes RBAC and the external cloud's own authorization govern their respective APIs.
On an existing Kubernetes cluster, the selected STS adapter exchanges a service-account token for a supported AWS role session. Tensor9 evaluates that role's AWS policy for adapter calls. Kubernetes RBAC separately governs cluster API access; real external-cloud calls need separately authorized credentials or federation. On an existing Kubernetes cluster, the selected STS adapter exchanges a service-account token for a supported AWS role session. Tensor9 evaluates that role's AWS policy for adapter calls. Kubernetes RBAC separately governs cluster API access; real external-cloud calls need separately authorized credentials or federation.

AWS adapter calls use the selected credential adapter and role policy. Kubernetes RBAC and real external-cloud credentials authorize separate destinations.

#### Limitations Workloads use the existing cluster's Kubernetes API. Review its identity, storage, ingress and installed controllers when adapting manifests; this mapping provisions no control plane or nodes. △ Where EKS and a self-managed cluster diverge * **You run the control plane and nodes.** Nothing is provisioned: no managed control plane, no node pools. Tensor9 references your existing cluster's endpoint and authentication. * **Native cloud identity bindings are not provisioned.** Supported AWS role sessions use the selected STS or EKS Auth adapter. Configure credentials or federation separately for real external-cloud services. * **Fargate serverless nodes have no analog.** You run the nodes, so there is no serverless node capacity to schedule onto. * **No cloud autoscaler or machine mapping is wired for you.** You own node sizing, scaling, and lifecycle. #### Other considerations Account for the following operating requirements. * **Deploy onto the existing cluster:** the build emits a reference to a cluster you already operate and schedules your adapted Kubernetes manifests onto it, so the move is wiring the workloads onto existing capacity, not standing up a cluster. * **Every operational duty stays with you:** control-plane HA, version upgrades, node lifecycle, and any platform layer (ingress, autoscaler, monitoring, backup) are yours to run; a managed target would have operated them. * **Configure each credential path.** Kubernetes RBAC authorizes cluster access. The selected STS or EKS Auth adapter supplies supported AWS sessions; real external-cloud services require separately authorized credentials or federation. * **Budget for cluster capacity and maintenance:** there is no managed control-plane charge, but the cluster's capacity, availability, and upkeep run on infrastructure you own and staff. #### Management and credentials on an existing cluster This target uses your existing cluster; it does not provision a managed Kubernetes service. Cluster RBAC and selected AWS role sessions are separate. External cloud access still requires credentials or federation authorized by that provider. See [cluster management and credential separation](/service-adapters/aws/compute-containers/eks#runtime-cluster-management-and-credentials) for the common model and its managed-target restriction. [Service Catalog](/service-adapters/catalog). # EKS ALB Ingress Source: https://docs.tensor9.com/service-adapters/aws/compute-containers/eks-alb-ingress AWS EKS ALB Ingress. An Ingress resource in EKS that is realized as an Application Load Balancer, routing HTTP requests to pods by host and path. ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## Mapping and limitations An EKS ALB Ingress describes HTTP routing from an ingress to Kubernetes services and pods. The registered mappings use the target cloud's load balancer or a Kubernetes ingress controller. Host and path routing belong to the ingress configuration; AWS controller annotations, ACM certificate references and ALB-specific actions must be checked against that target. This resource mapping does not make the AWS Load Balancer Controller run unchanged on another cloud. These target services provide the mapping described above, within its stated limits. | Cloud | Target service | | ------------------ | -------------------- | | Google Cloud | Traefik IngressRoute | | Azure | Traefik IngressRoute | | OCI | Traefik IngressRoute | | Private Kubernetes | Traefik IngressRoute | [Service Catalog](/service-adapters/catalog). # EKS IRSA Source: https://docs.tensor9.com/service-adapters/aws/compute-containers/eks-irsa AWS EKS IRSA. IAM Roles for Service Accounts: a pod's projected service account token is exchanged through an OIDC provider for temporary IAM credentials. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud, Azure, OCI, and Private Kubernetes](#on-google-cloud-azure-oci-and-private-kubernetes) * [Via Cedar](#via-cedar) * [On Azure](#on-azure) * [Via IAM](#via-iam) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of EKS IRSA with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | EKS IRSA | Google Cloud, Azure, OCI, and Private Kubernetes · Cedar | | ------------ | -------- | -------------------------------------------------------- | | API coverage | full | high | ### Infrastructure-only adaptation | Capability | EKS IRSA | Azure · IAM | | ------------ | -------- | ----------- | | API coverage | full | partial | ## On Google Cloud, Azure, OCI, and Private Kubernetes ### Via Cedar | Operation | Area | Support | Depth | Notes | | ------------------------- | ----------- | --------- | ------ | ----------------------------------------------------------------------------------------------- | | AssumeRoleWithWebIdentity | Credentials | Supported | Common | Verifies token issuer, audience, subject and role trust before issuing a temporary AWS session. | | GetCallerIdentity | Identity | Supported | Common | Returns the configured AWS account and assumed role for the adapter session. | #### AWS credential exchange The AWS SDK sends a projected Kubernetes token to STS AssumeRoleWithWebIdentity. The broker verifies issuer, audience and service-account subject, evaluates the requested role's trust policy, and issues a temporary AWS role session. The AWS API and session format remain those expected by the application's SDK. #### Permissions and native resource access The AssumeRoleWithWebIdentity response supplies a temporary AWS role session. With enforcement enabled, receiving adapters authorize AWS API requests against the role's supported IAM permissions using Cedar, the policy engine underpinning the adapter's authorization decisions. The adapter handles the AWS credential exchange; Cedar evaluates the supported permissions. Native cloud APIs use separate provider credentials and grants; Kubernetes RBAC governs access to the cluster API. Each appliance has its own identities and policies. Appliances do not share policies or distribute policy updates to one another. Policy changes are eventually consistent within that appliance. New grants may not be usable immediately, and revoked permissions may remain effective until the change propagates. Verify the dependent operation before relying on a policy change; a successful update alone does not prove propagation. #### Deployment and renewal Deploy the identity services in the customer's environment and configure the cluster issuer, token audience and role trust together. Verify a denied exchange and a denied resource action as well as successful calls. Long-running pods must renew credentials; existing sessions and later exchanges can have different outcomes after an association or policy change. ## On Azure ### Via IAM #### AWS credentials and native Azure access With maximum adaptation, an unchanged AWS SDK calls STS AssumeRoleWithWebIdentity. The selected IAM/STS adapter validates the Kubernetes token issuer, audience, subject and role trust before issuing an AWS role session. Native Azure access uses a separate exchange: Azure federation accepts a projected Kubernetes token and issues a managed-identity token for Azure services. That token does not replace the AWS access-key session. Configure the target identity for native resource access and the configured role for the AWS API path the application uses. Keep each grant scoped to the intended service account and resources. Test a denied role assumption as well as a successful one, and verify token renewal; a successful first exchange does not prove that a long-running pod can refresh credentials. #### Native Azure identity IAM Roles for Service Accounts (**IRSA**) lets an EKS pod exchange its Kubernetes service-account token with AWS Security Token Service (STS) for temporary IAM-role credentials. On Azure, the pod uses **OpenID Connect (OIDC) federation** to authenticate as a **user-assigned managed identity**. A federated identity credential records the AKS cluster's issuer, the service-account subject, and the token audience. This diagram shows native Azure access. An unchanged AWS SDK uses the separate STS adapter for an AWS role session; an Azure managed-identity token is not an AWS access-key credential. The generated Terraform contains no client secret. The pod authenticates with its service-account token, so the customer does not need to store a long-lived credential for this exchange.
Before: on AWS a Kubernetes service account's projected token is exchanged with STS via AssumeRoleWithWebIdentity for an IAM role, with no stored secret. Native Azure access: a service-account token issued by AKS authenticates a managed identity through a federated credential. This diagram does not show the separate AWS-compatible STS adapter path. Before: on AWS a Kubernetes service account's projected token is exchanged with STS via AssumeRoleWithWebIdentity for an IAM role, with no stored secret. Native Azure access: a service-account token issued by AKS authenticates a managed identity through a federated credential. This diagram does not show the separate AWS-compatible STS adapter path.
#### The three resources a grant becomes Each IRSA grant creates three Azure resources: * **A user-assigned managed identity** that identifies the workload. * **A role** that defines its permissions, using either a built-in Azure role or a custom role with the translated actions. * **A role assignment** that grants those permissions to the identity within a resource group. A federated credential connects the managed identity to the Kubernetes service account. Tensor9 places all credentials for one identity in the same stack and checks Azure's per-identity credential limit during compilation. Exceeding the limit stops the build. All role assignments are scoped to the resource group. This mapping does not create subscription-scoped assignments. #### Choosing built-in or custom roles Tensor9 translates each AWS IAM policy statement into an Azure role. The default, **hybrid** mode uses a built-in role only when its actions match the translated statement exactly; otherwise it creates a custom role. It does not select a built-in role that grants additional actions. **Prefer-built-in** selects the smallest built-in role that covers the statement, even if that role grants more permissions. Review those additional permissions before using it. **Prefer-custom** always creates a role with exactly the translated actions. Azure limits a tenant to around five thousand custom role definitions, so account for other deployments using the same tenant. #### Unsupported identities and trust The native Azure mapping has these identity boundaries: * **IAM Users and Groups have no native directory mapping.** This native Azure mapping creates workload identities; it does not create directory objects for people or groups. AWS IAM objects served through the Max adapter have a separate scope. * **Role assignments bind directly to the workload identity.** They do not grant permissions through group membership, where a membership change could grant access to more identities. * **AWS service-principal trust is not an Azure grant.** Trust allowing EC2, Lambda, or EKS to assume a role describes a relationship with an AWS service. The AWS role trust remains part of the AWS credential path; native Azure permissions use Azure role assignments. #### Limitations △ IRSA limitations on Azure * **Permission mapping is per-statement, not per-resource-per-condition.** AWS IAM expresses permissions with hundreds of service-specific condition keys against individual resources. Azure RBAC binds roles over a scope hierarchy with a narrower condition model. A policy that depends on conditions without an Azure equivalent needs an explicit permission review; a successful federation exchange does not establish equivalent access. * **Custom roles draw on a finite tenant pool.** Each custom role counts toward the tenant limit of roughly five thousand role definitions. Include roles used by other appliances and applications when planning capacity. * **The federated credential count per identity is capped.** Azure caps how many federated credentials one managed identity may hold. The limit is enforced during compilation. If too many service accounts share one identity, split them across additional identities. * **Assignments are resource-group scoped by construction.** The mapping cannot grant permissions across a subscription. Workloads that require subscription-level access need that access configured separately. #### Other considerations * **Your service account keeps its role; the annotation changes.** Pods keep their service accounts. Tensor9 changes workload-identity labels and the identity used for native Azure access through Kubernetes manifests. Unchanged AWS SDK calls also require the STS adapter and the role trust described in the runtime section. * **The trust path depends on the cluster's OIDC issuer.** A federated credential names the AKS cluster's issuer URL, so workload identity is only live when the cluster was created with its OIDC issuer enabled. The appliance's own template asserts that at apply for exactly this reason. An issuer that is quietly off produces credentials that bind to nothing and fail at the first token exchange rather than at deploy. * **Review role permissions before deployment.** The plan contains either built-in role names or custom definitions with explicit actions. Compare them with the AWS policy and confirm that they grant the intended permissions before deployment. * **Naming is deterministic and derived from logical identity.** Identities, role definitions and federated credentials are named from the stack's persisted identity rather than from Terraform addresses, so the same stack compiles to the same names every time and a re-plan does not propose replacing an identity that other assignments already point at. [Service Catalog](/service-adapters/catalog). # EKS NLB Service Source: https://docs.tensor9.com/service-adapters/aws/compute-containers/eks-nlb-service AWS EKS NLB Service. Exposes a Kubernetes Service through a Network Load Balancer, forwarding TCP and UDP traffic to pods at layer 4. ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | - | | Azure | - | | OCI | - | | Private Kubernetes | ✓ | ## Mapping and limitations An EKS NLB Service exposes Kubernetes service ports through a layer-4 load balancer. On private Kubernetes, MetalLB or the configured cluster load balancer supplies the external address. The cluster needs an address pool and network routing for that address. AWS NLB annotations and AWS target-group management are not portable Kubernetes Service settings. These target services provide the mapping described above, within its stated limits. | Cloud | Target service | | ------------------ | ------------------------------------------------------------------------------------------------------------ | | Private Kubernetes | MetalLB LoadBalancer, Ngrok K8s LoadBalancer, Traefik IngressRoute, Traefik LB (MetalLB), Traefik LB (ngrok) | [Service Catalog](/service-adapters/catalog). # EKS NLB Service (TLS) Source: https://docs.tensor9.com/service-adapters/aws/compute-containers/eks-nlb-service-tls AWS EKS NLB Service (TLS). A Kubernetes Service exposed through a Network Load Balancer that terminates TLS with an ACM certificate and forwards plaintext to pods. ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | - | | Azure | - | | OCI | - | | Private Kubernetes | ✓ | ## Mapping and limitations This EKS Service adds TLS termination and an AWS certificate reference to an NLB-backed service. On private Kubernetes, the load balancer supplies connectivity and certificate management must use the cluster's TLS configuration. MetalLB assigns addresses; it does not terminate TLS. An ACM certificate ARN cannot be used as the cluster certificate, so configure the certificate and the component that terminates TLS explicitly. These target services provide the mapping described above, within its stated limits. | Cloud | Target service | | ------------------ | ------------------------------------------------------------------------------------------------------------ | | Private Kubernetes | MetalLB LoadBalancer, Ngrok K8s LoadBalancer, Traefik IngressRoute, Traefik LB (MetalLB), Traefik LB (ngrok) | [Service Catalog](/service-adapters/catalog). # EKS Pod Identity Source: https://docs.tensor9.com/service-adapters/aws/compute-containers/eks-pod-identity AWS EKS Pod Identity. Associates a Kubernetes service account with an IAM role through a node agent, giving pods temporary credentials without per-cluster OIDC setup. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud, Azure, OCI, and Private Kubernetes](#on-google-cloud-azure-oci-and-private-kubernetes) * [Via Cedar](#via-cedar) * [On Azure](#on-azure) * [Via IAM](#via-iam) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of EKS Pod Identity with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | EKS Pod Identity | Google Cloud, Azure, OCI, and Private Kubernetes · Cedar | | ------------ | ---------------- | -------------------------------------------------------- | | API coverage | full | high | ### Infrastructure-only adaptation | Capability | EKS Pod Identity | Azure · IAM | | ------------ | ---------------- | ----------- | | API coverage | full | partial | ## On Google Cloud, Azure, OCI, and Private Kubernetes ### Via Cedar | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | --------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | CreatePodIdentityAssociation / DescribePodIdentityAssociation / ListPodIdentityAssociations / UpdatePodIdentityAssociation / DeletePodIdentityAssociation | Associations | Supported | Common | Maintains the cluster, namespace and service-account binding to one AWS role. Role chaining, inline session policies and disabling session tags are unsupported. | | AssumeRoleForPodIdentity | Credentials | Supported | Common | Verifies the relaying agent and pod token, looks up the server-side association and evaluates role trust. | #### AWS credential exchange The EKS association records a role for a cluster, namespace and service account. The EKS Auth broker verifies the relaying agent and projected pod token, looks up that association and evaluates role trust before issuing a temporary AWS session. The caller cannot choose an arbitrary role in the credential request. #### Permissions and native resource access The AssumeRoleForPodIdentity response supplies a temporary AWS role session. With enforcement enabled, receiving adapters authorize AWS API requests against the role's supported IAM permissions using Cedar, the policy engine underpinning the adapter's authorization decisions. The adapter handles the AWS credential exchange; Cedar evaluates the supported permissions. Native cloud APIs use separate provider credentials and grants; Kubernetes RBAC governs access to the cluster API. Each appliance has its own identities and policies. Appliances do not share policies or distribute policy updates to one another. Policy changes are eventually consistent within that appliance. New grants may not be usable immediately, and revoked permissions may remain effective until the change propagates. Verify the dependent operation before relying on a policy change; a successful update alone does not prove propagation. #### Deployment and renewal Deploy association management, the EKS Auth broker and the cluster's credential agent together. Register the broker's backend role, authorize the agent to request credentials for the cluster, and configure an HTTPS broker address the agent can resolve and reach. The broker must be able to verify the cluster's token issuer and signing keys. Configure admission to project a pod-bound token for audience `pods.eks.amazonaws.com` and select the AWS container credential provider. Check for other credentials that would take precedence. Verify the intended role in a newly admitted pod, a denied exchange and a denied resource action. Long-running pods must renew credentials; existing sessions and later exchanges can have different outcomes after an association or policy change. #### Association and trust limits Associations use one role in the cluster's appliance account. Role chaining, inline session policies and disabling session tags are unsupported. Omit `targetRoleArn`, `policy` and `disableSessionTags` from create and update requests, including an explicit `false` for `disableSessionTags`. The role must trust `pods.eks.amazonaws.com` for both `sts:AssumeRole` and `sts:TagSession`. The broker derives the Kubernetes session tags from the verified pod. Supported trust conditions use `StringEquals` or `StringLike` on the six derived `aws:RequestTag` keys; other trust conditions are refused. ## On Azure ### Via IAM #### Association management and AWS credential exchange Maximum adaptation retains the EKS association API and the EKS Auth credential exchange as separate services. The association records which role belongs to a cluster, namespace and service account. For AssumeRoleForPodIdentity, the broker verifies the relaying agent and the pod's projected token, looks up that server-side association and evaluates the role's current trust policy. The caller does not choose an arbitrary role in the exchange request. The result for an unchanged AWS SDK is an AWS role session. Azure workload-identity federation is a separate native access mechanism that produces Azure tokens; it does not replace the AWS session format. Verify the pod token issuer and audience, the association and role trust, and any Azure identity needed to access target resources. Changing an association affects subsequent exchanges; already issued sessions retain their own expiration. Associations use one role in the cluster's appliance account. Role chaining, inline session policies and disabling session tags are unsupported. Omit `targetRoleArn`, `policy` and `disableSessionTags` from create and update requests, including an explicit `false` for `disableSessionTags`. The role must trust `pods.eks.amazonaws.com` for both `sts:AssumeRole` and `sts:TagSession`. The broker derives the Kubernetes session tags from the verified pod. Supported trust conditions use `StringEquals` or `StringLike` on the six derived `aws:RequestTag` keys; other trust conditions are refused. Deploy association management, the EKS Auth broker and the cluster's credential agent together. Register the broker's backend role, authorize the agent to request credentials for the cluster, and configure an HTTPS broker address the agent can resolve and reach. The broker must be able to verify the cluster's token issuer and signing keys. Configure admission to project a pod-bound token for audience `pods.eks.amazonaws.com` and select the AWS container credential provider. Check for other credentials that would take precedence. Verify the intended role in a newly admitted pod, a denied exchange and a denied resource action. Long-running pods must renew credentials; existing sessions and later exchanges can have different outcomes after an association or policy change. #### Native Azure identity EKS Pod Identity associates a Kubernetes service account with an IAM role. An agent in the cluster gives the pod temporary credentials, without an OpenID Connect (OIDC) provider. Azure uses **OIDC federation**. The pod authenticates with its service-account token as a user-assigned managed identity. A federated identity credential records the AKS cluster's issuer and the service account that may use that identity. This diagram shows the native Azure binding. Maximum adaptation separately retains EKS association management and EKS Auth credential exchange for AWS SDK calls. The pod still receives permissions without a stored client secret, but the AKS cluster must have its OIDC issuer enabled. Deployments moving from Pod Identity need to configure this additional dependency.
Before: on AWS an EKS Pod Identity association binds a service account to an IAM role and an on-cluster agent hands credentials to the pod, with no OIDC provider involved. Native Azure access: the pod token issued by AKS authenticates a managed identity through a federated credential. The separate EKS association and EKS Auth adapter path is described in the AWS credential exchange section. Before: on AWS an EKS Pod Identity association binds a service account to an IAM role and an on-cluster agent hands credentials to the pod, with no OIDC provider involved. Native Azure access: the pod token issued by AKS authenticates a managed identity through a federated credential. The separate EKS association and EKS Auth adapter path is described in the AWS credential exchange section.
#### What the association becomes One Pod Identity association becomes a **user-assigned managed identity**, a **role** (a built-in Azure role when the catalogue covers the policy statement exactly, a custom role definition otherwise), a **role assignment** scoped to the resource group, and a **federated identity credential** naming the cluster's issuer and your service account. For native Azure access, the federated credential provides the binding. Its issuer, subject, and audience identify which service account and namespace may authenticate as the managed identity. All federated credentials for one identity are generated together. Tensor9 checks Azure's per-identity credential limit during compilation and stops the build if it is exceeded. If many service accounts share an identity, divide them across additional identities to stay within the limit. #### Limitations △ Pod Identity limitations on Azure * **An OIDC issuer is required.** Azure workload identity requires the AKS cluster's OIDC issuer to be enabled. EKS Pod Identity does not require an OIDC provider, so this adds a deployment requirement. * **Association state and Azure federation are separate.** EKS tooling uses the association adapter. Native Azure access uses federated credentials on managed identities. Review both bindings; an Azure credential alone does not create an EKS association or issue an AWS role session. * **Limit how many service accounts share an identity.** Azure caps federated credentials per managed identity, and the cap is enforced at compile time. Split service accounts across additional identities if their credential count exceeds the limit. * **IAM Users and Groups have no native directory mapping.** The native Azure mapping creates workload identities, not directory objects for people or groups. Configure native grants to people in Azure's directory separately. AWS IAM objects served through the Max adapter have a separate scope. * **Some permission conditions cannot be preserved.** AWS IAM statements become Azure RBAC roles over a scope hierarchy with a narrower condition model. Review effective access where a source IAM condition has no Azure equivalent. #### Other considerations * **Enable the cluster's OIDC issuer before deployment.** Without the issuer, federated credentials can be created but the first token exchange fails. The appliance template checks that the issuer is enabled when the stack is applied. * **Workload-identity changes are in the manifests.** Pods keep their service accounts. Configure Azure workload-identity labels for native Azure access and the agent, association and credential route for unchanged AWS SDK calls. These paths produce different credentials and require separate validation. * **Read the emitted roles before cutover.** The plan contains either built-in role names or custom definitions with explicit actions. Compare those actions with the original IAM policy before deployment, particularly where broader Azure scopes could grant additional access. [Service Catalog](/service-adapters/catalog). # Lambda Source: https://docs.tensor9.com/service-adapters/aws/compute-containers/lambda AWS Lambda. Runs a function on demand in response to events or HTTP calls, billed per request and millisecond, with a fifteen minute maximum runtime. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [Via AKS function Deployments](#via-aks-function-deployments) * [Via Azure Container Apps](#via-azure-container-apps) * [Via Azure Functions (Flex Consumption)](#via-azure-functions-flex-consumption) * [Via Azure Functions (Premium)](#via-azure-functions-premium) * [On OCI](#on-oci) * [Via OKE function Deployments](#via-oke-function-deployments) * [Via OCI Functions](#via-oci-functions) * [On Private Kubernetes](#on-private-kubernetes) * [Via Kubernetes Cluster](#via-kubernetes-cluster) * [Via Knative Service](#via-knative-service) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of Lambda with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | Lambda | Google Cloud | Azure · AKS function Deployments | Azure · Azure Functions (Flex Consumption) | Azure · Azure Functions (Premium) | OCI · OKE function Deployments | Private Kubernetes · Kubernetes Cluster | Private Kubernetes · Knative Service | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | Execution model · scaling behavior | managed, scales on demand | Cloud Run gen2 service, request-driven autoscaling with scale-to-zero | always-on AKS Deployment | Azure Functions Flex Consumption, with per-function target-based scaling to 1000 instances and scale-to-zero | Azure Functions Premium (Elastic Premium), with always-ready + prewarmed instances and HTTP scale-out to a 20-100 SKU ceiling | always-on OKE Deployment | always-on Deployment | Knative Service, request-driven autoscaling | | Concurrency · request isolation | 1 request per sandbox | 1 active HTTP request per instance; timed-out code can continue | Deployment replicas; no Lambda reserved-concurrency admission | one invoke per instance, set at the platform (per-instance concurrency 1), so concurrent invokes land on separate instances | one invoke per instance, with concurrent invokes scaling onto separate instances (verified to scale under burst) | Deployment replicas; no Lambda reserved-concurrency admission | no per-replica concurrency cap on Kubernetes | one request per pod through Knative containerConcurrency=1; fleet reserved concurrency is a separate limit | | Minimum warm capacity · cold-start behavior | provisioned concurrency (optional) | scales to zero by default; CPU throttled outside requests; cold start measured 365 ms p50 | - | - | always-ready and prewarmed instances reduce cold-start latency (\~200 ms); they are billed continuously | - | - | 1 replica minimum by default | | Scale bound · instance cap | reserved concurrency (optional) | revision max instance count; temporary excess and multiple revisions prevent a strict fleet cap | - | - | - | - | - | - | | Compute sizing · CPU from memory | CPU allocation is memory/1769 vCPU; this rate limits throughput independently of the number of visible cores | rounded up to gen2 CPU tiers \{1, 2, 4, 6, 8}; 512 MiB memory floor | - | mapped onto the plan's instance size, up to the \~4 GB ceiling shared with the runtime bundle | mapped onto the Premium instance SKU (EP1/EP2/EP3) | - | the same continuous rate, enforced by the Linux CFS scheduler | the same continuous rate, enforced by the Linux CFS scheduler | | Packaging · how functions ship | container image or Lambda zip | a built container image serving the Invoke API with the AWS-provided runtime client | function container in a cluster-accessible registry | a code package (zip): the package includes the AWS-provided runtime client and the Tensor9 adapter, and container images route elsewhere | a custom container image or a code package: Premium hosts both, so a container-image function runs as-is | function container in a cluster-accessible registry | a built container image serving the Invoke API with the AWS-provided runtime client | a built container image serving the Invoke API with the AWS-provided runtime client | | Invocation · how calls arrive | Invoke API + event sources | Invoke API, synchronous, via the caller's routing layer | Invoke API, synchronous, in-cluster | - | - | Invoke API, synchronous, in-cluster | Invoke API, synchronous, in-cluster | Invoke API, synchronous, in-cluster | | Endpoint access · who can reach the endpoint | public endpoint + SigV4 auth | public endpoint + IAM invoker auth, matching Lambda's own data-plane posture | - | - | - | - | - | - | | Keyless auth | Yes - IAM roles / SigV4 | Yes - audience-bound identity tokens, no keys | - | - | - | - | - | - | | API coverage | full | partial | partial | partial | partial | partial | partial | partial | | Per-invoke isolation · what the platform can't fully reproduce | private /tmp + memory cap per concurrent invoke | - | - | full per-invoke isolation (a separate instance each), bounded by the plan's \~0.8 GB /tmp and \~4 GB instance memory | full per-invoke isolation (a separate instance each), with GB-sized /tmp and larger memory than the Flex Consumption option | - | - | - | | Routing layer delivery · how the routing layer ships | n/a | - | - | a binary inside the code package (Flex Consumption is code-only, with no separately deployed service adapter), so a routing-layer update requires rebuilding the package on the next deploy | a binary inside the deployed artifact (Premium hosts a single container, no separately deployed service adapter), so a routing-layer update requires rebuilding the artifact on the next deploy | - | - | - | | Egress identity · keyless auth | IAM roles / SigV4 | - | - | egress mapped to an Azure managed identity, no keys | egress mapped to an Azure managed identity, no keys | - | - | - | | Callee resolution · by name at request time | native | - | - | the caller's routing layer resolves each callee by function name; ingress hostnames are platform-assigned, so hostnames are not fixed during compilation | the caller's routing layer resolves each callee by function name; ingress hostnames are platform-assigned, so hostnames are not fixed during compilation | - | - | - | | Scale bound · never unbounded | account concurrency limit | - | - | - | - | - | - | 10 replicas maximum by default | | Network isolation | Yes - VPC controls | - | - | - | - | - | - | Yes - no internet egress; cluster-local only | ### Infrastructure-only adaptation | Capability | Lambda | Azure · Azure Container Apps | OCI · OCI Functions | | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Execution model · scaling behavior | managed, scales on demand | managed container platform (KEDA / Envoy / Dapr), scales per request with scale-to-zero on HTTP/event triggers | managed FaaS, scales per request with scale-to-zero | | Concurrency · request isolation | 1 request per sandbox | one invoke per worker process, enforced by the adapter | 1 request per container, enforced by the platform; concurrent requests use separate containers, reusing warm capacity when available | | Compute sizing · CPU from memory | CPU allocation is memory/1769 vCPU; this rate limits throughput independently of the number of visible cores | mapped onto the Container App's cpu/memory allocation | memory rounds up to an OCI tier (e.g. 1769 MiB → 2048); the tier determines CPU | | Packaging · how functions ship | container image or Lambda zip | a built container image serving the Invoke API with the AWS-provided runtime client | a built container image serving the Invoke API with the AWS-provided runtime client | | Egress identity · keyless auth | IAM roles / SigV4 | egress mapped to an Azure managed identity, no keys | - | | Inbound admission · how one invoke per process is held | platform holds 1 request per sandbox | a routing layer admits each invoke to a free single-request worker, concurrency-aware | - | | Per-invoke isolation · what the platform can't fully reproduce | private /tmp + memory cap per concurrent invoke | full at one-invoke-per-replica; in packed mode co-located workers share /tmp + host resources | - | | Callee resolution · by name at request time | native | the caller's routing layer resolves each callee by function name; ingress hostnames are platform-assigned, so hostnames are not fixed during compilation | - | | API coverage | full | partial | partial | | Minimum warm capacity · cold-start behavior | provisioned concurrency (optional) | - | scales to zero; image-cached cold start measured 365 ms p50 (n=20); a cold-cache image pull took about 49 s in the recorded run. Cache availability is not guaranteed on later starts. | | Grouping · function resources | function | - | application + function | | Callee resolution by name | Yes - native | - | Yes - re-resolves after a redeploy | | Keyless auth | Yes - IAM roles / SigV4 | - | Yes - appliance resource-principal signing, no keys | ## On Google Cloud | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------------------------------------ | ----------------- | ------------ | ------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Compute sizing | Configuration | Supported | - | - | memory is applied as authored; CPU derived from Lambda's delivered compute (the continuous memory/1769 vCPU credit rate, not the stepped visible-core count), ceiled to Cloud Run gen2's CPU tier \{1, 2, 4, 6, 8}; a sub-1-vCPU function rounds up to a full vCPU (fractional CPU needs gen1), and memory below 512 MiB is raised to gen2's floor | | Environment variables | Configuration | Supported | - | - | set on the function's workload | | Ephemeral storage (/tmp) | Configuration | Supported | - | - | a sized in-memory /tmp matching the authored ephemeral storage (512 MB default); writes count against function memory | | Reserved concurrency | Configuration | Partial | - | - | Maps to the revision's maximum instance count with one request per instance. Cloud Run can temporarily exceed this setting, including during replacement; multiple revisions can also serve traffic. This is not strict fleet-wide admission control. Unauthored functions use the platform default. | | Timeout | Configuration | Partial | - | - | The authored timeout sets Cloud Run's request deadline. A missed deadline returns HTTP 504, but the handler can continue running; this setting does not provide Lambda's execution-termination guarantee. | | Event source mappings | Event integration | Out of scope | - | - | not compiled; the build stops with a clear error rather than silently dropping them | | Asynchronous (Event) invocation | Invocation | Out of scope | - | - | the Event invocation type of Invoke is not served; a synchronous RequestResponse Invoke is | | Layers | Packaging | Out of scope | - | - | not compiled; the build stops with a clear error | | Zip packaging | Packaging | Supported | - | - | a zip-packaged function is built into a container image at release: the runtime base plus your code, fronted by the AWS-provided runtime client | | Aliases / versions / provisioned concurrency / function URLs | Routing | Out of scope | - | - | not compiled; the build stops with a clear error | | Operation | Area | Support | Depth | Notes | | ------------------------ | ---------- | ------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Invoke | Invocation | Supported | Common | synchronous RequestResponse routed to the callee by function name through the caller's routing layer; verified on a live release (2026-07-08) | | InvokeWithResponseStream | Invocation | Out of scope | Most usage | response streaming is not served; the synchronous RequestResponse invoke is the served path | #### How it works On AWS your application calls Lambda through the AWS SDK; those calls are unchanged. At deploy time the compiler turns each function into a container image in the customer's own registry: a container-image function uses the configured Lambda-compatible entrypoint, and a zip-packaged function is built into a container image at release. The AWS runtime client runs the handler; an HTTP Invoke wrapper delivers requests to that runtime. At runtime every invocation uses the Lambda Invoke protocol: the function image serves the **Invoke** API on its port, and the reply uses Lambda's own framing, including function errors; execution deadlines follow the target limits below. Only synchronous request/response invokes are served. Cloud Run differs from the cluster targets by being container-serverless with true scale-to-zero, managed by Google . When no invokes arrive the service scales to zero (CPU throttled outside requests) and scales back up per request. Google, not Tensor9, enforces one request per instance, so Cloud Run holds one active HTTP request per instance. Code can continue after an HTTP timeout, so this setting alone does not guarantee that a timed-out handler has stopped before another request arrives. The configured timeout becomes Cloud Run's request deadline: expiration returns HTTP 504 without terminating the container. A sized in-memory `/tmp` matches the authored ephemeral storage. Reserved concurrency maps to the revision's maximum instance count, which Cloud Run can temporarily exceed; multiple revisions can also serve requests. This setting does not provide strict fleet-wide admission control. Cross-function invokes route through a caller-side adapter that resolves the callee by name, bound at apply, since the platform assigns a hostname the name alone cannot predict; the layer authenticates with identity tokens issued for the destination service, with no AWS keys on the path.
On Google Cloud, the application's unchanged SDK invoke reaches a Tensor9 adapter beside it, which resolves the callee by function name and forwards to a Google Cloud Run gen2 service that scales from zero and enforces one request per instance at the platform. On Google Cloud, the application's unchanged SDK invoke reaches a Tensor9 adapter beside it, which resolves the callee by function name and forwards to a Google Cloud Run gen2 service that scales from zero and enforces one request per instance at the platform.

Each function runs as a Cloud Run service that scales to zero; Google enforces one request per instance, and a caller-side adapter routes cross-function invokes by name with keyless auth.

#### Operations and migration **Operations.** Google operates Cloud Run's scaling and availability; Tensor9 operates the invoke path. Invoke permissions are granted manually, and services run on the project's default compute account. **Migration.** The function is compiled at the build and ships as its container image; there is no data to migrate at cutover. **Capacity and speed.** Throughput and latency are Cloud Run's own, reported by your monitoring. #### Limitations △ Where Lambda and Cloud Run stay different * **CPU rounds up to a Cloud Run tier.** CPU allocation rounds up to a gen2 tier; a sub-vCPU function rounds up to a full vCPU, and memory below the platform floor is brought up to it. * **The HTTP timeout does not terminate the handler.** Cloud Run can return HTTP 504 while code continues running. Workloads that require execution to stop at the deadline need handler cancellation or process termination beyond this native request setting. * **Maximum instances is not exact reserved concurrency.** Cloud Run can temporarily exceed the revision's maximum, and multiple revisions may serve traffic. One request per instance does not impose a strict admission limit across the fleet. * **Cross-function calls reach only the same stack.** A call to a function the build cannot see returns a clear error rather than reaching the wrong service. * **Synchronous invoke only.** An asynchronous (Event) invoke returns a clear error; layers, event source mappings, aliases, and function URLs stop the build with a clear error rather than dropping silently. #### Other considerations Consider request isolation, idle capacity, and how functions call one another. * **One active HTTP request per instance.** Google applies this request limit at the platform. Warm requests reuse process memory and `/tmp`; code left running after an HTTP timeout can overlap a later request. * **Scale-to-zero removes idle instance compute; the next invoke may cold-start:** the service scales to zero when no invokes arrive and back up per request, so instance compute follows the configured billing mode; registry storage, networking and other resources can still incur charges. A request arriving after scale-to-zero may need a container start. * **A callee address is bound at apply.** Cloud Run assigns a hostname the function name alone cannot predict, so cross-function routing is resolved when the stack is applied and a callee that moves is picked up on the next apply; every hop authenticates with keyless identity tokens rather than AWS keys. * **Store durable state in a backing service.** Function storage is temporary. Persist data through the services your handler calls so it survives instance replacement. #### Function lifecycle and invocation Maximum adaptation separates function lifecycle from invocation. Supported lifecycle requests record the desired function and reconcile it onto the selected hosting service; creation can return Pending before deployment finishes. The target service's observed address then supplies the invocation route. A function must be Active before callers rely on that route. Invoke remains a request to the deployed handler through the Lambda adapter. Function readiness, successful HTTP delivery and a successful handler result are separate outcomes. Lifecycle adaptation does not add asynchronous event delivery, event sources, aliases or other features excluded by this target's operation table. Existing performance results apply to the recorded invocation workload, not to runtime creation or code-update latency. ## On Azure ### Via AKS function Deployments | Capability | Area | Support | Required tier | Operations | Notes | | ---------------------------------------------------------- | ------------- | ------------ | ------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------- | | Environment variables | Configuration | Supported | - | - | Configured on the function workload. | | Ephemeral storage (/tmp) | Configuration | Out of scope | - | - | The container filesystem does not reproduce Lambda's configured /tmp cap. | | Function lifecycle | Configuration | Partial | - | - | Supported lifecycle requests reconcile function configuration onto the selected cluster; invocation requires observed Active readiness. | | Reserved concurrency | Configuration | Out of scope | - | - | A Deployment replica count is not a Lambda reserved-concurrency limit. | | Timeout | Configuration | Partial | - | - | The authored timeout does not provide a per-request Kubernetes process deadline. | | Event sources, layers, aliases, versions and function URLs | Integration | Out of scope | - | - | These source integrations are outside the selected Kubernetes function deployment. | | Asynchronous (Event) invocation | Invocation | Out of scope | - | - | This target serves synchronous calls; it does not provide an asynchronous event queue. | | Zip packaging | Packaging | Supported | - | - | The runtime base and function code are built into a container image. | | Operation | Area | Support | Depth | Notes | | ------------------------ | ---------- | ------------ | ---------- | -------------------------------------------------------------------------------------------- | | Invoke | Invocation | Supported | Common | Synchronous RequestResponse calls reach the configured function through its cluster Service. | | InvokeWithResponseStream | Invocation | Out of scope | Most usage | Response streaming is outside this deployment contract. | #### How it works Each function runs as an always-on Deployment on AKS, behind a cluster-local Service. A Lambda Invoke wrapper accepts synchronous requests and passes events to the function runtime. A runtime interface client runs the handler; the client alone does not make an arbitrary container expose the Invoke API. #### Function packaging and lifecycle Zip functions are built into a container image with their runtime. Container-image functions need the configured Lambda-compatible entrypoint and Invoke wrapper. Publish the image to a registry the cluster can access, configure environment values, and wait for the deployed function to become ready. Supported lifecycle calls record desired configuration and update the selected hosting resources; a pending deployment is not yet an invocation endpoint. #### Cluster networking and identity The customer configures the Azure virtual network, cluster access and node capacity. Callers must be able to reach the function Service, and the function needs network access to its dependencies. Azure workload identity can grant the function access to selected Azure services. The node or kubelet identity used for image pulls is a different identity. An AWS SDK call through another service adapter uses that adapter's authorization path; native cloud access is configured separately. #### Execution limits and operations A Deployment does not supply Lambda's per-invocation sandbox, reserved-concurrency admission or request-driven scale-to-zero behavior. The authored timeout is not a Kubernetes process deadline, and container storage does not reproduce the configured Lambda /tmp quota. Asynchronous Event invocation, response streaming and the listed event integrations remain outside this target. Plan node upgrades and capacity, collect function logs, and verify handler errors as well as successful HTTP calls. ### Via Azure Container Apps | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------------------------------------ | ----------------- | ------------ | ------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Compute sizing | Configuration | Supported | - | - | memory is applied as authored; CPU derived from Lambda's delivered compute (the continuous memory/1769 vCPU credit rate, its binding throughput ceiling, not the stepped visible-core count) mapped onto the Container App's cpu/memory allocation | | Environment variables | Configuration | Supported | - | - | your function's environment variables are set on its workload | | Ephemeral storage (/tmp) | Configuration | Partial | - | - | each invoke runs in its own process, but concurrent invokes on one replica share the container filesystem; a private, sized /tmp per concurrent invoke needs container privileges the managed platform withholds, and a single-invoke-per-replica mode restores it at a scale-up cost | | Reserved concurrency | Configuration | Supported | - | - | configured reserved concurrency maps to the app's replica/worker bound | | Timeout | Configuration | Partial | - | - | Container Apps has no per-request execution deadline. The authored value is passed through, but the connection backstop only limits the caller's response wait. It does not terminate the remote handler; the handler can continue after the wait ends. | | Event source mappings | Event integration | Out of scope | - | - | not compiled; the build stops with a clear error rather than silently dropping them | | Asynchronous (Event) invocation | Invocation | Out of scope | - | - | the Event invocation type of Invoke is not served; a synchronous RequestResponse Invoke is | | Layers | Packaging | Out of scope | - | - | not compiled; the build stops with a clear error | | Zip packaging | Packaging | Supported | - | - | a zip-packaged function is built into a container image at release: the runtime base plus your code, fronted by the AWS-provided runtime client | | Aliases / versions / provisioned concurrency / function URLs | Routing | Out of scope | - | - | not compiled; the build stops with a clear error | | Operation | Area | Support | Depth | Notes | | ------------------------ | ---------- | ------------ | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Invoke | Invocation | Supported | Common | a sync Invoke routes to the Container App, resolved to the callee by function name through the caller's routing layer (Container Apps co-locates it on shared localhost, like Cloud Run) | | InvokeWithResponseStream | Invocation | Out of scope | Most usage | response streaming is not served; the synchronous RequestResponse invoke is the served path | #### How it works On AWS your application calls Lambda through the AWS SDK; those calls are unchanged. At deploy time the compiler turns each function into a container image (a container-image function runs as-is; a zip-packaged function is built into a container image at release) and runs it on Azure Container Apps , a managed container platform, not Azure Functions. At runtime the adapter serves the synchronous **Invoke** and returns the supported Lambda response fields and function-error framing. Only synchronous request/response invokes are served. What differs on Container Apps is **where single-request concurrency comes from**. Cloud Run and OCI Functions enforce one request per instance at the platform; Container Apps does not, so Tensor9's adapter owns the ingress port and admits each invoke onto a free single-request worker itself, concurrency-aware, never co-scheduling two invokes on one worker. The platform's own autoscaler still owns fleet scaling, moving replicas up and down with load and to zero when idle; the adapter ensures one active invocation per worker. At the default of one invoke per replica the platform's ingress is not concurrency-aware, so a fraction of simultaneous invokes are shed and retried. Packing several single-request workers per replica behind a concurrency-aware router removes that, at the cost of those co-located workers sharing the container's `/tmp` and host resources, which needs your confirmation that the handler tolerates it. Egress is mapped to an Azure managed identity, so requests need no AWS keys.
On Azure, the application's unchanged SDK invoke reaches a Tensor9 adapter on shared localhost that owns the ingress port and admits one invoke per worker, then forwards by function name to an Azure Container App whose replica fleet KEDA scales up and down with load. On Azure, the application's unchanged SDK invoke reaches a Tensor9 adapter on shared localhost that owns the ingress port and admits one invoke per worker, then forwards by function name to an Azure Container App whose replica fleet KEDA scales up and down with load.

Each function runs on Azure Container Apps; an adapter owns the ingress port and admits one invoke per worker, since the platform does not hold single-request concurrency the way Cloud Run and OCI do.

#### Operations and migration **Operations.** Microsoft operates Azure Container Apps' scaling and availability; Tensor9 operates the invoke path, and the function code stays the vendor's. **Migration.** The function is compiled at the build and ships as its container image; there is no data to migrate at cutover. **Capacity and speed.** Throughput and latency are Azure Container Apps' own, reported by your monitoring. #### Limitations △ Where Lambda and Azure Container Apps stay different * **The adapter limits concurrent requests per worker.** One invoke per worker is enforced by the adapter rather than enforced by the platform as on Cloud Run and OCI Functions. * **Burst traffic can require retries.** At one invoke per replica the platform ingress is not concurrency-aware and sheds a fraction of simultaneous invokes (recovered by retry); a concurrency-aware router removes that but co-locates workers that share /tmp and host resources. * **No per-request platform deadline.** The configured value is retained, but the connection backstop limits the caller's response wait, not remote handler execution. The handler can continue after the caller times out; this does not enforce Lambda's configured execution deadline. * **Synchronous invoke only.** An asynchronous (Event) invoke returns a clear error; layers, event source mappings, aliases, and function URLs stop the build with a clear error rather than dropping silently. #### Other considerations Consider request isolation, idle capacity, and how functions call one another. * **Fleet scaling and admission are separate.** KEDA adjusts replicas within the configured bounds, including zero when the minimum permits it; independently, the adapter admits each invoke onto a free single-request worker, so isolation holds one invoke per worker regardless of how many replicas are up. * **Idle scales to zero; waking is a cold start:** when the configured minimum allows the fleet to reach zero after idle, the first invoke pays a container start and billing follows use rather than a minimum warm capacity, which is the trade against the Premium plan's always-ready instances. * **Egress needs no keys:** the function's outbound calls are mapped to an Azure managed identity, so credentials are the platform's to rotate and nothing on the invoke path holds an AWS key. * **Store durable state in a backing service.** Function storage is temporary. Persist data through the services your handler calls so it survives instance replacement. #### Function lifecycle on Container Apps Container App readiness establishes an invocation route, not a successful handler result. Per-worker admission, replica scaling and response-wait backstops remain distinct controls. See [function lifecycle and invocation](/service-adapters/aws/compute-containers/lambda#function-lifecycle-and-invocation) for Pending/Active state, supported lifecycle operations and invocation outcomes. ### Via Azure Functions (Flex Consumption) | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------------------------------------ | ----------------- | ------------ | ------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Compute sizing | Configuration | Supported | - | - | memory is applied where it fits the plan's instance size; CPU derived from Lambda's delivered compute (the continuous memory/1769 vCPU credit rate, its binding throughput ceiling, not the stepped visible-core count); see the memory-ceiling limit below | | Environment variables | Configuration | Supported | - | - | your function's environment variables are set on its workload | | Ephemeral storage (/tmp) | Configuration | Partial | - | - | each invoke gets its own instance and its own /tmp (platform-isolated), but Flex Consumption caps instance /tmp near 0.8 GB, so a function that authored more ephemeral storage routes to a container option instead of running here | | Reserved concurrency | Configuration | Supported | - | - | configured reserved concurrency maps to the plan's maximum instance count | | Timeout | Configuration | Supported | - | - | the authored timeout (Lambda's 3s default when unset) is enforced by the platform at the configured deadline; the adapter returns Lambda's timeout contract | | Event source mappings | Event integration | Out of scope | - | - | not compiled; the build stops with a clear error rather than silently dropping them | | Asynchronous (Event) invocation | Invocation | Out of scope | - | - | the Event invocation type of Invoke is not served; a synchronous RequestResponse Invoke is | | Container-image packaging | Packaging | Out of scope | - | - | Flex Consumption runs code packages, not container images, so a container-image function routes to a container option (Container Apps or the Premium plan) rather than stopping the build | | Layers | Packaging | Out of scope | - | - | not compiled; the build stops with a clear error | | Zip packaging | Packaging | Supported | - | - | the natural fit here, because Flex Consumption is code-only; the AWS-provided runtime client and the Tensor9 adapter ride in the same code package | | Aliases / versions / provisioned concurrency / function URLs | Routing | Out of scope | - | - | not compiled; the build stops with a clear error | | Operation | Area | Support | Depth | Notes | | ------------------------ | ---------- | ------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Invoke | Invocation | Supported | Common | a sync Invoke reaches the function through a translation layer that adapts Azure's HTTP-trigger invocation to the real Lambda runtime, with the callee resolved by function name through the caller's routing layer | | InvokeWithResponseStream | Invocation | Out of scope | Most usage | response streaming is not served; the synchronous RequestResponse invoke is the served path | #### How it works On AWS your application calls Lambda through the AWS SDK; those calls are unchanged. At deploy time the compiler builds each function into a **code package**. Flex Consumption is code-only, so a container-image function routes to a container option (Container Apps or the Premium plan) rather than stopping the build. At runtime a translation layer adapts Azure's HTTP-trigger invocation to the real Lambda runtime and returns the supported Lambda response fields and function-error framing. Only synchronous request/response invokes are served. Flex Consumption runs code packages. Tensor9 includes the routing binary with your handler and the AWS runtime client, and sets `http_concurrency` to one. The platform gives each concurrent invocation its own instance, with separate memory and `/tmp` storage. Each function can scale to a thousand instances and to zero when idle. The package shares about 4 GB of instance memory with the runtime and has about 0.8 GB of `/tmp` storage. Updating the routing binary requires rebuilding and redeploying the package. Container Apps runs the adapter as a separate process; Premium keeps instances warm to reduce cold-start latency.
On Azure, the application's unchanged SDK invoke reaches a Tensor9 routing layer shipped as a binary inside the deployed code package (packaged with the handler), which resolves and forwards calls by function name to Azure Functions on the Flex Consumption plan (code-only), scaling from zero toward a thousand instances at one invoke per instance, each platform-isolated. On Azure, the application's unchanged SDK invoke reaches a Tensor9 routing layer shipped as a binary inside the deployed code package (packaged with the handler), which resolves and forwards calls by function name to Azure Functions on the Flex Consumption plan (code-only), scaling from zero toward a thousand instances at one invoke per instance, each platform-isolated.

Each function ships as one code package (your handler, the AWS-provided runtime client, the adapter, and the routing-layer binary together) on the Flex Consumption plan, which scales per function toward a thousand instances.

#### Operations and migration **Operations.** Microsoft operates Azure Functions' Flex Consumption scaling and availability; Tensor9 operates the invoke path, and the function code stays the vendor's. **Migration.** The function is compiled at the build and ships as its code package; there is no data to migrate at cutover. **Capacity and speed.** Throughput and latency are the plan's own, reported by your monitoring. #### Limitations △ Where Lambda and the Flex Consumption plan stay different * **Code packages only.** Flex Consumption runs code, not container images; a container-image function routes to a container option (Container Apps or the Premium plan) rather than running here. * **Size ceilings.** About 0.8 GB /tmp and about 4 GB instance memory shared with the runtime bundle; a /tmp-heavy or high-memory function routes to a container option. * **The routing layer updates only on redeploy.** It ships inside the code package, so a routing-layer update requires rebuilding and redeploying the package; there is no separately-deployed process to update on its own. * **Synchronous invoke only.** An asynchronous (Event) invoke returns a clear error; layers, event source mappings, aliases, and function URLs stop the build with a clear error rather than dropping silently. #### Other considerations Consider request isolation, idle capacity, and how functions call one another. * **Each concurrent invoke gets its own instance.** Tensor9 sets `http_concurrency` to one and the platform schedules to it, so every invoke has its own memory and `/tmp`; and no in-process state is shared across concurrent calls. * **Flex Consumption scales to zero when idle:** per-function target-based scaling reaches toward a thousand instances and to zero when idle, so it can handle many concurrent invocations and billing follows use, with a cold start on the first invoke after idle. * **Callees are resolved by the packaged routing binary:** the routing layer is included as a binary beside the handler and resolves a callee by function name inside the deployed set, so a call to a function outside the stack returns a clear error rather than reaching real AWS. * **Store durable state in a backing service.** Function storage is temporary. Persist data through the services your handler calls so it survives instance replacement. #### Function lifecycle on Flex Consumption A ready Azure Functions deployment supplies the invocation route. Code packaging, instance-size limits and synchronous-only delivery remain unchanged by lifecycle adaptation. See [function lifecycle and invocation](/service-adapters/aws/compute-containers/lambda#function-lifecycle-and-invocation) for Pending/Active state, supported lifecycle operations and invocation outcomes. ### Via Azure Functions (Premium) | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------------------------------------ | ----------------- | ------------ | ------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Compute sizing | Configuration | Supported | - | - | memory is applied as authored; CPU derived from Lambda's delivered compute (the continuous memory/1769 vCPU credit rate, its binding throughput ceiling, not the stepped visible-core count), mapped onto the Premium instance SKU (EP1/EP2/EP3) | | Environment variables | Configuration | Supported | - | - | your function's environment variables are set on its workload | | Ephemeral storage (/tmp) | Configuration | Supported | - | - | each invoke gets platform-isolated instance storage; the Premium instance's /tmp is sized in GB (no \~0.8 GB code-plan cap), closer to Lambda's ephemeral storage than the Flex Consumption option | | Reserved concurrency | Configuration | Supported | - | - | configured reserved concurrency maps to the plan's maximum instance count (Premium's burst ceiling is 20-100 by SKU) | | Timeout | Configuration | Supported | - | - | the authored timeout (Lambda's 3s default when unset) is enforced by the platform at the configured deadline; the adapter returns Lambda's timeout contract | | Event source mappings | Event integration | Out of scope | - | - | not compiled; the build stops with a clear error rather than silently dropping them | | Asynchronous (Event) invocation | Invocation | Out of scope | - | - | the Event invocation type of Invoke is not served; a synchronous RequestResponse Invoke is | | Container-image packaging | Packaging | Supported | - | - | a container-image function runs directly, because Premium hosts custom containers, so the compiled image (AWS-provided runtime client + Tensor9 adapter + your handler + the routing-layer binary) runs as-is | | Layers | Packaging | Out of scope | - | - | not compiled; the build stops with a clear error | | Zip packaging | Packaging | Supported | - | - | a zip-packaged function ships as a code package (the runtime bundle rides in the package, as on the Flex Consumption option) | | Aliases / versions / provisioned concurrency / function URLs | Routing | Out of scope | - | - | not compiled; the build stops with a clear error | | Operation | Area | Support | Depth | Notes | | ------------------------ | ---------- | ------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Invoke | Invocation | Supported | Common | a sync Invoke reaches the function through a translation layer that adapts Azure's HTTP-trigger invocation to the real Lambda runtime, with the callee resolved by function name through the caller's routing layer | | InvokeWithResponseStream | Invocation | Out of scope | Most usage | response streaming is not served; the synchronous RequestResponse invoke is the served path | #### How it works On AWS your application calls Lambda through the AWS SDK; those calls are unchanged. At deploy time the compiler prepares each function for the Premium plan, which hosts the compiled container image directly or a code package. At runtime a translation layer adapts Azure's invocation to the real Lambda runtime and returns the supported Lambda response fields and function-error framing; the routing layer is included as a binary inside the deployed artifact, packaged with the handler. Only synchronous request/response invokes are served. Premium keeps prewarmed instances ready to serve requests, corresponding to Lambda's provisioned concurrency. These instances reduce first-request latency and are billed continuously, including when idle. Each concurrent invocation runs on a separate instance with its own memory and gigabytes of `/tmp` storage, more than the code-only Flex plan provides. HTTP scaling is limited to 20 to 100 instances, depending on the SKU, compared with a thousand on Flex Consumption. Premium suits functions that need low cold-start latency; Flex Consumption supports more concurrent instances and bills according to use.
On Azure, the application's unchanged SDK invoke reaches a Tensor9 routing layer shipped as a binary inside the deployed artifact (packaged with the handler), which resolves and forwards calls by function name to Azure Functions on the Premium plan: always-ready prewarmed instances holding a minimum warm capacity, hosting a container image or a code package, scaling HTTP to a per-SKU ceiling of 20 to 100 instances. On Azure, the application's unchanged SDK invoke reaches a Tensor9 routing layer shipped as a binary inside the deployed artifact (packaged with the handler), which resolves and forwards calls by function name to Azure Functions on the Premium plan: always-ready prewarmed instances holding a minimum warm capacity, hosting a container image or a code package, scaling HTTP to a per-SKU ceiling of 20 to 100 instances.

Each function runs on the Premium plan, which keeps always-ready prewarmed instances (to reduce cold-start latency) and hosts a container image or a code package.

#### Operations and migration **Operations.** Microsoft operates Azure Functions' Premium scaling and availability; Tensor9 operates the invoke path, and the function code stays the vendor's. **Migration.** The function is compiled at the build and ships as its container image or code package; there is no data to migrate at cutover. **Capacity and speed.** Throughput and latency are the plan's own, reported by your monitoring. #### Limitations △ Where Lambda and the Premium plan stay different * **Warm instances bill continuously.** The always-ready floor is billed whether or not it serves traffic, so Premium costs more than the scale-to-zero options (Flex Consumption, Container Apps). * **A lower ceiling than Flex Consumption.** HTTP scale-out reaches a 20-to-100-instance ceiling by SKU, well below Flex Consumption's thousand. * **The routing layer updates only on redeploy.** It ships inside the deployed artifact (a single container or code package, not a separately deployed service), so a routing-layer update requires rebuilding and redeploying. * **Synchronous invoke only.** An asynchronous (Event) invoke returns a clear error; layers, event source mappings, aliases, and function URLs stop the build with a clear error rather than dropping silently. #### Other considerations Consider request isolation, idle capacity, and how functions call one another. * **Compare cold-start latency and instance limits.** Premium prewarms instances to reduce first-request latency, like Lambda's provisioned concurrency. Flex Consumption supports more concurrent instances. * **Each concurrent invoke scales onto its own instance:** concurrent invokes spread across separate instances, each with its own memory and a GB-sized `/tmp` closer to Lambda's ephemeral storage than the code-only Flex plan, so no in-process state is shared across concurrent calls. * **The plan hosts a container image or a code package:** unlike code-only Flex Consumption, Premium runs the compiled container image directly or a code package, so a `/tmp`-heavy or image-based function that Flex would route elsewhere runs here as-is. * **Store durable state in a backing service.** Function storage is temporary. Persist data through the services your handler calls so it survives instance replacement. #### Function lifecycle on Premium The Premium deployment must become ready before invocation; its warm capacity and bundled routing-layer update lifecycle retain the costs and redeployment requirements above. See [function lifecycle and invocation](/service-adapters/aws/compute-containers/lambda#function-lifecycle-and-invocation) for Pending/Active state, supported lifecycle operations and invocation outcomes. ## On OCI ### Via OKE function Deployments | Capability | Area | Support | Required tier | Operations | Notes | | ---------------------------------------------------------- | ------------- | ------------ | ------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------- | | Environment variables | Configuration | Supported | - | - | Configured on the function workload. | | Ephemeral storage (/tmp) | Configuration | Out of scope | - | - | The container filesystem does not reproduce Lambda's configured /tmp cap. | | Function lifecycle | Configuration | Partial | - | - | Supported lifecycle requests reconcile function configuration onto the selected cluster; invocation requires observed Active readiness. | | Reserved concurrency | Configuration | Out of scope | - | - | A Deployment replica count is not a Lambda reserved-concurrency limit. | | Timeout | Configuration | Partial | - | - | The authored timeout does not provide a per-request Kubernetes process deadline. | | Event sources, layers, aliases, versions and function URLs | Integration | Out of scope | - | - | These source integrations are outside the selected Kubernetes function deployment. | | Asynchronous (Event) invocation | Invocation | Out of scope | - | - | This target serves synchronous calls; it does not provide an asynchronous event queue. | | Zip packaging | Packaging | Supported | - | - | The runtime base and function code are built into a container image. | | Operation | Area | Support | Depth | Notes | | ------------------------ | ---------- | ------------ | ---------- | -------------------------------------------------------------------------------------------- | | Invoke | Invocation | Supported | Common | Synchronous RequestResponse calls reach the configured function through its cluster Service. | | InvokeWithResponseStream | Invocation | Out of scope | Most usage | Response streaming is outside this deployment contract. | #### How it works Each function runs as an always-on Deployment on OKE, behind a cluster-local Service. A Lambda Invoke wrapper accepts synchronous requests and passes events to the function runtime. A runtime interface client runs the handler; the client alone does not make an arbitrary container expose the Invoke API. #### Function packaging and lifecycle Zip functions are built into a container image with their runtime. Container-image functions need the configured Lambda-compatible entrypoint and Invoke wrapper. Publish the image to a registry the cluster can access, configure environment values, and wait for the deployed function to become ready. Supported lifecycle calls record desired configuration and update the selected hosting resources; a pending deployment is not yet an invocation endpoint. #### Cluster networking and identity The customer configures the OCI VCN, cluster access and node capacity. Callers must be able to reach the function Service, and the function needs network access to its dependencies. Configure the supported OCI workload identity or another approved credential for native OCI requests. Image-pull credentials and the cluster control-plane identity are separate from the function's runtime permissions. An AWS SDK call through another service adapter uses that adapter's authorization path; native cloud access is configured separately. #### Execution limits and operations A Deployment does not supply Lambda's per-invocation sandbox, reserved-concurrency admission or request-driven scale-to-zero behavior. The authored timeout is not a Kubernetes process deadline, and container storage does not reproduce the configured Lambda /tmp quota. Asynchronous Event invocation, response streaming and the listed event integrations remain outside this target. Plan node upgrades and capacity, collect function logs, and verify handler errors as well as successful HTTP calls. ### Via OCI Functions | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------------------------------------ | ----------------- | ------------ | ------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Compute sizing | Configuration | Supported | - | - | memory selects the function's resource tier at build time, snapped up to an OCI tier (e.g. 1769 MiB → 2048); the tier determines CPU; memory ceils to an OCI tier up to 3072 MB; a larger Lambda stops the build with a clear error | | Environment variables | Configuration | Supported | - | - | set on the function's workload | | Ephemeral storage (/tmp) | Configuration | Out of scope | - | - | the function gets the container filesystem; Lambda's sized /tmp cap is not reproduced on OCI Functions | | Reserved concurrency | Configuration | Out of scope | - | - | configured reserved concurrency is not honored on OCI Functions | | Timeout | Configuration | Supported | - | - | the authored timeout applies up to OCI's 300s ceiling (a longer Lambda timeout stops the build with a clear error); enforced by the platform, and the adapter returns Lambda's timeout contract | | Event source mappings | Event integration | Out of scope | - | - | not compiled; the build stops with a clear error rather than silently dropping them | | Asynchronous (Event) invocation | Invocation | Out of scope | - | - | the Event invocation type of Invoke is not served; a synchronous RequestResponse Invoke is | | Layers | Packaging | Out of scope | - | - | not compiled; the build stops with a clear error | | Zip packaging | Packaging | Supported | - | - | a zip-packaged function is built into a container image at release: the runtime base plus your code, fronted by the AWS-provided runtime client | | Aliases / versions / provisioned concurrency / function URLs | Routing | Out of scope | - | - | not compiled; the build stops with a clear error | | Operation | Area | Support | Depth | Notes | | ------------------------ | ---------- | ------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Invoke | Invocation | Supported | Common | synchronous RequestResponse resolved by function name at request time; a redeployed callee is picked up automatically. Signing, idle-resume, and a real Lambda handler running through the runtime interface client validated on live OCI (2026-07-10); warm p50 8.5 ms and steady-state cold-start p50 365 ms measured in-region, with image-cached cold-start results distinguished from first image pulls | | InvokeWithResponseStream | Invocation | Out of scope | Most usage | response streaming is not served; the synchronous RequestResponse invoke is the served path | #### How it works On AWS your application calls Lambda through the AWS SDK; those calls are unchanged. At deploy time the compiler packages each function for OCI Functions: the function runs a real Lambda handler fronted by a supervisor image that speaks OCI Functions' own invocation contract, so only the image packaging differs from the cluster targets, not the handler. At runtime the adapter serves the synchronous **Invoke** and returns the supported Lambda response fields and function-error framing. Only synchronous request/response invokes are served. OCI Functions differs by being **managed function hosting**: Oracle's platform owns the invocation lifecycle end to end, reusing warm containers and scaling to zero when idle. Concurrency is enforced by the platform: concurrent requests use separate containers, reusing warm capacity when available, so no request shares process memory with another, Lambda's default isolation, the same isolation as Cloud Run. Container Apps enforces this through the adapter. The adapter resolves a callee by its function name against the platform's own directory at request time, so a redeployed callee is picked up automatically on the next call with no stored address table; requests are signed with the appliance's own OCI identity (resource-principal or API-key signing), so requests need no AWS keys. Because OCI's platform has firmer ceilings than the other clouds, the authored timeout applies up to OCI's 300-second maximum and memory rounds up to an OCI tier, and a function configured above those limits is refused at the build with a clear error rather than running degraded.
In OCI tenancy, the application's unchanged SDK invoke reaches a Tensor9 adapter beside it, which resolves the callee by function name at call time and forwards to OCI Functions, an application-and-function pair where the platform reuses warm containers and starts more for concurrent demand and holds one request per container. In OCI tenancy, the application's unchanged SDK invoke reaches a Tensor9 adapter beside it, which resolves the callee by function name at call time and forwards to OCI Functions, an application-and-function pair where the platform reuses warm containers and starts more for concurrent demand and holds one request per container.

Each function becomes an application-and-function pair on OCI Functions, Oracle's managed FaaS; the platform reuses warm containers and starts more for concurrent demand and holds one request per container.

#### Operations and migration **Operations.** Oracle operates OCI Functions' scaling and availability; Tensor9 operates the invoke path, and the function code stays the vendor's. **Migration.** The function is compiled at the build and ships as its container image; there is no data to migrate at cutover. **Capacity and speed.** Throughput and latency are OCI Functions' own, reported by your monitoring. #### Limitations △ Where Lambda and OCI Functions stay different * **Timeout and memory meet OCI ceilings.** The configured timeout applies up to OCI's 300-second maximum and memory rounds up to an OCI tier; a Lambda function above those limits is rejected at the build with a clear error. * **Lambda's /tmp cap is not restored.** Ephemeral /tmp is the container's own filesystem; Lambda's sized ephemeral-storage limit is not reproduced. * **Reserved concurrency is not honored.** The platform manages scaling per request, so an configured reserved-concurrency setting is not applied. * **Synchronous invoke only.** An asynchronous (Event) invoke returns a clear error; layers, event source mappings, aliases, and function URLs stop the build with a clear error rather than dropping silently. #### Other considerations Consider request isolation, idle capacity, and how functions call one another. * **Concurrency is enforced by the platform.** Oracle assigns concurrent calls to separate container instances, so no invoke shares process memory with another (Lambda's default isolation), and a handler that leaned on shared in-process state across concurrent calls does not get it here. * **Idle scales to zero; the first call is a cold start:** the platform owns the invocation lifecycle end to end, reusing warm containers and scaling to zero when idle, so billing follows use and a cold call pays a container start. * **Callees are resolved at request time:** the adapter looks a callee up by function name against the platform's own directory on each call, so a redeployed callee is picked up automatically with no stored address table, and requests are signed with the appliance's OCI identity rather than AWS keys. * **Store durable state in a backing service.** Function storage is temporary. Persist data through the services your handler calls so it survives instance replacement. #### Function lifecycle on OCI Functions OCI function creation and activation precede invocation on the observed function route. The invocation measurements above do not measure creation or update time; OCI execution and packaging limits remain as documented. See [function lifecycle and invocation](/service-adapters/aws/compute-containers/lambda#function-lifecycle-and-invocation) for Pending/Active state, supported lifecycle operations and invocation outcomes. ## On Private Kubernetes ### Via Kubernetes Cluster | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------------------------------------ | ----------------- | ------------ | ------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Compute sizing | Configuration | Supported | - | - | memory is applied as authored; CPU derived from Lambda's delivered compute (the continuous memory/1769 vCPU credit rate, its binding throughput ceiling, not the stepped visible-core count), emitted as a CFS CPU limit | | Environment variables | Configuration | Supported | - | - | set on the function's workload | | Ephemeral storage (/tmp) | Configuration | Out of scope | - | - | the function gets the container filesystem; Lambda's sized /tmp cap is not reproduced on Kubernetes | | Reserved concurrency | Configuration | Out of scope | - | - | configured reserved concurrency is not honored on Kubernetes | | Timeout | Configuration | Partial | - | - | Kubernetes has no per-request execution deadline. The authored value is passed through, but the connection backstop above Lambda's 900s maximum only limits the caller's response wait. It does not terminate the remote handler; the handler can continue after the wait ends. | | Event source mappings | Event integration | Out of scope | - | - | not compiled; the build stops with a clear error rather than silently dropping them | | Asynchronous (Event) invocation | Invocation | Out of scope | - | - | the Event invocation type of Invoke is not served; a synchronous RequestResponse Invoke is | | Layers | Packaging | Out of scope | - | - | not compiled; the build stops with a clear error | | Zip packaging | Packaging | Supported | - | - | a zip-packaged function is built into a container image at release: runtime base plus your code, fronted by the AWS-provided runtime client | | Aliases / versions / provisioned concurrency / function URLs | Routing | Out of scope | - | - | not compiled; the build stops with a clear error | | Operation | Area | Support | Depth | Notes | | ------------------------ | ---------- | ------------ | ---------- | --------------------------------------------------------------------------------------------------------- | | Invoke | Invocation | Supported | Common | routed in-cluster by function name; the function serves the real Invoke API (synchronous RequestResponse) | | InvokeWithResponseStream | Invocation | Out of scope | Most usage | response streaming is not served; the synchronous RequestResponse invoke is the served path | #### How it works On AWS your application calls Lambda through the AWS SDK; those calls are unchanged. At deploy time the compiler turns each function into a container image in the customer's own registry: a container-image function keeps its handler code, with deployment packaging supplying the Invoke frontend around the runtime, and a zip-packaged function is built into a container image at release. At runtime the **service adapter** serves only the synchronous **Invoke** API and forwards each call to the deployed function, returning supported Lambda response fields: a handler that returns and one that throws use the supported response and function-error framing. Only synchronous request/response invokes are served. Each function runs as an **always-on Kubernetes Deployment** with one warm replica. It stays running when idle and can handle concurrent invocations in the same pod, so the handler must support concurrent execution. Scaling uses the target cluster's autoscaling. The adapter derives the function's cluster address from its name, without a lookup table. Kubernetes does not enforce Lambda's request timeout, reserved concurrency, or configured `/tmp` capacity. The running replica avoids starting a container for each incoming request, but consumes cluster capacity while idle.
In the target cluster, the application's unchanged SDK invoke reaches a Tensor9 adapter beside it, which forwards the call by function name to the callee running as an always-on Kubernetes Deployment (one warm replica, no scale-to-zero) that serves the real Invoke API on port 8080. In the target cluster, the application's unchanged SDK invoke reaches a Tensor9 adapter beside it, which forwards the call by function name to the callee running as an always-on Kubernetes Deployment (one warm replica, no scale-to-zero) that serves the real Invoke API on port 8080.

Each function runs as its own always-on Deployment; the adapter serves the synchronous invoke and returns the supported Lambda response fields and function-error framing.

#### Operations and migration **Operations.** Tensor9 runs each function as a cluster workload and operates the invoke path; the nodes and capacity it runs on are the customer's, and the function code stays the vendor's. **Migration.** The function is compiled at the build and ships as its container image; there is no data to migrate at cutover. **Capacity and speed.** The invoke's latency and throughput are the cluster's own, reported by your monitoring, not anything measured here. #### Limitations △ Where Lambda and an always-on Deployment stay different * **No per-request deadline.** Kubernetes does not enforce the configured Lambda execution deadline. The connection timeout limits the caller's response wait; it does not terminate the remote handler, which can continue after that wait ends. * **Lambda's /tmp cap is not restored.** Ephemeral /tmp is the container's own filesystem; Lambda's sized ephemeral-storage limit is not reproduced. * **Reserved concurrency is not honored.** An always-on replica has no per-request admission point, so configured reserved concurrency is not honored. * **Synchronous invoke only.** An asynchronous (Event) invoke returns a clear error; layers, event source mappings, aliases, and function URLs stop the build with a clear error rather than dropping silently. #### Other considerations Consider request isolation, idle capacity, and how functions call one another. * **The warm replica consumes capacity while idle:** each function runs as one warm replica that stays up whether or not an invoke arrives, so there is no start-up latency and no scale-to-zero; cost comes from a continuously-running pod sized by the cluster autoscaling rather than pay-per-invoke. * **Concurrent invokes share the always-on replica:** unlike the FaaS targets there is no per-request admission point, so invokes run together in the one pod and the handler must tolerate concurrent execution; you scale it with the cluster autoscaling rather than per invoke. * **Function names determine their addresses:** the adapter composes the callee's in-cluster address straight from the function name with no lookup table, so a redeployed callee in the same stack is reached without a re-apply while a call outside the stack returns a clear error rather than reaching real AWS. * **Store durable state in a backing service.** Function storage is temporary. Persist data through the services your handler calls so it survives instance replacement. #### Function lifecycle on Kubernetes The function reconciles to an always-on Deployment on your cluster. Readiness does not establish a handler result or restore the absent execution deadline, reserved concurrency or sized /tmp cap. See [function lifecycle and invocation](/service-adapters/aws/compute-containers/lambda#function-lifecycle-and-invocation) for Pending/Active state, supported lifecycle operations and invocation outcomes. ### Via Knative Service | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------------------------------------ | ----------------- | ------------ | ------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Compute sizing | Configuration | Supported | - | - | memory is applied as authored; CPU derived from Lambda's delivered compute (the continuous memory/1769 vCPU credit rate, its binding throughput ceiling, not the stepped visible-core count), emitted as a CFS CPU limit | | Environment variables | Configuration | Supported | - | - | set on the function's workload | | Ephemeral storage (/tmp) | Configuration | Out of scope | - | - | the function gets the container filesystem; Lambda's sized /tmp cap is not reproduced on Knative | | Reserved concurrency | Configuration | Out of scope | - | - | configured reserved concurrency is not honored on Knative | | Timeout | Configuration | Partial | - | - | the Knative revision default (300s) limits the HTTP response wait, not the authored Lambda execution timeout; the handler can continue after a response timeout | | Event source mappings | Event integration | Out of scope | - | - | not compiled; the build stops with a clear error rather than silently dropping them | | Asynchronous (Event) invocation | Invocation | Out of scope | - | - | the Event invocation type of Invoke is not served; a synchronous RequestResponse Invoke is | | Layers | Packaging | Out of scope | - | - | not compiled; the build stops with a clear error | | Zip packaging | Packaging | Supported | - | - | a zip-packaged function is built into a container image at release: runtime base plus your code, fronted by the AWS-provided runtime client | | Aliases / versions / provisioned concurrency / function URLs | Routing | Out of scope | - | - | not compiled; the build stops with a clear error | | Operation | Area | Support | Depth | Notes | | ------------------------ | ---------- | ------------ | ---------- | --------------------------------------------------------------------------------------------------------- | | Invoke | Invocation | Supported | Common | routed in-cluster by function name; the function serves the real Invoke API (synchronous RequestResponse) | | InvokeWithResponseStream | Invocation | Out of scope | Most usage | response streaming is not served; the synchronous RequestResponse invoke is the served path | #### How it works On AWS your application calls Lambda through the AWS SDK; those calls are unchanged. At deploy time the compiler turns each function into a container image in the customer's own registry: a container-image function uses the configured Lambda-compatible entrypoint, and a zip-packaged function is built into a container image at release. The AWS runtime client runs the handler; an HTTP Invoke wrapper delivers requests to that runtime. At runtime the **adapter** serves only the synchronous **Invoke** API and forwards each call to the deployed function, returning the supported Lambda response fields and function-error framing. Only synchronous request/response invokes are served. Knative differs from the always-on Kubernetes Deployment by being **request-driven**: the platform scales the service up and down with load. By default it keeps one replica warm as a guard against cold starts and caps scale-out at a bounded ceiling (ten replicas by default, never unbounded). When a scaled-down revision is cold, the activator buffers the invoke while a new pod starts, so a cold call is delayed rather than dropped. Every function pod is restricted by a network policy: traffic in only from the Knative data plane and its own namespace, and no internet egress, so a function cannot quietly call back to real AWS. This option sits between the always-on Deployment and the managed function services (OCI Functions, Azure Functions): Tensor9 sets the replica limits (the minimum warm capacity and the ceiling) rather than a cloud provider. Knative's revision-default deadline limits the HTTP response wait, not Lambda's configured execution time. A timed-out response does not establish termination of the remote handler.
In the target cluster, the application's unchanged SDK invoke reaches a Tensor9 adapter beside it, which forwards the call by function name to the callee running as a cluster-local Knative Service (a minimum warm capacity of one replica scaling up to a bounded ceiling, request-driven and blocked from the internet by network policy) serving the real Invoke API on port 8080. In the target cluster, the application's unchanged SDK invoke reaches a Tensor9 adapter beside it, which forwards the call by function name to the callee running as a cluster-local Knative Service (a minimum warm capacity of one replica scaling up to a bounded ceiling, request-driven and blocked from the internet by network policy) serving the real Invoke API on port 8080.

Each function runs as a cluster-local Knative Service that scales with request load behind the activator, blocked from the public internet by network policy.

#### Operations and migration **Operations.** Tensor9 runs each function as a Knative Service and operates the invoke path; the nodes and capacity it runs on are the customer's, and the function code stays the vendor's. **Migration.** The function is compiled at the build and ships as its container image; there is no data to migrate at cutover. **Capacity and speed.** The invoke's latency and throughput are the cluster's own, reported by your monitoring, not anything measured here. #### Limitations △ Where Lambda and a Knative Service stay different * **Knative uses its default request deadline.** The Knative revision default limits the HTTP response wait rather than enforcing Lambda's configured execution deadline. The handler can continue after the response times out; that timeout is not a remote process-kill operation. * **Reserved concurrency does not change the replica limit.** Scale is bounded by a minimum warm capacity and a fixed ceiling, but your reserved-concurrency setting does not move that ceiling. * **Lambda's /tmp cap is not restored.** Ephemeral /tmp is the container's own filesystem; Lambda's sized ephemeral-storage limit is not reproduced. * **Synchronous invoke only.** An asynchronous (Event) invoke returns a clear error; layers, event source mappings, aliases, and function URLs stop the build with a clear error rather than dropping silently. #### Other considerations Consider request isolation, idle capacity, and how functions call one another. * **One replica stays warm by default:** the service keeps one replica warm by default and caps scale-out at a bounded ceiling, so a steady invoke is served warm while a scaled-down revision's first call is buffered by the activator and delayed rather than dropped. * **Tensor9 sets the replica limits.** Tensor9 configures the minimum and maximum replica counts. Set the maximum for peak traffic and budget for one replica that remains running while idle. * **Each function pod is blocked from the internet by network policy:** a network policy admits traffic only from the Knative data plane and the pod's own namespace and allows no internet egress, so a function cannot call back to real AWS; a callee is reached by composing its in-cluster address from the function name. * **Store durable state in a backing service.** Function storage is temporary. Persist data through the services your handler calls so it survives instance replacement. #### Function lifecycle on Knative The function reconciles to a Knative revision and invokes through its cluster-local route. Revision readiness, the activator's response deadline and handler completion are separate; warm and maximum replicas retain the limits above. See [function lifecycle and invocation](/service-adapters/aws/compute-containers/lambda#function-lifecycle-and-invocation) for Pending/Active state, supported lifecycle operations and invocation outcomes. [Service Catalog](/service-adapters/catalog). # Systems Manager (SSM) Source: https://docs.tensor9.com/service-adapters/aws/configuration-management/systems-manager-ssm AWS Systems Manager (SSM). Manages fleets of instances: inventory, patch baselines, remote shell sessions, Run Command documents, and Parameter Store for configuration values. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [On Google Cloud](#on-google-cloud) * [Via Google Parameter Manager](#via-google-parameter-manager) * [Via Secret Manager](#via-secret-manager) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of Systems Manager (SSM) with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | Systems Manager (SSM) | Google Cloud · Google Parameter Manager | Google Cloud · Secret Manager | Azure | OCI | Private Kubernetes | | ------------------------------------------ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | API served · what your app calls | Parameter Store API | the same API, served from durable adapter state | the same API, served from durable adapter state | the same API, served from durable adapter state | the same API, served from durable adapter state | the same API, served from durable adapter state | | Where parameters live · the backing store | AWS-managed storage | durable parameter records + Google Parameter Manager target copy | durable parameter records + Google Secret Manager target copy | durable parameter records + Azure App Configuration target copy | durable parameter records + OCI Vault target copy | durable parameter records + Kubernetes Secrets target copy | | Encryption at rest · SecureString handling | KMS-encrypted SecureString | all three type markers are retained in durable state; Parameter Manager encrypts its provider copy at rest; sensitivity follows actual values | all three type markers are retained in durable state; Secret Manager encrypts its provider copy at rest | String and StringList are supported; SecureString remains outside the App Configuration mapping. Direct Key Vault access is a separate application path | all three type markers are retained in durable state; the configured OCI vault key encrypts the provider copy | all three type markers are retained; protect durable state and configure Kubernetes storage encryption. Base64 encoding does not encrypt the target copy | | Path hierarchies · by-path reads | native | caller-scoped record queries; no per-value provider reads | caller-scoped record queries; no per-value provider reads | caller-scoped record queries; no per-value provider reads | caller-scoped record queries; no per-value provider reads | caller-scoped record queries; no per-value provider reads | | Versions & labels · history | 100-version history + labels | retained values and adapter-owned label references provide parameter history and named labels; Parameter Manager has no native version aliases | retained values and adapter-owned label references provide parameter history and named labels; native aliases do not decide AWS reads | numeric history is retained; the adapter owns AWS version selection. Named SSM labels remain outside this mapping | - | only the latest value and increasing AWS version counter are retained; previous values and named labels are unsupported | | API coverage | full | partial | partial | partial | partial | partial | | Versions · history | 100-version history + labels | - | - | - | numeric history is retained; the adapter owns AWS version identity and selection independently of native OCI versionNumber values | - | | Delete semantics · DeleteParameter | immediate delete | - | - | - | leaves AWS reads immediately; native secret cleanup is scheduled separately | - | ## On Google Cloud ### Via Google Parameter Manager | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------------- | -------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Consumed parameters (read-only references) | Build & deploy | Supported | Common | a parameter the stack reads but does not own is supplied by the customer during installation; that supply path is distinct from runtime parameter management | | Filters & discovery (DescribeParameters / ParameterFilters) | Discovery | Partial | Full surface | DescribeParameters serves paginated metadata without values; complete ParameterFilters behavior is outside the documented contract | | Change notifications | Events | Out of scope | Full surface | parameter-change events are outside this mapping | | Batch reads (GetParameters) | Parameters | Supported | Common | reads caller-scoped parameter records and partitions found and missing names | | Hierarchies & by-path reads (GetParametersByPath) | Parameters | Supported | Common | queries live adapter records by path, applies recursion rules, and returns paginated results; no provider value fetch per returned parameter | | Parameter reads (GetParameter) | Parameters | Supported | Common | reads the current value, type, and AWS version from the caller-scoped durable parameter record; the target copy is reconciled separately | | Parameter writes & deletes | Parameters | Supported | Common | PutParameter creates or updates durable parameter state and reconciles the target; DeleteParameter withdraws reads while target cleanup proceeds separately | | Parameter policies & Advanced tier | Policies | Out of scope | Most usage | parameter policies and the Advanced tier are outside the documented contract | | Public & shared parameters | Scope | Out of scope | Full surface | AWS-published public parameters and cross-account shared parameters are outside this mapping | | The wider Systems Manager suite | Scope | Out of scope | Full surface | Parameter Store only: remote commands, sessions, patching, state management, and documents remain out of scope | | Custom encryption keys (KeyId) | Types | Out of scope | Most usage | custom per-parameter AWS KMS keys are unsupported; protect durable adapter state and configure the target copy independently | | Parameter types (String / StringList / SecureString) | Types | Supported | Common | all three type markers are retained in durable state; Parameter Manager encrypts its provider copy at rest; sensitivity follows actual values | | Reading ciphertext (WithDecryption=false) | Types | Out of scope | Most usage | SecureString reads require WithDecryption=true; the adapter does not return AWS KMS ciphertext | | Versions & labels | Versions | Supported | Most usage | retained values and adapter-owned label references provide parameter history and named labels; Parameter Manager has no native version aliases | #### Requests and durable parameter state With Max adaptation, your application sends Parameter Store requests in its AWS SDK to the Tensor9 adapter. The adapter keeps a durable parameter record containing the name, type, current value, version counter, description, and tags. Reads use that record. A background worker reconciles its current value to Google Parameter Manager. `PutParameter` creates a parameter or updates it when `Overwrite=true`. An existing name with `Overwrite=false` returns `ParameterAlreadyExists`. Writes advance the AWS version counter and wait for target reconciliation within a bounded interval; a terminal target rejection is reported. `DeleteParameter` removes the parameter from reads while target cleanup proceeds separately. Protect and back up the adapter's state as well as the target store. Direct provider edits do not update the AWS parameter record and can be overwritten during reconciliation. Existing AWS values and provider history are not automatically imported.
Parameter Store requests read durable adapter records; reconciliation maintains the configured target copy. Parameter Store requests read durable adapter records; reconciliation maintains the configured target copy.

AWS parameter reads use durable adapter state. Target reconciliation and cleanup run separately.

#### Batch reads, paths, and metadata GetParameter and GetParameters read caller-scoped parameter records. Batch reads partition found and missing names. GetParametersByPath selects live records by path, applies recursion rules, and returns paginated results in name order. It does not fetch every matching value from the provider store, so the earlier direct-proxy per-parameter access cost is not the Max read path. DescribeParameters returns paginated metadata, including names, types, versions, and descriptions, without parameter values. Complete ParameterFilters behavior is outside the documented contract. Tag operations update the parameter's metadata. These operations cover Parameter Store; Run Command, Session Manager, patching, state management, and documents remain outside the mapping. #### Versions and labels This mapping retains parameter history and named SSM labels. The adapter maintains retained values and label-to-version references for GetParameterHistory, LabelParameterVersion, and version-qualified reads. Parameter Manager has no native version aliases; selecting its highest native version is not the Max request path. #### Target storage and encryption Google Parameter Manager holds the reconciled parameter value in the customer's project. The deployment uses Google workload identity. Parameter resources and their native versions are the target representation; the adapter owns the AWS parameter name, current version, and read behavior. All three parameter types are supported. Parameter Manager encrypts its copy at rest. Protect durable adapter state and backups too. Parameter resources can contain sensitive values; choose access controls from their contents, not the service's name. #### Limits and deployment Custom per-parameter KMS keys, AWS ciphertext reads with WithDecryption=false, parameter policies, and the Advanced tier are unsupported. SecureString reads on a supporting mapping require WithDecryption=true. AWS-published public parameters, cross-account shared parameters, and parameter-change events are outside this mapping. Load values through the Parameter Store API and verify reads, overwrite behavior, and deletion before switching the application. Deployment can also initialize stack-owned literal values from the release. The customer supplies parameters the stack only reads during installation; that secret-supply path is distinct from runtime parameter management. Provider storage, access permissions, reconciliation, and durable-state availability affect operation. Historical direct-proxy measurements describe their recorded request path; they do not establish Max latency, throughput, or per-read provider charges. ### Via Secret Manager | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------------- | -------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Consumed parameters (read-only references) | Build & deploy | Supported | Common | a parameter the stack reads but does not own is supplied by the customer during installation; that supply path is distinct from runtime parameter management | | Filters & discovery (DescribeParameters / ParameterFilters) | Discovery | Partial | Full surface | DescribeParameters serves paginated metadata without values; complete ParameterFilters behavior is outside the documented contract | | Change notifications | Events | Out of scope | Full surface | parameter-change events are outside this mapping | | Batch reads (GetParameters) | Parameters | Supported | Common | reads caller-scoped parameter records and partitions found and missing names | | Hierarchies & by-path reads (GetParametersByPath) | Parameters | Supported | Common | queries live adapter records by path, applies recursion rules, and returns paginated results; no provider value fetch per returned parameter | | Parameter reads (GetParameter) | Parameters | Supported | Common | reads the current value, type, and AWS version from the caller-scoped durable parameter record; the target copy is reconciled separately | | Parameter writes & deletes | Parameters | Supported | Common | PutParameter creates or updates durable parameter state and reconciles the target; DeleteParameter withdraws reads while target cleanup proceeds separately | | Parameter policies & Advanced tier | Policies | Out of scope | Most usage | parameter policies and the Advanced tier are outside the documented contract | | Public & shared parameters | Scope | Out of scope | Full surface | AWS-published public parameters and cross-account shared parameters are outside this mapping | | The wider Systems Manager suite | Scope | Out of scope | Full surface | Parameter Store only: remote commands, sessions, patching, state management, and documents remain out of scope | | Custom encryption keys (KeyId) | Types | Out of scope | Most usage | custom per-parameter AWS KMS keys are unsupported; protect durable adapter state and configure the target copy independently | | Parameter types (String / StringList / SecureString) | Types | Supported | Common | all three type markers are retained in durable state; Secret Manager encrypts its provider copy at rest | | Reading ciphertext (WithDecryption=false) | Types | Out of scope | Most usage | SecureString reads require WithDecryption=true; the adapter does not return AWS KMS ciphertext | | Versions & labels | Versions | Supported | Most usage | retained values and adapter-owned label references provide parameter history and named labels; native aliases do not decide AWS reads | #### Requests and durable parameter state With Max adaptation, your application sends Parameter Store requests in its AWS SDK to the Tensor9 adapter. The adapter keeps a durable parameter record containing the name, type, current value, version counter, description, and tags. Reads use that record. A background worker reconciles its current value to Google Secret Manager. `PutParameter` creates a parameter or updates it when `Overwrite=true`. An existing name with `Overwrite=false` returns `ParameterAlreadyExists`. Writes advance the AWS version counter and wait for target reconciliation within a bounded interval; a terminal target rejection is reported. `DeleteParameter` removes the parameter from reads while target cleanup proceeds separately. Protect and back up the adapter's state as well as the target store. Direct provider edits do not update the AWS parameter record and can be overwritten during reconciliation. Existing AWS values and provider history are not automatically imported.
Parameter Store requests read durable adapter records; reconciliation maintains the configured target copy. Parameter Store requests read durable adapter records; reconciliation maintains the configured target copy.

AWS parameter reads use durable adapter state. Target reconciliation and cleanup run separately.

#### Batch reads, paths, and metadata GetParameter and GetParameters read caller-scoped parameter records. Batch reads partition found and missing names. GetParametersByPath selects live records by path, applies recursion rules, and returns paginated results in name order. It does not fetch every matching value from the provider store, so the earlier direct-proxy per-parameter access cost is not the Max read path. DescribeParameters returns paginated metadata, including names, types, versions, and descriptions, without parameter values. Complete ParameterFilters behavior is outside the documented contract. Tag operations update the parameter's metadata. These operations cover Parameter Store; Run Command, Session Manager, patching, state management, and documents remain outside the mapping. #### Versions and labels This mapping retains parameter history and named SSM labels. The adapter maintains the retained values and label-to-version references needed by GetParameterHistory, LabelParameterVersion, and version-qualified reads. Moving a provider alias directly does not move an SSM label. #### Target storage and encryption Google Secret Manager holds the reconciled current value in the customer's project. The deployment uses Google workload identity. Its native versions and aliases describe the provider copy; AWS reads and version selection belong to the adapter. All three parameter types are supported. Secret Manager encrypts its copy at rest. Protect the adapter's durable parameter state and backups as well as the provider copy; storing an ordinary String as a secret does not change the sensitivity of its contents. #### Limits and deployment Custom per-parameter KMS keys, AWS ciphertext reads with WithDecryption=false, parameter policies, and the Advanced tier are unsupported. SecureString reads on a supporting mapping require WithDecryption=true. AWS-published public parameters, cross-account shared parameters, and parameter-change events are outside this mapping. Load values through the Parameter Store API and verify reads, overwrite behavior, and deletion before switching the application. Deployment can also initialize stack-owned literal values from the release. The customer supplies parameters the stack only reads during installation; that secret-supply path is distinct from runtime parameter management. Provider storage, access permissions, reconciliation, and durable-state availability affect operation. Historical direct-proxy measurements describe their recorded request path; they do not establish Max latency, throughput, or per-read provider charges. ## On Azure | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------------- | -------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Consumed parameters (read-only references) | Build & deploy | Supported | Common | a parameter the stack reads but does not own is supplied by the customer during installation; that supply path is distinct from runtime parameter management | | Filters & discovery (DescribeParameters / ParameterFilters) | Discovery | Partial | Full surface | DescribeParameters serves paginated metadata without values; complete ParameterFilters behavior is outside the documented contract | | Change notifications | Events | Out of scope | Full surface | parameter-change events are outside this mapping | | Batch reads (GetParameters) | Parameters | Supported | Common | reads caller-scoped parameter records and partitions found and missing names | | Hierarchies & by-path reads (GetParametersByPath) | Parameters | Supported | Common | queries live adapter records by path, applies recursion rules, and returns paginated results; no provider value fetch per returned parameter | | Parameter reads (GetParameter) | Parameters | Supported | Common | reads the current value, type, and AWS version from the caller-scoped durable parameter record; the target copy is reconciled separately | | Parameter writes & deletes | Parameters | Supported | Common | PutParameter creates or updates durable parameter state and reconciles the target; DeleteParameter withdraws reads while target cleanup proceeds separately | | Parameter policies & Advanced tier | Policies | Out of scope | Most usage | parameter policies and the Advanced tier are outside the documented contract | | Public & shared parameters | Scope | Out of scope | Full surface | AWS-published public parameters and cross-account shared parameters are outside this mapping | | The wider Systems Manager suite | Scope | Out of scope | Full surface | Parameter Store only: remote commands, sessions, patching, state management, and documents remain out of scope | | Custom encryption keys (KeyId) | Types | Out of scope | Most usage | custom per-parameter AWS KMS keys are unsupported; protect durable adapter state and configure the target copy independently | | Parameter types (String / StringList / SecureString) | Types | Partial | Common | String and StringList are supported; SecureString remains outside the App Configuration mapping. Direct Key Vault access is a separate application path | | Reading ciphertext (WithDecryption=false) | Types | Out of scope | Most usage | SecureString reads require WithDecryption=true; the adapter does not return AWS KMS ciphertext | | Versions & labels | Versions | Partial | Most usage | numeric history is retained; the adapter owns AWS version selection. Named SSM labels remain outside this mapping | #### Requests and durable parameter state With Max adaptation, your application sends Parameter Store requests in its AWS SDK to the Tensor9 adapter. The adapter keeps a durable parameter record containing the name, type, current value, version counter, description, and tags. Reads use that record. A background worker reconciles its current value to Azure App Configuration. `PutParameter` creates a parameter or updates it when `Overwrite=true`. An existing name with `Overwrite=false` returns `ParameterAlreadyExists`. Writes advance the AWS version counter and wait for target reconciliation within a bounded interval; a terminal target rejection is reported. `DeleteParameter` removes the parameter from reads while target cleanup proceeds separately. Protect and back up the adapter's state as well as the target store. Direct provider edits do not update the AWS parameter record and can be overwritten during reconciliation. Existing AWS values and provider history are not automatically imported.
Parameter Store requests read durable adapter records; reconciliation maintains the configured target copy. Parameter Store requests read durable adapter records; reconciliation maintains the configured target copy.

AWS parameter reads use durable adapter state. Target reconciliation and cleanup run separately.

#### Batch reads, paths, and metadata GetParameter and GetParameters read caller-scoped parameter records. Batch reads partition found and missing names. GetParametersByPath selects live records by path, applies recursion rules, and returns paginated results in name order. It does not fetch every matching value from the provider store, so the earlier direct-proxy per-parameter access cost is not the Max read path. DescribeParameters returns paginated metadata, including names, types, versions, and descriptions, without parameter values. Complete ParameterFilters behavior is outside the documented contract. Tag operations update the parameter's metadata. These operations cover Parameter Store; Run Command, Session Manager, patching, state management, and documents remain outside the mapping. #### Versions and labels This mapping retains numeric parameter versions; named SSM version labels are unsupported. App Configuration labels can represent retained target versions, but they are different from SSM labels. The adapter owns the AWS version counter and selection; the highest provider label does not decide a Max read. #### Target storage and encryption Azure App Configuration holds the reconciled configuration value in the customer's store, accessed with the deployment's managed identity. Its key prefixes and label dimension organize the provider copy. GetParametersByPath queries adapter records rather than using a provider prefix query as the application read path. String and StringList are supported. SecureString is unsupported for this App Configuration mapping. Accessing Azure Key Vault directly requires application changes and is a separate option. Protect adapter state and the encrypted provider store; storage encryption alone does not add SecureString support. #### Limits and deployment Custom per-parameter KMS keys, AWS ciphertext reads with WithDecryption=false, parameter policies, and the Advanced tier are unsupported. SecureString reads on a supporting mapping require WithDecryption=true. AWS-published public parameters, cross-account shared parameters, and parameter-change events are outside this mapping. Load values through the Parameter Store API and verify reads, overwrite behavior, and deletion before switching the application. Deployment can also initialize stack-owned literal values from the release. The customer supplies parameters the stack only reads during installation; that secret-supply path is distinct from runtime parameter management. Provider storage, access permissions, reconciliation, and durable-state availability affect operation. Historical direct-proxy measurements describe their recorded request path; they do not establish Max latency, throughput, or per-read provider charges. ## On OCI | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------------- | -------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Consumed parameters (read-only references) | Build & deploy | Supported | Common | a parameter the stack reads but does not own is supplied by the customer during installation; that supply path is distinct from runtime parameter management | | Filters & discovery (DescribeParameters / ParameterFilters) | Discovery | Partial | Full surface | DescribeParameters serves paginated metadata without values; complete ParameterFilters behavior is outside the documented contract | | Change notifications | Events | Out of scope | Full surface | parameter-change events are outside this mapping | | Batch reads (GetParameters) | Parameters | Supported | Common | reads caller-scoped parameter records and partitions found and missing names | | Hierarchies & by-path reads (GetParametersByPath) | Parameters | Supported | Common | queries live adapter records by path, applies recursion rules, and returns paginated results; no provider value fetch per returned parameter | | Parameter reads (GetParameter) | Parameters | Supported | Common | reads the current value, type, and AWS version from the caller-scoped durable parameter record; the target copy is reconciled separately | | Parameter writes & deletes | Parameters | Supported | Common | PutParameter creates or updates durable parameter state and reconciles the target; DeleteParameter withdraws reads while target cleanup proceeds separately | | Parameter policies & Advanced tier | Policies | Out of scope | Most usage | parameter policies and the Advanced tier are outside the documented contract | | Public & shared parameters | Scope | Out of scope | Full surface | AWS-published public parameters and cross-account shared parameters are outside this mapping | | The wider Systems Manager suite | Scope | Out of scope | Full surface | Parameter Store only: remote commands, sessions, patching, state management, and documents remain out of scope | | Custom encryption keys (KeyId) | Types | Out of scope | Most usage | custom per-parameter AWS KMS keys are unsupported; protect durable adapter state and configure the target copy independently | | Parameter types (String / StringList / SecureString) | Types | Supported | Common | all three type markers are retained in durable state; the configured OCI vault key encrypts the provider copy | | Reading ciphertext (WithDecryption=false) | Types | Out of scope | Most usage | SecureString reads require WithDecryption=true; the adapter does not return AWS KMS ciphertext | | Move-only version labels | Versions | Out of scope | Most usage | named SSM labels are outside this mapping; native OCI stages do not define the AWS contract | | Versions | Versions | Supported | Most usage | numeric history is retained; the adapter owns AWS version identity and selection independently of native OCI versionNumber values | #### Requests and durable parameter state With Max adaptation, your application sends Parameter Store requests in its AWS SDK to the Tensor9 adapter. The adapter keeps a durable parameter record containing the name, type, current value, version counter, description, and tags. Reads use that record. A background worker reconciles its current value to OCI Vault. `PutParameter` creates a parameter or updates it when `Overwrite=true`. An existing name with `Overwrite=false` returns `ParameterAlreadyExists`. Writes advance the AWS version counter and wait for target reconciliation within a bounded interval; a terminal target rejection is reported. `DeleteParameter` removes the parameter from reads while target cleanup proceeds separately. Protect and back up the adapter's state as well as the target store. Direct provider edits do not update the AWS parameter record and can be overwritten during reconciliation. Existing AWS values and provider history are not automatically imported.
Parameter Store requests read durable adapter records; reconciliation maintains the configured target copy. Parameter Store requests read durable adapter records; reconciliation maintains the configured target copy.

AWS parameter reads use durable adapter state. Target reconciliation and cleanup run separately.

#### Batch reads, paths, and metadata GetParameter and GetParameters read caller-scoped parameter records. Batch reads partition found and missing names. GetParametersByPath selects live records by path, applies recursion rules, and returns paginated results in name order. It does not fetch every matching value from the provider store, so the earlier direct-proxy per-parameter access cost is not the Max read path. DescribeParameters returns paginated metadata, including names, types, versions, and descriptions, without parameter values. Complete ParameterFilters behavior is outside the documented contract. Tag operations update the parameter's metadata. These operations cover Parameter Store; Run Command, Session Manager, patching, state management, and documents remain outside the mapping. #### Versions and labels This mapping retains numeric parameter versions; named SSM labels are unsupported. OCI supplies versioned target storage, while the adapter maintains AWS version identity and selection. A native versionNumber is not an instruction to read around the parameter record. #### Target storage and encryption OCI Vault holds the reconciled value as a secret, including ordinary String configuration. Configure the vault, compartment, encryption key, and resource-principal access for secret management and retrieval. Native secret versions describe the provider copy; the adapter owns the AWS parameter record and read path. All three parameter types use the configured vault key for the provider copy. OCI schedules physical secret deletion; that cleanup can finish after the parameter leaves the AWS-readable set. Reusing a name must be reconciled with any retained native secret, without confusing its version sequence with the new AWS parameter identity. Protect durable adapter state and backups as well as the vault. #### Limits and deployment Custom per-parameter KMS keys, AWS ciphertext reads with WithDecryption=false, parameter policies, and the Advanced tier are unsupported. SecureString reads on a supporting mapping require WithDecryption=true. AWS-published public parameters, cross-account shared parameters, and parameter-change events are outside this mapping. Load values through the Parameter Store API and verify reads, overwrite behavior, and deletion before switching the application. Deployment can also initialize stack-owned literal values from the release. The customer supplies parameters the stack only reads during installation; that secret-supply path is distinct from runtime parameter management. Provider storage, access permissions, reconciliation, and durable-state availability affect operation. Historical direct-proxy measurements describe their recorded request path; they do not establish Max latency, throughput, or per-read provider charges. ## On Private Kubernetes | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------------- | -------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Consumed parameters (read-only references) | Build & deploy | Supported | Common | a parameter the stack reads but does not own is supplied by the customer during installation; that supply path is distinct from runtime parameter management | | Filters & discovery (DescribeParameters / ParameterFilters) | Discovery | Partial | Full surface | DescribeParameters serves paginated metadata without values; complete ParameterFilters behavior is outside the documented contract | | Change notifications | Events | Out of scope | Full surface | parameter-change events are outside this mapping | | Batch reads (GetParameters) | Parameters | Supported | Common | reads caller-scoped parameter records and partitions found and missing names | | Hierarchies & by-path reads (GetParametersByPath) | Parameters | Supported | Common | queries live adapter records by path, applies recursion rules, and returns paginated results; no provider value fetch per returned parameter | | Parameter reads (GetParameter) | Parameters | Supported | Common | reads the current value, type, and AWS version from the caller-scoped durable parameter record; the target copy is reconciled separately | | Parameter writes & deletes | Parameters | Supported | Common | PutParameter creates or updates durable parameter state and reconciles the target; DeleteParameter withdraws reads while target cleanup proceeds separately | | Parameter policies & Advanced tier | Policies | Out of scope | Most usage | parameter policies and the Advanced tier are outside the documented contract | | Public & shared parameters | Scope | Out of scope | Full surface | AWS-published public parameters and cross-account shared parameters are outside this mapping | | The wider Systems Manager suite | Scope | Out of scope | Full surface | Parameter Store only: remote commands, sessions, patching, state management, and documents remain out of scope | | Custom encryption keys (KeyId) | Types | Out of scope | Most usage | custom per-parameter AWS KMS keys are unsupported; protect durable adapter state and configure the target copy independently | | Parameter types (String / StringList / SecureString) | Types | Supported | Common | all three type markers are retained; protect durable state and configure Kubernetes storage encryption. Base64 encoding does not encrypt the target copy | | Reading ciphertext (WithDecryption=false) | Types | Out of scope | Most usage | SecureString reads require WithDecryption=true; the adapter does not return AWS KMS ciphertext | | Versions & labels | Versions | Out of scope | Most usage | only the latest value and increasing AWS version counter are retained; previous values and named labels are unsupported | #### Requests and durable parameter state With Max adaptation, your application sends Parameter Store requests in its AWS SDK to the Tensor9 adapter. The adapter keeps a durable parameter record containing the name, type, current value, version counter, description, and tags. Reads use that record. A background worker reconciles its current value to Kubernetes Secrets. `PutParameter` creates a parameter or updates it when `Overwrite=true`. An existing name with `Overwrite=false` returns `ParameterAlreadyExists`. Writes advance the AWS version counter and wait for target reconciliation within a bounded interval; a terminal target rejection is reported. `DeleteParameter` removes the parameter from reads while target cleanup proceeds separately. Protect and back up the adapter's state as well as the target store. Direct provider edits do not update the AWS parameter record and can be overwritten during reconciliation. Existing AWS values and provider history are not automatically imported.
Parameter Store requests read durable adapter records; reconciliation maintains the configured target copy. Parameter Store requests read durable adapter records; reconciliation maintains the configured target copy.

AWS parameter reads use durable adapter state. Target reconciliation and cleanup run separately.

#### Batch reads, paths, and metadata GetParameter and GetParameters read caller-scoped parameter records. Batch reads partition found and missing names. GetParametersByPath selects live records by path, applies recursion rules, and returns paginated results in name order. It does not fetch every matching value from the provider store, so the earlier direct-proxy per-parameter access cost is not the Max read path. DescribeParameters returns paginated metadata, including names, types, versions, and descriptions, without parameter values. Complete ParameterFilters behavior is outside the documented contract. Tag operations update the parameter's metadata. These operations cover Parameter Store; Run Command, Session Manager, patching, state management, and documents remain outside the mapping. #### Versions and labels This mapping retains the latest value and its increasing version counter. Previous values and named SSM labels are unsupported. A counter records writes; it does not make old values readable. Choose a mapping with retained history when the application needs it. #### Target storage and encryption A Kubernetes Secret holds the reconciled current value in the customer cluster. The deployment's service account accesses the backing objects. AWS reads use durable parameter records, not a Kubernetes Secret lookup for each request; the parameter counter belongs to that record. All three parameter type markers are preserved. Kubernetes Secret values are base64-encoded; base64 is not encryption. Configure Kubernetes access control and storage encryption, and protect the adapter's durable state and backups. This storage option stays inside the customer cluster, including disconnected deployments. #### Limits and deployment Custom per-parameter KMS keys, AWS ciphertext reads with WithDecryption=false, parameter policies, and the Advanced tier are unsupported. SecureString reads on a supporting mapping require WithDecryption=true. AWS-published public parameters, cross-account shared parameters, and parameter-change events are outside this mapping. Load values through the Parameter Store API and verify reads, overwrite behavior, and deletion before switching the application. Deployment can also initialize stack-owned literal values from the release. The customer supplies parameters the stack only reads during installation; that secret-supply path is distinct from runtime parameter management. Provider storage, access permissions, reconciliation, and durable-state availability affect operation. Historical direct-proxy measurements describe their recorded request path; they do not establish Max latency, throughput, or per-read provider charges. [Service Catalog](/service-adapters/catalog). # Aurora PostgreSQL Source: https://docs.tensor9.com/service-adapters/aws/databases-storage/aurora-postgresql AWS Aurora PostgreSQL. PostgreSQL-compatible engine running on the same Aurora storage layer, where replicas read from shared storage rather than replaying a write-ahead log stream. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of Aurora PostgreSQL with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation #### Runtime surface | Capability | Aurora PostgreSQL | Google Cloud | Azure | OCI | | ------------ | ----------------- | ------------ | ----- | ---- | | API coverage | full | high | high | high | #### Management surface | Capability | Aurora PostgreSQL | Google Cloud | Azure | OCI | | ------------------------------------------------- | ----------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Snapshot / restore + PITR | Yes | Yes | Yes | Partial - OCI supports point-in-time recovery with a configured policy, retained WAL and periodic backups, creating a new database system within the active recovery window; the documented adapter mapping does not promise AWS point-in-time restore translation | | Zero-ETL integration · Redshift | Yes | Partial - replicates to BigQuery (Datastream); Redshift itself is not served | Partial - replicates to Microsoft Fabric (OneLake); Redshift itself is not served | No | | Read replicas · in-region | Yes | Yes | Yes | Yes | | Read replicas · cross-region | Yes | Yes | Yes | No - OCI supports readable warm standby systems in up to three disaster recovery regions, using asynchronous replication; the documented adapter mapping does not promise AWS cross-region replica or Aurora global-cluster semantics | | Managed connection pooling · RDS Proxy | Yes | Yes | Yes | Yes | | Trusted Language Extensions · pg\_tle | Yes | No | No | No | | Data API (HTTP SQL) · rds-data | Yes | Yes | Yes | Yes | | Query performance insights · Performance Insights | Yes | Partial - the vendor can't access the appliance's cloud console | Partial - the vendor can't access the appliance's cloud console | Partial - the vendor can't access the appliance's cloud console | #### Limits | Capability | Aurora PostgreSQL | Google Cloud | Azure | OCI | | ------------------------------------------- | ----------------- | ------------ | ----------- | --------------- | | Maximum storage | 256 TiB | 64 TiB | 64 TiB | 32 TiB | | vCPU range · smallest to largest instance | 2-192 | 1-128 | 1-192 | 2-128 (64 OCPU) | | Memory range · smallest to largest instance | 4-1,536 GiB | 0.6-864 GiB | 2-1,832 GiB | 16-1,024 GiB | | Storage autoscaling | Yes | Yes | Yes | Yes | ### Infrastructure-only adaptation #### Runtime surface | Capability | Aurora PostgreSQL | Google Cloud | Azure | OCI | Private Kubernetes | | ------------ | ----------------- | ------------ | ----- | ---- | ------------------ | | API coverage | full | high | high | high | high | #### Management surface | Capability | Aurora PostgreSQL | Google Cloud | Azure | OCI | Private Kubernetes | | ------------------------------------------------- | ----------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- | | Snapshot / restore + PITR | Yes | Yes | Yes | Partial - OCI supports point-in-time recovery with a configured policy, retained WAL and periodic backups, creating a new database system within the active recovery window; the documented adapter mapping does not promise AWS point-in-time restore translation | Yes | | Zero-ETL integration · Redshift | Yes | Partial - replicates to BigQuery (Datastream); Redshift itself is not served | Partial - replicates to Microsoft Fabric (OneLake); Redshift itself is not served | No | No | | Read replicas · in-region | Yes | Yes | Yes | Yes | Yes | | Read replicas · cross-region | Yes | Yes | Yes | No - OCI supports readable warm standby systems in up to three disaster recovery regions, using asynchronous replication; the documented adapter mapping does not promise AWS cross-region replica or Aurora global-cluster semantics | No | | Managed connection pooling · RDS Proxy | Yes | Yes | Yes | Yes | Yes | | Trusted Language Extensions · pg\_tle | Yes | No | No | No | Yes | | Data API (HTTP SQL) · rds-data | Yes | Yes | Yes | Yes | Yes | | Query performance insights · Performance Insights | Yes | Partial - the vendor can't access the appliance's cloud console | Partial - the vendor can't access the appliance's cloud console | Partial - the vendor can't access the appliance's cloud console | Partial - the vendor can't access the appliance's cloud console | #### Limits | Capability | Aurora PostgreSQL | Google Cloud | Azure | OCI | Private Kubernetes | | ------------------------------------------- | ----------------- | ------------ | ----------- | --------------- | --------------------------------------------------------------------- | | Maximum storage | 256 TiB | 64 TiB | 64 TiB | 32 TiB | limited by persistent volume capacity | | vCPU range · smallest to largest instance | 2-192 | 1-128 | 1-192 | 2-128 (64 OCPU) | limited by Kubernetes node resources | | Memory range · smallest to largest instance | 4-1,536 GiB | 0.6-864 GiB | 2-1,832 GiB | 16-1,024 GiB | limited by Kubernetes node resources | | Storage autoscaling | Yes | Yes | Yes | Yes | No - increase the volume size explicitly; it does not grow with usage | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------------------------------------------------------------ | ------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------- | | AWS/Aurora-proprietary SQL: aws\_s3.*, aws\_lambda.invoke, Aurora ML (aws\_sagemaker / aws\_comprehend), aurora\_* / apg\_plan\_mgmt | SQL functions | Out of scope | Full surface | AWS and Aurora extension functions with no counterpart on vanilla PostgreSQL; a query calling them fails | | pgwire protocol (application SQL) | Wire protocol | Supported | Common | wire-compatible PostgreSQL; application SQL is unchanged | #### How it works Your application's PostgreSQL driver connects directly to Cloud SQL for PostgreSQL. These database requests are the **data plane**. The driver and PostgreSQL wire protocol stay unchanged, and Tensor9 adds no proxy, translation, or latency to the query path. Management requests, such as snapshots, restore, replicas, failover, and configuration changes, form the **control plane**. Tensor9 serves the RDS API in the customer's appliance and translates those requests to the target's management API. The sections below describe which operations have equivalents and which return errors.
Database requests (SQL over pgwire) connect directly to Cloud SQL. The Tensor9 service adapter translates RDS API management calls to Cloud SQL admin API. Database requests (SQL over pgwire) connect directly to Cloud SQL. The Tensor9 service adapter translates RDS API management calls to Cloud SQL admin API.

Queries reach the database directly; only the management calls are translated.

#### Database connections Aurora uses an AWS-specific storage layer but exposes the PostgreSQL wire protocol to your application. Cloud SQL for PostgreSQL supports that protocol. Your SQL, drivers, object-relational mappers (ORMs), prepared statements, data types, and transaction semantics remain unchanged; the target replaces Aurora's storage layer. AWS and Aurora add SQL functions for moving data through S3, invoking Lambda, and machine learning. These functions have no equivalent on the target. Queries that call them return an error. #### Database management At runtime your application and operational tooling keep making the same RDS calls they make today: describe the cluster, take a snapshot, restore to a point in time, add a reader, trigger a failover, stop and start, change the instance class or a parameter. For the supported operations, the adapter accepts RDS requests and uses the RDS response format, translating the operation to Cloud SQL: on-demand backups and restores, point-in-time restore, read replicas, manual failover, stop / start, and machine-type, storage, and database-flag updates. The adapter also serves the Aurora Data API (SQL over HTTPS, transactions included), executing each statement over a native connection to the backend database. Aurora's global database is served by the adapter as well: Cloud SQL has no global-cluster object, so the adapter keeps the cluster container itself and maps its members and role changes onto Cloud SQL's cross-region read replicas, with planned switchover and unplanned failover translating to the target's managed promote. With Max, your tooling uses CreateDBCluster and CreateDBInstance through the RDS adapter to create a logical Aurora cluster and its members on Cloud SQL. Tensor9 retains durable logical cluster and member state and reconciles it with the target resources: accepted requests are recorded, then native resources are created or removed asynchronously. DescribeDBClusters and DescribeDBInstances report status and endpoints. DeleteDBInstance removes members; DeleteDBCluster removes the cluster after its members are gone. Your application's SQL connection goes directly to the database. With the Infrastructure-only alternative, Tensor9 compiles the declared cluster infrastructure into native Cloud SQL resources. You manage those resources through the target's own tools; serving runtime RDS API requests requires Max.
The Tensor9 service adapter handles RDS management requests using Cloud SQL admin API and returns RDS responses. The Tensor9 service adapter handles RDS management requests using Cloud SQL admin API and returns RDS responses.

Tensor9 translates RDS management requests to the target API and returns RDS responses.

#### Limitations △ Where Aurora and Cloud SQL stay different * **Aurora-specific settings have no counterpart.** Failover priority / promotion tiers, Serverless v2 autoscaling, global write forwarding, and I/O-Optimized storage are Aurora-specific. * **Event subscriptions are not served.** RDS event notifications publish to SNS; Cloud Monitoring is a different model with no direct counterpart. * **Parameter groups retain their logical identity.** Tensor9 retains named parameter groups and their overrides. Supported explicit PostgreSQL parameters become Cloud SQL database flags; the group object itself stays with Tensor9 rather than becoming a native object on Cloud SQL. The group family and PostgreSQL version must match, and target limits on allowed values and restart requirements still apply. RDS-specific parameters and settings outside the target's settable list are rejected. #### Other considerations **Migration.** Moving the data itself is a one-time step, covered by this page's migration note: logical dump / restore, or logical replication for a low-downtime cutover. This page describes steady state after cutover. **Operations.** The customer owns the Cloud SQL service in their Google Cloud: maintenance windows, quotas, and pricing are Google's. **Features and capacity.** The comparison table on this page lists database features and high-availability differences. Check the target service's pricing and capacity limits when sizing the deployment. ## On Azure | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------------------------------------------------------------ | ------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------- | | AWS/Aurora-proprietary SQL: aws\_s3.*, aws\_lambda.invoke, Aurora ML (aws\_sagemaker / aws\_comprehend), aurora\_* / apg\_plan\_mgmt | SQL functions | Out of scope | Full surface | AWS and Aurora extension functions with no counterpart on vanilla PostgreSQL; a query calling them fails | | pgwire protocol (application SQL) | Wire protocol | Supported | Common | wire-compatible PostgreSQL; application SQL is unchanged | #### How it works Your application's PostgreSQL driver connects directly to Azure Database for PostgreSQL Flexible Server. These database requests are the **data plane**. The driver and PostgreSQL wire protocol stay unchanged, and Tensor9 adds no proxy, translation, or latency to the query path. Management requests, such as snapshots, restore, replicas, failover, and configuration changes, form the **control plane**. Tensor9 serves the RDS API in the customer's appliance and translates those requests to the target's management API. The sections below describe which operations have equivalents and which return errors.
Database requests (SQL over pgwire) connect directly to PostgreSQL Flexible Server. The Tensor9 service adapter translates RDS API management calls to Azure management API. Database requests (SQL over pgwire) connect directly to PostgreSQL Flexible Server. The Tensor9 service adapter translates RDS API management calls to Azure management API.

Queries reach the database directly; only the management calls are translated.

#### Database connections Aurora uses an AWS-specific storage layer but exposes the PostgreSQL wire protocol to your application. Azure Database for PostgreSQL Flexible Server supports that protocol. Your SQL, drivers, object-relational mappers (ORMs), prepared statements, data types, and transaction semantics remain unchanged; the target replaces Aurora's storage layer. AWS and Aurora add SQL functions for moving data through S3, invoking Lambda, and machine learning. These functions have no equivalent on the target. Queries that call them return an error. #### Database management At runtime your application and operational tooling keep making the same RDS calls they make today: describe the cluster, take a snapshot, restore to a point in time, add a reader, trigger a failover, stop and start, change the instance class or a parameter. For the supported operations, the adapter accepts RDS requests and uses the RDS response format, translating the operation to the Flexible Server: on-demand backups and restores, point-in-time restore, read replicas, manual failover, stop / start, and compute, storage, and server-parameter updates. The adapter also serves the Aurora Data API (SQL over HTTPS, transactions included), executing each statement over a native connection to the backend database. Aurora's global database is served by the adapter as well: the Flexible Server has no global-cluster object, so the adapter keeps the cluster container itself and maps its members and role changes onto cross-region read replicas, with planned switchover and unplanned failover translating to the target's managed replica promotion operation. With Max, your tooling uses CreateDBCluster and CreateDBInstance through the RDS adapter to create a logical Aurora cluster and its members on Azure PostgreSQL Flexible Server. Tensor9 retains durable logical cluster and member state and reconciles it with the target resources: accepted requests are recorded, then native resources are created or removed asynchronously. DescribeDBClusters and DescribeDBInstances report status and endpoints. DeleteDBInstance removes members; DeleteDBCluster removes the cluster after its members are gone. Your application's SQL connection goes directly to the database. With the Infrastructure-only alternative, Tensor9 compiles the declared cluster infrastructure into native Azure PostgreSQL Flexible Server resources. You manage those resources through the target's own tools; serving runtime RDS API requests requires Max.
The Tensor9 service adapter handles RDS management requests using Azure management API and returns RDS responses. The Tensor9 service adapter handles RDS management requests using Azure management API and returns RDS responses.

Tensor9 translates RDS management requests to the target API and returns RDS responses.

#### Limitations △ Where Aurora and PostgreSQL Flexible Server stay different * **Aurora-specific settings have no counterpart.** Failover priority / promotion tiers, Serverless v2 autoscaling, global write forwarding, and I/O-Optimized storage are Aurora-specific. * **Event subscriptions are not served.** RDS event notifications publish to SNS; Azure Monitor is a different model with no direct counterpart. * **Parameter groups retain their logical identity.** Tensor9 retains named parameter groups and their overrides. Supported explicit PostgreSQL parameters become Azure server parameters; the group object itself stays with Tensor9 rather than becoming a native object on the Flexible Server. The group family and PostgreSQL version must match, and target limits on allowed values and restart requirements still apply. RDS-specific parameters and settings outside the target's settable list are rejected. #### Other considerations **Migration.** Moving the data itself is a one-time step, covered by this page's migration note: logical dump / restore, or logical replication for a low-downtime cutover. This page describes steady state after cutover. **Operations.** The customer owns the Flexible Server in their Azure subscription: maintenance windows, quotas, and pricing are Microsoft's. **Features and capacity.** The comparison table on this page lists database features and high-availability differences. Check the target service's pricing and capacity limits when sizing the deployment. ## On OCI | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------------------------------------------------------------ | ------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------- | | AWS/Aurora-proprietary SQL: aws\_s3.*, aws\_lambda.invoke, Aurora ML (aws\_sagemaker / aws\_comprehend), aurora\_* / apg\_plan\_mgmt | SQL functions | Out of scope | Full surface | AWS and Aurora extension functions with no counterpart on vanilla PostgreSQL; a query calling them fails | | pgwire protocol (application SQL) | Wire protocol | Supported | Common | wire-compatible PostgreSQL; application SQL is unchanged | #### How it works Your application's PostgreSQL driver connects directly to OCI Database with PostgreSQL. These database requests are the **data plane**. The driver and PostgreSQL wire protocol stay unchanged, and Tensor9 adds no proxy, translation, or latency to the query path. Management requests, such as snapshots, restore, replicas, failover, and configuration changes, form the **control plane**. Tensor9 serves the RDS API in the customer's appliance and translates those requests to the target's management API. The sections below describe which operations have equivalents and which return errors.
Database requests (SQL over pgwire) connect directly to OCI PostgreSQL. The Tensor9 service adapter translates RDS API management calls to OCI management API. Database requests (SQL over pgwire) connect directly to OCI PostgreSQL. The Tensor9 service adapter translates RDS API management calls to OCI management API.

Queries reach the database directly; only the management calls are translated.

#### Database connections Aurora uses an AWS-specific storage layer but exposes the PostgreSQL wire protocol to your application. OCI Database with PostgreSQL supports that protocol. Your SQL, drivers, object-relational mappers (ORMs), prepared statements, data types, and transaction semantics remain unchanged; the target replaces Aurora's storage layer. AWS and Aurora add SQL functions for moving data through S3, invoking Lambda, and machine learning. These functions have no equivalent on the target. Queries that call them return an error. #### Database management At runtime your application and operational tooling keep making the same RDS calls they make today. The adapter accepts RDS requests and uses the RDS response format for the supported operations, translating them to OCI: on-demand backups and restore-from-backup, in-region readers, stop / start, and compute, storage, and configuration updates. The documented adapter mapping does not promise translation of every native OCI capability into Aurora semantics; the limitations below distinguish those boundaries. Each unsupported operation returns an error. The adapter also serves the Aurora Data API (SQL over HTTPS, transactions included), executing each statement over a native connection to the backend database. With Max, your tooling uses CreateDBCluster and CreateDBInstance through the RDS adapter to create a logical Aurora cluster and its members on OCI PostgreSQL. Tensor9 retains durable logical cluster and member state and reconciles it with the target resources: accepted requests are recorded, then native resources are created or removed asynchronously. DescribeDBClusters and DescribeDBInstances report status and endpoints. DeleteDBInstance removes members; DeleteDBCluster removes the cluster after its members are gone. Your application's SQL connection goes directly to the database. With the Infrastructure-only alternative, Tensor9 compiles the declared cluster infrastructure into native OCI PostgreSQL resources. You manage those resources through the target's own tools; serving runtime RDS API requests requires Max.
The Tensor9 service adapter handles RDS management requests using OCI management API and returns RDS responses. The Tensor9 service adapter handles RDS management requests using OCI management API and returns RDS responses.

Tensor9 translates RDS management requests to the target API and returns RDS responses.

#### Limitations △ Where Aurora and OCI PostgreSQL stay different * **Parameter groups retain their logical identity.** Tensor9 retains named parameter groups and their overrides. Supported explicit PostgreSQL parameters become OCI PostgreSQL settings; the group object itself stays with Tensor9 rather than becoming a native object on OCI PostgreSQL. The group family and PostgreSQL version must match, and target limits on allowed values and restart requirements still apply. RDS-specific parameters and settings outside the target's settable list are rejected. * **Native point-in-time recovery needs a configured policy.** OCI retains WAL and periodic backups under a point-in-time recovery policy. Recovery creates a new database system at a timestamp within the active recovery window. The documented adapter mapping does not promise AWS point-in-time restore translation. * **Native local failover is separate from AWS failover translation.** OCI exposes FailoverDbSystem for user-initiated failover to an existing local replica. The documented adapter mapping does not promise translation of the AWS failover operation. * **Native cross-region standbys do not establish an Aurora global database mapping.** OCI supports readable warm standby systems in up to three disaster recovery regions. Replication is asynchronous and can lag; promotion and switchover are manual, with no automatic cross-region failover. Restore is not supported for either the primary or warm standby while configured for this replication. The documented adapter mapping does not promise Aurora global-cluster operations, global-reader routing, or automated promotion. * **No in-place major-version upgrade.** Major PostgreSQL upgrades on OCI are create-new-and-migrate. * **Aurora-specific settings and the shared RDS gaps apply too.** Failover priority tiers, Serverless v2, global write forwarding, I/O-Optimized storage and event subscriptions have no counterpart. #### Other considerations **Migration.** Moving the data itself is a one-time step, covered by this page's migration note: logical dump / restore, or logical replication for a low-downtime cutover. This page describes steady state after cutover. **Operations.** The customer owns the database service in their OCI tenancy: maintenance windows, quotas, and pricing are Oracle's. **Features and capacity.** The comparison table on this page lists database features and high-availability differences. Check the target service's pricing and capacity limits when sizing the deployment. ## On Private Kubernetes | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------------------------------------------------------------ | ------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------- | | AWS/Aurora-proprietary SQL: aws\_s3.*, aws\_lambda.invoke, Aurora ML (aws\_sagemaker / aws\_comprehend), aurora\_* / apg\_plan\_mgmt | SQL functions | Out of scope | Full surface | AWS and Aurora extension functions with no counterpart on vanilla PostgreSQL; a query calling them fails | | pgwire protocol (application SQL) | Wire protocol | Supported | Common | wire-compatible PostgreSQL; application SQL is unchanged | #### How it works Your application's PostgreSQL driver connects directly to CloudNativePG. These database requests are the **data plane**. The driver and PostgreSQL wire protocol stay unchanged, and Tensor9 adds no proxy, translation, or latency to the query path. Management requests, such as snapshots, restore, replicas, failover, and configuration changes, form the **control plane**. Tensor9 serves the RDS API in the customer's appliance and translates those requests to the target's management API. The sections below describe which operations have equivalents and which return errors.
Database requests (SQL over pgwire) connect directly to CloudNativePG. The Tensor9 service adapter translates RDS API management calls to CloudNativePG resources. Database requests (SQL over pgwire) connect directly to CloudNativePG. The Tensor9 service adapter translates RDS API management calls to CloudNativePG resources.

Queries reach the database directly; only the management calls are translated.

#### Database connections Aurora uses an AWS-specific storage layer but exposes the PostgreSQL wire protocol to your application. CloudNativePG supports that protocol. Your SQL, drivers, object-relational mappers (ORMs), prepared statements, data types, and transaction semantics remain unchanged; the target replaces Aurora's storage layer. AWS and Aurora add SQL functions for moving data through S3, invoking Lambda, and machine learning. These functions have no equivalent on the target. Queries that call them return an error. #### Database management At runtime your application and operational tooling keep making the same RDS calls they make today. The adapter accepts RDS requests and uses the RDS response format for the supported operations, translating them to CloudNativePG's Kubernetes resources: backups and restores onto its backup and recovery objects, point-in-time restore onto its native PITR, readers onto the cluster's instance count, manual failover onto replica promotion, stop / start onto scaling the cluster down and up, and parameter changes onto the cluster's PostgreSQL configuration. Managed connection pooling (the role RDS Proxy plays) is served by CloudNativePG's built-in pooler. The adapter also serves the Aurora Data API (SQL over HTTPS, transactions included), executing each statement over a native connection to the backend database. When your product is deployed into the customer environment, Tensor9 compiles the cluster your stack already declares into the equivalent CloudNativePG resources and sets the endpoint and credentials into your application's configuration. The control-plane adapter then covers the management calls your running system makes.
The Tensor9 service adapter handles RDS management requests using CloudNativePG resources and returns RDS responses. The Tensor9 service adapter handles RDS management requests using CloudNativePG resources and returns RDS responses.

Tensor9 translates RDS management requests to the target API and returns RDS responses.

#### Limitations △ Where Aurora and CloudNativePG stay different * **No global database.** The appliance runs a single CloudNativePG cluster; cross-region readers or a standby region would need a second appliance. * **Aurora-specific settings have no counterpart.** Failover priority / promotion tiers, Serverless v2 autoscaling, global write forwarding, and I/O-Optimized storage are Aurora-specific. * **Events are Kubernetes-native.** RDS event notifications publish to SNS; here the equivalents are Kubernetes events and Prometheus alerts, a different model. * **Parameter groups become individual settings.** Each PostgreSQL parameter translates to the cluster's configuration; the group object itself does not cross. #### Other considerations **Migration.** Moving the data itself is a one-time step, covered by this page's migration note: logical dump / restore, or logical replication for a low-downtime cutover. This page describes steady state after cutover. **Operations.** CloudNativePG is self-operated: after cutover the customer's team runs the cluster (backups, upgrades, failover drills), with no cloud database vendor behind it. **Features and capacity.** The comparison table on this page lists database features and high-availability differences. Check the target service's pricing and capacity limits when sizing the deployment. [Service Catalog](/service-adapters/catalog). # DocumentDB (MongoDB) Source: https://docs.tensor9.com/service-adapters/aws/databases-storage/documentdb-mongodb AWS DocumentDB (MongoDB). A document database compatible with the MongoDB API, run as a cluster of instances over shared replicated storage. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud, Azure, OCI, and Private Kubernetes](#on-google-cloud-azure-oci-and-private-kubernetes) * [Via MongoDB Atlas](#via-mongodb-atlas) * [On Google Cloud](#on-google-cloud) * [Via Firestore (MongoDB compatibility)](#via-firestore-mongodb-compatibility) * [On Azure](#on-azure) * [Via Azure Cosmos DB for MongoDB (vCore)](#via-azure-cosmos-db-for-mongodb-vcore) * [On OCI](#on-oci) * [Via Oracle Autonomous JSON Database (MongoDB API)](#via-oracle-autonomous-json-database-mongodb-api) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of DocumentDB (MongoDB) with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation #### Runtime surface | Capability | DocumentDB (MongoDB) | Google Cloud, Azure, OCI, and Private Kubernetes · MongoDB Atlas | Google Cloud · Firestore (MongoDB compatibility) | OCI · Oracle Autonomous JSON Database (MongoDB API) | | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------- | | Where your data actually sits · NOT in your own cloud account | your AWS account, in the region you chose | MongoDB's Atlas estate, operated by MongoDB Inc. | - | - | | Multi-document transactions | Yes - no retryable writes (set retryWrites=false); ambiguous commit on timeout | Yes - full ACID, cross-shard, retryable | Yes | Yes | | Change streams | Yes - 5.0+: primary or secondary nodes | Yes | Partial - preview | Partial - 26ai beta, not for production | | Correlated \$lookup | No | Yes | Yes | Partial - restricted (localField cannot be an array or missing) | | \$graphLookup | No | Yes | No | No | | Text search | Yes - since 5.0; English-only, case-insensitive, one text index / collection | Yes | Yes | Yes - no collation with it | | Vector search | Yes - HNSW / IVFFlat indexes, up to 2,000 dimensions (5.0+) | Yes - Atlas Vector Search | No | Partial - \$vectorSearch, Beta preview (26ai) | | Capped / time-series collections | No | Yes | No | No | | Server-side JS / mapReduce | Partial - mapReduce supported in 8.0; server-side JS (\$where / \$function) not supported | Yes - supported but deprecated (mapReduce since 5.0; server-side JS in 8.0) | No | No | | Collation | Yes - new in 8.0 | Yes | No | No | | API coverage | full | full | partial | partial | #### Limits | Capability | DocumentDB (MongoDB) | Google Cloud, Azure, OCI, and Private Kubernetes · MongoDB Atlas | Google Cloud · Firestore (MongoDB compatibility) | OCI · Oracle Autonomous JSON Database (MongoDB API) | | ----------------------------------------------------- | -------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------ | --------------------------------------------------- | | Maximum storage · cluster volume vs per node | 128 TiB | 14 TB / node | - | - | | vCPU range · smallest to largest instance | 2-192 | 2-96 | serverless | ECPU-based (2+, 3× auto-scale) | | Memory range · smallest to largest instance | 4-1,536 GiB | 2-768 GiB | serverless | auto (per ECPU) | | Storage autoscaling | Yes | Yes - compute-tier auto-scaling too | Yes - serverless: storage and throughput scale automatically | Yes - compute and storage auto-scale up to 3× | | Maximum storage | 128 TiB | - | serverless (auto) | - | | Maximum storage · cluster volume vs Autonomous DB max | 128 TiB | - | - | 384 TB | ### Infrastructure-only adaptation #### Runtime surface | Capability | DocumentDB (MongoDB) | Google Cloud, Azure, OCI, and Private Kubernetes · MongoDB Atlas | Google Cloud · Firestore (MongoDB compatibility) | Azure · Azure Cosmos DB for MongoDB (vCore) | OCI · Oracle Autonomous JSON Database (MongoDB API) | | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------ | ----------------------------------------------------------- | --------------------------------------------------------------- | | Where your data actually sits · NOT in your own cloud account | your AWS account, in the region you chose | MongoDB's Atlas estate, operated by MongoDB Inc. | - | - | - | | Multi-document transactions | Yes - no retryable writes (set retryWrites=false); ambiguous commit on timeout | Yes - full ACID, cross-shard, retryable | Yes | Partial - capped at a 30-second transaction lifetime | Yes | | Change streams | Yes - 5.0+: primary or secondary nodes | Yes | Partial - preview | Yes | Partial - 26ai beta, not for production | | Correlated \$lookup | No | Yes | Yes | No - no let / pipeline form | Partial - restricted (localField cannot be an array or missing) | | \$graphLookup | No | Yes | No | Yes | No | | Text search | Yes - since 5.0; English-only, case-insensitive, one text index / collection | Yes | Yes | Partial - one text index per collection, no regex / CJK | Yes - no collation with it | | Vector search | Yes - HNSW / IVFFlat indexes, up to 2,000 dimensions (5.0+) | Yes - Atlas Vector Search | No | Yes - IVF / HNSW / DiskANN, up to 16,000 dimensions | Partial - \$vectorSearch, Beta preview (26ai) | | Capped / time-series collections | No | Yes | No | No | No | | Server-side JS / mapReduce | Partial - mapReduce supported in 8.0; server-side JS (\$where / \$function) not supported | Yes - supported but deprecated (mapReduce since 5.0; server-side JS in 8.0) | No | No | No | | Collation | Yes - new in 8.0 | Yes | No | Partial - many operators; no general case-insensitive index | No | | API coverage | full | full | partial | high | partial | #### Limits | Capability | DocumentDB (MongoDB) | Google Cloud, Azure, OCI, and Private Kubernetes · MongoDB Atlas | Google Cloud · Firestore (MongoDB compatibility) | Azure · Azure Cosmos DB for MongoDB (vCore) | OCI · Oracle Autonomous JSON Database (MongoDB API) | | ----------------------------------------------------- | -------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | --------------------------------------------------- | | Maximum storage · cluster volume vs per node | 128 TiB | 14 TB / node | - | - | - | | vCPU range · smallest to largest instance | 2-192 | 2-96 | serverless | - | ECPU-based (2+, 3× auto-scale) | | Memory range · smallest to largest instance | 4-1,536 GiB | 2-768 GiB | serverless | - | auto (per ECPU) | | Storage autoscaling | Yes | Yes - compute-tier auto-scaling too | Yes - serverless: storage and throughput scale automatically | Partial - storage scales up on demand, but not automatically | Yes - compute and storage auto-scale up to 3× | | Maximum storage | 128 TiB | - | serverless (auto) | - | - | | Maximum storage · cluster volume vs per shard | 128 TiB | - | - | 32 TiB / shard | - | | vCPU range · smallest to largest, per shard | 2-192 | - | - | 1-64 | - | | Memory range · smallest to largest, per shard | 4-1,536 GiB | - | - | 2-256 GiB | - | | Maximum storage · cluster volume vs Autonomous DB max | 128 TiB | - | - | - | 384 TB | ## On Google Cloud, Azure, OCI, and Private Kubernetes ### Via MongoDB Atlas | Operation | Area | Support | Depth | Notes | | ------------------------------------------------ | ----------------------- | --------- | ------------ | -------------------------------------------------------------------------------------- | | collStats | Admin & diagnostics | Supported | Full surface | - | | dbStats | Admin & diagnostics | Supported | Full surface | - | | explain | Admin & diagnostics | Supported | Full surface | query plan | | hello | Admin & diagnostics | Supported | Full surface | topology + health handshake | | isMaster | Admin & diagnostics | Supported | Full surface | legacy topology handshake | | ping | Admin & diagnostics | Supported | Full surface | - | | serverStatus | Admin & diagnostics | Supported | Full surface | - | | delete | CRUD | Supported | Common | - | | find | CRUD | Supported | Common | - | | findAndModify | CRUD | Supported | Common | atomic read-modify-write | | getMore | CRUD | Supported | Common | cursor iteration | | insert | CRUD | Supported | Common | - | | killCursors | CRUD | Supported | Most usage | - | | update | CRUD | Supported | Common | - | | Change streams (\$changeStream / watch) | Change streams | Supported | Most usage | primary or secondary nodes | | collMod | Collections & databases | Supported | Full surface | - | | create | Collections & databases | Supported | Most usage | create collection, incl. capped / time-series which DocumentDB lacks | | drop | Collections & databases | Supported | Most usage | - | | dropDatabase | Collections & databases | Supported | Full surface | - | | listCollections | Collections & databases | Supported | Most usage | - | | listDatabases | Collections & databases | Supported | Most usage | - | | renameCollection | Collections & databases | Supported | Full surface | - | | createIndexes | Indexes | Supported | Most usage | - | | dropIndexes | Indexes | Supported | Most usage | - | | listIndexes | Indexes | Supported | Most usage | - | | aggregate | Query & aggregation | Supported | Common | the full pipeline, including correlated \$lookup / \$graphLookup that DocumentDB lacks | | count | Query & aggregation | Supported | Common | - | | distinct | Query & aggregation | Supported | Most usage | - | | mapReduce | Query & aggregation | Supported | Full surface | - | | Transactions (startTransaction / commit / abort) | Transactions | Supported | Most usage | full ACID, cross-shard, retryable; DocumentDB caps this | #### How it works Your application's MongoDB driver connects directly to MongoDB Atlas. These database requests are the **data plane**. The driver and MongoDB wire protocol stay unchanged, and Tensor9 does not proxy or translate these database requests. Management requests, such as snapshots, restore, replicas, failover, and configuration changes, form the **control plane**. Tensor9 serves the DocumentDB API in the customer's appliance and translates those requests to the target's management API. The sections below describe which operations have equivalents and which return errors.
Database requests (MongoDB wire protocol) connect directly to MongoDB Atlas. The Tensor9 service adapter translates DocumentDB API management calls to Atlas Admin API. Database requests (MongoDB wire protocol) connect directly to MongoDB Atlas. The Tensor9 service adapter translates DocumentDB API management calls to Atlas Admin API.

Queries reach the database directly; only the management calls are translated.

#### Database connections MongoDB Atlas runs MongoDB. It supports multi-document transactions, change streams, correlated lookups, and collation; their behavior and limits follow the selected MongoDB version and Atlas tier. Your driver connects directly to Atlas, where your queries and indexes run. Each target implements a different set of MongoDB features. The comparison table on this page covers transactions, change streams, lookups, text and vector search, and other features your application may use. #### Database management At runtime your application and operational tooling keep making the same DocumentDB calls they make today: describe the cluster, take a snapshot, restore to a point in time, add replica instances, pause and resume, change the tier or a parameter. The adapter accepts DocumentDB requests, returns DocumentDB response formats, and performs supported operations through the Atlas Admin API: snapshots and continuous-backup point-in-time restore, replica node layout, cluster pause / resume, and tier and storage changes. DocumentDB's global-cluster calls map to replica placement across Atlas regions. Atlas also offers geographic sharding, which partitions data by location and is a separate deployment choice. When your product is deployed into the customer environment, Tensor9 compiles the cluster your stack already declares into the equivalent MongoDB Atlas resources and sets the connection string and credentials into your application's configuration. The control-plane adapter then covers the management calls your running system makes.
The Tensor9 service adapter handles DocumentDB management requests using Atlas Admin API and returns DocumentDB responses. The Tensor9 service adapter handles DocumentDB management requests using Atlas Admin API and returns DocumentDB responses.

Tensor9 translates DocumentDB management requests to the target API and returns DocumentDB responses.

#### Limitations △ Where DocumentDB and MongoDB Atlas stay different * **Manual failover tests recovery.** Atlas elects a new primary automatically; the manual failover call maps to Atlas's test-failover, useful for verifying resilience rather than day-to-day operations. * **Event subscriptions are not served.** DocumentDB event notifications publish to SNS; Atlas alerts are a different model with no direct counterpart. * **DocumentDB-proprietary settings have no counterpart.** Standard cluster parameters translate to Atlas cluster configuration; parameters specific to DocumentDB are not supported on the target. #### Other considerations **Migration.** Copy existing data with MongoDB migration tools that the target supports. Check the workload's queries, indexes, and BSON types against the comparison table before switching connections; sharing a wire protocol does not make every MongoDB feature equivalent. **Operations.** The customer owns the Atlas organization: cluster tiers, cloud and region choices, and pricing are MongoDB's. **Features and capacity.** The comparison table on this page lists database features and high-availability differences. Check the target service's pricing and capacity limits when sizing the deployment. ## On Google Cloud ### Via Firestore (MongoDB compatibility) | Operation | Area | Support | Depth | Notes | | ------------------------------------------------ | ----------------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------- | | collStats | Admin & diagnostics | Supported | Full surface | - | | dbStats | Admin & diagnostics | Supported | Full surface | - | | explain | Admin & diagnostics | Supported | Full surface | query plan | | hello | Admin & diagnostics | Supported | Full surface | topology handshake | | isMaster | Admin & diagnostics | Supported | Full surface | legacy topology handshake | | ping | Admin & diagnostics | Supported | Full surface | - | | serverStatus | Admin & diagnostics | Supported | Full surface | - | | delete | CRUD | Supported | Common | - | | find | CRUD | Supported | Common | - | | findAndModify | CRUD | Supported | Common | atomic read-modify-write | | getMore | CRUD | Supported | Common | cursor iteration | | insert | CRUD | Supported | Common | - | | killCursors | CRUD | Supported | Most usage | - | | update | CRUD | Supported | Common | - | | Change streams (\$changeStream / watch) | Change streams | Partial | Most usage | in preview | | create | Collections & databases | Supported | Most usage | - | | drop | Collections & databases | Supported | Most usage | - | | dropDatabase | Collections & databases | Supported | Full surface | - | | listCollections | Collections & databases | Supported | Most usage | - | | listDatabases | Collections & databases | Supported | Most usage | - | | createIndexes | Indexes | Supported | Most usage | - | | dropIndexes | Indexes | Supported | Most usage | - | | listIndexes | Indexes | Supported | Most usage | - | | aggregate | Query & aggregation | Partial | Common | supports \$lookup + text search; omits \$graphLookup and a large set of stages / accumulators / geospatial operators | | count | Query & aggregation | Supported | Common | - | | distinct | Query & aggregation | Partial | Most usage | - | | mapReduce | Query & aggregation | Out of scope | Full surface | not supported | | Transactions (startTransaction / commit / abort) | Transactions | Supported | Most usage | - | #### How it works Your application's MongoDB driver connects directly to Firestore with MongoDB compatibility. These database requests are the **data plane**. The driver and MongoDB wire protocol stay unchanged, and Tensor9 does not proxy or translate these database requests. Management requests, such as snapshots, restore, replicas, failover, and configuration changes, form the **control plane**. Tensor9 serves the DocumentDB API in the customer's appliance and translates those requests to the target's management API. The sections below describe which operations have equivalents and which return errors.
Database requests (MongoDB wire protocol) connect directly to Firestore (Mongo compat). The Tensor9 service adapter translates DocumentDB API management calls to Firestore API. Database requests (MongoDB wire protocol) connect directly to Firestore (Mongo compat). The Tensor9 service adapter translates DocumentDB API management calls to Firestore API.

Queries reach the database directly; only the management calls are translated.

#### Database connections Firestore with MongoDB compatibility supports the MongoDB 7.0 wire protocol. It manages instance sizing, read scaling, and multi-region high availability. Its MongoDB compatibility has limits: no vector search or collation, many unsupported aggregation stages, and change streams in preview. Your driver connects directly to Firestore. Each target implements a different set of MongoDB features. The comparison table on this page covers transactions, change streams, lookups, text and vector search, and other features your application may use. #### Database management At runtime your application and operational tooling keep making the same DocumentDB calls they make today. The adapter accepts DocumentDB requests, returns DocumentDB response formats, and performs supported operations through the Firestore API: on-demand backups, point-in-time recovery within Firestore's 7-day window, and database-level settings. Much of the rest has nothing to translate to, because Firestore is serverless: reads scale automatically, failover is automatic, and there is no cluster to stop, start, or size. The limitations below identify those operations. Each returns an error. When your product is deployed into the customer environment, Tensor9 compiles the cluster your stack already declares into the equivalent Firestore with MongoDB compatibility resources and sets the connection string and credentials into your application's configuration. The control-plane adapter then covers the management calls your running system makes.
The Tensor9 service adapter handles DocumentDB management requests using Firestore API and returns DocumentDB responses. The Tensor9 service adapter handles DocumentDB management requests using Firestore API and returns DocumentDB responses.

Tensor9 translates DocumentDB management requests to the target API and returns DocumentDB responses.

#### Limitations △ Where DocumentDB and Firestore (Mongo compat) stay different * **Serverless operation removes instance controls.** There are no replica instances to add, no manual failover, no stop / start, and no compute or storage sizing; reads scale and fail over automatically. * **Multi-region is a create-time choice.** The database's location is fixed at creation; there is no global-cluster object and no switchover call. * **Few cluster settings have equivalents.** Database-level settings (point-in-time recovery, delete protection) map; most DocumentDB cluster parameters have no counterpart. * **Event subscriptions are not served.** DocumentDB event notifications publish to SNS; Cloud Monitoring is a different model with no direct counterpart. #### Other considerations **Migration.** Copy existing data with MongoDB migration tools that the target supports. Check the workload's queries, indexes, and BSON types against the comparison table before switching connections; sharing a wire protocol does not make every MongoDB feature equivalent. **Operations.** The customer owns the Firestore database in their Google Cloud: quotas and pricing are Google's. **Features and capacity.** The comparison table on this page lists database features and high-availability differences. Check the target service's pricing and capacity limits when sizing the deployment. ## On Azure ### Via Azure Cosmos DB for MongoDB (vCore) | Operation | Area | Support | Depth | Notes | | ------------------------------------------------ | ----------------------- | ------------ | ------------ | ---------------------------------------------------- | | collStats | Admin & diagnostics | Supported | Full surface | - | | dbStats | Admin & diagnostics | Supported | Full surface | - | | explain | Admin & diagnostics | Supported | Full surface | query plan | | hello | Admin & diagnostics | Supported | Full surface | topology handshake | | isMaster | Admin & diagnostics | Supported | Full surface | legacy topology handshake | | ping | Admin & diagnostics | Supported | Full surface | - | | serverStatus | Admin & diagnostics | Supported | Full surface | - | | delete | CRUD | Supported | Common | - | | find | CRUD | Supported | Common | - | | findAndModify | CRUD | Supported | Common | atomic read-modify-write | | getMore | CRUD | Supported | Common | cursor iteration | | insert | CRUD | Supported | Common | - | | killCursors | CRUD | Supported | Most usage | - | | update | CRUD | Supported | Common | - | | Change streams (\$changeStream / watch) | Change streams | Supported | Most usage | - | | collMod | Collections & databases | Supported | Full surface | - | | create | Collections & databases | Partial | Most usage | collections yes; capped collections not supported | | drop | Collections & databases | Supported | Most usage | - | | dropDatabase | Collections & databases | Supported | Full surface | - | | listCollections | Collections & databases | Supported | Most usage | - | | listDatabases | Collections & databases | Supported | Most usage | - | | renameCollection | Collections & databases | Supported | Full surface | - | | createIndexes | Indexes | Supported | Most usage | - | | dropIndexes | Indexes | Supported | Most usage | - | | listIndexes | Indexes | Supported | Most usage | - | | aggregate | Query & aggregation | Supported | Common | gains \$graphLookup + richer operators vs DocumentDB | | count | Query & aggregation | Supported | Common | - | | distinct | Query & aggregation | Supported | Most usage | - | | mapReduce | Query & aggregation | Out of scope | Full surface | not offered on Cosmos vCore | | Transactions (startTransaction / commit / abort) | Transactions | Partial | Most usage | multi-document ACID, capped at 30 seconds | #### How it works Your application's MongoDB driver connects directly to Azure Cosmos DB for MongoDB (vCore). These database requests are the **data plane**. The driver and MongoDB wire protocol stay unchanged, and Tensor9 does not proxy or translate these database requests. Management requests, such as snapshots, restore, replicas, failover, and configuration changes, form the **control plane**. Tensor9 serves the DocumentDB API in the customer's appliance and translates those requests to the target's management API. The sections below describe which operations have equivalents and which return errors.
Database requests (MongoDB wire protocol) connect directly to Cosmos DB (vCore). The Tensor9 service adapter translates DocumentDB API management calls to Azure management API. Database requests (MongoDB wire protocol) connect directly to Cosmos DB (vCore). The Tensor9 service adapter translates DocumentDB API management calls to Azure management API.

Queries reach the database directly; only the management calls are translated.

#### Database connections DocumentDB and Cosmos vCore each implement part of the MongoDB API. Cosmos vCore supports graph lookups and more aggregation operations than DocumentDB, but does not support server-side mapReduce. It limits multi-document transactions to 30 seconds. Your driver connects directly to Cosmos vCore, where your queries and indexes run. Each target implements a different set of MongoDB features. The comparison table on this page covers transactions, change streams, lookups, text and vector search, and other features your application may use. #### Database management At runtime your application and operational tooling keep making the same DocumentDB calls they make today: describe the cluster, take a snapshot, restore to a point in time, add replicas, trigger a failover, change the tier or a parameter. The adapter accepts DocumentDB requests, returns DocumentDB response formats, and performs supported operations on Azure: snapshots and point-in-time restore to a new cluster, replica clusters, manual failover (forced or graceful promotion), and tier and storage updates. Global-cluster calls map to Cosmos vCore's single secondary region. When your product is deployed into the customer environment, Tensor9 compiles the cluster your stack already declares into the equivalent Azure Cosmos DB for MongoDB (vCore) resources and sets the connection string and credentials into your application's configuration. The control-plane adapter then covers the management calls your running system makes.
The Tensor9 service adapter handles DocumentDB management requests using Azure management API and returns DocumentDB responses. The Tensor9 service adapter handles DocumentDB management requests using Azure management API and returns DocumentDB responses.

Tensor9 translates DocumentDB management requests to the target API and returns DocumentDB responses.

#### Limitations △ Where DocumentDB and Cosmos DB (vCore) stay different * **No stop / start.** Cosmos vCore has no pause; a managed cluster runs continuously. * **Only one secondary region is supported.** There is no multi-region global-cluster object; cross-region maps to a single secondary region with forced / graceful failover. * **Event subscriptions are not served.** DocumentDB event notifications publish to SNS; Azure Monitor is a different model with no direct counterpart. * **DocumentDB-proprietary settings have no counterpart.** Standard cluster parameters translate to the cluster's configuration; parameters specific to DocumentDB are not supported on the target. #### Other considerations **Migration.** Copy existing data with MongoDB migration tools that the target supports. Check the workload's queries, indexes, and BSON types against the comparison table before switching connections; sharing a wire protocol does not make every MongoDB feature equivalent. **Operations.** The customer owns the Cosmos DB cluster in their Azure subscription: maintenance windows, quotas, and pricing are Microsoft's. **Features and capacity.** The comparison table on this page lists database features and high-availability differences. Check the target service's pricing and capacity limits when sizing the deployment. ## On OCI ### Via Oracle Autonomous JSON Database (MongoDB API) | Operation | Area | Support | Depth | Notes | | ------------------------------------------------ | ----------------------- | ------------ | ------------ | -------------------------------------------------------------------- | | collStats | Admin & diagnostics | Supported | Full surface | - | | dbStats | Admin & diagnostics | Supported | Full surface | - | | explain | Admin & diagnostics | Supported | Full surface | query plan | | hello | Admin & diagnostics | Supported | Full surface | topology handshake | | isMaster | Admin & diagnostics | Supported | Full surface | legacy topology handshake | | ping | Admin & diagnostics | Supported | Full surface | - | | serverStatus | Admin & diagnostics | Supported | Full surface | - | | delete | CRUD | Supported | Common | - | | find | CRUD | Supported | Common | - | | findAndModify | CRUD | Supported | Common | atomic read-modify-write | | getMore | CRUD | Supported | Common | cursor iteration | | insert | CRUD | Supported | Common | - | | killCursors | CRUD | Supported | Most usage | - | | update | CRUD | Supported | Common | - | | Change streams (\$changeStream / watch) | Change streams | Partial | Full surface | 26ai beta preview | | create | Collections & databases | Supported | Most usage | - | | drop | Collections & databases | Supported | Most usage | - | | dropDatabase | Collections & databases | Supported | Full surface | - | | listCollections | Collections & databases | Supported | Most usage | - | | listDatabases | Collections & databases | Supported | Most usage | - | | createIndexes | Indexes | Supported | Most usage | - | | dropIndexes | Indexes | Supported | Most usage | - | | listIndexes | Indexes | Supported | Most usage | - | | \$sql stage / cross-collection SQL joins | Query & aggregation | Supported | Full surface | query collections through Oracle SQL, in addition to the MongoDB API | | aggregate | Query & aggregation | Partial | Common | MongoDB 4.2-era pipeline | | count | Query & aggregation | Supported | Common | - | | distinct | Query & aggregation | Supported | Most usage | - | | mapReduce | Query & aggregation | Out of scope | Full surface | not supported | | Transactions (startTransaction / commit / abort) | Transactions | Partial | Most usage | supported; some BSON types coerce on load | #### How it works Your application's MongoDB driver connects directly to Oracle Autonomous JSON Database. These database requests are the **data plane**. The driver and MongoDB wire protocol stay unchanged, and Tensor9 does not proxy or translate these database requests. Management requests, such as snapshots, restore, replicas, failover, and configuration changes, form the **control plane**. Tensor9 serves the DocumentDB API in the customer's appliance and translates those requests to the target's management API. The sections below describe which operations have equivalents and which return errors.
Database requests (MongoDB wire protocol) connect directly to Autonomous JSON Database. The Tensor9 service adapter translates DocumentDB API management calls to OCI management API. Database requests (MongoDB wire protocol) connect directly to Autonomous JSON Database. The Tensor9 service adapter translates DocumentDB API management calls to OCI management API.

Queries reach the database directly; only the management calls are translated.

#### Database connections Autonomous JSON Database serves your driver through the Oracle Database API for MongoDB, which supports a MongoDB-4.2-era protocol and executes operations on Oracle's SQL / JSON engine. Your driver connects directly using a load-balanced connection string. You can also query those collections with SQL, a feature DocumentDB does not offer. Some newer MongoDB features are missing; see the comparison table on this page. Each target implements a different set of MongoDB features. The comparison table on this page covers transactions, change streams, lookups, text and vector search, and other features your application may use. #### Database management At runtime your application and operational tooling keep making the same DocumentDB calls they make today. The adapter accepts DocumentDB requests, returns DocumentDB response formats, and performs supported operations on OCI: backups and point-in-time recovery within the retention window, stop / start, and online compute and storage scaling. The operations OCI has no counterpart for are listed under Limitations. Each returns an error. When your product is deployed into the customer environment, Tensor9 compiles the cluster your stack already declares into the equivalent Oracle Autonomous JSON Database resources and sets the connection string and credentials into your application's configuration. The control-plane adapter then covers the management calls your running system makes.
The Tensor9 service adapter handles DocumentDB management requests using OCI management API and returns DocumentDB responses. The Tensor9 service adapter handles DocumentDB management requests using OCI management API and returns DocumentDB responses.

Tensor9 translates DocumentDB management requests to the target API and returns DocumentDB responses.

#### Limitations △ Where DocumentDB and Autonomous JSON Database stay different * **No manual failover and no cross-region standby.** Autonomous Data Guard is not available for JSON workloads, so there is no failover / switchover call and no global database; cross-region DR is backup copies, refreshable clones, or replication tooling. * **No replica instances.** Read scale-out is served by compute auto-scaling (up to 3x), not replicas you add. * **Most cluster settings have no equivalent.** Compute and storage scale online; most DocumentDB cluster parameters have no counterpart. * **Event subscriptions are not served.** DocumentDB event notifications publish to SNS; OCI Events is a different model with no direct counterpart. #### Other considerations **Migration.** Copy existing data with MongoDB migration tools that the target supports. Check the workload's queries, indexes, and BSON types against the comparison table before switching connections; sharing a wire protocol does not make every MongoDB feature equivalent. **Operations.** The customer owns the Autonomous Database in their OCI tenancy: maintenance windows, quotas, and pricing are Oracle's. **Features and capacity.** The comparison table on this page lists database features and high-availability differences. Check the target service's pricing and capacity limits when sizing the deployment. [Service Catalog](/service-adapters/catalog). # DynamoDB (control) Source: https://docs.tensor9.com/service-adapters/aws/databases-storage/dynamodb-control AWS DynamoDB (control). Creates and alters DynamoDB tables, secondary indexes, capacity mode, time to live, streams and backups through the management API. This page covers table management and configuration. For item reads and writes, conditional writes, transactions and index queries, see [DynamoDB item API coverage](/service-adapters/aws/databases-storage/dynamodb-table). The two pages describe distinct operation scopes; support for one does not imply support for the other. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [Via Firestore](#via-firestore) * [Via Cloud Spanner](#via-cloud-spanner) * [Via Cloud Bigtable](#via-cloud-bigtable) * [Via Cloud SQL for PostgreSQL](#via-cloud-sql-for-postgresql) * [On Azure](#on-azure) * [Via Azure Cosmos DB (provisioned)](#via-azure-cosmos-db-provisioned) * [Via Azure Cosmos DB (serverless)](#via-azure-cosmos-db-serverless) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of DynamoDB with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | DynamoDB | OCI | | ----------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ | | Adaptation mechanism | DynamoDB infrastructure and runtime API | infrastructure mapping to native OCI NoSQL | | Item layout | partition key, optional sort key and typed attributes | typed primary-key columns plus value JSON | | Capacity | on-demand or provisioned; storage grows with use | OCI capacity mode, read/write units and explicit storage cap | | DynamoDB runtime API | Yes | No - applications use OCI NoSQL APIs in this mapping | | Per-item TTL | Yes | No - the source epoch attribute is not translated; emitted default has no automatic expiry | | Declared DynamoDB secondary indexes | Yes | No - configure native OCI indexes and adapt queries separately | | API coverage | full | minimal | ## On Google Cloud ### Via Firestore #### Table administration on Google Cloud Firestore The application's DynamoDB client sends table-management requests to the Tensor9 adapter in the customer environment. The control endpoint and the table endpoint use the same target database. CreateTable stores the key schema and table settings in a registry document. The table shares a Firestore collection with other logical tables. ListTables reads the registry, so it can return newly created tables before they contain any items. #### Capacity settings Firestore bills native document operations and storage. UpdateTable retains DynamoDB billing settings in the registry; those settings do not create DynamoDB-style reserved read or write capacity in Firestore. #### Deletion and durable state DeleteTable changes the registry to mark the table deleted and advances its generation number. Item reads, index entries and stream records use that generation, so recreating the same table name starts with an empty logical table even while old documents await cleanup. #### Configuration and retry constraints Physical cleanup is bounded and may leave unreachable documents behind. Those documents still consume storage until removed. Account for cleanup and Firestore document charges in environments that repeatedly create and delete large tables. #### Operating the target The adapter stores table settings durably in Firestore and uses workload identity to access the database. Configure native backups and database access separately. Stream-enabled tables retain the single-writer requirement described on the DynamoDB table page. ### Via Cloud Spanner #### Table administration on Google Cloud Spanner The application's DynamoDB client sends table-management requests to the Tensor9 adapter in the customer environment. The control endpoint and the table endpoint use the same target database. CreateTable records the requested key and index definitions, then creates the corresponding Spanner tables and indexes. DeleteTable removes those objects. ListTables and DescribeTable use the durable table catalog. #### Capacity settings Spanner capacity belongs to the instance. DynamoDB billing-mode and throughput settings are retained as table metadata; changing them does not allocate a separate pool of Spanner processing units for that table. #### Deletion and durable state The catalog records creation and deletion before the associated schema work finishes. A retried create can resume matching unfinished work; a request with a different schema cannot reuse that unfinished creation. This makes retry behavior relevant when a client times out during a schema change. #### Configuration and retry constraints Spanner schema changes can take longer than item requests. Allow time for table creation and retain normal SDK retry handling. A deleted table must finish its recorded teardown before its name can be created again. #### Operating the target Configure the Spanner instance, database access and backup policy for the customer environment. This control endpoint manages DynamoDB tables within that database; it does not turn per-table capacity settings into Spanner instance administration. DynamoDB Streams remain outside this mapping. ### Via Cloud Bigtable #### Table lifecycle The DynamoDB adapter manages logical tables inside an existing Bigtable table. CreateTable records the table name and key schema; it does not create a Bigtable instance or allocate a separate cluster. DescribeTable and ListTables return the adapter's table catalog. Provision the Bigtable instance, physical table, column families and single-cluster app profile before using these APIs. #### Capacity and supported configuration Bigtable cluster capacity is shared by the logical tables. Changing DynamoDB billing-mode metadata does not add Bigtable nodes or enforce provisioned read and write units. The adapter rejects secondary indexes, DynamoDB Streams and per-item TTL; selecting those options cannot make the underlying row store provide those DynamoDB behaviors. #### Deletion and storage cleanup DeleteTable removes a logical table's visibility through the DynamoDB API. A table generation separates a recreated name from its predecessor's rows. Logical deletion does not delete the Bigtable cluster or its shared physical table; the provisioned store remains. Plan storage cleanup and account for retained storage costs after deleting a logical table. ### Via Cloud SQL for PostgreSQL #### Table administration on Google Cloud SQL for PostgreSQL The application's DynamoDB client sends table-management requests to the Tensor9 adapter in the customer environment. The control endpoint and the table endpoint use the same target database. CreateTable creates a PostgreSQL table, its declared secondary indexes and a registry entry. A stream-enabled table also receives a change-log table. The DynamoDB endpoint serves both table administration and item requests against this same database. #### Capacity settings Cloud SQL instance size, storage and availability settings determine capacity. UpdateTable records DynamoDB billing settings without resizing the Cloud SQL instance. The adapter applies a 500-table guardrail per namespace; this is an adapter limit rather than a PostgreSQL service quota. #### Deletion and durable state Table creation writes the schema and registry in a database transaction. DeleteTable drops the item table and its change log and removes the registry and stream-owner records in the same transaction. Recreating the name therefore does not expose the deleted table's records. #### Configuration and retry constraints Enabling time to live builds the needed PostgreSQL index before advertising the setting as enabled. The API waits for that index work while normal item reads and writes remain available. Plan schema changes with enough database capacity and request timeout allowance. #### Operating the target Google operates the Cloud SQL database; Tensor9 operates the DynamoDB adapter and uses Cloud SQL IAM database authentication. Configure database backups and availability through Cloud SQL. The table page describes transaction behavior and stream-related restrictions. ## On Azure ### Via Azure Cosmos DB (provisioned) #### Table administration on Azure Cosmos DB (provisioned throughput) The application's DynamoDB client sends table-management requests to the Tensor9 adapter in the customer environment. The control endpoint and the table endpoint use the same target database. The deployed table uses a Cosmos DB container with an autoscale range of request units. Runtime CreateTable records logical table metadata in the configured container; it does not create another Cosmos account or a new independently sized container for every request. #### Capacity settings The provisioned target uses an autoscale range of request units, or RUs, sized for the declared table. UpdateTable retains DynamoDB billing settings in the table registry. Change the Cosmos throughput configuration to change the actual capacity available to the container. #### Deletion and durable state The registry stores table names, key definitions and lifecycle state. DeleteTable marks the logical table deleted and advances its generation; a recreated name uses the new generation so older items are no longer reachable through it. Physical cleanup is separate from this logical deletion. #### Configuration and retry constraints Runtime CreateTable index definitions must match the deployed secondary-index configuration. The stream specification must also match the deployed configuration. Provisioned throughput can support Azure-managed global secondary indexes; plan those indexes with the deployment rather than treating runtime table creation as an unrestricted container-design API. #### Operating the target Microsoft operates database durability, backups and availability. Tensor9 translates DynamoDB control requests and maintains the logical table registry. Native Cosmos account and container management remain separate from this endpoint; use the table page to assess numeric precision, transactions and stream ownership. ### Via Azure Cosmos DB (serverless) #### Table administration on Azure Cosmos DB (serverless) The application's DynamoDB client sends table-management requests to the Tensor9 adapter in the customer environment. The control endpoint and the table endpoint use the same target database. The adapter serves table administration over a configured serverless Cosmos DB container. CreateTable registers a logical table and its key schema in that container. ListTables and DescribeTable read the stored registry rather than discovering a separate Azure resource for each logical name. #### Capacity settings Cosmos DB charges for consumed request units and storage in this target. DynamoDB billing settings are retained as metadata; UpdateTable cannot switch the Cosmos account from serverless to provisioned throughput. Choose that operating mode when selecting the target. #### Deletion and durable state DeleteTable hides a table by changing its registry state and advancing its generation. Recreating the same name starts a new logical generation, with old items excluded from reads and listings. Cleanup of old documents can continue after the logical deletion. #### Configuration and retry constraints Azure-managed global secondary indexes require the provisioned target and are not offered on this serverless target. Runtime index definitions and stream settings must match the deployed configuration. A table that needs managed global secondary indexes should select provisioned Cosmos DB before deployment. #### Operating the target Microsoft operates the Cosmos database while Tensor9 operates the adapter. Configure native backup and account access separately. Serverless consumption billing does not remove the documented Cosmos limits on numeric precision, multi-item transactions or stream ownership. ## On OCI | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------------------- | -------------- | ------------ | ------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------- | | aws\_dynamodb\_table provisioning | Infrastructure | Partial | - | - | creates oci\_nosql\_table with the mapped key schema and capacity settings; review storage, index, TTL and integration differences | | DynamoDB item and table-management requests | Runtime API | Out of scope | - | - | this OCI NoSQL mapping has no DynamoDB API translator; use OCI NoSQL APIs and SDKs | #### Infrastructure mapping Tensor9 converts the declared aws\_dynamodb\_table to an oci\_nosql\_table in the customer's OCI compartment. This is the infrastructure-only adaptation: it provisions storage but does not provide an AWS DynamoDB endpoint. Application reads, writes, queries and runtime table administration use OCI NoSQL APIs. The table and control pages describe this same target choice. #### Keys and item data The DynamoDB partition key becomes the OCI SHARD key. An optional sort key becomes the second primary-key column. String, number and binary key types become corresponding OCI column types; other item attributes occupy a value JSON column. Adapt queries and serialization to that layout rather than sending DynamoDB attribute documents directly. #### Capacity and storage PAY\_PER\_REQUEST selects OCI ON\_DEMAND; provisioned DynamoDB capacity selects OCI PROVISIONED settings. OCI read and write units differ from DynamoDB units, so validate them against the workload. OCI also requires an explicit storage cap. Increase that cap before data reaches it; the source DynamoDB table had no equivalent fixed cap. #### Indexes, expiry and integrations The current infrastructure mapping does not create declared GSIs or LSIs. Configure native OCI indexes and update the application's access patterns. A source TTL declaration produces a no-expiry default; its per-item epoch attribute is not translated into automatic expiry. DynamoDB Streams, AWS PITR configuration, source KMS key references and replica settings are not reproduced by this table resource. Configure the corresponding target data-protection and event requirements separately. #### Migration and operation The new table starts empty. Convert application access, load the data, and test key lookups, queries, expiry and recovery before switching traffic. Oracle operates the NoSQL service; the customer manages compartment access, capacity, storage limits and the chosen recovery procedures. Use a different listed adaptation when the application must keep runtime DynamoDB API calls. ## On Private Kubernetes #### Table administration on Tensor9-managed PostgreSQL The application's DynamoDB client sends table-management requests to the Tensor9 adapter in the customer environment. The control endpoint and the table endpoint use the same target database. Tensor9 deploys PostgreSQL with the appliance and serves the DynamoDB control API through the adapter. CreateTable creates the item table, secondary indexes and registry metadata inside that database. Applications continue to use DynamoDB table names and key definitions. #### Capacity settings Capacity comes from the database resources allocated in the customer environment. DynamoDB billing-mode changes remain table metadata and do not resize the deployment. The adapter applies a 500-table guardrail per namespace, so deployments with larger table fleets need a different layout or target. #### Deletion and durable state Creation writes the table schema and registry together in a PostgreSQL transaction. Deletion drops the item table and its stream change log and removes table metadata and stream-owner records in the same transaction. ListTables reports the surviving registry entries. #### Configuration and retry constraints Time-to-live configuration builds the required index before enabling expiry. Table creation, deletion and index work consume the same database resources used by item requests. Size the deployment for both schema changes and steady application traffic. #### Operating the target Tensor9 operates this database within the appliance, including disconnected deployments, rather than relying on a cloud-managed PostgreSQL service. Storage durability, backup and recovery depend on the appliance configuration and available customer infrastructure. The DynamoDB table contract remains the same PostgreSQL-backed contract described for the managed target. [Service Catalog](/service-adapters/catalog). # DynamoDB (table) Source: https://docs.tensor9.com/service-adapters/aws/databases-storage/dynamodb-table AWS DynamoDB (table). A key value and document store where items are addressed by partition and sort key and read or written in single digit milliseconds. This page covers item APIs and their database backends. For table administration, capacity and other table configuration, see [DynamoDB table management](/service-adapters/aws/databases-storage/dynamodb-control). Item API support does not imply support for every table-management operation. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [Via Firestore](#via-firestore) * [Via Cloud Spanner](#via-cloud-spanner) * [Via Cloud Bigtable](#via-cloud-bigtable) * [Via Cloud SQL for PostgreSQL](#via-cloud-sql-for-postgresql) * [On Azure](#on-azure) * [Via Azure Cosmos DB (provisioned)](#via-azure-cosmos-db-provisioned) * [Via Azure Cosmos DB (serverless)](#via-azure-cosmos-db-serverless) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of DynamoDB with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | DynamoDB | Google Cloud · Firestore | Google Cloud · Cloud Spanner | Google Cloud · Cloud Bigtable | Google Cloud · Cloud SQL for PostgreSQL | Azure · Azure Cosmos DB (provisioned) | Azure · Azure Cosmos DB (serverless) | Private Kubernetes | | ------------------------------------------------ | ------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------- | | Secondary indexes · index types | GSI (eventually consistent) / LSI (strongly consistent) | Firestore composite indexes over the item's own fields, written with the item | Spanner secondary index / interleaved index, both written with the row | - | Postgres indexes updated in the same transaction as the item | Azure-maintained GSI container / native LSI index | LSI only (a GSI needs the provisioned target) | - | | Read consistency · per-read control | per-request ConsistentRead | always strong (flag-independent) | - | - | always strong (flag-independent) | strong base-table and LSI reads; GSI visibility is eventual | strong base-table and LSI reads; managed GSIs require provisioned Cosmos | always strong (flag-independent) | | Write path · latency tradeoff | managed item writes and transactions | limited direct writes; guarded read-modify-write for other requests | replicated commits; eligible updates avoid a pre-image read | direct row mutations or guarded read-modify-write | one keyed upsert, or a row-locked read-modify-write | - | - | - | | Item size · optional relaxed size check | 400 KiB per item | 400 KiB by default; opt-in blind patches may exceed it | - | - | - | - | - | - | | Per-item TTL | Yes - epoch attribute; lazy delete | Yes - native TTL field policy; asynchronous delete | Yes - generated column plus a row-deletion policy | - | - | - | - | - | | API coverage | full | high | high | partial | high | partial | partial | high | | Read consistency · default + on request | eventually consistent, ConsistentRead on request | - | strong reads, including ConsistentRead=false | - | - | - | - | - | | Cross-partition transactions · multi-item writes | Yes - serializable, up to 100 actions | - | Yes - external consistency, spans tables | - | - | - | - | - | | Cost vs DynamoDB · billing model | on-demand requests plus storage | - | provisioned compute plus storage | provisioned nodes plus storage | - | - | - | - | | Item size · enforcement | 400 KiB per item | - | 400 KiB enforced, plus DynamoDB's 4 MiB transaction aggregate | - | 400 KiB enforced, including the attributes a local index copies | - | - | - | | DynamoDB Streams | Yes | - | No - stream-enabled writes are rejected | No - no atomic DynamoDB change log | - | Partial - one writing instance per streamed table | Partial - one writing instance per streamed table | Partial - change records commit with writes; one writer per streamed table | | Scaling · capacity and concurrency | on-demand or provisioned capacity | - | instance processing units and request concurrency | cluster nodes, row distribution and connection pool | - | - | - | - | | Read consistency · default + on request | eventual by default; strong on request | - | - | strong through one cluster | - | - | - | - | | Secondary indexes · index types | GSI / LSI | - | - | base-table access only | - | - | - | PostgreSQL indexes updated with the item | | Cross-partition transactions · multi-item writes | Yes - up to 100 items across tables | - | - | No - one item row is the atomic unit | Partial - native Postgres transaction at SERIALIZABLE isolation; stream-enabled tables are excluded | - | - | - | | Per-item TTL | Yes - epoch attribute set per item; lazy delete | - | - | No - DynamoDB TTL attributes are rejected | - | - | - | - | | Item size · update size validation | 400 KiB per item | - | - | blind SET/REMOVE may exceed 400 KiB | - | - | - | - | | DynamoDB Streams · change capture | Yes - 24-hour retention | - | - | - | Partial - outbox co-committed with the write; one writer per streamed table | - | - | - | | Per-item TTL · expiry | Yes - epoch attribute set per item; lazy delete | - | - | - | Yes - expression index plus a leader-elected reaper | - | - | - | | Conditional writes · atomicity | Yes - native | - | - | - | Yes - row-locked read-modify-write inside one transaction | - | - | - | | Numeric precision · accepted values | up to 38 significant digits | - | - | - | the same value, stored exactly | at most 15 significant digits | at most 15 significant digits | the same value, stored exactly | | Autoscale · capacity model | provisioned / on-demand | - | - | - | - | dedicated autoscaling range of request units (RUs) | consumption billing without a dedicated RU band | - | | Item size · enforcement | 400 KiB | - | - | - | - | 400 KiB enforced | 400 KiB enforced | - | | Per-item TTL | Yes - absolute epoch attribute | - | - | - | - | Yes - per-item ttl tag, relative to the last write | Yes - per-item ttl tag, relative to the last write | - | | Cost · capacity model | request-based or provisioned capacity | - | - | - | - | dedicated autoscaling range of request units (RUs) | consumption billing without a dedicated RU band | - | | Conditional writes | Yes - native | - | - | - | - | Yes - stored procedure, conditional PATCH or etag guard | Yes - stored procedure, conditional PATCH or etag guard | - | | Transactions | Yes | - | - | - | - | No - DynamoDB multi-item transaction APIs are rejected | No - DynamoDB multi-item transaction APIs are rejected | - | | Cross-partition transactions · multi-item writes | Yes | - | - | - | - | - | - | Partial - serializable transactions; stream-enabled tables are excluded | | Per-item TTL | Yes | - | - | - | - | - | - | Yes - expired items remain readable until background deletion | | Capacity | managed on-demand or provisioned capacity | - | - | - | - | - | - | database and storage resources allocated to the appliance | ### Infrastructure-only adaptation | Capability | DynamoDB | OCI | | ----------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ | | Adaptation mechanism | DynamoDB infrastructure and runtime API | infrastructure mapping to native OCI NoSQL | | Item layout | partition key, optional sort key and typed attributes | typed primary-key columns plus value JSON | | Capacity | on-demand or provisioned; storage grows with use | OCI capacity mode, read/write units and explicit storage cap | | DynamoDB runtime API | Yes | No - applications use OCI NoSQL APIs in this mapping | | Per-item TTL | Yes | No - the source epoch attribute is not translated; emitted default has no automatic expiry | | Declared DynamoDB secondary indexes | Yes | No - configure native OCI indexes and adapt queries separately | | API coverage | full | minimal | ## On Google Cloud ### Via Firestore | Operation | Area | Support | Depth | Notes | | ------------------- | ------------------ | --------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ConditionExpression | Conditional writes | Supported | Common | evaluated against the live document under an optimistic precondition; a lost race retries, never a spurious condition failure | | DescribeStream | Indexes + streams | Supported | Full surface | shard list + stream ARN | | GetRecords | Indexes + streams | Supported | Full surface | DynamoDB Streams served from an ordered change log co-committed with each write; all four view types | | GetShardIterator | Indexes + streams | Supported | Full surface | - | | ListStreams | Indexes + streams | Supported | Full surface | the streamed-table registry | | Query (GSI / LSI) | Indexes + streams | Supported | Most usage | served by a real maintained secondary index (a per-index seek), not a table scan | | Query | Reads | Supported | Common | ordering, filters and pagination honored | | Scan | Reads | Supported | Common | streamed page by page | | DeleteItem | Single item | Supported | Common | - | | GetItem | Single item | Supported | Common | - | | PutItem | Single item | Supported | Common | Documents are keyed by partition and sort key. Signed 64-bit integers remain exact; other numbers must fit the supported 15-significant-digit native range. | | UpdateItem | Single item | Supported | Common | default: eligible REMOVE-only requests use a masked write; SET reads the item to enforce 400 KiB. FIRESTORE\_ALLOW\_OVERSIZED\_BLIND\_UPDATES=true permits eligible blind SET/REMOVE while relaxing that size check; conditions, returned attributes and complex updates retain read-modify-write | | TransactWriteItems | Transactions | Supported | Most usage | a real cross-document Firestore transaction, atomic up to the 100-action limit DynamoDB itself imposes | #### How it works Tensor9 runs a DynamoDB adapter beside your application in the customer's environment. It configures `AWS_ENDPOINT_URL_DYNAMODB` to send the AWS SDK's requests to the adapter over the local loopback network. Your SDK and application code stay unchanged. The adapter accepts DynamoDB requests and data types, returns DynamoDB responses and errors, and stores data in Google Cloud Firestore. The adapter maps DynamoDB items, conditions, indexes and transactions onto Firestore documents. Numeric values must fit the native representation described below. Reads are strongly consistent, and write latency depends on whether the operation needs an initial read or encounters contention.
Before: on AWS the application's DynamoDB SDK calls Amazon DynamoDB. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the DynamoDB API from Google Cloud Firestore. Before: on AWS the application's DynamoDB SDK calls Amazon DynamoDB. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the DynamoDB API from Google Cloud Firestore.

Your application uses the DynamoDB API through an adapter in the customer environment.

#### Architecture The adapter parses DynamoDB requests and expressions, executes Firestore operations and returns DynamoDB responses. Durable item and table state lives in Firestore. It authenticates with the customer's workload identity. Instances can restart without recovering local data; stream-enabled tables still require one active writer. On the Firestore side there is no per-table container to provision. Every logical DynamoDB table lives in **one Firestore collection, `ddb`**, and each item is a document whose id encodes the table and the item's key (`orders|u#42`), with a `_tbl` field that discriminates which logical table a document belongs to and a table-prefixed `_pk` field that routes a base `Query` to one partition's documents. A reserved registry document holds the table registry (declared schemas and creation time), which is what lets a table your app creates at runtime be served with no Firestore provisioning (see the section on where tables live). Every DynamoDB item is stored so its exact types survive, and each declared secondary index is served by a Firestore composite index over the item's existing tagged scalar fields. Because the adapter is stateless, this Firestore layout is the entire durable state of the system.
Architecture: the app's DynamoDB SDK calls a stateless Rust Tensor9 adapter, which serves the API from Google Cloud Firestore. Firestore holds all state in ONE collection named ddb, where every logical table's documents live together and secondary indexes address native tagged item fields. Architecture: the app's DynamoDB SDK calls a stateless Rust Tensor9 adapter, which serves the API from Google Cloud Firestore. Firestore holds all state in ONE collection named ddb, where every logical table's documents live together and secondary indexes address native tagged item fields.
A request is compiled in two stages: the DynamoDB API request becomes a backend-neutral logical plan, then a Firestore physical plan, which executes as REST calls: a document GET, a runQuery or a commit. Firestore holds every table's documents in one ddb collection, with a composite index per declared secondary index. A request is compiled in two stages: the DynamoDB API request becomes a backend-neutral logical plan, then a Firestore physical plan, which executes as REST calls: a document GET, a runQuery or a commit. Firestore holds every table's documents in one ddb collection, with a composite index per declared secondary index.

A stateless Rust adapter serves the DynamoDB API in front of Firestore; all durable state lives in one Firestore collection, "ddb", where every logical table's documents live together and are told apart by a \_tbl field.

The adapter first builds a storage-independent request plan, then converts it to a Firestore-specific plan for execution.

#### Schemas The envelope preserves DynamoDB's type system without making values opaque to Firestore. Each attribute is a one-tag map: strings use `{"S": stringValue}`, numbers use `{"N": integerValue|doubleValue}`, binary uses `{"B": bytesValue}`, and sets, lists, maps, booleans, and null retain their own tags. That keeps a numeric value distinct from a numeric-looking string while allowing native filters and indexes to address paths such as `item.total.N`. Native number storage has an explicit boundary. Signed 64-bit integral values remain exact Firestore integers; other numbers are accepted only inside a conservative 15-significant-digit double envelope. A DynamoDB number outside that envelope fails before storage instead of being rounded silently. The `item` map sits beside the routing fields `id`, `_pk`, `_tbl`, `_okey`, and `_keyAttrs`.
A DynamoDB item is wrapped in a Firestore document whose one-tag attribute maps contain native Firestore scalars. Integers remain exact and fractional numbers pass a precision check before becoming doubles. A DynamoDB item is wrapped in a Firestore document whose one-tag attribute maps contain native Firestore scalars. Integers remain exact and fractional numbers pass a precision check before becoming doubles.

Each attribute keeps its DynamoDB type tag while scalar payloads use native Firestore values, so filters and secondary indexes can execute in storage.

#### Where tables live Firestore is **fully serverless**: there is no account tier, no throughput mode, and no capacity band to size. Every DynamoDB table you declare is compiled into the same Firestore collection (`ddb`) , and each table's documents are told apart by a `_tbl` discriminator and a table-prefixed document id and `_pk` route. Firestore shards documents automatically, and billing follows operations and stored data. Idle tables can still incur storage charges. A reserved registry document holds the declared schemas and creation times. Runtime `CreateTable` writes the logical schema into the registry; `DescribeTable` and `ListTables` read it. `DeleteTable` invalidates the old table generation with one metadata write. Recreated tables use a new generation, such as `orders#2`. Old documents become invisible to the application and remain stored until bounded cleanup on a later same-name deletion. * **Key schema is required.** A composite-key table's key schema comes from a declared CreateTable, the registry document, or is learned from the first key-bearing request. A Put that can't resolve its key schema is rejected outright, never written under a guessed key. * **Isolation is logical, not physical.** Tables are kept apart by the `_tbl` discriminator and the table-prefixed id and `_pk`, all inside the project's one Firestore database, which is shared across the project's Firestore-backed apps. A point `GetItem` and a base `Query` isolate by the table-prefixed route; only a `Scan` (and the registry sweeps) filters on `_tbl`. Per-install physical isolation (a dedicated named database and a database-scoped identity) is not provided; the separation stays logical.
Every declared table lives in ONE Firestore collection named ddb, told apart by a _tbl discriminator, not one collection each. Firestore is fully serverless: no throughput mode to choose, no capacity to size, documents auto-sharded, billed for operations and storage. CreateTable and DeleteTable are pure data-plane operations on the registry. Every declared table lives in ONE Firestore collection named ddb, told apart by a _tbl discriminator, not one collection each. Firestore is fully serverless: no throughput mode to choose, no capacity to size, documents auto-sharded, billed for operations and storage. CreateTable and DeleteTable are pure data-plane operations on the registry.

Every declared table shares one "ddb" collection, told apart by a \_tbl discriminator; Firestore is fully serverless, so there is no throughput mode to pick. At runtime, CreateTable / DeleteTable / UpdateTable are pure data-plane ops on the registry, no Firestore admin call.

#### Conditional writes A conditional write reads the document and its `updateTime`, evaluates the DynamoDB condition in the adapter, then writes only if that version still matches. Creation uses a does-not-exist precondition. A false condition returns `ConditionalCheckFailed`. A concurrent write changing the document version returns a retryable conflict. The SDK can retry the latter without treating it as a failed application condition. This path adds an item read before the guarded write. The retry is driven by your SDK's backoff, not looped inside the layer. Even without contention, the read adds latency and a billed read operation. On a hot, heavily-contended document the conflict-and-retry loop can spend more time colliding than making progress, and a finite SDK retry budget can surface that contention to your app as a throttling failure. Which writes avoid reading the item first? Firestore's native API supports masked field updates and atomic transforms, including increments. The DynamoDB adapter currently uses a narrower set of these capabilities: * **PutItem and DeleteItem:** one direct document write when there is no condition, `ReturnValues=NONE` and DynamoDB Streams is disabled. * **UpdateItem with only top-level REMOVE actions:** one masked document write when there is no condition, `ReturnValues=NONE`, Streams is disabled and the update does not touch the configured TTL attribute. * **SET and other updates, by default:** read-modify-write. A blind SET cannot validate the resulting DynamoDB item's 400-KiB limit, so the adapter reads the existing item before applying and validating the update. Arithmetic, nested updates, returned item attributes and TTL-attribute changes also take this path; Firestore's native increment support is not currently used for DynamoDB numeric updates. **Optional relaxed size validation:** set `FIRESTORE_ALLOW_OVERSIZED_BLIND_UPDATES` to `"true"` in the adapter configuration to enable blind top-level SET/REMOVE updates under the same no-condition, no-return-values, no-Streams and no-TTL-change requirements. Primary-key and secondary-index-key changes, and updates on tables with an LSI, retain the validated path. This option defaults to `"false"`. The option skips only the aggregate 400-KiB resulting-item check. Firestore still enforces its [1-MiB document limit](https://docs.cloud.google.com/firestore/quotas), which includes the adapter's type tags and metadata; that is not a 1-MiB DynamoDB payload allowance. PutItem, read-modify-write and transactions retain their checks. An item grown above 400 KiB may need a blind removal or replacement of a large attribute before validated writes succeed. No performance measurement is claimed for this opt-in. The read-modify-write path computes the new item in the adapter and writes with a document-version precondition, preserving atomicity when another writer races. Performance for a direct write does not establish the latency of this path. Native primitives are described in the [Firestore Write API](https://docs.cloud.google.com/firestore/docs/reference/rest/v1/Write).
A conditional write reads the current document and its update time, evaluates the DynamoDB condition in the layer, then writes with an update-time precondition; a losing race returns a retryable conflict, not a condition failure. A conditional write reads the current document and its update time, evaluates the DynamoDB condition in the layer, then writes with an update-time precondition; a losing race returns a retryable conflict, not a condition failure.

False conditions and concurrent document changes return different errors.

#### Secondary indexes Each declared global secondary index is served **off the base document**. The compiler creates ascending and descending Firestore composite indexes over `_tbl`, the tagged hash scalar, the optional tagged range scalar, and the document's stable `id`. There are no synthetic GSI fields and no separate projected data copy. A re-key updates the index with the item write, a delete disappears with the item, and a sparse item is absent because a missing hash or range field has no matching index entry. Consistency is therefore **stronger than DynamoDB**, whose GSIs are eventually consistent: here an index query reflects the write that just committed. Hash/range predicates, ordering, cursor, limit, and supported filters are pushed into Firestore; an unrepresentable application filter is evaluated over that bounded native window before the declared DynamoDB attribute set is returned. Native numeric fields compare numerically, and the `id` tie-break makes pagination exact when several items share one GSI range value. The index must still be **declared at compile time**; an undeclared index fails with a validation error rather than falling back to a silent table scan.
A base write stores tagged native item fields. Firestore maintains composite indexes over those fields, so a GSI query pushes hash, range, cursor, order, and limit into storage. A base write stores tagged native item fields. Firestore maintains composite indexes over those fields, so a GSI query pushes hash, range, cursor, order, and limit into storage.

A declared GSI is a Firestore composite index over the item's native tagged scalar fields; the full query window is selected and ordered in storage.

#### Transactions `TransactWriteItems` becomes a real **Firestore read-write transaction**. The layer opens a transaction, reads each targeted document inside it, evaluates each item's DynamoDB condition in the adapter (Firestore's document preconditions aren't expressive enough), stages the writes, and commits them atomically. A condition that fails rolls the whole transaction back with nothing written; a commit that loses a race is a transient conflict the SDK retries. Transactions can span documents, logical tables and partition keys. The adapter enforces DynamoDB's 100-action limit. A cancelled transaction returns a single conditional-failure signal rather than per-item `CancellationReasons`, so the application cannot identify the failed condition from that response.
A TransactWriteItems whose items span two different partition keys commits as one atomic Firestore transaction across documents; both the debit and credit commit, or neither does. A TransactWriteItems whose items span two different partition keys commits as one atomic Firestore transaction across documents; both the debit and credit commit, or neither does.

TransactWriteItems maps to a real Firestore transaction across documents: atomic, durable, and isolated, with no single-partition restriction; both the debit and credit commit, or neither does.

#### Limitations △ Where DynamoDB and Firestore diverge, read before you adopt * **Concurrent writes to one document.** Repeated updates to the same document can encounter version conflicts or throttling. SDK retries add latency and billed operations, and a finite retry budget can surface an error to the application. Size and test the workload for its actual key distribution. * **Most updates need read-modify-write by default.** SET reads the item to enforce the 400-KiB resulting-item limit; conditions, arithmetic, nested updates and returned attributes also need a read. Eligible REMOVE-only updates use a direct masked write. The explicit oversized-blind-update option extends this to eligible SET/REMOVE while relaxing item-size fidelity. A document-version conflict on read-modify-write returns a retryable error; SDK retries repeat the work and add latency and billed operations. * **Isolation is logical, not physical.** Every table lives in the project's one shared Firestore database, kept apart by the `_tbl` discriminator and the table-prefixed id and `_pk`, not by a separate database or a per-table identity. There is no per-install physical isolation (a dedicated named database, a database-scoped identity); the separation stays logical. * **Secondary indexes must be declared at compile time.** A GSI is served by Firestore composite indexes over native tagged item fields, which is synchronous and strongly consistent (stronger read visibility than DynamoDB's eventually-consistent GSIs). The runtime cannot learn an index definition from traffic: an undeclared index returns a validation error rather than a silent table scan. `ConsistentRead=true` is rejected on a global secondary index, matching DynamoDB's GSI contract; `ConsistentRead=false` is accepted. Local secondary indexes accept strongly consistent reads. The flag restriction does not change the synchronous physical index updates. * **Reads are always strongly consistent.** A single-document read reflects the latest committed write, regardless of `ConsistentRead`. Estimate Firestore document and index-entry read charges for the actual queries. * **Some filters run in the compute layer.** Equality, bounded `IN`, and compatible type predicates compile to native tagged item fields and run in Firestore. In `FilterExpression`, `<>`, `attribute_not_exists`, `contains`, `begins_with`, `NOT`, and predicates whose Firestore ordering would conflict with DynamoDB key order are evaluated by the service adapter over the bounded native key/range/cursor window. A string `begins_with` on a GSI sort key is still pushed down as a native access range. * **A streamed table has one writing instance at a time.** DynamoDB Streams are served from an ordered change-log document co-committed with each write in one transaction, which requires a single writer per streamed table: a second concurrent instance's write is rejected as a retryable throttle until the writer lease passes. Run streamed-table writers at one replica; tables without a stream take concurrent writers safely, and reads are unaffected. * **Dropped-table storage needs cleanup.** `DeleteTable` invalidates the current table generation with one metadata write. Old documents become invisible immediately but remain stored until bounded cleanup on a later deletion of the same table name. Large tables can therefore continue consuming storage after deletion. * **Transaction cancellation is coarse.** A cancelled `TransactWriteItems` currently returns a single conditional-failure signal rather than DynamoDB's per-item `CancellationReasons`, so an app can't yet tell which item's condition failed. #### Other considerations * **Data migration.** The Firestore store starts empty. Move existing DynamoDB data through an export/import, backfill or dual-write procedure, and validate it before switching traffic. * **Operations and ownership.** Google operates Firestore; Tensor9 operates the adapter. The adapter uses workload identity and short-lived credentials. The customer manages the Google Cloud project, cluster maintenance and monitoring. * **Capacity planning.** Firestore bills operations and storage without provisioned throughput. Query and Scan select bounded native windows; unsupported application filters run over those windows in the adapter. Estimate charges from examined documents and index entries, and test contention on frequently updated documents. * **Read consistency.** The request flag does not select an eventual-read path. Include strongly consistent reads when sizing the workload and estimating charges. * **Change streams.** DynamoDB Streams are served from an ordered change-log document co-committed with each write, retained for a fixed window, and read back through the Streams API (all four view types). Ordering requires a single writer per streamed table; tables without a stream are unaffected. ### Via Cloud Spanner | Operation | Area | Support | Depth | Notes | | ---------------------------------------------------- | ------------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | BatchGetItem / BatchWriteItem | Batch | Supported | Common | lowered to native Spanner batch reads and mutations rather than issued one row at a time | | DynamoDB Streams | Indexes + streams | Out of scope | Full surface | the write executor rejects stream-enabled writes; use another backend when the application requires change records | | Query (GSI) | Indexes + streams | Supported | Most usage | a native index maintained with the row; ConsistentRead=true and ALL\_ATTRIBUTES on an index without all attributes are rejected, matching DynamoDB's GSI restrictions | | Query (LSI) | Indexes + streams | Supported | Most usage | a local secondary index is interleaved with its table, so the query is an index seek and the index is written inside the same transaction as the row | | Query | Query + scan | Supported | Common | key conditions, filters, ordering and limits compile into GoogleSQL; selective key ranges reduce work, but filters can still examine rows that are not returned | | Scan | Query + scan | Supported | Most usage | paged table scan; consumes provisioned compute and competes with point reads and writes, so use selective Query access where possible | | PutItem / GetItem / UpdateItem / DeleteItem | Single item | Supported | Common | each item is a row; attribute types round-trip exactly, with numbers at their full decimal precision rather than through a float | | CreateTable / UpdateTable / DeleteTable / ListTables | Table control plane | Supported | Most usage | table and index definitions are applied as Spanner DDL; throughput-mode changes are a no-op because capacity is the instance's processing units, not the table's | | TransactWriteItems / TransactGetItems | Transactions | Supported | Most usage | a real Spanner read-write transaction spanning tables and partition keys exactly as DynamoDB's does, at external consistency; the 100-action and one-op-per-item limits are enforced and ClientRequestToken gives the same idempotency window | #### How it works Your application keeps its AWS SDK, endpoint configuration, and DynamoDB item formats. The Tensor9 adapter accepts DynamoDB requests and stores data in Spanner. The adapter compiles a `Query`'s key condition, filter, ordering, and limit into one GoogleSQL statement for Spanner to execute. Selective key ranges reduce work. Filters can still examine rows that are not returned, so compute consumption depends on more than the result size.
Before: on AWS the application's DynamoDB SDK calls Amazon DynamoDB. After: on Google Cloud the same application and SDK call a Tensor9 adapter, which serves the DynamoDB API from Google Cloud Spanner. Before: on AWS the application's DynamoDB SDK calls Amazon DynamoDB. After: on Google Cloud the same application and SDK call a Tensor9 adapter, which serves the DynamoDB API from Google Cloud Spanner.

The application keeps calling the DynamoDB JSON API. The adapter translates it into GoogleSQL against Spanner.

#### Architecture
A request is compiled in two stages: the DynamoDB API request becomes a backend-neutral logical plan, then a Spanner physical plan, whose reads execute as one GoogleSQL statement and whose writes commit as mutations. Spanner holds one database with one table per DynamoDB table and a secondary index per declared index. A request is compiled in two stages: the DynamoDB API request becomes a backend-neutral logical plan, then a Spanner physical plan, whose reads execute as one GoogleSQL statement and whose writes commit as mutations. Spanner holds one database with one table per DynamoDB table and a secondary index per declared index.

Spanner executes the SQL query, including its key condition, filter, ordering, and limit.

#### Rows and keys Each DynamoDB table becomes a Spanner table. An item is one row, keyed first by its partition key and then by its sort key. This layout lets Spanner read a key range in order and apply `ScanIndexForward` and pagination through the index. Attribute values retain their DynamoDB types: decimal numbers preserve precision, binary remains binary, and strings and sets retain the distinctions the API requires. The adapter encodes numeric values as sortable decimal text and normalizes sets. Spanner can evaluate supported comparisons in GoogleSQL over that representation without converting numbers to floating point. The table also stores local index and expiry information. A local secondary index is interleaved with its table, so its entries are stored with the row they describe. Where a table declares a TTL attribute, the expiry is a generated column derived from that attribute, which a Spanner row-deletion policy uses, so expiry is physical deletion on Spanner's schedule, not a hide-on-read filter the adapter applies.
A DynamoDB item becomes a Spanner row whose primary key is the partition key then the sort key, so a key-range Query is a contiguous scan of the primary index. A DynamoDB item becomes a Spanner row whose primary key is the partition key then the sort key, so a key-range Query is a contiguous scan of the primary index.

Partition and sort keys form the Spanner primary key, which supports ordered range reads.

#### Transactions and consistency `TransactWriteItems` uses a Spanner read-write transaction across tables and partition keys. Spanner's external consistency also orders transactions in real time. The adapter enforces the 100-action limit, one operation per item and the `ClientRequestToken` idempotency window. The adapter reads and locks items whose conditions or changes require their current values, evaluates the conditions, then commits the transaction. A failed condition and a concurrency conflict return distinct errors so the client can decide whether to retry. A requested failed-item image is returned with the condition error. An unconditional `Put` can skip reading the item because it supplies the complete replacement. Conditions, returned prior values, updates computed from existing attributes and delete-size accounting require the existing item to be read. DynamoDB's aggregate limits are enforced as its own, not Spanner's: each resulting item is validated against the 400 KiB item limit, and the transaction as a whole against DynamoDB's 4 MiB aggregate item-data cap. A request that would exceed either fails the way it fails on AWS. The adapter's normal reads are strongly consistent, including requests with `ConsistentRead=false`. That flag does not select a cheaper or faster stale-read path. Strong reads on a global secondary index remain rejected for DynamoDB compatibility.
One TransactWriteItems becomes one Spanner transaction. Conditional actions read the current items before checking their conditions; eligible unconditional puts can skip the read. All writes commit together. One TransactWriteItems becomes one Spanner transaction. Conditional actions read the current items before checking their conditions; eligible unconditional puts can skip the read. All writes commit together.

Transactions can span tables and partition keys. External consistency preserves transaction order in real time.

#### Secondary indexes A global secondary index becomes a Spanner secondary index, and a local secondary index an index interleaved with its table. A `Query` against either is an index seek, not a table scan. Spanner commits secondary-index updates with the row. A subsequent strong index read includes that committed update. DynamoDB GSIs propagate asynchronously, so applications that tolerate index lag do not need to add that wait on this target. Because the index is a real Spanner index rather than a projected copy, the query engine does the selection. The key condition, ordering, cursor and limit all lower into the index scan, so a selective index query reads close to what it returns rather than filtering a wider read afterwards. A sparse item, one missing the index's key attribute, simply has no index entry, matching DynamoDB's own sparse-index behaviour. Two rules are inherited from the origin rather than from Spanner. You still cannot request a strongly-consistent read of a global secondary index: `ConsistentRead=true` against one is rejected, exactly as DynamoDB rejects it. And a query that asks for attributes the index does not project is rejected rather than silently fetching them from the base row, so an index with too few attributes for the query returns an error without fetching extra rows. The wire behaviour is the same as the origin's, so code that handles those errors on AWS handles them here unchanged.
Spanner updates the row and secondary index in one transaction. A subsequent strong index read includes the committed update. Spanner updates the row and secondary index in one transaction. A subsequent strong index read includes the committed update.

Spanner commits row and index updates together. A subsequent strong index read includes the update.

#### Limitations △ Where DynamoDB and Spanner diverge, read before you adopt * **Provisioned compute bills through quiet periods.** Both services charge for stored data; Spanner also charges for provisioned compute. Steady utilization can amortize it, but savings depend on workload, capacity, region and replication topology. * **A hot item still contends.** The adapter hashes partition routes to spread independent keys. Repeated updates to one item still compete; a uniform-key benchmark does not establish hot-item performance. * **A projected index cannot serve a full-item read.** `Select=ALL_ATTRIBUTES` against a global secondary index that does not project every attribute is rejected rather than quietly fetching the missing columns from the base table. The error names the index, so the fix is either to query the attributes the index already has, or to declare it so it includes them all. * **Scan competes for provisioned compute.** Paging limits response size, but a broad scan still reads the table and competes with point operations. Prefer a selective `Query` when possible. * **DynamoDB Streams are not served.** The write executor rejects stream-enabled writes. Use another backend if the application requires an atomic DynamoDB change log. * **Some writes use read-modify-write.** Conditions, responses that need the existing item, nested updates, arithmetic, changes to secondary-index keys and updates to tables with an LSI take this path. The adapter reads the item, evaluates the condition and computes the change, then commits it in the same Spanner transaction. A transaction conflict can repeat that work. Eligible top-level SET/REMOVE updates avoid the item read; this can include `UPDATED_NEW` when the response is known from assigned values. * **Measured latency depends on the request type.** The 2026-08-31 workload measured simple unconditional updates. It does not measure the extra read or conflict retries of read-modify-write. The run used 1,000 processing units in us-west1, 100,000 records and a 50/50 read/update workload. With 16 worker threads, steady median throughput was 2,403.0 operations/s through Spanner versus 5,929.5 on native DynamoDB in us-west-2, using three 200,000-operation repetitions after warm-up. Spanner served strong reads; DynamoDB served eventual reads. The standalone adapter capture excluded appliance authorization and durable control-plane logging. These are complete-path results with different regions and consistency, not isolated adapter overhead or measurements of transactions and hot-item contention. #### Other considerations * **Data migration.** The Spanner database is provisioned empty. Existing DynamoDB items are not moved in place. Load them through the adapter's DynamoDB API, validate the data and representative queries before switching application traffic. For an online migration, coordinate backfill and ongoing writes so changes made during the copy reach Spanner before cutover. * **Capacity planning.** Size Spanner compute in processing units. Start from the read and write rates the DynamoDB table was provisioned for rather than from its storage size, and expect to tune once under real load. Spanner's throughput per unit depends heavily on how well the primary key distributes. * **Operations and ownership.** Google operates Spanner: replication, backups, and failover are theirs. Tensor9 operates the adapter. Spanner stores the data; the adapter runs beside the application. * **Authentication.** The adapter authenticates to Spanner with the workload's own identity through GKE Workload Identity. There is no key file and no secret to rotate. ### Via Cloud Bigtable | Operation | Area | Support | Depth | Notes | | -------------------------------------------------------------------- | ------------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | BatchGetItem / BatchWriteItem | Batch | Supported | Most usage | independent item operations, without atomicity across the batch | | ConditionExpression | Conditional writes | Supported | Common | read-modify-write under an atomic row-version guard; conflicting writes repeat the read and condition check, up to eight attempts | | DynamoDB Streams | Indexes + streams | Out of scope | Full surface | no DynamoDB-compatible atomic change log; Bigtable change streams are not exposed as DynamoDB Streams | | Query (GSI / LSI) | Indexes + streams | Out of scope | Most usage | the adapter does not implement DynamoDB secondary-index behavior; native Bigtable materialized views are not exposed as GSI or LSI queries | | Query / Scan | Reads | Supported | Common | base-table ordered row ranges with pagination and adapter-side filters; filters can read more rows than they return | | PutItem / GetItem / DeleteItem | Single item | Supported | Common | exact DynamoDB item types; reads use a verified single-cluster route | | UpdateItem | Single item | Partial | Common | eligible top-level SET/REMOVE avoids a read but does not enforce the aggregate 400-KiB post-image limit; conditions, returned item images and other update shapes use guarded read-modify-write, except eligible native counters | | UpdateTimeToLive | TTL | Out of scope | Most usage | column-family garbage collection does not implement a per-item DynamoDB TTL attribute | | CreateTable / DescribeTable / UpdateTable / DeleteTable / ListTables | Table control plane | Partial | Most usage | logical table lifecycle inside a pre-provisioned physical table; billing-mode metadata does not provision capacity, and indexes, streams and TTL are rejected | | TransactWriteItems / TransactGetItems / ExecuteTransaction | Transactions | Out of scope | Most usage | Bigtable cannot atomically commit or snapshot multiple item rows; these requests are rejected | #### How it works Tensor9 runs a DynamoDB adapter beside your application in the customer's environment. It configures `AWS_ENDPOINT_URL_DYNAMODB` to send the AWS SDK's requests to the adapter over the local loopback network. Your SDK and application code stay unchanged. The adapter accepts DynamoDB requests and data types, returns DynamoDB responses and errors, and stores data in Google Cloud Bigtable. It connects to Bigtable over pooled gRPC using the workload identity; no key file is required. Bigtable provides atomic writes for one row, not across rows , and the adapter maps each DynamoDB item to exactly one row. The adapter uses row mutations and version guards for item writes. This mapping rejects multi-item transactions, secondary indexes, DynamoDB Streams and per-item TTL; the limitations below describe the missing behavior.
Before: on AWS the application's DynamoDB SDK calls Amazon DynamoDB. After: on Google Cloud the same application and SDK call a Tensor9 adapter, which serves the DynamoDB API from Cloud Bigtable. Before: on AWS the application's DynamoDB SDK calls Amazon DynamoDB. After: on Google Cloud the same application and SDK call a Tensor9 adapter, which serves the DynamoDB API from Cloud Bigtable.

Your application uses the DynamoDB API through an adapter in the customer environment.

#### Architecture The adapter translates DynamoDB keys and operations into Bigtable row keys, row ranges and mutations. Ordered key ranges implement base-table queries. DynamoDB attribute filters run in the adapter over the selected rows. A conditional write reads the item, evaluates the DynamoDB condition and uses `CheckAndMutateRow` to commit only if the row version still matches. Another writer changing the row triggers a retry of the read and condition check.
A request is compiled in two stages: the DynamoDB API request becomes a backend-neutral logical plan, then a Bigtable physical plan, which executes as one ReadRows range, or a MutateRow or CheckAndMutateRow. Bigtable stores one row per item and is atomic for one row, not across rows. A request is compiled in two stages: the DynamoDB API request becomes a backend-neutral logical plan, then a Bigtable physical plan, which executes as one ReadRows range, or a MutateRow or CheckAndMutateRow. Bigtable stores one row per item and is atomic for one row, not across rows.

The adapter first builds a storage-independent request plan, then converts it to a Bigtable-specific plan for execution.

#### One row per item Every logical DynamoDB table lives in one provisioned Bigtable table, and each item is one row there. A metadata row records each logical schema and its generation, so the logical tables stay separated inside the shared physical one rather than colliding. The row key is what makes reads efficient. It is laid out as the logical table, then the partition route, then the *collated* sort key, then an item digest. A generation identifier separates each table incarnation, so a table dropped and recreated cannot read its predecessor's rows. Because the sort key is collated into the key in DynamoDB's own order, one DynamoDB partition becomes one **contiguous** Bigtable range, and a `Query` over a key range is a range read in the right order rather than a filter over unrelated rows. Item data lives in a raw column family. A second family is an INT64 SUM aggregate, which exists so an integral counter can be updated by Bigtable itself in a single round trip instead of being read, added to and written back. See the next section.
A data row key is composed of the logical table, the partition route, the collated sort key and an item digest, which makes one DynamoDB partition a contiguous Bigtable range in sort-key order. A data row key is composed of the logical table, the partition route, the collated sort key and an item digest, which makes one DynamoDB partition a contiguous Bigtable range in sort-key order.

The row key is built so a DynamoDB partition is one contiguous Bigtable range, in the origin's sort order.

#### Writes and atomicity An unconditional `PutItem` or `DeleteItem`, and a top-level SET/REMOVE `UpdateItem`, use a single `MutateRow`, with no read of the old item and one round trip. A single integral numeric `ADD`, or a SET computed from the attribute's own current value, uses Bigtable's INT64 SUM aggregate cell, so Bigtable performs the addition itself in one RPC rather than the adapter reading, adding and writing back. Conditions, returned item values, nested changes and arithmetic beyond the supported counter case require a read. The adapter computes the change and commits it with `CheckAndMutateRow` against a version field. Conflicting writes repeat the read and condition check, up to eight attempts. All of this is atomic for one item and linearizable because the configured app profile routes to a single cluster. Multi-cluster routing would break the row-transaction contract, so the adapter verifies the routing and the column-family contracts through the Bigtable Admin API at startup, before it serves any traffic, rather than discovering a misconfiguration under load. A top-level SET/REMOVE that skips the item read cannot check the size of the item it produces, so that path relaxes DynamoDB's aggregate 400 KiB limit. An application that depends on that limit being enforced exactly should use a backend that reads before writing.
Three write paths: an unconditional write is one MutateRow, an integral counter add uses the aggregate cell in one RPC, and everything else reads the row and commits through CheckAndMutateRow against a version cell. Three write paths: an unconditional write is one MutateRow, an integral counter add uses the aggregate cell in one RPC, and everything else reads the row and commits through CheckAndMutateRow against a version cell.

Simple mutations and supported counters can avoid a read; other writes read the item and use a version guard.

#### Reads and filters A `Query` is a range read. Because the sort key is encoded in DynamoDB order in the row key, the key condition, the direction and the page boundary are all served by the row range itself, and results come back in DynamoDB's order without the adapter re-sorting them. For a `FilterExpression`, **filters are evaluated in the adapter, not in storage**. Bigtable's own row filters work on families, qualifiers and versions rather than on DynamoDB's typed attribute predicates, so the adapter reads the range and applies your filter to the items it read. As a result, a filtered read can *read more rows than it returns*, and a highly selective filter over a wide range costs what the range costs, not what the result costs. Prefer a selective key condition over a broad range plus a filter. `Scan` pages through the table the same way. Paging bounds the response, but a broad scan still reads the rows it passes over and competes with point operations for the instance's provisioned capacity. Reads are strongly consistent through the single-cluster app profile. `ConsistentRead=false` does not select a cheaper or staler path. A request that would be eventually consistent on DynamoDB is simply served strongly here.
A Query's key condition becomes a Bigtable row range, so storage returns every row in that range. The adapter then applies the FilterExpression to those rows, so a filtered read can examine far more rows than it returns: the range cost is what you pay, not the result cost. A Query's key condition becomes a Bigtable row range, so storage returns every row in that range. The adapter then applies the FilterExpression to those rows, so a filtered read can examine far more rows than it returns: the range cost is what you pay, not the result cost.

A filtered read examines every row in the selected key range. The adapter returns only rows that match the filter.

#### Limitations The following features require operations across multiple rows or per-item deletion rules that this Bigtable layout cannot provide. The adapter rejects these requests. * **Multi-item transactions.** `TransactWriteItems`, `TransactGetItems` and transactional PartiQL are rejected: Bigtable cannot atomically commit or snapshot several item rows. An application that needs cross-item atomicity wants Spanner or a Postgres target. * **Secondary indexes.** A GSI or LSI query is not served. There is no maintained index to seek, so the adapter rejects these requests without scanning the table. Native Bigtable materialized views are not exposed as DynamoDB indexes either. * **DynamoDB Streams.** Not served. Bigtable change streams are not a DynamoDB-compatible atomic change log. A stream record cannot be committed in the same atomic unit as the write it describes. * **Per-item TTL.** Bigtable does expire data, but only through a column-family garbage-collection policy: a uniform max age or version count for the whole family. DynamoDB's TTL is per item, driven by an epoch attribute the application sets on each row, and a family-wide rule cannot express that. Configuring TTL fails rather than silently applying a different expiry rule. * **The blind-write size check.** A top-level SET/REMOVE that skips the read also skips the aggregate 400 KiB validation, as described above. * **Filters are adapter-side.** A filtered read can read more rows than it returns; see the previous section. * **A hot item still contends.** Row keys spread independent partitions, but repeated updates to one item serialize on that row. A uniform-key benchmark does not predict hot-item behaviour. ### Via Cloud SQL for PostgreSQL | Operation | Area | Support | Depth | Notes | | ------------------- | ------------------ | --------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ConditionExpression | Conditional writes | Supported | Common | evaluated against the live item under a row-level lock; commits atomically | | DescribeStream | Indexes + streams | Supported | Full surface | shard list + stream ARN | | GetRecords | Indexes + streams | Supported | Full surface | DynamoDB Streams served from an in-transaction outbox co-committed with each write | | GetShardIterator | Indexes + streams | Supported | Full surface | - | | ListStreams | Indexes + streams | Supported | Full surface | the streamed-table registry | | Query (GSI / LSI) | Indexes + streams | Supported | Most usage | indexes declared before deployment and maintained in the item transaction; queries use the index | | Query | Reads | Supported | Common | ordering, filters and pagination honored | | Scan | Reads | Supported | Common | streamed page by page | | DeleteItem | Single item | Supported | Common | - | | GetItem | Single item | Supported | Common | - | | PutItem | Single item | Supported | Common | items kept in native shape; numbers at exact decimal precision | | UpdateItem | Single item | Supported | Common | update expressions evaluated by the adapter | | TransactWriteItems | Transactions | Supported | Most usage | one serializable PostgreSQL transaction across tables; at most 100 actions and one operation per item; ClientRequestToken idempotency; stream-enabled tables excluded | #### How it works Tensor9 runs a DynamoDB adapter beside your application in the customer's environment. It configures `AWS_ENDPOINT_URL_DYNAMODB` to send the AWS SDK's requests to the adapter over the local loopback network. Your SDK and application code stay unchanged. The adapter accepts DynamoDB requests and data types, returns DynamoDB responses and errors, and stores data in PostgreSQL. DynamoDB supports numbers with up to 38 significant digits, typed sets, conditional writes, secondary indexes, and atomic transactions. The adapter implements these using PostgreSQL storage and transactions. PostgreSQL supports multi-table transactions and synchronously updated indexes, but has different concurrency, capacity, and identifier limits. Unsupported requests return errors; the limitations below describe the differences. For base-table `Query` and `Scan` requests, the adapter compiles supported filters into parameterized SQL `WHERE` predicates over the JSONB item. Number comparisons cast stored decimal text to PostgreSQL `numeric`. An order-preserving key column supplies sort bounds, cursor position, and direction. The query uses a server-side cursor to fetch bounded batches until the requested `Limit` or 1 MiB of item data. Expressions that cannot be represented in SQL are evaluated in the adapter over the rows selected by the SQL predicate. Secondary-index queries use the separate path described below. For conditional writes, the adapter opens a transaction, locks the row with `SELECT ... FOR UPDATE`, evaluates the `ConditionExpression`, and writes within that transaction. This requires an extra round trip, but prevents a concurrent writer from changing the item between the check and write. A failed condition returns `ConditionalCheckFailed` and can return the item that failed it. Some unconditional writes skip the read: a top-level SET/REMOVE patch or integral counter add uses one statement when it has no condition, requests no returned item image, and affects no secondary index or stream.
Before: on AWS the application's DynamoDB SDK calls Amazon DynamoDB. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the DynamoDB API from a PostgreSQL database. Before: on AWS the application's DynamoDB SDK calls Amazon DynamoDB. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the DynamoDB API from a PostgreSQL database.

Your application uses the DynamoDB API through an adapter in the customer environment.

#### Architecture The adapter is a stateless Rust service. It parses DynamoDB expressions, translates requests into PostgreSQL operations, and returns DynamoDB responses. It connects to PostgreSQL through a connection pool using the customer's credentials. Durable state stays in PostgreSQL, so adapter instances can restart or scale out without coordinating local state. Stream-enabled tables have the separate single-writer restriction below. Each DynamoDB table has a PostgreSQL table named `ddb_` in one database. The `ddb_tables` registry records its generated physical name, key schema, billing mode, time-to-live (TTL) settings, global secondary indexes (GSIs), and stream settings. A write or query for a table absent from the registry returns `ResourceNotFoundException`; writes do not create tables implicitly. At runtime, `CreateTable` creates a PostgreSQL expression index for each declared GSI. Each stream-enabled table also has a `_strm` table that stores change records.
Architecture: the app's DynamoDB SDK calls a stateless Rust Tensor9 adapter, which serves the API from PostgreSQL. Postgres holds all state: each DynamoDB table is its own physical relation ddb_name in one database, an authoritative ddb_tables registry relation, a native per-table expression index per declared GSI, and a co-located _strm outbox relation per streamed table. Architecture: the app's DynamoDB SDK calls a stateless Rust Tensor9 adapter, which serves the API from PostgreSQL. Postgres holds all state: each DynamoDB table is its own physical relation ddb_name in one database, an authoritative ddb_tables registry relation, a native per-table expression index per declared GSI, and a co-located _strm outbox relation per streamed table.
A request is compiled in two stages: the DynamoDB API request becomes a backend-neutral logical plan, then a Postgres physical plan, whose reads execute as one SQL statement and whose writes run as one transaction. Postgres holds a relation per DynamoDB table plus a native expression index per declared secondary index. A request is compiled in two stages: the DynamoDB API request becomes a backend-neutral logical plan, then a Postgres physical plan, whose reads execute as one SQL statement and whose writes run as one transaction. Postgres holds a relation per DynamoDB table plus a native expression index per declared secondary index.

A stateless Rust adapter serves the DynamoDB API in front of Postgres; all durable state lives in one Postgres database, where each DynamoDB table is its own physical relation.

The adapter first builds a storage-independent request plan, then converts it to a Postgres-specific plan for execution.

#### Schemas A DynamoDB item occupies one PostgreSQL row with columns `pk TEXT`, `sk TEXT`, `okey TEXT` and `item JSONB`. The `PRIMARY KEY (pk, sk)` index serves `GetItem`. A separate index over `pk` and bytewise-collated `okey` serves ordered base-table `Query` requests. The `item JSONB` column preserves DynamoDB's typed attribute representation, including `{"N":"123.45"}`, `{"B":""}`, and typed sets. Numbers remain decimal strings, so a 38-digit amount or a Snowflake ID is not rounded through a floating-point conversion. Type tags preserve the distinction between numbers and numeric strings. Conditional writes use row locks, not a separate version column. Enabling TTL builds an expression index over the configured epoch attribute inside the JSONB item. A bounded, leader-elected reaper deletes eligible rows through that index. Expired items remain readable until physical deletion; reads do not filter on TTL.
A DynamoDB item becomes one row: pk TEXT, sk TEXT, okey TEXT and item JSONB, with PRIMARY KEY (pk, sk). The okey column preserves sort order. The whole item lives in JSONB in its DynamoDB wire form, so a number keeps its exact decimal text instead of becoming a float. TTL uses an expression index over the item, not a separate column. A DynamoDB item becomes one row: pk TEXT, sk TEXT, okey TEXT and item JSONB, with PRIMARY KEY (pk, sk). The okey column preserves sort order. The whole item lives in JSONB in its DynamoDB wire form, so a number keeps its exact decimal text instead of becoming a float. TTL uses an expression index over the item, not a separate column.

Each attribute keeps its DynamoDB type tag inside item JSONB , so \{"N":"123.45"} keeps its exact decimal string.

#### Where tables live Each DynamoDB table has its own PostgreSQL table, indexes and optional `_strm` change-record table. `CreateTable` creates those database objects and their registry entry; `DeleteTable` drops them. Storage, indexes and vacuum settings can be tuned per table. The adapter checks a 500-table guardrail per namespace when creating a physical DynamoDB table. Concurrent creates of different tables can exceed that guardrail; it is not a hard PostgreSQL quota. Plan database capacity and table placement for larger deployments. Provisioned instances continue to incur compute costs while idle.
Each declared DynamoDB table is its own physical relation inside one Postgres database, alongside the authoritative ddb_tables registry and each table's native expression indexes. The adapter applies a 500-table guardrail per namespace. Larger deployments need capacity and table-placement planning. Each declared DynamoDB table is its own physical relation inside one Postgres database, alongside the authoritative ddb_tables registry and each table's native expression indexes. The adapter applies a 500-table guardrail per namespace. Larger deployments need capacity and table-placement planning.

Each declared table is its own physical relation. CreateTable and DeleteTable create and drop PostgreSQL relations. The adapter applies a 500-table guardrail per namespace.

#### Conditional writes To evaluate a conditional write, the adapter locks the item's `(pk, sk)` row with `SELECT … FOR UPDATE`, checks the `ConditionExpression` against that row, then writes and commits. The lock prevents another writer from changing the item between the check and write. A false condition returns the non-retryable `ConditionalCheckFailed` error. A PostgreSQL serialization failure or lock timeout (`40001`/lock-not-available) instead returns a retryable throughput error, allowing the SDK to back off and retry. Concurrency failures are not reported as failed conditions. For `attribute_not_exists(pk)`, the PostgreSQL primary-key constraint prevents two concurrent requests from creating the same item. Only one insert can succeed for a given `(pk, sk)`.
A conditional write takes a row lock with SELECT for update, evaluates the DynamoDB condition inside the layer against the locked row, then writes and commits. A false condition returns a faithful ConditionalCheckFailed; a losing concurrency race returns a retryable conflict, never a spurious condition failure. Unique creation is enforced by the primary key. A conditional write takes a row lock with SELECT for update, evaluates the DynamoDB condition inside the layer against the locked row, then writes and commits. A false condition returns a faithful ConditionalCheckFailed; a losing concurrency race returns a retryable conflict, never a spurious condition failure. Unique creation is enforced by the primary key.

Failed conditions and PostgreSQL concurrency conflicts return different error types.

#### Secondary indexes On each write, the adapter adds two index fields to the item's JSONB. The partition-key field, `_idx__h`, normalizes equivalent values such as `1` and `1.0`. The sort-key field, `_idx__o`, preserves sort order and uses the base key to break ties. A PostgreSQL expression index over these fields locates matching partition keys. The adapter then evaluates sort-key conditions, ordering, pagination, and any `FilterExpression` over the retrieved items. The index fields and item commit in the same transaction, so GSI queries are strongly consistent. DynamoDB's own GSIs update asynchronously and are eventually consistent. Each index lookup is limited to 10,000 candidate items for one index partition-key value. Exceeding this limit returns an error; choose index keys with this limit in mind.
On each write the layer stamps a normalized-hash field and a collated-range field into the item's JSONB, and Postgres maintains a native expression index over them. A GSI query is an index seek by the stamped hash plus filtering in the adapter. The stamps co-commit with the item, so the index is strongly consistent. On each write the layer stamps a normalized-hash field and a collated-range field into the item's JSONB, and Postgres maintains a native expression index over them. A GSI query is an index seek by the stamped hash plus filtering in the adapter. The stamps co-commit with the item, so the index is strongly consistent.

A GSI uses a PostgreSQL expression index to find candidates, then the adapter filters and orders them. Index updates commit with the item, making GSI queries strongly consistent.

#### Transactions `TransactWriteItems` executes in one PostgreSQL `BEGIN…COMMIT` transaction at `SERIALIZABLE` isolation. A debit in one table and a credit in another either both commit or both fail, as in DynamoDB. Transactions enforce DynamoDB's 100-action limit and one-operation-per-item rule. The adapter records `ClientRequestToken` in the same transaction as the writes. Within DynamoDB's idempotency window, a retry with the same token returns the prior success without committing the writes again. The adapter acquires row locks in a consistent order to prevent opposite-direction transfers from deadlocking. Serialization or deadlock failures return retryable conflicts. `TransactWriteItems` is not supported on stream-enabled tables. A request that touches any such table returns `ValidationException` before execution, because the transaction path does not create the required stream records.
A TransactWriteItems spanning two tables, a debit in one relation and a credit in another, commits atomically in one BEGIN…COMMIT at SERIALIZABLE isolation, exactly like real DynamoDB. A TransactWriteItems spanning two tables, a debit in one relation and a credit in another, commits atomically in one BEGIN…COMMIT at SERIALIZABLE isolation, exactly like real DynamoDB.

TransactWriteItems runs in one native Postgres BEGIN…COMMIT at SERIALIZABLE, atomic and isolated across tables.

#### Limitations PostgreSQL limitations * **One PostgreSQL instance serves each table.** Heavy contention on one key can cause serialization failures and row-lock conflicts. The adapter returns retryable errors, but sustained contention can exhaust the SDK retry budget and appear as throttling. * **Stream-enabled tables allow one writer at a time.** An owner lease of about 90 seconds preserves stream record order. A second adapter instance attempting to write while that lease is active receives a retryable `ThrottlingException`. Throughput depends on the writer, database and request workload. Tables without streams support concurrent writers. * **`DescribeStream` lists at most 100 shards.** A stream with more shards is not fully enumerated in one call. * **Transactions on stream-enabled tables are unsupported.** A `TransactWriteItems` request touching one returns `ValidationException` because the transaction path does not create stream records. * **DynamoDB backup and global-table APIs are unsupported.** Use PostgreSQL backups, snapshots, point-in-time recovery (PITR), and replica/failover procedures. DynamoDB on-demand backup/restore, PITR, and global-table replication APIs are not implemented. * **Instances incur costs when idle.** Size and pay for PostgreSQL capacity ahead of demand. Larger table counts require more instances and table-placement planning. * **Index lookups have a 10,000-item limit.** A lookup that exceeds 10,000 candidate items for one index partition-key value returns an error. * **Plan for the table-count guardrail.** The adapter checks 500 physical DynamoDB tables per namespace. Larger deployments need database capacity and table-placement planning. * **Expired items remain readable until deletion.** A bounded, leader-elected reaper uses the TTL expression index to delete eligible rows asynchronously. Until physical deletion, GetItem, Query and Scan can return the expired item, and it continues to consume storage. #### Other considerations * **Stream records commit with the write.** The adapter stores each record in the table's `_strm` table within the same transaction as the item write. A record exists if and only if its write committed. The single-writer lease keeps stream sequence order aligned with commit order so a cursor can resume without skipping records. * **Use PostgreSQL recovery procedures.** Backups, PITR, snapshots, and replica/failover procedures operate on the PostgreSQL database. The adapter does not implement DynamoDB backup, PITR, or global-table APIs. * **Connection pooling.** The adapter uses pooled PostgreSQL connections. Deployments spread across many instances may need a separate pooler to keep total connections within each database's limit. * **Migrate and test one table at a time.** Separate PostgreSQL tables allow per-table export/import, dual-write, or backfill-and-switch procedures. Application code continues to use the DynamoDB API. * **Use PostgreSQL operational tools.** Monitor SQL activity, maintain indexes, vacuum tables, and run backups with your existing PostgreSQL tools and procedures. ## On Azure ### Via Azure Cosmos DB (provisioned) | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------------------- | ------------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DescribeContinuousBackups | Backups | Partial | Full surface | PITR status mapped from the Cosmos continuous-backup mode | | On-demand backups (CreateBackup / RestoreTableFromBackup / …) | Backups | Out of scope | Full surface | the snapshot-backup model differs from Cosmos; Use the target backup and restore workflow. | | UpdateContinuousBackups | Backups | Partial | Full surface | toggles PITR onto Cosmos continuous backup (the retention window differs) | | ConditionExpression | Conditional writes | Supported | Common | condition and mutation execute atomically in a stored procedure, eligible PATCH or etag-guarded fallback; write conflicts are retryable | | Global tables (CreateGlobalTable / UpdateGlobalTable / …) | Global | Out of scope | Full surface | DynamoDB global-table control APIs are outside this mapping; configure the target deployment and its region separately. | | DescribeStream | Indexes + streams | Supported | Full surface | returns the shard list and stream ARN | | GetRecords | Indexes + streams | Supported | Full surface | records co-commit atomically with the write; all four view types; 24-hour retention with TrimmedDataAccessException below the horizon; one writing instance per streamed table | | GetShardIterator | Indexes + streams | Supported | Full surface | - | | ListStreams | Indexes + streams | Supported | Full surface | the streamed-table registry | | Query (GSI) | Indexes + streams | Supported | Most usage | each global secondary index is a dedicated Azure-maintained index container, so a GSI query reads its own index rather than scanning the table; propagation is eventually consistent, matching DynamoDB's own GSI behavior, and a strongly-consistent read on a GSI is rejected exactly as DynamoDB rejects it | | Query (LSI) | Indexes + streams | Supported | Most usage | a local secondary index shares the table's own container and partition route, so an LSI query reflects a write immediately; large index partitions are subject to a per-query candidate limit | | S3 import/export + Kinesis streaming + ContributorInsights + ResourcePolicy | Out of scope | Out of scope | Full surface | These AWS integrations are outside the listed mapping. | | BatchExecuteStatement | PartiQL | Partial | Most usage | batched PartiQL statements, each statement gets its own real outcome | | ExecuteStatement | PartiQL | Partial | Most usage | PartiQL select/insert/update/delete compiled to the native Cosmos query / item path; some PartiQL functions differ | | ExecuteTransaction | PartiQL | Out of scope | Most usage | transactional PartiQL is unavailable on the current Cosmos adapter | | BatchGetItem | Reads | Supported | Most usage | the adapter executes each item operation and reports its result | | BatchWriteItem | Reads | Supported | Most usage | the adapter executes each item operation and reports its result | | Query | Reads | Partial | Common | native key/range/order/filter pushdown plus residual evaluation; pagination follows Cosmos result windows | | Scan | Reads | Partial | Most usage | bounded native result windows; Limit and ScannedCount differ from DynamoDB pre-filter examined-item accounting | | DeleteItem | Single item | Supported | Common | - | | GetItem | Single item | Supported | Common | strongly consistent from any instance, whatever the ConsistentRead flag (never weaker than asked) | | PutItem | Single item | Supported | Common | DynamoDB type tags preserved; native Cosmos numbers require at most 15 significant digits and representable range | | UpdateItem | Single item | Supported | Common | eligible removals use native conditional PATCH; ordinary updates run in a partition-scoped stored procedure; other shapes use guarded read-modify-write | | DescribeTimeToLive | TTL | Supported | Most usage | reads the container's TTL policy | | UpdateTimeToLive | TTL | Supported | Most usage | the DynamoDB per-item TTL attribute is mapped to the Cosmos TTL policy | | CreateTable | Table control plane | Supported | Most usage | registers a logical table in a deployed container; declared tables receive containers at compile time | | DeleteTable | Table control plane | Supported | Most usage | logical generation change with bounded cleanup and metadata propagation | | DescribeEndpoints | Table control plane | Supported | Full surface | returns the adapter endpoint | | DescribeLimits | Table control plane | Supported | Full surface | account / table limits reported from the compiled config | | DescribeTable | Table control plane | Supported | Most usage | returns the key schema and stream identifiers | | ListTables | Table control plane | Supported | Most usage | - | | UpdateTable | Table control plane | Supported | Most usage | index and stream definitions must match deployed configuration; throughput mode is selected at compile time | | ListTagsOfResource | Tags | Supported | Full surface | - | | TagResource | Tags | Supported | Full surface | - | | UntagResource | Tags | Supported | Full surface | - | | TransactGetItems | Transactions | Out of scope | Most usage | the current Cosmos adapter rejects DynamoDB transactional reads | | TransactWriteItems | Transactions | Out of scope | Most usage | the current Cosmos adapter rejects DynamoDB multi-item transactions | #### How it works Tensor9 runs a DynamoDB adapter beside your application in the customer's environment. It configures `AWS_ENDPOINT_URL_DYNAMODB` to send the AWS SDK's requests to the adapter over the local loopback network. Your SDK and application code stay unchanged. The adapter accepts DynamoDB requests and data types, returns DynamoDB responses and errors, and stores data in Azure Cosmos DB. The adapter maps DynamoDB items and supported operations onto Cosmos documents. Native numeric precision, result pagination and transaction support differ from DynamoDB; the following sections describe those application-visible limits.
Before: on AWS the application's DynamoDB SDK calls Amazon DynamoDB. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the DynamoDB API from Azure Cosmos DB. Before: on AWS the application's DynamoDB SDK calls Amazon DynamoDB. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the DynamoDB API from Azure Cosmos DB.

Your application uses the DynamoDB API through an adapter in the customer environment.

#### Architecture The adapter parses DynamoDB requests and expressions, executes Cosmos operations and returns DynamoDB responses. Durable item and registry state lives in Cosmos, and requests use the customer's workload identity. Adapter instances can restart without recovering local data; stream-enabled tables still require one active writer. Tensor9 provisions one Cosmos account and one `ddb` database per stack, with a container for each declared table. The selected target sets provisioned or serverless throughput for the account. Items use table-prefixed partition keys; a reserved `_tablemeta` partition stores the schemas and creation times. Tables created at runtime share an existing container, as described below. LSIs share the source container; the provisioned target uses separate, asynchronously maintained GSI containers.
Architecture: the app's DynamoDB SDK calls a stateless Rust Tensor9 adapter, which serves the API from Azure Cosmos DB. Cosmos holds all state: the stack shares one account with one database named ddb, each declared table has its own container inside it partitioned on a synthesized partition-key path, with items in table-prefixed logical partitions, a registry partition, a dedicated Azure-maintained index container per global secondary index, and a local secondary index inside the table's own container. Runtime-created tables share a deployed container and its capacity. Architecture: the app's DynamoDB SDK calls a stateless Rust Tensor9 adapter, which serves the API from Azure Cosmos DB. Cosmos holds all state: the stack shares one account with one database named ddb, each declared table has its own container inside it partitioned on a synthesized partition-key path, with items in table-prefixed logical partitions, a registry partition, a dedicated Azure-maintained index container per global secondary index, and a local secondary index inside the table's own container. Runtime-created tables share a deployed container and its capacity.
A request is compiled in two stages: the DynamoDB API request becomes a backend-neutral logical plan, then a Cosmos physical plan, which executes as a point read, a Cosmos SQL query, a native PATCH or a stored procedure. Cosmos holds a container per declared table, partitioned on a synthesized partition-key path, with an Azure-maintained index container per global secondary index. Runtime-created tables share a deployed container and its capacity. A request is compiled in two stages: the DynamoDB API request becomes a backend-neutral logical plan, then a Cosmos physical plan, which executes as a point read, a Cosmos SQL query, a native PATCH or a stored procedure. Cosmos holds a container per declared table, partitioned on a synthesized partition-key path, with an Azure-maintained index container per global secondary index. Runtime-created tables share a deployed container and its capacity.

A stateless Rust adapter serves the DynamoDB API in front of Cosmos. One account and one "ddb" database hold a container per declared table, partitioned on /\_pk. Runtime-created tables share a deployed container and its capacity.

The adapter first builds a storage-independent request plan, then converts it to a Cosmos-specific plan for execution. Each declared table has its own container; runtime-created tables share a deployed container and its capacity.

#### Schemas The adapter preserves DynamoDB type tags inside the `item` envelope. Accepted numbers are stored as native Cosmos JSON numbers; binary payloads use order-preserving hex. Keys and routing fields identify the logical table and partition. **Numeric precision differs:** values must have at most 15 significant digits and fit the native numeric range. Values outside this supported representation fail validation before storage; the adapter does not preserve DynamoDB's full 38-digit numeric domain as decimal strings. **Items remain limited to 400 KiB.** Updates check the resulting item, including projected local-index entries where applicable. A larger Cosmos document limit does not disable this DynamoDB compatibility check.
DynamoDB type tags remain in a Cosmos document envelope. Accepted numeric payloads are native JSON numbers, after precision and range validation. DynamoDB type tags remain in a Cosmos document envelope. Accepted numeric payloads are native JSON numbers, after precision and range validation.

Type tags distinguish numbers, strings, binary values and sets; native payloads make predicates queryable in Cosmos.

#### Where tables live Each declared table has its own container, indexing policy and storage within the stack's shared account and database. Containers share account administration and lifecycle. Monitor account limits as the number of tables grows. Choose provisioned or serverless throughput when selecting the target. Provisioned containers have an autoscale range of request units (RUs) derived from declared capacity; a `PAY_PER_REQUEST` source table gets a configurable default range. Serverless containers bill consumed requests and do not reserve the source table's requested capacity. Throughput depends on request-unit use and partition distribution. Changing the selected target recreates the account and containers, so plan a data migration. Within a container, a table-prefixed partition key such as `orders|S:user#42` separates logical tables. Point reads and base queries use that `_pk` route; scans and registry reads filter on `_tbl`. Runtime `CreateTable` writes a registry document in the stack's first deployed container. Its data identity has no permission to provision Cosmos containers. `DescribeTable` and `ListTables` read that registry. * **Key schema is required.** A composite-key table's key schema comes from a declared CreateTable, the registry doc, or is learned from the first key-bearing request. A Put that can't resolve its key schema is rejected outright, never written under a guessed key. * **Declared tables are physically isolated; runtime-created tables are logically isolated.** A declared table's container (its throughput, its index, its storage) is its own. A table created at runtime shares a deployed container: its logical partitions are disjoint by table-prefixing, but physical partitions are shared. A hot logical partition remains bounded by the service capacity available to its physical partition.
All of a stack's tables share one Cosmos account with one database named ddb; each declared table keeps its own container, its own index, and its own storage. The selected target sets the account's throughput mode for the deployment: provisioned gives each container a dedicated autoscale RU band, serverless makes it a consumption account with no provisioned per-container throughput. Runtime-created tables share a deployed container and its capacity. All of a stack's tables share one Cosmos account with one database named ddb; each declared table keeps its own container, its own index, and its own storage. The selected target sets the account's throughput mode for the deployment: provisioned gives each container a dedicated autoscale RU band, serverless makes it a consumption account with no provisioned per-container throughput. Runtime-created tables share a deployed container and its capacity.

A stack's tables share one account and one "ddb" database, using the selected throughput mode. Each declared table has its own container; runtime-created tables share a deployed container and its capacity. At runtime, CreateTable / DeleteTable / UpdateTable change registry documents, not Cosmos resources.

#### Conditional writes **The operation and its options determine how the adapter writes an item.** Ordinary SET updates use a versioned, partition-scoped stored procedure. Cosmos reads the current item, evaluates the condition, applies the update and checks the resulting 400 KiB item limit inside one atomic operation. This is read-modify-write inside the database, with no separate adapter read round trip. Native PATCH is narrower: the condition must prove existence, every action must be an eligible top-level removal, and TTL, streams and old-image requirements must be absent. A size-increasing SET cannot use this path because native PATCH cannot check the resulting DynamoDB item size. LSI updates, unsupported expressions and requests needing the old image on condition failure fall back to an adapter read and an etag-guarded write. Streamed writes use the guarded batch path to commit the item and outbox record together. A concurrent write conflict is retryable; it is not reported as a false condition failure. The Yahoo! Cloud Serving Benchmark (YCSB) workload F measures application-side read-modify-write: a separate GetItem before UpdateItem. Its full latency includes both API calls and is distinct from the update phase. That sequence is not a multi-item transaction. In the recorded 2026-09-09 provisioned capture with 16 workers, full read-modify-write p50 was 21.071 ms and update-phase p50 was 15.935 ms, reported as medians of three completed repetitions. These are different portions of the same workload, not two competing implementations.
Three write paths: conditional removals use native PATCH; ordinary writes use an atomic stored procedure; remaining shapes use guarded read-modify-write. Three write paths: conditional removals use native PATCH; ordinary writes use an atomic stored procedure; remaining shapes use guarded read-modify-write.
#### Secondary indexes A local secondary index shares the source container and partition route, so its reads reflect committed writes immediately. Its projected entry size also contributes to DynamoDB's item-size check. On the provisioned target, Azure asynchronously maintains a dedicated container for each declared global secondary index. Queries target that index container and push down its key predicate, ordering and cursor. GSI visibility is eventual; `ConsistentRead=true` is rejected, matching DynamoDB's GSI contract. Serverless tables cannot declare these managed GSIs. Index definitions must match the provisioned configuration; the adapter does not silently build a substitute index at runtime.
An LSI shares the source container and is immediately visible. Azure maintains a separate GSI container asynchronously on provisioned accounts. An LSI shares the source container and is immediately visible. Azure maintains a separate GSI container asynchronously on provisioned accounts.
#### Transactions **The current Cosmos adapter rejects DynamoDB multi-item transactions.** This includes `TransactWriteItems`, `TransactGetItems` and transactional PartiQL, even when all items share a partition key. Cosmos partition-local stored procedures and batches still make individual writes and stream outbox commits atomic. That internal mechanism does not expose the DynamoDB transaction APIs. Applications that require DynamoDB multi-item transactions need another listed target that supports those APIs. #### Limitations Compatibility limits * **Numeric domain:** at most 15 significant digits and representable native range, compared with DynamoDB's 38-digit domain. Unsupported values fail validation. * **Item size:** 400 KiB remains enforced. Ordinary SET updates use the stored procedure; LSI updates need the guarded fallback so projected entry size is checked too. * **Transactions:** the current adapter rejects all DynamoDB multi-item transaction APIs, including single-partition requests. * **Read consistency:** the compiled account uses Strong consistency regardless of the request's ConsistentRead flag. Plan capacity for strongly consistent reads. * **Pagination:** Query and Scan use native result windows. Limit and ScannedCount do not exactly reproduce DynamoDB's pre-filter examined-item counter; residual predicates may be evaluated in the adapter. * **Indexes:** managed GSIs are eventually consistent and require the provisioned target; LSIs share the source container. Declared index definitions must match the deployment. * **Streams:** records co-commit with the item, but a streamed table permits one writing instance at a time to preserve ordering. Writes from another instance can be rejected until its writer lease is available. * **Hot partitions:** capacity depends on key distribution and the account's throughput mode. A hot item can contend even when aggregate capacity remains available. * **Table lifecycle:** DeleteTable changes the logical generation. Other adapter instances can observe that change after the bounded metadata cache delay, approximately five seconds; physical cleanup is bounded. #### Other considerations * **Migration:** the Cosmos containers start empty. Export/import, backfill or a dual-write cutover must move existing DynamoDB data before switching traffic. * **Deployment:** the compiler provisions one account with the chosen throughput mode, a shared database and containers for declared tables. Managed identities provide data access; account keys are unnecessary. * **Capacity:** provisioned containers use dedicated autoscale RU bands. Serverless containers have no provisioned throughput and bill consumed requests. The 2026-09-09 adapter captures used separate Standard\_D8s\_v5 driver and adapter VMs in westus2, Strong consistency and gateway-mode connectivity. The provisioned container had a 40,000-RU/s autoscale maximum. YCSB used 100,000 verified records with ten 100-byte fields and 200,000 logical operations per repetition. Each mode has its own results; medians summarize three completed repetitions. There is no matching native DynamoDB baseline for this configuration, so these captures do not establish a performance advantage over DynamoDB. * **Runtime-created tables:** these occupy separate logical namespaces inside an already-deployed container and share its capacity. Declared tables have separate containers. * **Operations:** Microsoft operates Cosmos storage; Tensor9 operates the adapter. Monitor request units, throttling, write conflicts and latency alongside adapter and application host utilization. ### Via Azure Cosmos DB (serverless) | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------------------- | ------------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DescribeContinuousBackups | Backups | Partial | Full surface | PITR status mapped from the Cosmos continuous-backup mode | | On-demand backups (CreateBackup / RestoreTableFromBackup / …) | Backups | Out of scope | Full surface | the snapshot-backup model differs from Cosmos; Use the target backup and restore workflow. | | UpdateContinuousBackups | Backups | Partial | Full surface | toggles PITR onto Cosmos continuous backup (the retention window differs) | | ConditionExpression | Conditional writes | Supported | Common | condition and mutation execute atomically in a stored procedure, eligible PATCH or etag-guarded fallback; write conflicts are retryable | | Global tables (CreateGlobalTable / UpdateGlobalTable / …) | Global | Out of scope | Full surface | DynamoDB global-table control APIs are outside this mapping; configure the target deployment and its region separately. | | DescribeStream | Indexes + streams | Supported | Full surface | returns the shard list and stream ARN | | GetRecords | Indexes + streams | Supported | Full surface | records co-commit atomically with the write; all four view types; 24-hour retention with TrimmedDataAccessException below the horizon; one writing instance per streamed table | | GetShardIterator | Indexes + streams | Supported | Full surface | - | | ListStreams | Indexes + streams | Supported | Full surface | the streamed-table registry | | Query (GSI) | Indexes + streams | Out of scope | Most usage | a global secondary index needs autoscale throughput to maintain its index container, which a consumption account does not offer; a table declaring one stops the build and names the provisioned target rather than deploying without the index. Local secondary indexes are unaffected | | Query (LSI) | Indexes + streams | Supported | Most usage | a local secondary index shares the table's own container and partition route, so an LSI query reflects a write immediately; large index partitions are subject to a per-query candidate limit | | S3 import/export + Kinesis streaming + ContributorInsights + ResourcePolicy | Out of scope | Out of scope | Full surface | These AWS integrations are outside the listed mapping. | | BatchExecuteStatement | PartiQL | Partial | Most usage | batched PartiQL statements, each statement gets its own real outcome | | ExecuteStatement | PartiQL | Partial | Most usage | PartiQL select/insert/update/delete compiled to the native Cosmos query / item path; some PartiQL functions differ | | ExecuteTransaction | PartiQL | Out of scope | Most usage | transactional PartiQL is unavailable on the current Cosmos adapter | | BatchGetItem | Reads | Supported | Most usage | the adapter executes each item operation and reports its result | | BatchWriteItem | Reads | Supported | Most usage | the adapter executes each item operation and reports its result | | Query | Reads | Partial | Common | native key/range/order/filter pushdown plus residual evaluation; pagination follows Cosmos result windows | | Scan | Reads | Partial | Most usage | bounded native result windows; Limit and ScannedCount differ from DynamoDB pre-filter examined-item accounting | | DeleteItem | Single item | Supported | Common | - | | GetItem | Single item | Supported | Common | strongly consistent from any instance, whatever the ConsistentRead flag (never weaker than asked) | | PutItem | Single item | Supported | Common | DynamoDB type tags preserved; native Cosmos numbers require at most 15 significant digits and representable range | | UpdateItem | Single item | Supported | Common | eligible removals use native conditional PATCH; ordinary updates run in a partition-scoped stored procedure; other shapes use guarded read-modify-write | | DescribeTimeToLive | TTL | Supported | Most usage | reads the container's TTL policy | | UpdateTimeToLive | TTL | Supported | Most usage | the DynamoDB per-item TTL attribute is mapped to the Cosmos TTL policy | | CreateTable | Table control plane | Supported | Most usage | registers a logical table in a deployed container; declared tables receive containers at compile time | | DeleteTable | Table control plane | Supported | Most usage | logical generation change with bounded cleanup and metadata propagation | | DescribeEndpoints | Table control plane | Supported | Full surface | returns the adapter endpoint | | DescribeLimits | Table control plane | Supported | Full surface | account / table limits reported from the compiled config | | DescribeTable | Table control plane | Supported | Most usage | returns the key schema and stream identifiers | | ListTables | Table control plane | Supported | Most usage | - | | UpdateTable | Table control plane | Supported | Most usage | index and stream definitions must match deployed configuration; throughput mode is selected at compile time | | ListTagsOfResource | Tags | Supported | Full surface | - | | TagResource | Tags | Supported | Full surface | - | | UntagResource | Tags | Supported | Full surface | - | | TransactGetItems | Transactions | Out of scope | Most usage | the current Cosmos adapter rejects DynamoDB transactional reads | | TransactWriteItems | Transactions | Out of scope | Most usage | the current Cosmos adapter rejects DynamoDB multi-item transactions | #### Requests and database state Your DynamoDB client sends requests to the Rust adapter in the customer environment. The adapter parses DynamoDB expressions, performs Cosmos operations and returns DynamoDB responses. Cosmos stores items, table definitions and stream records. The adapter uses workload identity to access that data; restarting an adapter instance does not require restoring local item state. #### Choosing serverless capacity Tensor9 provisions a serverless Cosmos account and one ddb database for the deployment. Charges follow consumed request units and stored data, without a dedicated autoscale range. The selected account mode applies to its containers; a source table's provisioned capacity does not reserve Cosmos request units. Test the workload's bursts, retries and partition distribution against the serverless limits. #### Declared and runtime-created tables Each declared table gets a container. A table created through the runtime CreateTable API instead uses the first deployed container: the adapter records its schema and separates its items with a table-prefixed partition key. The data identity cannot provision a new Cosmos container. DescribeTable and ListTables read the durable registry; DeleteTable invalidates the table generation and performs bounded cleanup. Runtime-created tables therefore share container capacity. #### Items, reads and conditional writes DynamoDB attribute type tags are retained, but numbers must fit the native Cosmos representation: at most 15 significant digits and a representable range. Unsupported numbers are rejected. The adapter enforces a 400 KiB item limit and uses strong reads regardless of ConsistentRead. Ordinary writes use a Cosmos stored procedure; eligible conditional removals use PATCH, and other request shapes use guarded read-modify-write. Query and Scan use Cosmos result windows, so Limit and ScannedCount differ from DynamoDB's examined-item accounting. #### Indexes, transactions and streams Local secondary indexes share the table's container and reflect a write immediately. A global secondary index requires the provisioned target because the separate Azure-maintained index container needs autoscale throughput; declaring one on serverless stops the build. DynamoDB multi-item transaction APIs are rejected. Streams commit each change record with the item and require one writing instance per streamed table. All four view types and 24-hour retention are available; an older position returns TrimmedDataAccessException. #### Expiry, migration and operation The adapter converts the configured absolute TTL epoch into Cosmos's relative expiry value. Cosmos removes expired documents asynchronously. Deploy the destination, load existing items and validate the application's queries and stream consumers before switching traffic. Changing between provisioned and serverless accounts requires a new destination and data migration. Microsoft operates Cosmos, while Tensor9 operates the adapter; configure account access, backups and monitoring for the customer environment. ## On OCI | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------------------- | -------------- | ------------ | ------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------- | | aws\_dynamodb\_table provisioning | Infrastructure | Partial | - | - | creates oci\_nosql\_table with the mapped key schema and capacity settings; review storage, index, TTL and integration differences | | DynamoDB item and table-management requests | Runtime API | Out of scope | - | - | this OCI NoSQL mapping has no DynamoDB API translator; use OCI NoSQL APIs and SDKs | #### Infrastructure mapping Tensor9 converts the declared aws\_dynamodb\_table to an oci\_nosql\_table in the customer's OCI compartment. This is the infrastructure-only adaptation: it provisions storage but does not provide an AWS DynamoDB endpoint. Application reads, writes, queries and runtime table administration use OCI NoSQL APIs. The table and control pages describe this same target choice. #### Keys and item data The DynamoDB partition key becomes the OCI SHARD key. An optional sort key becomes the second primary-key column. String, number and binary key types become corresponding OCI column types; other item attributes occupy a value JSON column. Adapt queries and serialization to that layout rather than sending DynamoDB attribute documents directly. #### Capacity and storage PAY\_PER\_REQUEST selects OCI ON\_DEMAND; provisioned DynamoDB capacity selects OCI PROVISIONED settings. OCI read and write units differ from DynamoDB units, so validate them against the workload. OCI also requires an explicit storage cap. Increase that cap before data reaches it; the source DynamoDB table had no equivalent fixed cap. #### Indexes, expiry and integrations The current infrastructure mapping does not create declared GSIs or LSIs. Configure native OCI indexes and update the application's access patterns. A source TTL declaration produces a no-expiry default; its per-item epoch attribute is not translated into automatic expiry. DynamoDB Streams, AWS PITR configuration, source KMS key references and replica settings are not reproduced by this table resource. Configure the corresponding target data-protection and event requirements separately. #### Migration and operation The new table starts empty. Convert application access, load the data, and test key lookups, queries, expiry and recovery before switching traffic. Oracle operates the NoSQL service; the customer manages compartment access, capacity, storage limits and the chosen recovery procedures. Use a different listed adaptation when the application must keep runtime DynamoDB API calls. ## On Private Kubernetes | Operation | Area | Support | Depth | Notes | | ------------------- | ------------------ | --------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ConditionExpression | Conditional writes | Supported | Common | evaluated against the live item under a row-level lock; commits atomically | | DescribeStream | Indexes + streams | Supported | Full surface | shard list + stream ARN | | GetRecords | Indexes + streams | Supported | Full surface | DynamoDB Streams served from an in-transaction outbox co-committed with each write | | GetShardIterator | Indexes + streams | Supported | Full surface | - | | ListStreams | Indexes + streams | Supported | Full surface | the streamed-table registry | | Query (GSI / LSI) | Indexes + streams | Supported | Most usage | indexes declared before deployment and maintained in the item transaction; queries use the index | | Query | Reads | Supported | Common | ordering, filters and pagination honored | | Scan | Reads | Supported | Common | streamed page by page | | DeleteItem | Single item | Supported | Common | - | | GetItem | Single item | Supported | Common | - | | PutItem | Single item | Supported | Common | items kept in native shape; numbers at exact decimal precision | | UpdateItem | Single item | Supported | Common | update expressions evaluated by the adapter | | TransactWriteItems | Transactions | Supported | Most usage | one serializable PostgreSQL transaction across tables; at most 100 actions and one operation per item; ClientRequestToken idempotency; stream-enabled tables excluded | #### PostgreSQL in the appliance Tensor9 deploys and operates the PostgreSQL store with the customer appliance, including disconnected deployments. Application requests use the same Rust DynamoDB-to-PostgreSQL implementation as the managed PostgreSQL target. Database access uses the configured connection credentials; Cloud SQL IAM authentication is specific to the Google-managed target. The customer supplies the infrastructure, and the deployment's storage, backup and recovery configuration determines availability. #### How it works Tensor9 runs a DynamoDB adapter beside your application in the customer's environment. It configures `AWS_ENDPOINT_URL_DYNAMODB` to send the AWS SDK's requests to the adapter over the local loopback network. Your SDK and application code stay unchanged. The adapter accepts DynamoDB requests and data types, returns DynamoDB responses and errors, and stores data in PostgreSQL. DynamoDB supports numbers with up to 38 significant digits, typed sets, conditional writes, secondary indexes, and atomic transactions. The adapter implements these using PostgreSQL storage and transactions. PostgreSQL supports multi-table transactions and synchronously updated indexes, but has different concurrency, capacity, and identifier limits. Unsupported requests return errors; the limitations below describe the differences. For base-table `Query` and `Scan` requests, the adapter compiles supported filters into parameterized SQL `WHERE` predicates over the JSONB item. Number comparisons cast stored decimal text to PostgreSQL `numeric`. An order-preserving key column supplies sort bounds, cursor position, and direction. The query uses a server-side cursor to fetch bounded batches until the requested `Limit` or 1 MiB of item data. Expressions that cannot be represented in SQL are evaluated in the adapter over the rows selected by the SQL predicate. Secondary-index queries use the separate path described below. For conditional writes, the adapter opens a transaction, locks the row with `SELECT ... FOR UPDATE`, evaluates the `ConditionExpression`, and writes within that transaction. This requires an extra round trip, but prevents a concurrent writer from changing the item between the check and write. A failed condition returns `ConditionalCheckFailed` and can return the item that failed it. Some unconditional writes skip the read: a top-level SET/REMOVE patch or integral counter add uses one statement when it has no condition, requests no returned item image, and affects no secondary index or stream.
Before: on AWS the application's DynamoDB SDK calls Amazon DynamoDB. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the DynamoDB API from a PostgreSQL database. Before: on AWS the application's DynamoDB SDK calls Amazon DynamoDB. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the DynamoDB API from a PostgreSQL database.

Your application uses the DynamoDB API through an adapter in the customer environment.

#### Architecture The adapter is a stateless Rust service. It parses DynamoDB expressions, translates requests into PostgreSQL operations, and returns DynamoDB responses. It connects to PostgreSQL through a connection pool using the customer's credentials. Durable state stays in PostgreSQL, so adapter instances can restart or scale out without coordinating local state. Stream-enabled tables have the separate single-writer restriction below. Each DynamoDB table has a PostgreSQL table named `ddb_` in one database. The `ddb_tables` registry records its generated physical name, key schema, billing mode, time-to-live (TTL) settings, global secondary indexes (GSIs), and stream settings. A write or query for a table absent from the registry returns `ResourceNotFoundException`; writes do not create tables implicitly. At runtime, `CreateTable` creates a PostgreSQL expression index for each declared GSI. Each stream-enabled table also has a `_strm` table that stores change records.
Architecture: the app's DynamoDB SDK calls a stateless Rust Tensor9 adapter, which serves the API from PostgreSQL. Postgres holds all state: each DynamoDB table is its own physical relation ddb_name in one database, an authoritative ddb_tables registry relation, a native per-table expression index per declared GSI, and a co-located _strm outbox relation per streamed table. Architecture: the app's DynamoDB SDK calls a stateless Rust Tensor9 adapter, which serves the API from PostgreSQL. Postgres holds all state: each DynamoDB table is its own physical relation ddb_name in one database, an authoritative ddb_tables registry relation, a native per-table expression index per declared GSI, and a co-located _strm outbox relation per streamed table.
A request is compiled in two stages: the DynamoDB API request becomes a backend-neutral logical plan, then a Postgres physical plan, whose reads execute as one SQL statement and whose writes run as one transaction. Postgres holds a relation per DynamoDB table plus a native expression index per declared secondary index. A request is compiled in two stages: the DynamoDB API request becomes a backend-neutral logical plan, then a Postgres physical plan, whose reads execute as one SQL statement and whose writes run as one transaction. Postgres holds a relation per DynamoDB table plus a native expression index per declared secondary index.

A stateless Rust adapter serves the DynamoDB API in front of Postgres; all durable state lives in one Postgres database, where each DynamoDB table is its own physical relation.

The adapter first builds a storage-independent request plan, then converts it to a Postgres-specific plan for execution.

#### Schemas A DynamoDB item occupies one PostgreSQL row with columns `pk TEXT`, `sk TEXT`, `okey TEXT` and `item JSONB`. The `PRIMARY KEY (pk, sk)` index serves `GetItem`. A separate index over `pk` and bytewise-collated `okey` serves ordered base-table `Query` requests. The `item JSONB` column preserves DynamoDB's typed attribute representation, including `{"N":"123.45"}`, `{"B":""}`, and typed sets. Numbers remain decimal strings, so a 38-digit amount or a Snowflake ID is not rounded through a floating-point conversion. Type tags preserve the distinction between numbers and numeric strings. Conditional writes use row locks, not a separate version column. Enabling TTL builds an expression index over the configured epoch attribute inside the JSONB item. A bounded, leader-elected reaper deletes eligible rows through that index. Expired items remain readable until physical deletion; reads do not filter on TTL.
A DynamoDB item becomes one row: pk TEXT, sk TEXT, okey TEXT and item JSONB, with PRIMARY KEY (pk, sk). The okey column preserves sort order. The whole item lives in JSONB in its DynamoDB wire form, so a number keeps its exact decimal text instead of becoming a float. TTL uses an expression index over the item, not a separate column. A DynamoDB item becomes one row: pk TEXT, sk TEXT, okey TEXT and item JSONB, with PRIMARY KEY (pk, sk). The okey column preserves sort order. The whole item lives in JSONB in its DynamoDB wire form, so a number keeps its exact decimal text instead of becoming a float. TTL uses an expression index over the item, not a separate column.

Each attribute keeps its DynamoDB type tag inside item JSONB , so \{"N":"123.45"} keeps its exact decimal string.

#### Where tables live Each DynamoDB table has its own PostgreSQL table, indexes and optional `_strm` change-record table. `CreateTable` creates those database objects and their registry entry; `DeleteTable` drops them. Storage, indexes and vacuum settings can be tuned per table. The adapter checks a 500-table guardrail per namespace when creating a physical DynamoDB table. Concurrent creates of different tables can exceed that guardrail; it is not a hard PostgreSQL quota. Plan database capacity and table placement for larger deployments. Provisioned instances continue to incur compute costs while idle.
Each declared DynamoDB table is its own physical relation inside one Postgres database, alongside the authoritative ddb_tables registry and each table's native expression indexes. The adapter applies a 500-table guardrail per namespace. Larger deployments need capacity and table-placement planning. Each declared DynamoDB table is its own physical relation inside one Postgres database, alongside the authoritative ddb_tables registry and each table's native expression indexes. The adapter applies a 500-table guardrail per namespace. Larger deployments need capacity and table-placement planning.

Each declared table is its own physical relation. CreateTable and DeleteTable create and drop PostgreSQL relations. The adapter applies a 500-table guardrail per namespace.

#### Conditional writes To evaluate a conditional write, the adapter locks the item's `(pk, sk)` row with `SELECT … FOR UPDATE`, checks the `ConditionExpression` against that row, then writes and commits. The lock prevents another writer from changing the item between the check and write. A false condition returns the non-retryable `ConditionalCheckFailed` error. A PostgreSQL serialization failure or lock timeout (`40001`/lock-not-available) instead returns a retryable throughput error, allowing the SDK to back off and retry. Concurrency failures are not reported as failed conditions. For `attribute_not_exists(pk)`, the PostgreSQL primary-key constraint prevents two concurrent requests from creating the same item. Only one insert can succeed for a given `(pk, sk)`.
A conditional write takes a row lock with SELECT for update, evaluates the DynamoDB condition inside the layer against the locked row, then writes and commits. A false condition returns a faithful ConditionalCheckFailed; a losing concurrency race returns a retryable conflict, never a spurious condition failure. Unique creation is enforced by the primary key. A conditional write takes a row lock with SELECT for update, evaluates the DynamoDB condition inside the layer against the locked row, then writes and commits. A false condition returns a faithful ConditionalCheckFailed; a losing concurrency race returns a retryable conflict, never a spurious condition failure. Unique creation is enforced by the primary key.

Failed conditions and PostgreSQL concurrency conflicts return different error types.

#### Secondary indexes On each write, the adapter adds two index fields to the item's JSONB. The partition-key field, `_idx__h`, normalizes equivalent values such as `1` and `1.0`. The sort-key field, `_idx__o`, preserves sort order and uses the base key to break ties. A PostgreSQL expression index over these fields locates matching partition keys. The adapter then evaluates sort-key conditions, ordering, pagination, and any `FilterExpression` over the retrieved items. The index fields and item commit in the same transaction, so GSI queries are strongly consistent. DynamoDB's own GSIs update asynchronously and are eventually consistent. Each index lookup is limited to 10,000 candidate items for one index partition-key value. Exceeding this limit returns an error; choose index keys with this limit in mind.
On each write the layer stamps a normalized-hash field and a collated-range field into the item's JSONB, and Postgres maintains a native expression index over them. A GSI query is an index seek by the stamped hash plus filtering in the adapter. The stamps co-commit with the item, so the index is strongly consistent. On each write the layer stamps a normalized-hash field and a collated-range field into the item's JSONB, and Postgres maintains a native expression index over them. A GSI query is an index seek by the stamped hash plus filtering in the adapter. The stamps co-commit with the item, so the index is strongly consistent.

A GSI uses a PostgreSQL expression index to find candidates, then the adapter filters and orders them. Index updates commit with the item, making GSI queries strongly consistent.

#### Transactions `TransactWriteItems` executes in one PostgreSQL `BEGIN…COMMIT` transaction at `SERIALIZABLE` isolation. A debit in one table and a credit in another either both commit or both fail, as in DynamoDB. Transactions enforce DynamoDB's 100-action limit and one-operation-per-item rule. The adapter records `ClientRequestToken` in the same transaction as the writes. Within DynamoDB's idempotency window, a retry with the same token returns the prior success without committing the writes again. The adapter acquires row locks in a consistent order to prevent opposite-direction transfers from deadlocking. Serialization or deadlock failures return retryable conflicts. `TransactWriteItems` is not supported on stream-enabled tables. A request that touches any such table returns `ValidationException` before execution, because the transaction path does not create the required stream records.
A TransactWriteItems spanning two tables, a debit in one relation and a credit in another, commits atomically in one BEGIN…COMMIT at SERIALIZABLE isolation, exactly like real DynamoDB. A TransactWriteItems spanning two tables, a debit in one relation and a credit in another, commits atomically in one BEGIN…COMMIT at SERIALIZABLE isolation, exactly like real DynamoDB.

TransactWriteItems runs in one native Postgres BEGIN…COMMIT at SERIALIZABLE, atomic and isolated across tables.

#### Limitations PostgreSQL limitations * **One PostgreSQL instance serves each table.** Heavy contention on one key can cause serialization failures and row-lock conflicts. The adapter returns retryable errors, but sustained contention can exhaust the SDK retry budget and appear as throttling. * **Stream-enabled tables allow one writer at a time.** An owner lease of about 90 seconds preserves stream record order. A second adapter instance attempting to write while that lease is active receives a retryable `ThrottlingException`. Throughput depends on the writer, database and request workload. Tables without streams support concurrent writers. * **`DescribeStream` lists at most 100 shards.** A stream with more shards is not fully enumerated in one call. * **Transactions on stream-enabled tables are unsupported.** A `TransactWriteItems` request touching one returns `ValidationException` because the transaction path does not create stream records. * **DynamoDB backup and global-table APIs are unsupported.** Use PostgreSQL backups, snapshots, point-in-time recovery (PITR), and replica/failover procedures. DynamoDB on-demand backup/restore, PITR, and global-table replication APIs are not implemented. * **Instances incur costs when idle.** Size and pay for PostgreSQL capacity ahead of demand. Larger table counts require more instances and table-placement planning. * **Index lookups have a 10,000-item limit.** A lookup that exceeds 10,000 candidate items for one index partition-key value returns an error. * **Plan for the table-count guardrail.** The adapter checks 500 physical DynamoDB tables per namespace. Larger deployments need database capacity and table-placement planning. * **Expired items remain readable until deletion.** A bounded, leader-elected reaper uses the TTL expression index to delete eligible rows asynchronously. Until physical deletion, GetItem, Query and Scan can return the expired item, and it continues to consume storage. #### Other considerations * **Stream records commit with the write.** The adapter stores each record in the table's `_strm` table within the same transaction as the item write. A record exists if and only if its write committed. The single-writer lease keeps stream sequence order aligned with commit order so a cursor can resume without skipping records. * **Use PostgreSQL recovery procedures.** Backups, PITR, snapshots, and replica/failover procedures operate on the PostgreSQL database. The adapter does not implement DynamoDB backup, PITR, or global-table APIs. * **Connection pooling.** The adapter uses pooled PostgreSQL connections. Deployments spread across many instances may need a separate pooler to keep total connections within each database's limit. * **Migrate and test one table at a time.** Separate PostgreSQL tables allow per-table export/import, dual-write, or backfill-and-switch procedures. Application code continues to use the DynamoDB API. * **Use PostgreSQL operational tools.** Monitor SQL activity, maintain indexes, vacuum tables, and run backups with your existing PostgreSQL tools and procedures. [Service Catalog](/service-adapters/catalog). # EFS Source: https://docs.tensor9.com/service-adapters/aws/databases-storage/efs AWS EFS. Presents an NFS file system that many instances and containers mount at once, growing and shrinking as files are written and deleted. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of EFS with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | EFS | Google Cloud | Azure | OCI | Private Kubernetes | | ---------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------- | | NFS protocol version · POSIX / RWX | NFS v4.1 | NFS v3 | NFS v4.1 (premium FileStorage) | NFS v3 | - | | Elastic capacity · growth model | elastic; grows per use, no size | provisioned per tier (1 TiB minimum) | provisioned quota (100 GiB minimum) | elastic; grows to 8 EiB, no size | - | | Throughput model · how throughput scales | bursting / elastic / provisioned modes | depends on tier and capacity (ENTERPRISE) | scales with the provisioned quota | scales automatically with used capacity | - | | Access-point POSIX identity | Yes - server-side (uid/gid + root dir) | Partial - client-side subpath + securityContext | Partial - client-side subpath + securityContext | Partial - server-enforced primary uid/gid; no isolated subdirectory | - | | Encryption at rest + CMK | Yes - KMS key | Yes - Cloud KMS CMEK | Yes - Key Vault customer-managed key | Yes - OCI Vault customer-managed key | - | | Encryption in transit | Yes - TLS mount | No - none on NFS v3; private VPC IP | Partial - TLS via AZNFS mount helper | Yes - TLS 1.2 via oci-fss-utils | - | | Availability topology · failure domain | regional; every AZ in the region | regional / multi-zone (ENTERPRISE) | single-zone (LRS); ZRS in select regions | - | - | | Lifecycle tiering + replication | Yes - IA/Archive + cross-region | No - no equivalent on the instance | No - no equivalent on the share | No - separate OCI data-protection configuration | - | | Backup / snapshots · data protection | AWS Backup (aws\_efs\_backup\_policy) | Filestore backups + snapshots | Share snapshots; separately scheduled backup | FSS snapshots + clones | - | | API coverage | full | high | high | high | minimal | | Availability topology · failure domain | regional; multiple availability zones | - | - | single availability domain | - | | Share protocol · POSIX / RWX | NFS v4.1 (POSIX, ReadWriteMany) | - | - | - | NFS (v4.x / v3) RWX PersistentVolume | | Management model · managed vs self-host | fully managed, elastic, multi-AZ | - | - | - | self-hosted on the cluster; your customer owns capacity, HA, and backup | | Capacity model · elastic vs backed | elastic; grows/shrinks automatically | - | - | - | bounded by the backing PV / block storage the NFS server sits on | | Access points · per-app entry | aws\_efs\_access\_point: enforced user/group IDs (uid/gid) + root directory per app | - | - | - | subPath / per-PVC directory + fsGroup on the pod securityContext | | Encryption at rest · backing storage | encrypted + kms\_key\_id (EFS at rest) | - | - | - | inherited from the backing block storage's encryption | | Backup · self-managed | aws\_efs\_backup\_policy → AWS Backup | - | - | - | customer-run backup of the NFS PV (e.g. Velero + volume snapshots) | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------------------------- | --------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | EFS control plane (CreateFileSystem / CreateMountTarget / PutBackupPolicy / …) | Control plane | Out of scope | Full surface | provisioning happens through your infrastructure-as-code at apply, not runtime EFS API calls | | Encryption at rest (encrypted + kms\_key\_id) | Data at rest | Supported | Common | encrypted at rest by default; a customer kms\_key\_id maps to a CMEK via Cloud KMS (kms\_key\_name) on the ENTERPRISE tier | | Encryption in transit | Data in transit | Out of scope | Most usage | NFS v3 on Filestore has no in-transit encryption; the instance is reached at a private IP on your VPC, so the traffic stays on your private network | | Lifecycle tiering + replication (transition\_to\_ia / replication\_configuration) | Data management | Out of scope | Full surface | EFS Infrequent-Access/Archive tiering + cross-region replication have no Filestore analog; Filestore has its own backups + snapshots | | NFS mount + POSIX file operations | Data plane | Supported | Common | the instance serves NFS v3; reads, writes, and POSIX permissions behave the same, ReadWriteMany from many pods at once (the common POSIX surface) | | Access point (aws\_efs\_access\_point) | Identity | Partial | Most usage | no per-instance access-point object; posix\_user uid/gid + root\_directory become a mount subpath + the pod securityContext (Filestore's NFSv3 export options have no rich per-export identity-squash config) | | Mount target (aws\_efs\_mount\_target) | Mount endpoint | Partial | Most usage | the per-AZ mount targets collapse to the instance's own IP address on your VPC; pods mount \:/\ over the private VPC IP | | File system provisioning (aws\_efs\_file\_system) | Provisioning | Supported | Common | compiles to a native google\_filestore\_instance on the ENTERPRISE tier (regional/multi-zone) with one NFS file\_shares export on your VPC network | #### How it works Your pods share files through a **Filestore ENTERPRISE instance**. Tensor9 maps `aws_efs_file_system` to `google_filestore_instance`, with one `file_shares` NFS export on the customer VPC. Multiple pods mount the export directly. Google operates the storage. This mapping uses NFS v3, a regional instance, and provisioned capacity. Application reads and writes go directly to Filestore.
The aws_efs_file_system compiles to a native Filestore ENTERPRISE instance with one NFS export on your VPC network, mounted directly, with no proxy in the data path. The aws_efs_file_system compiles to a native Filestore ENTERPRISE instance with one NFS export on your VPC network, mounted directly, with no proxy in the data path.

The aws\_efs\_file\_system compiles to a native Filestore ENTERPRISE instance with one NFS export on your VPC network, mounted directly, with no proxy in the data path.

#### Architecture EFS uses a mount target in each availability zone. Filestore provides one instance IP address, exposed as `networks[].ip_addresses[0]`. Pods mount `:/`. The instance IP is private and reached through the customer VPC. Application mounts use that address instead of separate EFS mount-target addresses.
EFS's per-AZ mount targets collapse to the Filestore instance's own IP on your VPC network; every pod mounts the one instance IP. EFS's per-AZ mount targets collapse to the Filestore instance's own IP on your VPC network; every pod mounts the one instance IP.

EFS's per-AZ mount targets collapse to the Filestore instance's own IP on your VPC network; every pod mounts the one instance IP.

#### NFS protocol and locking This mapping uses **NFS v3**; EFS uses NFS v4.1. Shared file access, ownership, and POSIX mode bits remain available. Filestore also offers NFS v4.1, but that is a different configuration from the one described here. NFS v3 handles locks through Network Lock Manager (`NLM`); NFS v4.1 has integrated lease-based locking and different client-recovery behavior. Test workloads that rely on locks or reconnect after node failure. NFS ACLs and delegations are not lost EFS capabilities: EFS itself does not support them.
The mapped NFS v3 export preserves shared POSIX access but uses different locking and client-recovery behavior from EFS. The mapped NFS v3 export preserves shared POSIX access but uses different locking and client-recovery behavior from EFS.

The mapped NFS v3 export preserves shared POSIX access but uses different locking and client-recovery behavior from EFS.

#### Provisioned capacity The mapped ENTERPRISE instance is provisioned from **1 TiB (1024 GiB)** and scales in steps up to 10 TiB. Increase provisioned capacity as data grows; EFS adjusts capacity automatically. Throughput and IOPS depend on the selected tier and provisioned capacity. Size the instance for storage and performance together; EFS's separate throughput modes do not map directly to this configuration.
EFS grows per use with no size; a Filestore ENTERPRISE instance is provisioned from a 1 TiB minimum up to 10 TiB, and its throughput and IOPS scale with the tier and capacity. EFS grows per use with no size; a Filestore ENTERPRISE instance is provisioned from a 1 TiB minimum up to 10 TiB, and its throughput and IOPS scale with the tier and capacity.

EFS grows per use with no size; a Filestore ENTERPRISE instance is provisioned from a 1 TiB minimum up to 10 TiB, and its throughput and IOPS scale with the tier and capacity.

#### Regional availability: the ENTERPRISE match Filestore ENTERPRISE replicates data across multiple zones and has a published **99.99% availability SLA**. This provides regional storage for applications that use EFS Standard's multi-zone availability. The mapping selects ENTERPRISE for regional availability. BASIC and ZONAL instances are single-zone configurations with different outage coverage.
EFS Standard is regional across every AZ; the Filestore ENTERPRISE tier is regional too, replicated across zones in the region with a 99.99% availability SLA. EFS Standard is regional across every AZ; the Filestore ENTERPRISE tier is regional too, replicated across zones in the region with a 99.99% availability SLA.

EFS Standard is regional across every AZ; the Filestore ENTERPRISE tier is regional too, replicated across zones in the region with a 99.99% availability SLA.

#### Access points and POSIX identity An EFS access point enforces a user/group identity and root directory on the server. Filestore has no matching access-point resource. The application mounts `root_directory` as a subpath, while pod `securityContext` settings such as `fsGroup` and `runAsUser` set its identity. Pod and mount configuration control application identity in this mapping. They do not provide the same server-enforced per-application boundary as an EFS access point. Review permissions and mount access when several applications share an export.
Filestore has no access-point object; the access point's posix identity and root directory become a mount subpath plus the pod's securityContext, a weaker mapping than a managed object. Filestore has no access-point object; the access point's posix identity and root directory become a mount subpath plus the pod's securityContext, a weaker mapping than a managed object.

Filestore has no access-point object; the access point's posix identity and root directory become a mount subpath plus the pod's securityContext, a weaker mapping than a managed object.

#### Encryption and network reach Filestore encrypts stored data by default. An EFS customer-managed `kms_key_id` maps to a **Cloud KMS** key through `kms_key_name` on the ENTERPRISE instance. The mapped NFS v3 export does not encrypt traffic. It uses a private VPC address. Filestore's optional NFS v4.1 configuration supports Kerberos encryption, but requires separate authentication and mount configuration.
At-rest encryption and a customer key map to Cloud KMS on ENTERPRISE; but NFS v3 has no encryption in transit, so the instance is reached at a private VPC IP. At-rest encryption and a customer key map to Cloud KMS on ENTERPRISE; but NFS v3 has no encryption in transit, so the instance is reached at a private VPC IP.

At-rest encryption and a customer key map to Cloud KMS on ENTERPRISE; but NFS v3 has no encryption in transit, so the instance is reached at a private VPC IP.

#### Limitations Filestore supplies shared POSIX access, regional storage, and customer-managed encryption at rest. The following differences affect protocol behavior, capacity, and application permissions. △ Where AWS EFS and GCP Filestore diverge * **NFS v3 locking.** The mapped export uses separate NLM lock management. Test locking and client recovery during migration; shared POSIX access remains available. * **Capacity is provisioned from a 1 TiB minimum.** ENTERPRISE is provisioned at a 1 TiB (1024 GiB) minimum and bills on provisioned capacity where EFS is provision-free; growing means raising the capacity in fixed steps to 10 TiB, and there is no separate throughput mode, since throughput scales with the tier and capacity. * **No encryption in transit on NFS v3.** The export does not encrypt on the wire the way EFS's TLS mount does; the instance is reached at a private IP on your VPC so the traffic stays on your network instead. * **No managed access-point object, and NFS v3 export controls are limited.** The access point's posix identity and root directory become a mount subpath plus the pod's `securityContext`, and Filestore's NFS v3 export has no rich per-export identity-squash configuration, so this is a weaker mapping than a managed object the client must go through. * **Per-AZ mount targets collapse to one instance IP.** A mount that expected a specific per-AZ mount IP resolves to the instance's own IP on your VPC (`networks[].ip_addresses[0]`); the file tree remains unchanged. * **EFS lifecycle tiering and replication have no Filestore analog.** EFS's Infrequent-Access / Archive lifecycle policies and cross-region replication are AWS-managed features; the ENTERPRISE instance stores on its single provisioned tier. Filestore has its own backups and snapshots, managed separately. #### Other considerations Stop writes while copying EFS files to the new export. Preserve file ownership, permissions, and paths, then test locks and client reconnection before switching application mounts. ## On Azure | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------------------------- | --------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | EFS control plane (CreateFileSystem / CreateMountTarget / PutBackupPolicy / …) | Control plane | Out of scope | Full surface | provisioning happens through your infrastructure-as-code at apply, not runtime EFS API calls | | Encryption at rest (encrypted + kms\_key\_id) | Data at rest | Supported | Common | encrypted at rest by default; a customer kms\_key\_id maps to a storage-account customer-managed key in Key Vault | | Encryption in transit | Data in transit | Partial | Most usage | Azure Files supports TLS for NFS through the AZNFS mount helper; clients and the storage-account encryption setting must be configured | | Lifecycle tiering + replication (transition\_to\_ia / replication\_configuration) | Data management | Out of scope | Full surface | EFS Infrequent-Access/Archive tiering + cross-region replication have no premium-share analog; schedule share snapshots separately | | NFS v4.1 mount + POSIX file operations | Data plane | Supported | Common | the share speaks the same NFS v4.1 dialect as EFS: reads, writes, locking, rename, and POSIX permissions all behave the same, ReadWriteMany from many pods at once | | Access point (aws\_efs\_access\_point) | Identity | Partial | Most usage | no per-share access-point object; posix\_user uid/gid + root\_directory become a mount subpath + the pod securityContext (fsGroup/runAsUser), with the export's root-squash policy governing client root | | Mount target (aws\_efs\_mount\_target) | Mount endpoint | Partial | Most usage | the per-AZ mount targets collapse to one share endpoint (\.file.core.windows.net) reached over a private endpoint in your VNet; a per-AZ mount IP resolves to the one endpoint | | File system provisioning (aws\_efs\_file\_system) | Provisioning | Supported | Common | compiles to a native azurerm\_storage\_share (enabled\_protocol=NFS) on a Premium FileStorage account of the equivalent quota | #### How it works Your pods share files through an **Azure Files NFS v4.1 share**. Tensor9 maps `aws_efs_file_system` to `azurerm_storage_share` with `enabled_protocol = "NFS"` on a Premium FileStorage account. Multiple pods mount the share and use ordinary POSIX file operations. Tensor9 provisions the share; applications access it directly. Microsoft operates the storage, and the share determines read and write throughput. Capacity follows a provisioned quota, and the customer chooses the redundancy configuration.
The aws_efs_file_system compiles to a native Azure Files NFS v4.1 share on a Premium FileStorage account: the same NFS v4.1 protocol EFS speaks, mounted directly, no proxy in the data path. The aws_efs_file_system compiles to a native Azure Files NFS v4.1 share on a Premium FileStorage account: the same NFS v4.1 protocol EFS speaks, mounted directly, no proxy in the data path.

The aws\_efs\_file\_system compiles to a native Azure Files NFS v4.1 share on a Premium FileStorage account: the same NFS v4.1 protocol EFS speaks, mounted directly, no proxy in the data path.

#### Architecture EFS provides a mount target in each availability zone. Azure Files uses one share endpoint, `.file.core.windows.net`. Every application mount resolves to that endpoint. The share is reached through a private endpoint in the customer virtual network. Network rules deny public access. For encrypted NFS traffic, configure the **AZNFS mount helper** and the storage-account encryption setting; the private endpoint alone does not encrypt a connection.
EFS's per-AZ mount targets collapse to one storage account plus one share, reached at a single endpoint over a private endpoint in your VNet. EFS's per-AZ mount targets collapse to one storage account plus one share, reached at a single endpoint over a private endpoint in your VNet.

EFS's per-AZ mount targets collapse to one storage account plus one share, reached at a single endpoint over a private endpoint in your VNet.

#### Provisioned capacity EFS grows and shrinks as files change. This mapping uses an Azure premium share with a provisioned quota, from **100 GiB to 100 TiB**. Increase the quota as data grows. Under this provisioned-capacity model, baseline IOPS and throughput scale with the quota. Size it for both data volume and required performance. EFS's separate bursting, elastic, and provisioned throughput modes do not map directly to this configuration.
EFS grows per use with no size; an Azure premium share is provisioned to a quota, and its IOPS and throughput scale with that quota. EFS grows per use with no size; an Azure premium share is provisioned to a quota, and its IOPS and throughput scale with that quota.

EFS grows per use with no size; an Azure premium share is provisioned to a quota, and its IOPS and throughput scale with that quota.

#### Access points and POSIX identity An EFS access point enforces a user/group identity and root directory on the server. Azure Files has no matching access-point resource. The application mounts `root_directory` as a subpath, while pod `securityContext` settings such as `fsGroup` and `runAsUser` set its identity. The share supports root-squash settings that control how a client root identity is treated. These settings and pod configuration do not create EFS's per-application access-point boundary. Review the mount configuration for applications that share a volume but require separate access.
Azure Files has no access-point object; the access point's posix identity and root directory become a mount subpath plus the pod's securityContext, with root-squash governing client root. Azure Files has no access-point object; the access point's posix identity and root directory become a mount subpath plus the pod's securityContext, with root-squash governing client root.

Azure Files has no access-point object; the access point's posix identity and root directory become a mount subpath plus the pod's securityContext, with root-squash governing client root.

#### Encryption and network reach Azure Files encrypts stored data by default. An EFS customer-managed `kms_key_id` maps to a storage-account customer-managed key in **Azure Key Vault**. Azure Files supports NFS encryption in transit through TLS. Install and configure the **AZNFS mount helper** on clients and set the storage-account requirement for NFS encryption. Keep the private endpoint and network restrictions as well.
Azure Files uses Key Vault for customer-managed encryption at rest and the AZNFS helper for TLS mounts. Azure Files uses Key Vault for customer-managed encryption at rest and the AZNFS helper for TLS mounts.

Azure Files uses Key Vault for customer-managed encryption at rest and the AZNFS helper for TLS mounts.

#### Availability and durability Locally redundant storage (LRS) keeps three copies within one zone. Zone-redundant storage (ZRS) distributes copies across zones and is available for premium shares in supported regions. Choose a region with premium ZRS when the workload needs protection from a zone outage. An LRS share protects against disk and node failure within its zone; recovery from a whole-zone outage requires a separate plan.
EFS Standard is regional across every AZ; a premium share is single-zone (LRS) by default, with zone-redundant storage available for premium shares only in select regions. EFS Standard is regional across every AZ; a premium share is single-zone (LRS) by default, with zone-redundant storage available for premium shares only in select regions.

EFS Standard is regional across every AZ; a premium share is single-zone (LRS) by default, with zone-redundant storage available for premium shares only in select regions.

#### Limitations Azure Files provides shared POSIX access over NFS v4.1 and customer-managed encryption at rest. The following differences affect storage sizing, access controls, and recovery. △ Where AWS EFS and Azure Files diverge * **Capacity is provisioned, not provision-free.** A premium share has a quota (100 GiB minimum) and bills on provisioned size where EFS bills only for stored bytes; growing means raising the quota, and there is no separate throughput mode; baseline throughput scales with the quota. * **TLS client configuration.** NFS encryption in transit requires the AZNFS mount helper and the storage-account encryption setting. * **Single-zone (LRS) by default.** A premium share is locally redundant within one zone; EFS Standard is regional across every AZ. Zone-redundant (ZRS) premium shares exist only in select regions; pick one to match EFS's multi-zone durability. * **No managed access-point object.** The access point's per-app posix identity and root directory become a mount subpath plus the pod's `securityContext` (`fsGroup` / `runAsUser`), enforced by how the pod mounts rather than a share-side boundary the client cannot bypass. * **Per-AZ mount targets collapse to one share endpoint.** A mount that expected a specific per-AZ mount IP resolves to the single `.file.core.windows.net` endpoint; the file tree remains unchanged. * **EFS lifecycle tiering and replication have no share analog.** EFS's Infrequent-Access / Archive lifecycle policies and cross-region replication are AWS-managed features; the premium share stores on its single provisioned tier and is not cross-region replicated by this mapping. NFS shares support snapshots, but Azure Backup does not cover them; schedule snapshots and additional backup separately. #### Other considerations Stop writes while copying EFS files to the new share. Preserve file ownership, permissions, and paths, then test application mounts before switching traffic. ## On OCI | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------------------------- | --------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | EFS control plane (CreateFileSystem / CreateMountTarget / PutBackupPolicy / …) | Control plane | Out of scope | Full surface | provisioning happens through your infrastructure-as-code at apply, not runtime EFS API calls | | Encryption at rest (encrypted + kms\_key\_id) | Data at rest | Supported | Common | encrypted at rest by default; a customer kms\_key\_id maps to a customer-managed key via OCI Vault (kms\_key\_id on the file system) | | Encryption in transit | Data in transit | Partial | Most usage | OCI FSS supports in-transit encryption for NFS mounts over TLS 1.2 (the oci-fss-utils client package); the mount target also lives on a private subnet in your VCN | | Lifecycle tiering + replication (transition\_to\_ia / replication\_configuration) | Data management | Out of scope | Full surface | EFS lifecycle tiers and replication\_configuration are not mapped; OCI has separately configured snapshots, clones, and asynchronous file-system replication | | NFS mount + POSIX file operations | Data plane | Supported | Common | the export serves NFS v3; reads, writes, and POSIX permissions behave the same, ReadWriteMany from many instances at once (the common POSIX surface) | | Access point (aws\_efs\_access\_point) | Identity | Partial | Most usage | export options enforce the primary uid/gid through identity\_squash=ALL and anonymous\_uid/gid; an export path names the file-system root and does not reproduce an access point's isolated subdirectory | | Mount target (aws\_efs\_mount\_target) | Mount endpoint | Supported | Most usage | an oci\_file\_storage\_mount\_target is a VNIC in a subnet in an availability domain, serving the network-mount role of EFS mount targets; pods mount the mount-target IP + export path | | File system provisioning (aws\_efs\_file\_system) | Provisioning | Supported | Common | compiles to a native oci\_file\_storage\_file\_system + oci\_file\_storage\_mount\_target + oci\_file\_storage\_export, an elastic FSS file system with a mount endpoint | #### How it works Your application shares files through an **OCI File Storage export**. Tensor9 creates an `oci_file_storage_file_system`, an `oci_file_storage_mount_target`, and an `oci_file_storage_export`. Pods mount the export directly at the mount-target IP address. Oracle operates the storage. The mapped file system grows as data is written, serves NFS v3, and resides in one availability domain in the customer virtual cloud network (VCN).
The aws_efs_file_system compiles to a native OCI File Storage file system, mount target, and export. The pods mount the export directly at the mount-target IP, with nothing of Tensor9 in the data path. The aws_efs_file_system compiles to a native OCI File Storage file system, mount target, and export. The pods mount the export directly at the mount-target IP, with nothing of Tensor9 in the data path.

The aws\_efs\_file\_system compiles to a native OCI File Storage file system, mount target, and export. The pods mount the export directly at the mount-target IP, with nothing of Tensor9 in the data path.

#### Architecture The file system holds the data. The mount target supplies a virtual network interface (VNIC) and IP address in a subnet. The export publishes the file system at a `path` and applies its `export_options`. Applications mount the export through the mount-target IP address. The mount target determines its availability domain, and export options control how client identities are handled.
EFS's file system plus per-AZ mount targets become three OCI resources in your VCN: a file system, a mount-target VNIC in one availability domain, and an export that publishes it. EFS's file system plus per-AZ mount targets become three OCI resources in your VCN: a file system, a mount-target VNIC in one availability domain, and an export that publishes it.

EFS's file system plus per-AZ mount targets become three OCI resources in your VCN: a file system, a mount-target VNIC in one availability domain, and an export that publishes it.

#### Elastic capacity OCI File Storage grows as files are written, up to **8 EiB**, and bills for stored data. No capacity quota must be reserved before applications write. This preserves the automatic capacity growth used with EFS. The Azure Files and Filestore configurations described here require provisioned capacity.
EFS grows per use with no size; OCI File Storage is elastic the same way, to a maximum of 8 EiB, with no capacity to reserve in advance. EFS grows per use with no size; OCI File Storage is elastic the same way, to a maximum of 8 EiB, with no capacity to reserve in advance.

EFS grows per use with no size; OCI File Storage is elastic the same way, to a maximum of 8 EiB, with no capacity to reserve in advance.

#### Access points and export options The mapping sets `identity_squash = "ALL"` and takes `anonymous_uid` and `anonymous_gid` from the EFS access point's `posix_user`. The emitted export `path` identifies the file system; it does not select a directory within it. The export maps client user/group IDs to the configured anonymous\_uid and anonymous\_gid for file operations. This preserves the enforced primary identity used by the EFS access point. It does not reproduce the access point's restricted root directory. A client-side subdirectory mount does not provide that isolation; use separate file systems or an appropriate export layout when applications must be separated.
OCI export options enforce primary user/group IDs. The export path names the file-system root; it does not isolate an application subdirectory. OCI export options enforce primary user/group IDs. The export path names the file-system root; it does not isolate an application subdirectory.

OCI export options enforce primary user/group IDs. The export path names the file-system root; it does not isolate an application subdirectory.

#### NFS protocol and locking The mapped OCI export uses **NFS v3**, while EFS uses NFS v4.1. Applications retain shared POSIX file access, ownership, and permission bits. NFS v3 uses Network Lock Manager (`NLM`) for byte-range locks. NFS v4.1 integrates lease-based locking and handles client recovery differently. Test lock use and reconnection after node failure. EFS itself does not support NFS ACLs or delegations.
The mapped NFS v3 export preserves shared POSIX access but uses different locking and client-recovery behavior from EFS. The mapped NFS v3 export preserves shared POSIX access but uses different locking and client-recovery behavior from EFS.

The mapped NFS v3 export preserves shared POSIX access but uses different locking and client-recovery behavior from EFS.

#### Availability within one domain OCI File Storage replicates data within one availability domain. EFS Standard distributes data across availability zones in a region. The mapped OCI file system protects against storage-node and disk failures within its availability domain. Plan recovery separately for an outage of the whole domain.
EFS Standard is regional across availability zones; an FSS file system and its mount target live in one availability domain, replicated within the AD but not region-wide. EFS Standard is regional across availability zones; an FSS file system and its mount target live in one availability domain, replicated within the AD but not region-wide.

EFS Standard is regional across availability zones; an FSS file system and its mount target live in one availability domain, replicated within the AD but not region-wide.

#### Encryption and network reach OCI File Storage encrypts stored data by default. An EFS customer-managed `kms_key_id` maps to a **customer-managed key in OCI Vault** through the file system's `kms_key_id`. TLS 1.2 encryption for NFS mounts uses the `oci-fss-utils` client package. The mount target is also reached through a private subnet in the customer VCN. EFS lifecycle tiers and replication settings are not mapped. OCI provides snapshots, clones, and asynchronous file-system replication to another availability domain or region as separate configuration.
OCI Vault supplies the customer-managed key. Configure oci-fss-utils for TLS on the NFS client. OCI Vault supplies the customer-managed key. Configure oci-fss-utils for TLS on the NFS client.

OCI Vault supplies the customer-managed key. Configure oci-fss-utils for TLS on the NFS client.

#### Limitations OCI File Storage provides elastic capacity, shared POSIX access, export identity settings, and customer-managed encryption at rest. The following differences affect protocol behavior and availability. △ Where AWS EFS and OCI File Storage diverge * **Access-point directory isolation.** Export options enforce primary user/group IDs, but an export path does not restrict clients to a subdirectory. * **NFS v3 locking.** The mapped export uses separate NLM lock management. Test locking and client recovery during migration; shared POSIX access remains available. * **One availability domain, not regional.** An FSS file system and its mount target live in a single availability domain, replicated within it; EFS Standard is regional across availability zones, so a whole-AD loss is not covered the way EFS covers a whole-AZ loss. Weigh this where the availability target is strict. * **In-transit encryption is opt-in via a client package.** FSS supports NFS-over-TLS 1.2 only through the `oci-fss-utils` package on the client; without it, the mount relies on the mount target's private subnet for isolation, not on-the-wire encryption. * **Data protection requires OCI configuration.** EFS IA/Archive lifecycle tiers are not reproduced. OCI snapshots, clones, and asynchronous replication are configured separately; verify the destination and recovery procedure for an availability-domain or regional outage. #### Other considerations Stop writes while copying EFS files to the new export. Preserve file ownership, permissions, and paths, then test root and non-root client access before switching mounts. ## On Private Kubernetes #### Shared file access A Kubernetes ReadWriteMany (RWX) NFS volume lets multiple pods access the same files, matching the shared POSIX access pattern used with EFS. The application mounts the volume and performs file operations through NFS; no Tensor9 adapter processes those reads and writes. #### Storage configuration The customer supplies the NFS server or RWX-capable storage provisioner and PersistentVolume. A private Kubernetes cluster does not have one universal managed file service or guaranteed RWX StorageClass, so this mapping uses the cluster's storage arrangement rather than creating a separate file server for each EFS resource. #### Capacity and availability The customer manages capacity, high availability, backups, and recovery. Storage size follows the backing volume, and throughput depends on the NFS server and its storage. Encryption at rest comes from the backing storage rather than an EFS-managed key. #### Migration Provision the RWX storage, configure the application mount, and copy the data during a period when writes are stopped. Preserve file ownership, permissions, and paths, then test access from every pod that shares the volume. AWS EFS management APIs are not served by this mapping. [Service Catalog](/service-adapters/catalog). # ElastiCache Serverless (Valkey/Redis) Source: https://docs.tensor9.com/service-adapters/aws/databases-storage/elasticache-serverless-valkey-redis AWS ElastiCache Serverless (Valkey/Redis). A Valkey or Redis cache with no nodes to size, scaling capacity on demand and billing on data stored and requests. ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## Mapping and limitations ElastiCache Serverless maps to Redis/Valkey-compatible target caches. The application retains the cache wire protocol, but a compatible endpoint does not reproduce AWS serverless capacity scaling or billing. Configure target capacity, availability and connection limits explicitly; a Kubernetes Valkey deployment is a provisioned cache that you operate. These target services provide the mapping described above, within its stated limits. | Cloud | Target service | | ------------------ | ------------------------- | | Google Cloud | Memorystore for Redis | | Azure | Azure Managed Redis | | OCI | OCI Cache | | Private Kubernetes | Bitnami Valkey, Dragonfly | [Service Catalog](/service-adapters/catalog). # ElastiCache (Valkey/Redis) Source: https://docs.tensor9.com/service-adapters/aws/databases-storage/elasticache-valkey-redis AWS ElastiCache (Valkey/Redis). A managed Valkey or Redis cache running on node types you choose, with primaries and replicas placed across availability zones. ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## Mapping and limitations Provisioned ElastiCache maps to Redis/Valkey-compatible caches: Memorystore on Google Cloud, Azure Cache for Redis, OCI Cache or Valkey on Kubernetes. Applications use the cache's native wire protocol. Review engine version, cluster topology, persistence and failover settings on the selected target; AWS node types, parameter groups and maintenance controls are provider-specific. These target services provide the mapping described above, within its stated limits. | Cloud | Target service | | ------------------ | ------------------------- | | Google Cloud | Memorystore for Redis | | Azure | Azure Managed Redis | | OCI | OCI Cache | | Private Kubernetes | Bitnami Valkey, Dragonfly | [Service Catalog](/service-adapters/catalog). # OpenSearch Domain Source: https://docs.tensor9.com/service-adapters/aws/databases-storage/opensearch-domain AWS OpenSearch Domain. A managed OpenSearch cluster that indexes JSON documents for full-text search and aggregations, reached over a REST API and OpenSearch Dashboards. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud, Azure, and Private Kubernetes](#on-google-cloud-azure-and-private-kubernetes) * [Via OpenSearch](#via-opensearch) * [On Google Cloud](#on-google-cloud) * [Via Elastic Cloud (managed)](#via-elastic-cloud-managed) * [On Azure](#on-azure) * [Via Azure Native Elastic](#via-azure-native-elastic) * [On OCI](#on-oci) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of OpenSearch Domain with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | OpenSearch Domain | Google Cloud, Azure, and Private Kubernetes · OpenSearch | Google Cloud · Elastic Cloud (managed) | Azure · Azure Native Elastic | OCI | | ------------------------------------------------ | ------------------------------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | OpenSearch REST API | OpenSearch / Elasticsearch REST | native OpenSearch API; match engine and client versions | adapter-translated OpenSearch requests on Elasticsearch 8.x | adapter-translated OpenSearch requests on Elasticsearch 8.x | native OpenSearch API; match engine and client versions | | Engine + version | OpenSearch (AWS-managed domain) | OpenSearch (self-hosted; OpenSearch Operator / Helm chart) | Elasticsearch 8.x (Elastic Cloud Hosted) | Elasticsearch 8.x (Elastic-managed via Azure Native ISV Service) | OpenSearch 2.11 / 2.15 / 2.18 / 2.19.1 / 3.2 / 3.6 (OCI-managed) | | Durability / HA owner · who operates the cluster | AWS-managed domain | you (self-managed on Kubernetes) | Elastic (Elastic Cloud) | Elastic / Microsoft (Azure Native Elastic) | Oracle (OCI Search with OpenSearch) | | Cluster topology / sharding | data + dedicated-master + UltraWarm tiers | nodePools you define (data + dedicated cluster\_manager + optional warm role) | ec\_deployment full topology: hot/warm/cold/frozen tiers, size + zone\_count (1-3) | resource without topology fields - size in the Elastic console (default 16 GB / 560 GB / 2 AZ) | mandatory 3-tier: data + leader (master) + Dashboard nodes; optional coordinator / ML / search tiers | | Replication / HA · replicas + failover | Multi-AZ (up to 99.99% with standby) | index replicas + shard allocation awareness you configure; no SLA | 99.5 / 99.9 / 99.95% by AZ count (1 / 2 / 3 zones) | governed by the Elastic Cloud SLA (99.5 / 99.9 / 99.95% by AZ count) | OCI spreads data nodes across availability domains; no published availability SLA | | Storage | EBS gp3 (to 36 TiB/node) / 3 PiB per domain | PersistentVolume per nodePool (diskSize / storageClass); IOPS is StorageClass-derived | disk bundled at a fixed disk:RAM ratio per tier | included in the plan (no topology field on the resource) | per-node data\_node\_storage\_gb (FLEX sizing; up to \~314 TB/node, OCPU 1-32, memory 20-1024 GB) | | Encryption at rest | encrypt\_at\_rest + KMS CMK | the PersistentVolume / StorageClass encryption (no AWS-KMS CMK analog) | Elastic-managed (CMK is an EC Enterprise feature) | Elastic-managed | service-managed encryption; no per-domain AWS KMS customer-key setting in this mapping | | Security / FGAC | fine-grained access control + IAM policies | the Security plugin, config + internal\_users you own | Elastic native realm (security.enabled), not the OpenSearch Security plugin | Elastic native realm / API keys / SSO, not the OpenSearch Security plugin | OpenSearch Security plugin - security\_mode ENFORCING + master user; SAML/LDAP/OIDC; DLS/FLS (2.3+) | | Plugins (k-NN / SQL / ISM / Alerting / ML) | bundled \_plugins/\* (Apache-2.0) | the full bundled \_plugins/\* set (k-NN, SQL/PPL, ISM, Alerting, AD, ML Commons) | ES-native; the OpenSearch \_plugins/\* APIs diverge | ES-native (\_ml, \_watcher, dense\_vector); the OpenSearch \_plugins/\* APIs diverge (k-NN/SQL adapter-served, PPL/security out of scope) | the full supported set: k-NN, SQL/PPL, ISM, Alerting, AD, ML Commons, Neural Search, Security Analytics | | Snapshots / backups | managed automated + manual \_snapshot | self-managed: register a snapshot repository + schedule OpenSearch Snapshot Management | Elastic SLM (snapshot.retention) | Elastic SLM (console) | managed daily backups (14-day, non-configurable) + manual Snapshot API to Object Storage | | API coverage | full | high | partial | partial | high | ## On Google Cloud, Azure, and Private Kubernetes ### Via OpenSearch | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------------ | ------------------ | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Cluster health / stats / \_cat APIs | Cluster | Supported | Most usage | - | | Tasks API (\_tasks) | Cluster | Supported | Full surface | drives async reindex / update-by-query | | Bulk (\_bulk) | Documents | Supported | Common | batch index/update/delete | | Delete document | Documents | Supported | Common | - | | Get document (GET /\{index}/\_doc/\{id}) | Documents | Supported | Common | - | | Index document (PUT/POST /\{index}/\_doc) | Documents | Supported | Common | - | | Multi-get (\_mget) | Documents | Supported | Most usage | - | | Update document (\_update) | Documents | Supported | Common | - | | Snapshots / backups (\_snapshot) | Durability | Partial | Full surface | the \_snapshot API is native, but you register the repository and schedule snapshots with OpenSearch Snapshot Management that AWS managed for you | | Aliases (\_aliases) | Index management | Supported | Most usage | - | | Analyze (\_analyze) | Index management | Supported | Full surface | tokenizer/analyzer testing | | Create / Delete / Get index | Index management | Supported | Common | - | | Data streams (\_data\_stream) | Index management | Supported | Full surface | - | | Open / Close index | Index management | Supported | Full surface | - | | Put / Get mapping | Index management | Supported | Common | - | | Refresh / Flush / Forcemerge | Index management | Supported | Most usage | - | | Rollover / Shrink / Split / Clone | Index management | Supported | Full surface | - | | Update / Get settings | Index management | Supported | Most usage | - | | Index & component templates (\_index\_template / \_component\_template) | Ingest & templates | Supported | Full surface | - | | Ingest pipelines (\_ingest/pipeline: put / get / delete / \_simulate) | Ingest & templates | Supported | Full surface | - | | License / X-Pack (\_license, \_xpack) | Licensing | Out of scope | Full surface | Elasticsearch licensing endpoints \_license and \_xpack are not OpenSearch APIs. | | Index State Management (\_plugins/\_ism) | Lifecycle | Supported | Full surface | Index State Management (\_plugins/\_ism) is bundled | | Alerting / anomaly detection / ML Commons (\_plugins/\*) | Observability & ML | Supported | Full surface | Alerting, Anomaly Detection, ML Commons, Notifications, Observability all ship in the distribution | | Notifications / observability (\_plugins/\_notifications, \_plugins/\_observability) | Observability & ML | Supported | Full surface | Alerting, Anomaly Detection, ML Commons, Notifications, Observability all ship in the distribution | | PPL (\_plugins/\_ppl) | Query languages | Supported | Full surface | PPL (\_plugins/\_ppl) is bundled | | SQL (\_plugins/\_sql) | Query languages | Supported | Full surface | the SQL plugin (\_plugins/\_sql) is bundled | | Stored scripts & Painless (\_scripts, \_scripts/painless/\_execute) | Scripting | Supported | Full surface | - | | Aggregations | Search | Supported | Common | metrics + bucket aggregations in \_search | | Count / Explain / Suggest | Search | Supported | Most usage | - | | Field capabilities (\_field\_caps) | Search | Supported | Full surface | - | | Multi-search (\_msearch) | Search | Supported | Most usage | - | | Point-in-time (\_search/point\_in\_time) | Search | Supported | Most usage | native - same \_search/point\_in\_time endpoint | | Reindex / Update-by-query / Delete-by-query | Search | Supported | Full surface | - | | Scroll (\_search/scroll) | Search | Supported | Most usage | deep pagination | | Search (\_search + query DSL) | Search | Supported | Common | - | | Search templates (\_search/template, \_render/template) | Search | Supported | Full surface | - | | Term vectors (\_termvectors / \_mtermvectors) | Search | Supported | Full surface | - | | Security plugin / fine-grained access control (\_plugins/\_security) | Security | Partial | Full surface | the Security plugin runs natively (the full plugin API, more than a managed target exposes), but you now own the roles/mappings/internal-users config that AWS FGAC managed, the same ownership shift as snapshots | | Vector search / k-NN (knn\_vector, \_plugins/\_knn) | Vector & ML | Supported | Full surface | the k-NN plugin (knn\_vector + \_plugins/\_knn) is bundled in the OpenSearch distribution | #### OpenSearch in the customer cluster Tensor9 provisions OpenSearch on Kubernetes through an OpenSearchCluster resource or Helm chart and configures the application endpoint. Document and search requests go directly to the cluster's OpenSearch API. The customer operates the cluster and its storage.
Application requests reach OpenSearch on Kubernetes directly through its OpenSearch endpoint. Tensor9 provisions the cluster and configures its endpoint. Application requests reach OpenSearch on Kubernetes directly through its OpenSearch endpoint. Tensor9 provisions the cluster and configures its endpoint.
#### Engine and plugin compatibility The target runs the OpenSearch engine and its bundled plugins. The listed document, index, search, aggregation, ingest and scripting operations retain their OpenSearch APIs. Match the engine and plugin versions to the application; sharing an engine name does not make different versions byte-identical. The distribution includes k-NN vector search, SQL, PPL, Index State Management, Alerting, Anomaly Detection and ML Commons. Their native \_plugins APIs run on the cluster. Elasticsearch licensing endpoints \_license and \_xpack are not OpenSearch APIs. #### Topology and access Define node pools with CPU, memory and PersistentVolume capacity. A dedicated cluster\_manager pool maintains the cluster quorum; use at least three eligible nodes and distribute replicas across failure domains. OpenSearch renamed the master role to cluster\_manager in version 2.0. Configure shard allocation and plan rolling upgrades for the selected version. The OpenSearch Security plugin controls roles, role mappings and internal users, including supported SAML, OIDC or LDAP sign-in and document- or field-level access. Translate AWS IAM-based access into this configuration. Replace bootstrap certificates and default credentials before exposing the cluster. #### Storage and recovery PersistentVolumes and their StorageClasses determine disk capacity, IOPS and encryption. They do not inherit a domain-specific AWS KMS key setting. Availability depends on node placement, quorum and index replicas; the self-managed cluster has no managed uptime SLA. Register a snapshot repository and schedule creation and retention with OpenSearch Snapshot Management. An S3-compatible repository requires the repository-s3 plugin and object-store endpoint configuration. Keep backups outside the cluster's failure domain and verify a restore. OpenSearch Snapshot Management is distinct from Elasticsearch Snapshot Lifecycle Management. #### Migration and operation The cluster starts empty. Restore a compatible snapshot or reindex from the source, then verify index mappings and queries before cutover. Size shards, replicas and storage for the workload; shard sizes in the tens of gigabytes are a starting point to evaluate, not a fixed limit. The customer operates Kubernetes capacity, storage expansion, cluster health, upgrades and snapshots. Tensor9 provisions the deployment and endpoint; it does not add a query proxy or a managed durability guarantee. ## On Google Cloud ### Via Elastic Cloud (managed) | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------------ | ------------------ | -------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | Cluster health / stats / \_cat APIs | Cluster | Supported | Most usage | - | | Tasks API (\_tasks) | Cluster | Supported | Full surface | drives async reindex / update-by-query | | Bulk (\_bulk) | Documents | Supported | Common | batch index/update/delete | | Delete document | Documents | Supported | Common | - | | Get document (GET /\{index}/\_doc/\{id}) | Documents | Supported | Common | - | | Index document (PUT/POST /\{index}/\_doc) | Documents | Supported | Common | - | | Multi-get (\_mget) | Documents | Supported | Most usage | - | | Update document (\_update) | Documents | Supported | Common | - | | Snapshots / backups (\_snapshot) | Durability | Partial | Full surface | Elastic SLM - a different API | | Aliases (\_aliases) | Index management | Supported | Most usage | - | | Analyze (\_analyze) | Index management | Supported | Full surface | tokenizer/analyzer testing | | Create / Delete / Get index | Index management | Supported | Common | - | | Data streams (\_data\_stream) | Index management | Supported | Full surface | - | | Open / Close index | Index management | Supported | Full surface | - | | Put / Get mapping | Index management | Supported | Common | - | | Refresh / Flush / Forcemerge | Index management | Supported | Most usage | - | | Rollover / Shrink / Split / Clone | Index management | Supported | Full surface | - | | Update / Get settings | Index management | Supported | Most usage | - | | Index & component templates (\_index\_template / \_component\_template) | Ingest & templates | Supported | Full surface | - | | Ingest pipelines (\_ingest/pipeline: put / get / delete / \_simulate) | Ingest & templates | Supported | Full surface | - | | License / X-Pack (\_license, \_xpack) | Licensing | Out of scope | Full surface | Elasticsearch licensing endpoints are outside the OpenSearch mapping. | | Index State Management (\_plugins/\_ism) | Lifecycle | Adapter-served | Full surface | Maps representable ISM actions to ILM phases; custom states and transitions cannot round-trip. | | Alerting / anomaly detection / ML Commons (\_plugins/\*) | Observability & ML | Adapter-served | Full surface | ES-native \_ml/\_watcher, several Platinum-gated; notifications/observability have no ES REST equivalent | | Notifications / observability (\_plugins/\_notifications, \_plugins/\_observability) | Observability & ML | Out of scope | Full surface | These OpenSearch APIs are outside the Elasticsearch mapping; use target-native configuration or change the application. | | PPL (\_plugins/\_ppl) | Query languages | Out of scope | Full surface | no PPL in Elasticsearch; ES\|QL is a different language | | SQL (\_plugins/\_sql) | Query languages | Adapter-served | Full surface | ES \_sql at a different path/shape | | Stored scripts & Painless (\_scripts, \_scripts/painless/\_execute) | Scripting | Supported | Full surface | - | | Aggregations | Search | Supported | Common | metrics + bucket aggregations in \_search | | Count / Explain / Suggest | Search | Supported | Most usage | - | | Field capabilities (\_field\_caps) | Search | Supported | Full surface | - | | Multi-search (\_msearch) | Search | Supported | Most usage | - | | Point-in-time (\_search/point\_in\_time) | Search | Partial | Most usage | ES uses \_pit, not \_search/point\_in\_time - path rewrite | | Reindex / Update-by-query / Delete-by-query | Search | Supported | Full surface | - | | Scroll (\_search/scroll) | Search | Supported | Most usage | deep pagination | | Search (\_search + query DSL) | Search | Supported | Common | - | | Search templates (\_search/template, \_render/template) | Search | Supported | Full surface | - | | Term vectors (\_termvectors / \_mtermvectors) | Search | Supported | Full surface | - | | Security plugin / fine-grained access control (\_plugins/\_security) | Security | Out of scope | Full surface | disjoint API + model - \_security/\* vs \_plugins/\_security/\*; some OpenSearch-free FLS/DLS/multi-tenancy are Platinum-gated or absent on ES | | Vector search / k-NN (knn\_vector, \_plugins/\_knn) | Vector & ML | Adapter-served | Full surface | rewrite knn\_vector→dense\_vector, OpenSearch knn query→ES knn option | #### OpenSearch requests on Elasticsearch Elastic Cloud Hosted runs Elasticsearch 8.x. The Tensor9 adapter accepts the application's OpenSearch requests, translates supported APIs and authentication, and sends them to the managed Elasticsearch endpoint. Tensor9 provisions an ec\_deployment in the customer's Google Cloud environment. Elastic operates Elasticsearch and Kibana; the customer selects the deployment configuration.
Application requests reach Elastic Cloud Hosted through the Tensor9 translating adapter. The adapter translates authentication and supported APIs. Application requests reach Elastic Cloud Hosted through the Tensor9 translating adapter. The adapter translates authentication and supported APIs.
#### Core requests and authentication The adapter forwards the supported document, search, aggregation, index, cluster, ingest and scripting operations. It translates request and response differences, including the point-in-time path from \_search/point\_in\_time to \_pit. AWS SigV4 authentication is checked by the adapter; target requests use Elastic credentials or API keys. Configure target permissions in Elastic's security model. Elasticsearch 8.x provides compatible-with=7 headers for its earlier REST API contract. The adapter uses that facility where applicable, alongside the required translations. It does not make every OpenSearch request or client version compatible. Validate the application's queries, response parsing and client behavior against the listed operations. See [Elastic's API compatibility documentation](https://www.elastic.co/docs/reference/elasticsearch/rest-apis/compatibility). #### Plugin translations OpenSearch and Elasticsearch use different plugin APIs. The mapping translates k-NN vector mappings from knn\_vector to dense\_vector and converts the query to Elasticsearch's knn form. SQL requests move from \_plugins/\_sql to \_sql with translated response fields. Index State Management policies map to Elasticsearch Index Lifecycle Management where their actions can be represented. Custom ISM states and transitions do not round-trip through ILM's fixed phases. The described alerting, anomaly-detection and machine-learning mappings use Elasticsearch \_watcher and \_ml capabilities. Some need a paid Elastic subscription. The operation table separates those translations from excluded APIs: PPL, the OpenSearch Security API, notifications and observability objects remain outside the mapping. Elasticsearch \_license and \_xpack are not OpenSearch APIs. #### Deployment and availability The ec\_deployment resource exposes hot, warm, cold and frozen tiers, instance size, one to three availability zones, and per-tier autoscaling. Storage is bundled at the selected tier's disk-to-memory ratio. The subscription determines commercial features and the applicable availability commitment. Elastic Cloud publishes monthly availability commitments of 99.5%, 99.9% and 99.95% for one-, two- and three-zone deployments, subject to the subscription and high-availability conditions. Choose topology and subscription together. The target uses Elastic security configuration, API keys and single sign-on rather than OpenSearch Security roles. Document- and field-level security and other commercial features depend on the purchased subscription. The hosted offering's subscription conditions apply regardless of the source-code license. Elastic added an AGPLv3 option for the free source portions in 2024; see its [licensing FAQ](https://www.elastic.co/pricing/faq/licensing/). #### Snapshots and migration The deployment starts empty. Move data with a compatible remote reindex or bulk export/import workflow; do not assume an OpenSearch snapshot restores across the engine fork. Review mappings, analyzers and index settings before loading data, and verify representative queries before switching traffic. Elastic Snapshot Lifecycle Management controls target backup schedules and retention. Preserve the application's recovery requirements when replacing AWS-managed snapshots. The adapter does not translate OpenSearch Security configuration, PPL or notification/observability objects; these need target-native configuration or an application change. #### Sizing and performance Choose shard layout, replicas, data tiers and memory for the application's indexing and query workload. Published engine benchmarks describe their particular versions and datasets; they do not measure this adaptation. Measure the deployed application, including translated operations, when selecting capacity. ## On Azure ### Via Azure Native Elastic | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------------ | ------------------ | -------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | Cluster health / stats / \_cat APIs | Cluster | Supported | Most usage | - | | Tasks API (\_tasks) | Cluster | Supported | Full surface | drives async reindex / update-by-query | | Bulk (\_bulk) | Documents | Supported | Common | batch index/update/delete | | Delete document | Documents | Supported | Common | - | | Get document (GET /\{index}/\_doc/\{id}) | Documents | Supported | Common | - | | Index document (PUT/POST /\{index}/\_doc) | Documents | Supported | Common | - | | Multi-get (\_mget) | Documents | Supported | Most usage | - | | Update document (\_update) | Documents | Supported | Common | - | | Snapshots / backups (\_snapshot) | Durability | Partial | Full surface | Elastic SLM - a different API | | Aliases (\_aliases) | Index management | Supported | Most usage | - | | Analyze (\_analyze) | Index management | Supported | Full surface | tokenizer/analyzer testing | | Create / Delete / Get index | Index management | Supported | Common | - | | Data streams (\_data\_stream) | Index management | Supported | Full surface | - | | Open / Close index | Index management | Supported | Full surface | - | | Put / Get mapping | Index management | Supported | Common | - | | Refresh / Flush / Forcemerge | Index management | Supported | Most usage | - | | Rollover / Shrink / Split / Clone | Index management | Supported | Full surface | - | | Update / Get settings | Index management | Supported | Most usage | - | | Index & component templates (\_index\_template / \_component\_template) | Ingest & templates | Supported | Full surface | - | | Ingest pipelines (\_ingest/pipeline: put / get / delete / \_simulate) | Ingest & templates | Supported | Full surface | - | | License / X-Pack (\_license, \_xpack) | Licensing | Out of scope | Full surface | Elasticsearch licensing endpoints are outside the OpenSearch mapping. | | Index State Management (\_plugins/\_ism) | Lifecycle | Adapter-served | Full surface | Maps representable ISM actions to ILM phases; custom states and transitions cannot round-trip. | | Alerting / anomaly detection / ML Commons (\_plugins/\*) | Observability & ML | Adapter-served | Full surface | ES-native \_ml/\_watcher, several Platinum-gated; notifications/observability have no ES REST equivalent | | Notifications / observability (\_plugins/\_notifications, \_plugins/\_observability) | Observability & ML | Out of scope | Full surface | These OpenSearch APIs are outside the Elasticsearch mapping; use target-native configuration or change the application. | | PPL (\_plugins/\_ppl) | Query languages | Out of scope | Full surface | no PPL in Elasticsearch; ES\|QL is a different language | | SQL (\_plugins/\_sql) | Query languages | Adapter-served | Full surface | ES \_sql at a different path/shape | | Stored scripts & Painless (\_scripts, \_scripts/painless/\_execute) | Scripting | Supported | Full surface | - | | Aggregations | Search | Supported | Common | metrics + bucket aggregations in \_search | | Count / Explain / Suggest | Search | Supported | Most usage | - | | Field capabilities (\_field\_caps) | Search | Supported | Full surface | - | | Multi-search (\_msearch) | Search | Supported | Most usage | - | | Point-in-time (\_search/point\_in\_time) | Search | Partial | Most usage | ES uses \_pit, not \_search/point\_in\_time - path rewrite | | Reindex / Update-by-query / Delete-by-query | Search | Supported | Full surface | - | | Scroll (\_search/scroll) | Search | Supported | Most usage | deep pagination | | Search (\_search + query DSL) | Search | Supported | Common | - | | Search templates (\_search/template, \_render/template) | Search | Supported | Full surface | - | | Term vectors (\_termvectors / \_mtermvectors) | Search | Supported | Full surface | - | | Security plugin / fine-grained access control (\_plugins/\_security) | Security | Out of scope | Full surface | disjoint API + model - \_security/\* vs \_plugins/\_security/\*; some OpenSearch-free FLS/DLS/multi-tenancy are Platinum-gated or absent on ES | | Vector search / k-NN (knn\_vector, \_plugins/\_knn) | Vector & ML | Adapter-served | Full surface | rewrite knn\_vector→dense\_vector, OpenSearch knn query→ES knn option | #### OpenSearch requests on Elasticsearch Azure Native Elastic runs Elasticsearch 8.x. The Tensor9 adapter accepts the application's OpenSearch requests, translates supported APIs and authentication, and sends them to the managed Elasticsearch endpoint. Tensor9 provisions the Elastic resource in the customer's Azure tenant through the Azure Native integration. Elastic operates Elasticsearch; the resource is purchased and billed through the customer's Azure subscription.
Application requests reach Azure Native Elastic through the Tensor9 translating adapter. The adapter translates authentication and supported APIs. Application requests reach Azure Native Elastic through the Tensor9 translating adapter. The adapter translates authentication and supported APIs.
#### Core requests and authentication The adapter forwards the supported document, search, aggregation, index, cluster, ingest and scripting operations. It translates request and response differences, including the point-in-time path from \_search/point\_in\_time to \_pit. AWS SigV4 authentication is checked by the adapter; target requests use Elastic credentials or API keys. Configure target permissions in Elastic's security model. Elasticsearch 8.x provides compatible-with=7 headers for its earlier REST API contract. The adapter uses that facility where applicable, alongside the required translations. It does not make every OpenSearch request or client version compatible. Validate the application's queries, response parsing and client behavior against the listed operations. See [Elastic's API compatibility documentation](https://www.elastic.co/docs/reference/elasticsearch/rest-apis/compatibility). #### Plugin translations OpenSearch and Elasticsearch use different plugin APIs. The mapping translates k-NN vector mappings from knn\_vector to dense\_vector and converts the query to Elasticsearch's knn form. SQL requests move from \_plugins/\_sql to \_sql with translated response fields. Index State Management policies map to Elasticsearch Index Lifecycle Management where their actions can be represented. Custom ISM states and transitions do not round-trip through ILM's fixed phases. The described alerting, anomaly-detection and machine-learning mappings use Elasticsearch \_watcher and \_ml capabilities. Some need a paid Elastic subscription. The operation table separates those translations from excluded APIs: PPL, the OpenSearch Security API, notifications and observability objects remain outside the mapping. Elasticsearch \_license and \_xpack are not OpenSearch APIs. #### Deployment and availability The Azure resource does not expose the full deployment topology. Set size and topology in the Elastic Cloud console. The documented default is 16 GB RAM, 560 GB storage and two availability zones, with instances up to 256 GB RAM. The subscription determines commercial features and the applicable availability commitment. Elastic Cloud publishes monthly availability commitments of 99.5%, 99.9% and 99.95% for one-, two- and three-zone deployments, subject to the subscription and high-availability conditions. Choose topology and subscription together. The target uses Elastic security configuration, API keys and single sign-on rather than OpenSearch Security roles. Document- and field-level security and other commercial features depend on the purchased subscription. The hosted offering's subscription conditions apply regardless of the source-code license. Elastic added an AGPLv3 option for the free source portions in 2024; see its [licensing FAQ](https://www.elastic.co/pricing/faq/licensing/). #### Snapshots and migration The deployment starts empty. Move data with a compatible remote reindex or bulk export/import workflow; do not assume an OpenSearch snapshot restores across the engine fork. Review mappings, analyzers and index settings before loading data, and verify representative queries before switching traffic. Elastic Snapshot Lifecycle Management controls target backup schedules and retention. Preserve the application's recovery requirements when replacing AWS-managed snapshots. The adapter does not translate OpenSearch Security configuration, PPL or notification/observability objects; these need target-native configuration or an application change. #### Sizing and performance Choose shard layout, replicas, data tiers and memory for the application's indexing and query workload. Published engine benchmarks describe their particular versions and datasets; they do not measure this adaptation. Measure the deployed application, including translated operations, when selecting capacity. ## On OCI | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------------ | ------------------ | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Cluster health / stats / \_cat APIs | Cluster | Supported | Most usage | - | | Tasks API (\_tasks) | Cluster | Supported | Full surface | drives async reindex / update-by-query | | Bulk (\_bulk) | Documents | Supported | Common | batch index/update/delete | | Delete document | Documents | Supported | Common | - | | Get document (GET /\{index}/\_doc/\{id}) | Documents | Supported | Common | - | | Index document (PUT/POST /\{index}/\_doc) | Documents | Supported | Common | - | | Multi-get (\_mget) | Documents | Supported | Most usage | - | | Update document (\_update) | Documents | Supported | Common | - | | Snapshots / backups (\_snapshot) | Durability | Partial | Full surface | managed daily backups (14-day retention, non-configurable) + the manual \_snapshot API to an Object Storage bucket in your tenancy - a different model from AWS managed snapshots | | Aliases (\_aliases) | Index management | Supported | Most usage | - | | Analyze (\_analyze) | Index management | Supported | Full surface | tokenizer/analyzer testing | | Create / Delete / Get index | Index management | Supported | Common | - | | Data streams (\_data\_stream) | Index management | Supported | Full surface | - | | Open / Close index | Index management | Supported | Full surface | - | | Put / Get mapping | Index management | Supported | Common | - | | Refresh / Flush / Forcemerge | Index management | Supported | Most usage | - | | Rollover / Shrink / Split / Clone | Index management | Supported | Full surface | - | | Update / Get settings | Index management | Supported | Most usage | - | | Index & component templates (\_index\_template / \_component\_template) | Ingest & templates | Supported | Full surface | - | | Ingest pipelines (\_ingest/pipeline: put / get / delete / \_simulate) | Ingest & templates | Supported | Full surface | - | | License / X-Pack (\_license, \_xpack) | Licensing | Out of scope | Full surface | Elasticsearch licensing endpoints \_license and \_xpack are not OpenSearch APIs. | | Index State Management (\_plugins/\_ism) | Lifecycle | Supported | Full surface | Index Management (ISM) is a supported OCI Search plugin | | Alerting / anomaly detection / ML Commons (\_plugins/\*) | Observability & ML | Supported | Full surface | Alerting, Anomaly Detection, ML Commons, Neural Search, Notifications, Observability, Security Analytics are all supported OCI Search plugins | | Notifications / observability (\_plugins/\_notifications, \_plugins/\_observability) | Observability & ML | Supported | Full surface | Alerting, Anomaly Detection, ML Commons, Neural Search, Notifications, Observability, Security Analytics are all supported OCI Search plugins | | PPL (\_plugins/\_ppl) | Query languages | Supported | Full surface | PPL ships with the supported SQL plugin | | SQL (\_plugins/\_sql) | Query languages | Supported | Full surface | the SQL plugin is supported on OCI Search | | Stored scripts & Painless (\_scripts, \_scripts/painless/\_execute) | Scripting | Supported | Full surface | - | | Aggregations | Search | Supported | Common | metrics + bucket aggregations in \_search | | Count / Explain / Suggest | Search | Supported | Most usage | - | | Field capabilities (\_field\_caps) | Search | Supported | Full surface | - | | Multi-search (\_msearch) | Search | Supported | Most usage | - | | Point-in-time (\_search/point\_in\_time) | Search | Supported | Most usage | native - OpenSearch engine | | Reindex / Update-by-query / Delete-by-query | Search | Supported | Full surface | - | | Scroll (\_search/scroll) | Search | Supported | Most usage | deep pagination | | Search (\_search + query DSL) | Search | Supported | Common | - | | Search templates (\_search/template, \_render/template) | Search | Supported | Full surface | - | | Term vectors (\_termvectors / \_mtermvectors) | Search | Supported | Full surface | - | | Security plugin / fine-grained access control (\_plugins/\_security) | Security | Partial | Full surface | the OpenSearch Security plugin runs (RBAC, SAML/LDAP/OIDC, DLS/FLS), but AWS FGAC's IAM/master-user model is re-expressed as OCI's security\_mode (ENFORCING) + a master user - not the IAM access\_policies | | Vector search / k-NN (knn\_vector, \_plugins/\_knn) | Vector & ML | Supported | Full surface | k-NN is a supported OCI Search plugin | #### OpenSearch managed by Oracle Tensor9 provisions OCI Search with OpenSearch in the customer's Oracle Cloud tenancy and configures the application to use its opensearch\_fqdn endpoint. Document and search requests reach the managed cluster directly. Oracle operates the OpenSearch service.
Application requests reach OCI Search with OpenSearch directly through its OpenSearch endpoint. Tensor9 provisions the cluster and configures its endpoint. Application requests reach OCI Search with OpenSearch directly through its OpenSearch endpoint. Tensor9 provisions the cluster and configures its endpoint.
#### Engine and plugins The target runs OpenSearch. The listed core REST operations and supported plugins use native OpenSearch APIs, including k-NN, SQL/PPL, Index State Management, Alerting, Anomaly Detection, ML Commons, Neural Search and Security Analytics. Match the selected engine and plugin versions to the application before migration. Elasticsearch \_license and \_xpack endpoints are not part of OpenSearch. The documented OCI versions are 2.11, 2.15, 2.18, 2.19.1, 3.2 and 3.6. The same engine across providers still requires a version-compatibility review; it does not establish byte-identical behavior for every request. #### Topology and security Size data, leader and OpenSearch Dashboards nodes, with optional coordinator, machine-learning and search tiers. The cluster uses the customer's VCN and subnet; Oracle manages the software and distributes data nodes across availability domains. Configure the OpenSearch Security plugin with security\_mode set to ENFORCING and a master user. Supported authentication includes SAML, LDAP and OIDC, with document- and field-level security on supported versions. AWS IAM access\_policies do not transfer directly. Storage encryption is service-managed; this mapping has no per-domain AWS KMS customer-key setting. #### Backups and availability OCI takes daily managed backups with 14-day retention. The manual Snapshot API can use an Object Storage repository in the customer tenancy. These backup controls differ from AWS domain snapshots, so configure recovery destinations and test restoration before cutover. The documented OCI FAQ describes a 99.9% service-level objective, not a contractual availability SLA. Node placement and replicas provide high availability, but the objective is not a contractual guarantee. Evaluate the current service terms for the customer deployment. #### Migration and sizing The new cluster starts empty. Restore a compatible snapshot through a registered repository or reindex the source; OCI does not automatically import the AWS domain's indexes. Verify mappings, analyzers, queries and Security-plugin configuration before switching the endpoint. The service uses Oracle compute and storage shapes, with the limits listed in the comparison. AWS instance families, EBS limits and UltraWarm settings are not interchangeable sizing inputs. Measure the customer workload on the selected topology and retain adequate storage and replica capacity. [Service Catalog](/service-adapters/catalog). # OpenSearch Serverless Source: https://docs.tensor9.com/service-adapters/aws/databases-storage/opensearch-serverless OpenSearch Serverless collections, with AWS-managed capacity, data-access and security policies, and collection endpoints. Provisioned OpenSearch domains are listed separately. Preview ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | - | | Azure | - | | OCI | - | | Private Kubernetes | - | ## Mapping and limitations This entry represents the OpenSearch collection resources used by the application's search configuration. On another cloud, search capacity is supplied by the appliance's target environment; the AWS collection-management resources are not recreated. This does not promise the OpenSearch Serverless control API, AWS collection policies or AWS capacity model. Provisioned OpenSearch domains have a separate mapping and their own target-specific limits. [Service Catalog](/service-adapters/catalog). # RDS MySQL Source: https://docs.tensor9.com/service-adapters/aws/databases-storage/rds-mysql AWS RDS MySQL. Community MySQL running on instances that RDS provisions, patches and backs up, with point-in-time restore and an optional Multi-AZ standby. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of RDS MySQL with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation #### Runtime surface | Capability | RDS MySQL | Google Cloud | Azure | OCI | | ------------ | --------- | ------------ | ----- | ---- | | API coverage | full | high | high | high | #### Management surface | Capability | RDS MySQL | Google Cloud | Azure | OCI | | ------------------------------------------------- | --------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Snapshot / restore + PITR | Yes | Yes | Yes | Yes | | Zero-ETL integration · Redshift | Yes | Partial - replicates to BigQuery (Datastream); Redshift itself is not served | Partial - replicates to Microsoft Fabric (OneLake); Redshift itself is not served | No | | Read replicas · in-region | Yes | Yes | Yes | Yes | | Read replicas · cross-region | Yes | Yes | Yes | Partial - cross-region reads require a separate DB system and replication channel; a backup copy supports restore only | | Managed connection pooling · RDS Proxy | Yes | Yes - Cloud SQL Managed Connection Pooling (Enterprise Plus) | No - no managed pooler; ProxySQL is customer-managed on your own VM | No - no managed pooler; an OCI load balancer can front it but is not a pooler | | Query performance insights · Performance Insights | Yes | Partial - target-native performance tools; access is controlled by the customer | Partial - target-native performance tools; access is controlled by the customer | Partial - target-native performance tools; access is controlled by the customer | #### Limits | Capability | RDS MySQL | Google Cloud | Azure | OCI | | ------------------------------------------- | ----------- | ------------ | --------- | ------------ | | Maximum storage | 64 TiB | 64 TiB | 16-32 TiB | 128 TiB | | vCPU range · smallest to largest instance | 2-384 | 1-128 | 1-96 | 2-256 (ECPU) | | Memory range · smallest to largest instance | 1-4,096 GiB | 0.6-864 GiB | 2-672 GiB | 8-1,024 GiB | | Storage autoscaling | Yes | Yes | Yes | Yes | ### Infrastructure-only adaptation #### Runtime surface | Capability | RDS MySQL | Google Cloud | Azure | OCI | Private Kubernetes | | ------------ | --------- | ------------ | ----- | ---- | ------------------ | | API coverage | full | high | high | high | high | #### Management surface | Capability | RDS MySQL | Google Cloud | Azure | OCI | Private Kubernetes | | ------------------------------------------------- | --------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | Snapshot / restore + PITR | Yes | Yes | Yes | Yes | Yes | | Zero-ETL integration · Redshift | Yes | Partial - replicates to BigQuery (Datastream); Redshift itself is not served | Partial - replicates to Microsoft Fabric (OneLake); Redshift itself is not served | No | No | | Read replicas · in-region | Yes | Yes | Yes | Yes | Yes | | Read replicas · cross-region | Yes | Yes | Yes | Partial - cross-region reads require a separate DB system and replication channel; a backup copy supports restore only | No | | Managed connection pooling · RDS Proxy | Yes | Yes - Cloud SQL Managed Connection Pooling (Enterprise Plus) | No - no managed pooler; ProxySQL is customer-managed on your own VM | No - no managed pooler; an OCI load balancer can front it but is not a pooler | No - HAProxy routes connections but does not pool them; configure an application pool or separate pooler | | Query performance insights · Performance Insights | Yes | Partial - target-native performance tools; access is controlled by the customer | Partial - target-native performance tools; access is controlled by the customer | Partial - target-native performance tools; access is controlled by the customer | Partial - target-native performance tools; access is controlled by the customer | #### Limits | Capability | RDS MySQL | Google Cloud | Azure | OCI | Private Kubernetes | | ------------------------------------------- | ----------- | ------------ | --------- | ------------ | --------------------------------------------------------------------- | | Maximum storage | 64 TiB | 64 TiB | 16-32 TiB | 128 TiB | limited by persistent volume capacity | | vCPU range · smallest to largest instance | 2-384 | 1-128 | 1-96 | 2-256 (ECPU) | limited by Kubernetes node resources | | Memory range · smallest to largest instance | 1-4,096 GiB | 0.6-864 GiB | 2-672 GiB | 8-1,024 GiB | limited by Kubernetes node resources | | Storage autoscaling | Yes | Yes | Yes | Yes | No - increase the volume size explicitly; it does not grow with usage | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | -------------------------------------------------------------------------------------------------------------- | ------------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------- | | AWS integration SQL: LOAD DATA FROM S3 / SELECT INTO OUTFILE S3, mysql.lambda\_async, mysql.rds\_\* procedures | SQL functions | Out of scope | Full surface | RDS MySQL extension procedures with no counterpart on vanilla MySQL; a query calling them fails | | MySQL client/server protocol (application SQL) | Wire protocol | Supported | Common | same MySQL engine; application SQL is unchanged | #### How it works Your application's MySQL driver connects directly to Cloud SQL for MySQL. Tensor9 provisions the database from the RDS instance declaration and sets the application's endpoint and credentials. SQL requests form the **data plane**. RDS management requests form the **control plane**: Tensor9 accepts those requests in the customer's appliance and translates snapshots, restores, replicas, failover, and configuration changes to the Cloud SQL admin API.
Before: on AWS your app talks to an RDS for MySQL instance over the MySQL protocol. After: on Google Cloud the same app with the same driver talks over the same MySQL protocol to a native Cloud SQL for MySQL instance, without Tensor9 proxying database traffic. Before: on AWS your app talks to an RDS for MySQL instance over the MySQL protocol. After: on Google Cloud the same app with the same driver talks over the same MySQL protocol to a native Cloud SQL for MySQL instance, without Tensor9 proxying database traffic.

An aws\_db\_instance (engine = mysql) compiles to a native Cloud SQL for MySQL instance; the workload keeps its driver and speaks the MySQL protocol straight to the database, no proxy in the query path.

#### Cross-region read replicas A Cloud SQL primary replicates asynchronously to up to **seven read replicas** in total, within its region or across regions. A cross-region replica can be promoted to a standalone primary for migration or disaster recovery. RDS allows up to fifteen replicas, so check the required replica count before cutover. Replicas can lag behind the primary; send reads that must see a just-completed write to the primary.
A Cloud SQL primary in Region A replicates asynchronously to an in-region read replica and to a cross-region read replica in Region B. The cross-region replica is promotable for regional migration or disaster recovery. Up to seven read replicas per primary, in-region or cross-region. A Cloud SQL primary in Region A replicates asynchronously to an in-region read replica and to a cross-region read replica in Region B. The cross-region replica is promotable for regional migration or disaster recovery. Up to seven read replicas per primary, in-region or cross-region.

Cloud SQL keeps cross-region read replicas: a primary serves in-region and cross-region read replicas (up to seven per primary), and a cross-region replica is promotable for regional failover or DR.

#### Regional-instance HA RDS Multi-AZ maps to a Cloud SQL regional instance with a primary in one zone and a synchronous standby in another. Writes replicate to regional persistent disk in both zones before acknowledgement. This provides a recovery point objective of zero (RPO 0) for HA failover: committed writes are retained. Enterprise Plus has a 99.99% monthly-uptime SLA and reduced-downtime maintenance options. The applicable availability terms depend on the edition and configuration.
A Cloud SQL HA regional instance has a primary in zone A and a synchronous standby in zone B of the same region, replicated at the block level over a regional persistent disk. Failover loses no committed write (RPO 0); Enterprise Plus comes with a 99.99 percent availability SLA. A Cloud SQL HA regional instance has a primary in zone A and a synchronous standby in zone B of the same region, replicated at the block level over a regional persistent disk. Failover loses no committed write (RPO 0); Enterprise Plus comes with a 99.99 percent availability SLA.

Cloud SQL HA is a regional instance: a synchronous standby in a second zone over a regional persistent disk, so failover loses no committed write; Enterprise Plus backs a 99.99% availability SLA.

#### Parameters as database flags Settable MySQL parameters become per-instance **database flags**, such as `max_connections` and `long_query_time`. The reusable RDS parameter-group object has no counterpart. If several RDS instances share a parameter group, its supported values are applied to each Cloud SQL instance. Check the selected MySQL version and Cloud SQL's allowed flag values; parameters outside that list cannot be applied.
An RDS parameter group flattens to Cloud SQL database flags: each settable MySQL parameter becomes the matching per-instance flag. The parameter-group object itself and any parameter outside Cloud SQL's settable list do not cross. An RDS parameter group flattens to Cloud SQL database flags: each settable MySQL parameter becomes the matching per-instance flag. The parameter-group object itself and any parameter outside Cloud SQL's settable list do not cross.

Each MySQL parameter becomes a Cloud SQL database flag; the parameter-group object itself and any parameter outside Cloud SQL's settable list do not cross.

#### Pooling, IAM auth, and the customer key RDS Proxy connection pooling maps to Cloud SQL Managed Connection Pooling, which requires Enterprise Plus. RDS IAM database authentication maps to Cloud SQL IAM database authentication. A customer KMS key maps to a customer-managed encryption key (CMEK) in Cloud KMS. The customer controls access to that key in the target project.
Three RDS control-plane features map to Cloud SQL: RDS Proxy connection pooling to Cloud SQL Managed Connection Pooling on Enterprise Plus, RDS IAM database authentication to Cloud SQL IAM database authentication, and a KMS customer key to a CMEK in Cloud KMS. Three RDS control-plane features map to Cloud SQL: RDS Proxy connection pooling to Cloud SQL Managed Connection Pooling on Enterprise Plus, RDS IAM database authentication to Cloud SQL IAM database authentication, and a KMS customer key to a CMEK in Cloud KMS.

Managed pooling maps to Cloud SQL Managed Connection Pooling (Enterprise Plus), RDS IAM auth to Cloud SQL IAM database authentication, and a KMS customer key to a CMEK in Cloud KMS.

#### Limitations Check these differences when selecting the Cloud SQL edition, instance size, and database version. △ Where RDS for MySQL and Cloud SQL diverge * **Read replicas cap at seven per primary.** Cloud SQL allows seven replicas per primary against RDS's fifteen, in- or cross-region, so a large read fan-out is sized against seven before cutover. * **Managed pooling requires Enterprise Plus.** RDS Proxy's pooling maps to Cloud SQL Managed Connection Pooling, available only on the Enterprise Plus edition, so an instance that must pool is placed on that edition. * **Parameter groups flatten to per-instance database flags.** Each MySQL parameter becomes a Cloud SQL flag; the reusable group object and any parameter outside Cloud SQL's settable-flag list do not cross. * **The engine build and available versions are Google's.** Cloud SQL runs the MySQL versions and flags its catalog exposes, so an instance requiring a version or flag Cloud SQL does not offer is reconciled to a supported one before cutover. * **RDS-proprietary extras have no counterpart.** Event subscriptions (SNS), S3 / Lambda integration SQL, and Enhanced Monitoring are RDS-specific; zero-ETL replicates to BigQuery via Datastream, not Redshift. #### Other considerations Move data with `mysqldump` or Cloud SQL Database Migration Service. Validate the copied data and required engine features before switching connections. Google operates the database; the customer controls maintenance settings and access to Query Insights in their project. Query Insights uses Google's performance tools rather than the RDS Performance Insights interface. ## On Azure | Operation | Area | Support | Depth | Notes | | -------------------------------------------------------------------------------------------------------------- | ------------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------- | | AWS integration SQL: LOAD DATA FROM S3 / SELECT INTO OUTFILE S3, mysql.lambda\_async, mysql.rds\_\* procedures | SQL functions | Out of scope | Full surface | RDS MySQL extension procedures with no counterpart on vanilla MySQL; a query calling them fails | | MySQL client/server protocol (application SQL) | Wire protocol | Supported | Common | same MySQL engine; application SQL is unchanged | #### How it works Your application's MySQL driver connects directly to Azure Database for MySQL Flexible Server. Tensor9 provisions the server from the RDS instance declaration and sets the application's endpoint and credentials. SQL requests form the **data plane**. RDS management requests form the **control plane**: Tensor9 accepts those requests in the customer's appliance and translates snapshots, restores, replicas, failover, and configuration changes to Azure's management API.
Before: on AWS your app talks to an RDS for MySQL instance over the MySQL protocol. After: on Azure the same app with the same driver talks over the same MySQL protocol to a native Azure Database for MySQL Flexible Server, without Tensor9 proxying database traffic. Before: on AWS your app talks to an RDS for MySQL instance over the MySQL protocol. After: on Azure the same app with the same driver talks over the same MySQL protocol to a native Azure Database for MySQL Flexible Server, without Tensor9 proxying database traffic.

An aws\_db\_instance (engine = mysql) compiles to a native Azure Database for MySQL Flexible Server; the workload keeps its driver and speaks the MySQL protocol straight to the database, no proxy in the query path.

#### Compute tiers and zone-redundant HA Flexible Server has Burstable, General Purpose, and Business Critical compute tiers. The mapping selects a size for the requested CPU and memory, and a tier that supports the required database features. High availability and read replicas require General Purpose or Business Critical; Burstable supports neither. Zone-redundant HA places a synchronous standby in a second zone and uses zone-redundant storage. It provides a recovery point objective of zero (RPO 0) for HA failover and a 99.99% availability SLA, subject to the service's terms.
Flexible Server offers Burstable, General Purpose, and Business Critical compute tiers. Zone-redundant HA places a synchronous standby in a second availability zone over zone-redundant storage; it is available on the General Purpose and Business Critical tiers, and the Burstable tier supports neither HA nor read replicas. Flexible Server offers Burstable, General Purpose, and Business Critical compute tiers. Zone-redundant HA places a synchronous standby in a second availability zone over zone-redundant storage; it is available on the General Purpose and Business Critical tiers, and the Burstable tier supports neither HA nor read replicas.

Flexible Server has Burstable, General Purpose, and Business Critical tiers; zone-redundant HA is a synchronous standby in a second zone (ZRS), available on General Purpose and Business Critical, with a 99.99% availability SLA.

#### Read replicas A Flexible Server source uses MySQL binary-log replication to maintain up to **ten read replicas**, within its region or across regions. Replicas require General Purpose or Business Critical. Replication is asynchronous. Send reads that must see a just-completed write to the source; replicas may not yet have received that write.
A Flexible Server source replicates asynchronously with binary-log position-based replication to up to ten read replicas, which can be in the same region or in another region for read scale-out. A Flexible Server source replicates asynchronously with binary-log position-based replication to up to ten read replicas, which can be in the same region or in another region for read scale-out.

A Flexible Server source replicates asynchronously (binlog position-based) to up to ten read replicas, in the same region or another region, for read scale-out.

#### Server parameters Settable values in an RDS parameter group map to **Flexible Server parameters**, including settings such as `max_connections`. Azure configures each server separately, so the reusable parameter-group object has no counterpart. The mapping applies supported values to each server that used the group. Azure controls the allowed values; static parameters require a restart, and some settings can be chosen only when the server is created.
Settable RDS parameter-group values map to individual Flexible Server parameters. Azure has no reusable parameter-group object. Settable RDS parameter-group values map to individual Flexible Server parameters. Azure has no reusable parameter-group object.

Parameter values are applied separately to each Flexible Server.

#### Pooling, Entra ID auth, and the customer key RDS IAM database authentication maps to Microsoft Entra ID authentication. A customer KMS key maps to a customer-managed key in Key Vault. Flexible Server has no managed connection pooler equivalent to RDS Proxy. If the workload needs pooling, configure an application pool or operate a separate pooler such as ProxySQL.
RDS IAM database authentication maps to Microsoft Entra ID authentication and a KMS customer key maps to a customer-managed key in Key Vault. RDS Proxy's managed connection pooling has no counterpart: the Flexible Server has no managed pooler, so ProxySQL is customer-managed on the customer's own VM. RDS IAM database authentication maps to Microsoft Entra ID authentication and a KMS customer key maps to a customer-managed key in Key Vault. RDS Proxy's managed connection pooling has no counterpart: the Flexible Server has no managed pooler, so ProxySQL is customer-managed on the customer's own VM.

RDS IAM auth maps to Microsoft Entra ID authentication and a KMS customer key to a Key Vault customer-managed key; RDS Proxy's managed pooling has no counterpart (customer-managed ProxySQL).

#### Limitations Check these differences when selecting the Flexible Server tier, instance size, and database version. △ Where RDS for MySQL and Azure Flexible Server diverge * **The Burstable tier supports neither HA nor replicas.** Zone-redundant HA and read replicas require the General Purpose or Business Critical tier, so an instance that needs either is placed off Burstable. * **No managed connection pooler.** The Flexible Server has no RDS-Proxy equivalent; pooling is customer-managed ProxySQL on the customer's own VM, an operational component to plan for. * **Read replicas cap at ten and lag asynchronously.** A source serves up to ten binlog-based replicas, so a read path needing read-after-write goes to the source and a fan-out near the ceiling is sized against ten. * **Parameter groups flatten to per-server parameters.** Each MySQL parameter becomes a server parameter; the reusable group object and any parameter outside the Flexible Server's settable list do not cross. * **The engine build and available versions are Azure's.** The Flexible Server runs the MySQL major/minor versions and server plugins Azure's catalog offers, so an instance requiring a version or plugin Azure does not expose is reconciled to a supported one before cutover. * **RDS-proprietary extras have no counterpart.** Event subscriptions (SNS), S3 / Lambda integration SQL, and Enhanced Monitoring are RDS-specific; zero-ETL replicates to Microsoft Fabric, not Redshift. #### Other considerations Move data with `mysqldump` or Azure Database Migration Service. Validate the copied data and required engine features before switching connections. Microsoft operates the database. The customer controls maintenance settings and access to Azure's Query Performance Insight; it uses Azure's performance tools rather than the RDS Performance Insights interface. ## On OCI | Operation | Area | Support | Depth | Notes | | -------------------------------------------------------------------------------------------------------------- | ------------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------- | | AWS integration SQL: LOAD DATA FROM S3 / SELECT INTO OUTFILE S3, mysql.lambda\_async, mysql.rds\_\* procedures | SQL functions | Out of scope | Full surface | RDS MySQL extension procedures with no counterpart on vanilla MySQL; a query calling them fails | | MySQL client/server protocol (application SQL) | Wire protocol | Supported | Common | same MySQL engine; application SQL is unchanged | #### How it works Your application's MySQL driver connects directly to an OCI MySQL HeatWave DB system. Tensor9 provisions it from the RDS instance declaration and sets the application's endpoint and credentials. SQL requests form the **data plane**. RDS management requests form the **control plane**: Tensor9 accepts those requests in the customer's appliance and translates snapshots, restores, replicas, failover, and configuration changes to OCI's management API.
Before: on AWS your app talks to an RDS for MySQL instance over the MySQL protocol. After: on OCI the same app with the same driver talks over the same MySQL protocol to a native OCI MySQL HeatWave DB system, without Tensor9 proxying database traffic. Before: on AWS your app talks to an RDS for MySQL instance over the MySQL protocol. After: on OCI the same app with the same driver talks over the same MySQL protocol to a native OCI MySQL HeatWave DB system, without Tensor9 proxying database traffic.

An aws\_db\_instance (engine = mysql) compiles to a native OCI MySQL HeatWave DB system; the workload keeps its driver and speaks the MySQL protocol straight to the database, no proxy in the query path.

#### Optional analytics with HeatWave The DB system runs MySQL for transactional queries. An optional **HeatWave cluster** adds in-memory analytics nodes and loads table data into its analytics engine. Applications can use those tables for analytics without maintaining a separate extraction and loading pipeline. The analytics cluster is optional. An RDS migration can use the MySQL DB system without enabling it. Check the selected MySQL version and supported plugins for the workload.
OCI MySQL HeatWave runs MySQL with optional analytics. The MySQL DB system serves transactional (OLTP) queries against your tables, and a HeatWave cluster of in-memory analytics nodes attaches to the same DB system to serve analytics (OLAP) queries against the same data without a separate extraction and loading pipeline. OCI MySQL HeatWave runs MySQL with optional analytics. The MySQL DB system serves transactional (OLTP) queries against your tables, and a HeatWave cluster of in-memory analytics nodes attaches to the same DB system to serve analytics (OLAP) queries against the same data without a separate extraction and loading pipeline.

A HeatWave cluster loads MySQL table data into its analytics engine.

#### Read replicas and cross-region replication An OCI MySQL DB system supports up to **eighteen read replicas** in its own region, distributed across availability and fault domains. Each replica has an endpoint. OCI's managed read-replica resource does not span regions. Cross-region replication instead uses a separate DB system and an asynchronous replication channel. That database can serve reads, but requires separate configuration and can lag behind the source. A cross-region backup copy supports restore, not live reads.
An OCI MySQL DB system serves up to eighteen in-region read replicas, automatically distributed across availability and fault domains. Managed read replicas stay in-region. A replication channel can maintain a readable DB system in another region; a backup copy must be restored. An OCI MySQL DB system serves up to eighteen in-region read replicas, automatically distributed across availability and fault domains. Managed read replicas stay in-region. A replication channel can maintain a readable DB system in another region; a backup copy must be restored.

Managed read replicas stay in-region; a cross-region replication channel targets a separate DB system.

#### Group Replication HA OCI MySQL high availability uses a three-node Group Replication group across availability or fault domains. A majority must acknowledge transaction changes before commit. The service provides a recovery point objective of zero (RPO 0) for automatic HA failover, retaining committed writes. Oracle operates the HA group. Its 99.99% monthly-uptime SLA applies under the service's availability terms.
OCI MySQL HeatWave HA is a three-node Group Replication quorum with Paxos consensus, spread across availability or fault domains. A transaction is acknowledged by a quorum before commit, so failover loses no committed write (RPO 0), with a 99.99 percent availability SLA. OCI MySQL HeatWave HA is a three-node Group Replication quorum with Paxos consensus, spread across availability or fault domains. A transaction is acknowledged by a quorum before commit, so failover loses no committed write (RPO 0), with a 99.99 percent availability SLA.

OCI MySQL HeatWave HA is a three-node Group Replication quorum (Paxos) across availability or fault domains; a quorum acknowledges each commit, so failover loses no committed write, with a 99.99% SLA.

#### Parameters, native auth, and the customer key Settable MySQL parameters map to OCI configuration variables. The reusable RDS parameter-group object and parameters outside OCI's allowed list have no counterpart. A customer KMS key maps to a customer-managed key in OCI Vault. OCI MySQL uses native MySQL accounts rather than RDS IAM database authentication. A workload using RDS IAM authentication needs target database credentials.
An RDS parameter group flattens to OCI MySQL configuration variables and a KMS customer key maps to a customer-managed key in OCI Vault. RDS IAM database authentication has no counterpart: OCI MySQL uses native MySQL accounts, with no cloud-IAM database authentication. An RDS parameter group flattens to OCI MySQL configuration variables and a KMS customer key maps to a customer-managed key in OCI Vault. RDS IAM database authentication has no counterpart: OCI MySQL uses native MySQL accounts, with no cloud-IAM database authentication.

MySQL parameters map to OCI MySQL configuration variables and a KMS customer key to an OCI Vault customer-managed key; RDS IAM database auth has no counterpart (native MySQL accounts).

#### Limitations Check these differences when sizing the DB system and planning authentication, connection pooling, and recovery. △ Where RDS for MySQL and OCI MySQL HeatWave diverge * **Cross-region replication needs a separate DB system.** Managed read replicas stay in-region (up to eighteen). A replication channel can maintain a readable database in another region, but it needs separate configuration and can lag behind the source. * **No cloud-IAM database authentication.** OCI MySQL authenticates with native MySQL accounts, so a workload on RDS IAM database auth moves that path to native credentials. * **No managed connection pooler.** The DB system has no RDS-Proxy equivalent; connection pooling is a component to run in front of it. * **Parameters flatten to configuration variables.** Each settable MySQL parameter becomes a configuration value; the group object and any parameter outside OCI's settable list do not cross. * **The engine build and available versions are Oracle's.** The DB system runs the MySQL versions OCI offers, so an instance requiring a version OCI does not publish is reconciled to a supported one before cutover. * **RDS-proprietary extras have no counterpart.** S3 / Lambda integration SQL and Enhanced Monitoring are RDS-specific, and zero-ETL has no Redshift counterpart. #### Other considerations Move data with MySQL Shell dump utilities or a HeatWave inbound replication channel. Validate the copied data before switching connections. Oracle operates the database; the customer controls maintenance and access to OCI's performance tools. These tools use OCI's interface rather than RDS Performance Insights. Configure a separate pooler if needed: an OCI load balancer routes connections but does not pool them. ## On Private Kubernetes | Operation | Area | Support | Depth | Notes | | -------------------------------------------------------------------------------------------------------------- | ------------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------- | | AWS integration SQL: LOAD DATA FROM S3 / SELECT INTO OUTFILE S3, mysql.lambda\_async, mysql.rds\_\* procedures | SQL functions | Out of scope | Full surface | RDS MySQL extension procedures with no counterpart on vanilla MySQL; a query calling them fails | | MySQL client/server protocol (application SQL) | Wire protocol | Supported | Common | same MySQL engine; application SQL is unchanged | #### How it works An `aws_db_instance` with `engine = "mysql"` compiles to a **PerconaXtraDBCluster** custom resource (`pxc.percona.com/v1`), which the Percona operator reconciles into a running Galera cluster on Kubernetes. The workload connects with its unchanged MySQL driver and speaks the MySQL wire protocol straight to the cluster's **HAProxy** endpoint; Tensor9 adds no proxy or protocol translation to this connection. Your application sends SQL over the MySQL protocol; these requests are the **data plane**. Management requests, called the **control plane**, go through the Tensor9 adapter in the customer's appliance. It translates RDS requests for snapshots, restore, replicas, failover, and configuration changes to the Percona operator's Kubernetes resources.
Before: on AWS your app talks to an RDS for MySQL instance over the MySQL protocol. After: in Kubernetes cluster the same app with the same driver talks over the same MySQL protocol to a PerconaXtraDBCluster custom resource, an HAProxy endpoint fronting three pxc pods, with no Tensor9 adapter in the query path. Before: on AWS your app talks to an RDS for MySQL instance over the MySQL protocol. After: in Kubernetes cluster the same app with the same driver talks over the same MySQL protocol to a PerconaXtraDBCluster custom resource, an HAProxy endpoint fronting three pxc pods, with no Tensor9 adapter in the query path.

An aws\_db\_instance (engine = mysql) compiles to a PerconaXtraDBCluster custom resource, a Galera cluster fronted by HAProxy, which the workload reaches over the native MySQL protocol.

#### Architecture Percona XtraDB Cluster uses **Galera** to distribute transaction changes in a common order. Each node checks those changes for conflicts, a process called certification. A successful commit does not mean every other node has already applied the change; reads sent to another node may need synchronization. This differs from RDS Multi-AZ's primary-and-standby architecture. The cluster accepts writes only while a majority of nodes are in contact. Use an odd node count, typically three. If a two-node cluster loses the connection between its nodes, neither can form a majority, so both refuse writes to prevent conflicting changes. An odd node count avoids adding a node without increasing the number of failures the cluster can tolerate.
Three Percona nodes replicate transaction changes using Galera. A majority must remain connected for the cluster to accept writes. Three Percona nodes replicate transaction changes using Galera. A majority must remain connected for the cluster to accept writes.

Galera checks replicated transactions for conflicts. Three nodes can retain a majority after losing one node.

#### Instance sizing and storage `engine_version` selects the Percona Server for MySQL image; `allocated_storage` sizes each pod's PersistentVolumeClaim (PVC); and `instance_class` maps to CPU and memory requests. The selected Kubernetes StorageClass determines volume type and encryption. Each database pod must fit on a cluster node. Storage growth requires explicitly increasing the PVC size on a StorageClass that supports expansion; it does not happen automatically as usage grows.
The RDS instance's fields map into the PerconaXtraDBCluster spec: engine_version becomes the Percona Server for MySQL image tag, allocated_storage becomes the PersistentVolumeClaim size, and instance_class becomes the pods' CPU and memory resource requests. The RDS instance's fields map into the PerconaXtraDBCluster spec: engine_version becomes the Percona Server for MySQL image tag, allocated_storage becomes the PersistentVolumeClaim size, and instance_class becomes the pods' CPU and memory resource requests.

Instance fields configure the cluster: engine\_version to the image tag, allocated\_storage to the PVC size, and instance\_class to the pods' CPU and memory.

#### Backups and point-in-time recovery RDS backup settings map to the Percona operator's backup configuration. The operator schedules full backups to S3-compatible or Azure-compatible object storage and supports on-demand backup objects. **Point-in-time recovery** comes with it: with PITR enabled, the operator runs a **binlog-collector** pod that continuously streams the cluster's binary logs to the same object storage, and a restore replays those logs forward onto the nearest full backup to reach a chosen transaction or timestamp. Set the retention and recovery window on the operator. Backups are stored in the customer's bucket.
RDS backup settings become Percona operator backup configuration. The operator writes full backups to S3-compatible or Azure-compatible object storage. Point-in-time recovery runs a binlog-collector pod that streams binary logs to the same object storage; a restore replays them onto the nearest full backup. RDS backup settings become Percona operator backup configuration. The operator writes full backups to S3-compatible or Azure-compatible object storage. Point-in-time recovery runs a binlog-collector pod that streams binary logs to the same object storage; a restore replays them onto the nearest full backup.

The Percona operator schedules full backups and collects binary logs for point-in-time recovery.

#### Parameters, pooling, and the RDS extras Settable MySQL parameters, such as `innodb_buffer_pool_size` and `max_connections`, map to the cluster configuration. RDS-only parameters and the reusable parameter-group object have no counterpart. HAProxy routes connections without RDS Proxy-style pooling. Configure an application pool or separate pooler if required. The cluster uses native MySQL accounts and Kubernetes monitoring; RDS IAM authentication, S3 and Lambda integration SQL, Enhanced Monitoring, and the RDS-managed CA certificate are unsupported.
A parameter group flattens to the cluster's MySQL configuration and RDS-only parameters drop. HAProxy routes MySQL connections but does not provide RDS Proxy-style connection pooling. IAM database authentication, S3 and Lambda integration SQL, Enhanced Monitoring, and the RDS-managed CA certificate have no counterpart. A parameter group flattens to the cluster's MySQL configuration and RDS-only parameters drop. HAProxy routes MySQL connections but does not provide RDS Proxy-style connection pooling. IAM database authentication, S3 and Lambda integration SQL, Enhanced Monitoring, and the RDS-managed CA certificate have no counterpart.

MySQL parameters map to the cluster configuration. HAProxy routes connections; it does not replace RDS Proxy connection pooling. IAM auth, integration SQL, and Enhanced Monitoring have no counterpart.

#### Operating the cluster The customer's team operates the cluster after migration, including upgrades, backup verification, failover tests, and capacity. There is no managed database availability SLA. Additional nodes and the HAProxy read endpoint serve reads within one region; cross-region reads require a second cluster. The adapter returns RDS request and response formats for management calls and executes those calls on the Percona operator's resources. Moving existing data and operating the cluster are separate tasks.
RDS Multi-AZ is AWS-operated with an availability SLA. The Percona XtraDB Cluster is a self-operated Galera quorum on Kubernetes with no vendor SLA, and cross-region reads need a second cluster. RDS Multi-AZ is AWS-operated with an availability SLA. The Percona XtraDB Cluster is a self-operated Galera quorum on Kubernetes with no vendor SLA, and cross-region reads need a second cluster.

RDS is AWS-operated with an availability SLA; the Percona cluster is self-operated on Kubernetes with no vendor SLA, and cross-region reads need a second cluster.

#### Limitations Check the operating requirements and RDS-specific gaps before choosing a self-managed cluster. △ Where RDS for MySQL and Percona XtraDB Cluster diverge * **No vendor availability SLA.** The cluster is self-operated on Kubernetes, so unlike RDS Multi-AZ there is no cloud vendor SLA to point to; availability is a property of how the team runs the cluster. * **No cross-region read scale-out.** The appliance runs one cluster, so live cross-region reads or a standby region require a second cluster and a replication link. * **A majority must remain connected.** Three nodes tolerate one unavailable node; losing the majority interrupts normal database service until quorum is restored. * **Synchronous replication has a per-write cost.** Write-set certification across the cluster adds coordination to every commit, so a write-heavy workload tuned for a single RDS writer validates throughput on the cluster before cutover. * **Capacity is cluster-bound.** A pod fits a Kubernetes node and a PersistentVolumeClaim sizes on its StorageClass; there is no RDS instance-type catalog and no usage-triggered storage autoscaling. * **RDS-specific settings have no counterpart.** IAM database authentication, S3 / Lambda integration SQL, Enhanced Monitoring, and the RDS-managed CA certificate are RDS-specific; the cluster uses native MySQL accounts and Kubernetes-native observability. #### Other considerations Move data with `mysqldump` or replication, then validate the copy before switching connections. Test backup restoration and failover as part of cutover. The customer's team continues to operate the cluster, including upgrades, monitoring, and capacity. [Service Catalog](/service-adapters/catalog). # RDS PostgreSQL Source: https://docs.tensor9.com/service-adapters/aws/databases-storage/rds-postgresql AWS RDS PostgreSQL. Upstream PostgreSQL on instances RDS provisions and patches, keeping extensions, roles and logical replication, with automated backups and Multi-AZ failover. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of RDS PostgreSQL with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation #### Runtime surface | Capability | RDS PostgreSQL | Google Cloud | Azure | OCI | | ------------ | -------------- | ------------ | ----- | ---- | | API coverage | full | high | high | high | #### Management surface | Capability | RDS PostgreSQL | Google Cloud | Azure | OCI | | ------------------------------------------------- | -------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Snapshot / restore + PITR | Yes | Yes | Yes | Partial - OCI supports point-in-time recovery with a configured policy, retained WAL and periodic backups, creating a new database system within the active recovery window; the documented adapter mapping does not promise AWS point-in-time restore translation | | Zero-ETL integration · Redshift | Yes | Partial - replicates to BigQuery (Datastream); Redshift itself is not served | Partial - replicates to Microsoft Fabric (OneLake); Redshift itself is not served | No | | Read replicas · in-region | Yes | Yes | Yes | Yes | | Read replicas · cross-region | Yes | Yes | Yes | No - OCI supports readable warm standby systems in up to three disaster recovery regions, using asynchronous replication; the documented adapter mapping does not promise AWS cross-region replica or Aurora global-cluster semantics | | Managed connection pooling · RDS Proxy | Yes | Yes | Yes | Yes | | Trusted Language Extensions · pg\_tle | Yes | No | No | No | | Query performance insights · Performance Insights | Yes | Partial - target-native performance tools; access is controlled by the customer | Partial - target-native performance tools; access is controlled by the customer | Partial - target-native performance tools; access is controlled by the customer | #### Limits | Capability | RDS PostgreSQL | Google Cloud | Azure | OCI | | ------------------------------------------- | -------------- | ------------ | ----------- | --------------- | | Maximum storage | 64 TiB | 64 TiB | 64 TiB | 32 TiB | | vCPU range · smallest to largest instance | 2-384 | 1-128 | 1-192 | 2-128 (64 OCPU) | | Memory range · smallest to largest instance | 1-4,096 GiB | 0.6-864 GiB | 2-1,832 GiB | 16-1,024 GiB | | Storage autoscaling | Yes | Yes | Yes | Yes | ### Infrastructure-only adaptation #### Runtime surface | Capability | RDS PostgreSQL | Google Cloud | Azure | OCI | Private Kubernetes | | ------------ | -------------- | ------------ | ----- | ---- | ------------------ | | API coverage | full | high | high | high | high | #### Management surface | Capability | RDS PostgreSQL | Google Cloud | Azure | OCI | Private Kubernetes | | ------------------------------------------------- | -------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | | Snapshot / restore + PITR | Yes | Yes | Yes | Partial - OCI supports point-in-time recovery with a configured policy, retained WAL and periodic backups, creating a new database system within the active recovery window; the documented adapter mapping does not promise AWS point-in-time restore translation | Yes | | Zero-ETL integration · Redshift | Yes | Partial - replicates to BigQuery (Datastream); Redshift itself is not served | Partial - replicates to Microsoft Fabric (OneLake); Redshift itself is not served | No | No | | Read replicas · in-region | Yes | Yes | Yes | Yes | Yes | | Read replicas · cross-region | Yes | Yes | Yes | No - OCI supports readable warm standby systems in up to three disaster recovery regions, using asynchronous replication; the documented adapter mapping does not promise AWS cross-region replica or Aurora global-cluster semantics | No | | Managed connection pooling · RDS Proxy | Yes | Yes | Yes | Yes | Yes | | Trusted Language Extensions · pg\_tle | Yes | No | No | No | Yes | | Query performance insights · Performance Insights | Yes | Partial - target-native performance tools; access is controlled by the customer | Partial - target-native performance tools; access is controlled by the customer | Partial - target-native performance tools; access is controlled by the customer | Partial - target-native performance tools; access is controlled by the customer | #### Limits | Capability | RDS PostgreSQL | Google Cloud | Azure | OCI | Private Kubernetes | | ------------------------------------------- | -------------- | ------------ | ----------- | --------------- | --------------------------------------------------------------------- | | Maximum storage | 64 TiB | 64 TiB | 64 TiB | 32 TiB | limited by persistent volume capacity | | vCPU range · smallest to largest instance | 2-384 | 1-128 | 1-192 | 2-128 (64 OCPU) | limited by Kubernetes node resources | | Memory range · smallest to largest instance | 1-4,096 GiB | 0.6-864 GiB | 2-1,832 GiB | 16-1,024 GiB | limited by Kubernetes node resources | | Storage autoscaling | Yes | Yes | Yes | Yes | No - increase the volume size explicitly; it does not grow with usage | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------- | ------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------- | | AWS integration SQL: aws\_s3.\*, aws\_lambda.invoke, rds\_tools | SQL functions | Out of scope | Full surface | RDS extension functions with no counterpart on vanilla PostgreSQL; a query calling them fails | | pgwire protocol (application SQL) | Wire protocol | Supported | Common | same PostgreSQL engine; application SQL is unchanged | #### How it works Your application's PostgreSQL driver connects directly to Cloud SQL for PostgreSQL. These database requests are the **data plane**. The driver and PostgreSQL wire protocol stay unchanged, and Tensor9 does not proxy or translate these queries. Management requests, such as snapshots, restore, replicas, failover, and configuration changes, form the **control plane**. Tensor9 serves the RDS API in the customer's appliance and translates those requests to the target's management API. The sections below describe which operations have equivalents and which return errors.
Database requests (SQL over pgwire) connect directly to Cloud SQL. The Tensor9 service adapter translates RDS API management calls to Cloud SQL admin API. Database requests (SQL over pgwire) connect directly to Cloud SQL. The Tensor9 service adapter translates RDS API management calls to Cloud SQL admin API.

Queries reach the database directly; only the management calls are translated.

#### Database connections RDS for PostgreSQL and Cloud SQL run PostgreSQL. Your SQL, drivers, object-relational mappers (ORMs), prepared statements, data types, and transaction semantics remain unchanged. Your application connects directly to the database. RDS adds SQL helper functions for importing and exporting through S3 and invoking Lambda from a query. These AWS-specific functions have no equivalent on the target. Queries that call them return an error. #### Database management At runtime your application and operational tooling keep making the same RDS calls they make today: describe the instance, take a snapshot, restore to a point in time, add or promote a read replica, trigger a failover, stop and start, change the instance class or a parameter. The adapter accepts RDS requests, returns RDS response formats, and performs the supported operations on Cloud SQL: on-demand backups and restores, point-in-time restore, read replicas in-region and cross-region, manual failover, stop / start, and machine-type, storage, and database-flag updates. With Max, your tooling uses CreateDBInstance and DeleteDBInstance through the RDS adapter to create and remove a database on Cloud SQL. Tensor9 retains durable logical database state and reconciles it with the managed service: accepted requests are recorded, then the target resources are created or removed asynchronously. Use DescribeDBInstances to follow status and obtain the database endpoint. Your application's SQL connection goes directly to the database. With the Infrastructure-only alternative, Tensor9 compiles the declared database infrastructure into native Cloud SQL resources. You manage those resources through the target's own tools; serving runtime RDS API requests requires Max.
The Tensor9 service adapter handles RDS management requests using Cloud SQL admin API and returns RDS responses. The Tensor9 service adapter handles RDS management requests using Cloud SQL admin API and returns RDS responses.

Tensor9 translates RDS management requests to the target API and returns RDS responses.

#### Limitations △ Where RDS and Cloud SQL stay different * **Deletion is asynchronous and skips a final snapshot.** DeleteDBInstance requires SkipFinalSnapshot=true and does not retain automated backups. Deletion protection must be disabled. The database remains in the deleting state until its native resources are gone; a failed teardown reports failure rather than successful removal. * **Subnet groups place the private endpoint.** An explicit DB subnet group retains adapted subnets from one VPC spanning at least two availability zones. Tensor9 selects a subnet deterministically and places the private connection endpoint there; Google manages the database compute in its own service network. The selected subnet must match the configured Google Cloud project, region, network and IPv4 range. Readiness requires an accepted private connection and, for public access, the public address. Public access with an explicit group also requires pre-existing private services access on that VPC and the configured client allowlist. Tensor9 does not create shared private services access, translate AWS security groups, or provide AWS split-horizon DNS. Without an explicit group, the configured network applies; Tensor9 does not create an AWS default subnet group. * **Event subscriptions are not served.** RDS event notifications publish to SNS; Cloud Monitoring is a different model with no direct counterpart. * **Parameter groups retain their logical identity.** Tensor9 retains named parameter groups and their overrides. Supported explicit PostgreSQL parameters become Cloud SQL database flags; the group object itself stays with Tensor9 rather than becoming a native object on Cloud SQL. The group family and PostgreSQL version must match, and target limits on allowed values and restart requirements still apply. RDS-specific parameters and settings outside the target's settable list are rejected. Default groups are read-only. Changes marked pending-reboot wait for RebootDBInstance; immediate changes that require a restart are rejected. An attached group cannot be deleted or replaced, and source formulas are not supported. * **RDS-specific settings have no counterpart.** The CA certificate, Enhanced Monitoring, processor features, and Kerberos / AD domain join are RDS-specific. #### Other considerations **Migration.** Move existing data with a logical dump and restore, or use logical replication to reduce the cutover pause. Check the selected PostgreSQL version and required extensions, then switch application connections after validating the copied data. **Operations.** The customer owns the Cloud SQL service in their Google Cloud: maintenance windows, quotas, and pricing are Google's. **Features and capacity.** The comparison table on this page lists database features and high-availability differences. Check the target service's pricing and capacity limits when sizing the deployment. ## On Azure | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------- | ------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------- | | AWS integration SQL: aws\_s3.\*, aws\_lambda.invoke, rds\_tools | SQL functions | Out of scope | Full surface | RDS extension functions with no counterpart on vanilla PostgreSQL; a query calling them fails | | pgwire protocol (application SQL) | Wire protocol | Supported | Common | same PostgreSQL engine; application SQL is unchanged | #### How it works Your application's PostgreSQL driver connects directly to Azure Database for PostgreSQL Flexible Server. These database requests are the **data plane**. The driver and PostgreSQL wire protocol stay unchanged, and Tensor9 does not proxy or translate these queries. Management requests, such as snapshots, restore, replicas, failover, and configuration changes, form the **control plane**. Tensor9 serves the RDS API in the customer's appliance and translates those requests to the target's management API. The sections below describe which operations have equivalents and which return errors.
Database requests (SQL over pgwire) connect directly to PostgreSQL Flexible Server. The Tensor9 service adapter translates RDS API management calls to Azure management API. Database requests (SQL over pgwire) connect directly to PostgreSQL Flexible Server. The Tensor9 service adapter translates RDS API management calls to Azure management API.

Queries reach the database directly; only the management calls are translated.

#### Database connections Azure Database for PostgreSQL Flexible Server runs the same PostgreSQL engine as RDS. SQL, drivers, object-relational mappers (ORMs), prepared statements, data types, and transaction semantics remain unchanged. Your application connects directly to the server. RDS adds SQL helper functions for importing and exporting through S3 and invoking Lambda from a query. These AWS-specific functions have no equivalent on the target. Queries that call them return an error. #### Database management At runtime your application and operational tooling keep making the same RDS calls they make today: describe the instance, take a snapshot, restore to a point in time, add or promote a read replica, trigger a failover, stop and start, change the instance class or a parameter. The adapter accepts RDS requests, returns RDS response formats, and performs the supported operations on the Flexible Server: on-demand backups and restores, point-in-time restore, read replicas in-region and cross-region, manual failover, stop / start, and compute, storage, and server-parameter updates. With Max, your tooling uses CreateDBInstance and DeleteDBInstance through the RDS adapter to create and remove a database on Azure PostgreSQL Flexible Server. Tensor9 retains durable logical database state and reconciles it with the managed service: accepted requests are recorded, then the target resources are created or removed asynchronously. Use DescribeDBInstances to follow status and obtain the database endpoint. Your application's SQL connection goes directly to the database. With the Infrastructure-only alternative, Tensor9 compiles the declared database infrastructure into native Azure PostgreSQL Flexible Server resources. You manage those resources through the target's own tools; serving runtime RDS API requests requires Max.
The Tensor9 service adapter handles RDS management requests using Azure management API and returns RDS responses. The Tensor9 service adapter handles RDS management requests using Azure management API and returns RDS responses.

Tensor9 translates RDS management requests to the target API and returns RDS responses.

#### Limitations △ Where RDS and PostgreSQL Flexible Server stay different * **Event subscriptions are not served.** RDS event notifications publish to SNS; Azure Monitor is a different model with no direct counterpart. * **Parameter groups retain their logical identity.** Tensor9 retains named parameter groups and their overrides. Supported explicit PostgreSQL parameters become Azure server parameters; the group object itself stays with Tensor9 rather than becoming a native object on the Flexible Server. The group family and PostgreSQL version must match, and target limits on allowed values and restart requirements still apply. RDS-specific parameters and settings outside the target's settable list are rejected. * **RDS-specific settings have no counterpart.** The CA certificate, Enhanced Monitoring, processor features, and Kerberos / AD domain join are RDS-specific. #### Other considerations **Migration.** Move existing data with a logical dump and restore, or use logical replication to reduce the cutover pause. Check the selected PostgreSQL version and required extensions, then switch application connections after validating the copied data. **Operations.** The customer owns the Flexible Server in their Azure subscription: maintenance windows, quotas, and pricing are Microsoft's. **Features and capacity.** The comparison table on this page lists database features and high-availability differences. Check the target service's pricing and capacity limits when sizing the deployment. ## On OCI | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------- | ------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------- | | AWS integration SQL: aws\_s3.\*, aws\_lambda.invoke, rds\_tools | SQL functions | Out of scope | Full surface | RDS extension functions with no counterpart on vanilla PostgreSQL; a query calling them fails | | pgwire protocol (application SQL) | Wire protocol | Supported | Common | same PostgreSQL engine; application SQL is unchanged | #### How it works Your application's PostgreSQL driver connects directly to OCI Database with PostgreSQL. These database requests are the **data plane**. The driver and PostgreSQL wire protocol stay unchanged, and Tensor9 does not proxy or translate these queries. Management requests, such as snapshots, restore, replicas, failover, and configuration changes, form the **control plane**. Tensor9 serves the RDS API in the customer's appliance and translates those requests to the target's management API. The sections below describe which operations have equivalents and which return errors.
Database requests (SQL over pgwire) connect directly to OCI PostgreSQL. The Tensor9 service adapter translates RDS API management calls to OCI management API. Database requests (SQL over pgwire) connect directly to OCI PostgreSQL. The Tensor9 service adapter translates RDS API management calls to OCI management API.

Queries reach the database directly; only the management calls are translated.

#### Database connections OCI Database with PostgreSQL runs the same engine as RDS for PostgreSQL. SQL, drivers, object-relational mappers (ORMs), prepared statements, data types, and transaction semantics remain unchanged. Your application connects directly to the database. RDS adds SQL helper functions for importing and exporting through S3 and invoking Lambda from a query. These AWS-specific functions have no equivalent on the target. Queries that call them return an error. #### Database management At runtime your application and operational tooling keep making the same RDS calls they make today. The adapter accepts RDS requests, returns RDS response formats, and performs the supported operations on OCI where a counterpart exists: on-demand backups and restore-from-backup, in-region read replicas, stop / start, and compute, storage, and configuration updates. The documented adapter mapping does not promise translation of every native OCI capability into RDS semantics; the limitations below distinguish those boundaries. Each unsupported operation returns an error. With Max, your tooling uses CreateDBInstance and DeleteDBInstance through the RDS adapter to create and remove a database on OCI PostgreSQL. Tensor9 retains durable logical database state and reconciles it with the managed service: accepted requests are recorded, then the target resources are created or removed asynchronously. Use DescribeDBInstances to follow status and obtain the database endpoint. Your application's SQL connection goes directly to the database. With the Infrastructure-only alternative, Tensor9 compiles the declared database infrastructure into native OCI PostgreSQL resources. You manage those resources through the target's own tools; serving runtime RDS API requests requires Max.
The Tensor9 service adapter handles RDS management requests using OCI management API and returns RDS responses. The Tensor9 service adapter handles RDS management requests using OCI management API and returns RDS responses.

Tensor9 translates RDS management requests to the target API and returns RDS responses.

#### Limitations △ Where RDS and OCI PostgreSQL stay different * **Parameter groups retain their logical identity.** Tensor9 retains named parameter groups and their overrides. Supported explicit PostgreSQL parameters become OCI PostgreSQL settings; the group object itself stays with Tensor9 rather than becoming a native object on OCI PostgreSQL. The group family and PostgreSQL version must match, and target limits on allowed values and restart requirements still apply. RDS-specific parameters and settings outside the target's settable list are rejected. * **Native point-in-time recovery needs a configured policy.** OCI retains WAL and periodic backups under a point-in-time recovery policy. Recovery creates a new database system at a timestamp within the active recovery window. The documented adapter mapping does not promise AWS point-in-time restore translation. * **Native local failover is separate from AWS failover translation.** OCI exposes FailoverDbSystem for user-initiated failover to an existing local replica. The documented adapter mapping does not promise translation of the AWS failover operation. * **Native cross-region standbys do not establish the AWS replica mapping.** OCI supports readable warm standby systems in up to three disaster recovery regions. Replication is asynchronous and can lag; promotion and switchover are manual, with no automatic cross-region failover. Restore is not supported for either the primary or warm standby while configured for this replication. The documented adapter mapping does not promise AWS cross-region replica semantics. * **No in-place major-version upgrade.** Major PostgreSQL upgrades on OCI are create-new-and-migrate. * **The shared RDS gaps apply here too.** Event subscriptions, rds.\* parameters, and RDS-specific settings (the CA certificate, Enhanced Monitoring, Kerberos / AD) have no counterpart, as on every target. #### Other considerations **Migration.** Move existing data with a logical dump and restore, or use logical replication to reduce the cutover pause. Check the selected PostgreSQL version and required extensions, then switch application connections after validating the copied data. **Operations.** The customer owns the database service in their OCI tenancy: maintenance windows, quotas, and pricing are Oracle's. **Features and capacity.** The comparison table on this page lists database features and high-availability differences. Check the target service's pricing and capacity limits when sizing the deployment. ## On Private Kubernetes | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------- | ------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------- | | AWS integration SQL: aws\_s3.\*, aws\_lambda.invoke, rds\_tools | SQL functions | Out of scope | Full surface | RDS extension functions with no counterpart on vanilla PostgreSQL; a query calling them fails | | pgwire protocol (application SQL) | Wire protocol | Supported | Common | same PostgreSQL engine; application SQL is unchanged | #### How it works Your application's PostgreSQL driver connects directly to CloudNativePG. These database requests are the **data plane**. The driver and PostgreSQL wire protocol stay unchanged, and Tensor9 does not proxy or translate these queries. Management requests, such as snapshots, restore, replicas, failover, and configuration changes, form the **control plane**. Tensor9 serves the RDS API in the customer's appliance and translates those requests to the target's management API. The sections below describe which operations have equivalents and which return errors.
Database requests (SQL over pgwire) connect directly to CloudNativePG. The Tensor9 service adapter translates RDS API management calls to CloudNativePG resources. Database requests (SQL over pgwire) connect directly to CloudNativePG. The Tensor9 service adapter translates RDS API management calls to CloudNativePG resources.

Queries reach the database directly; only the management calls are translated.

#### Database connections CloudNativePG runs PostgreSQL as a replicated cluster on Kubernetes. Your SQL, drivers, object-relational mappers (ORMs), prepared statements, data types, and transaction semantics remain unchanged. Your application connects directly to the cluster. RDS adds SQL helper functions for importing and exporting through S3 and invoking Lambda from a query. These AWS-specific functions have no equivalent on the target. Queries that call them return an error. #### Database management At runtime your application and operational tooling keep making the same RDS calls they make today. The adapter accepts RDS requests, returns RDS response formats, and performs the supported operations through CloudNativePG's Kubernetes resources: backups and restores onto its backup and recovery objects, point-in-time restore onto its native PITR, read replicas onto the cluster's instance count, manual failover onto replica promotion, stop / start onto hibernating and resuming the cluster while retaining its storage, and parameter changes onto the cluster's PostgreSQL configuration. Managed connection pooling (the role RDS Proxy plays) is served by CloudNativePG's built-in pooler. When your product is deployed into the customer environment, Tensor9 compiles the database your stack already declares into the equivalent CloudNativePG resources and sets the endpoint and credentials in your application's configuration. The control-plane adapter then covers the management calls your running system makes.
The Tensor9 service adapter handles RDS management requests using CloudNativePG resources and returns RDS responses. The Tensor9 service adapter handles RDS management requests using CloudNativePG resources and returns RDS responses.

Tensor9 translates RDS management requests to the target API and returns RDS responses.

#### Limitations △ Where RDS and CloudNativePG stay different * **No cross-region read scale-out.** The appliance runs a single CloudNativePG cluster; a second region would need a second appliance. * **Events are Kubernetes-native.** RDS event notifications publish to SNS; here the equivalents are Kubernetes events and Prometheus alerts, a different model. * **Parameter groups become individual settings.** Each PostgreSQL parameter translates to the cluster's configuration; the group object itself and rds.\* parameters are not supported on the target. * **RDS-specific settings have no counterpart.** The CA certificate, Enhanced Monitoring, processor features, and Kerberos / AD domain join are RDS-specific. #### Other considerations **Migration.** Move existing data with a logical dump and restore, or use logical replication to reduce the cutover pause. Check the selected PostgreSQL version and required extensions, then switch application connections after validating the copied data. **Operations.** CloudNativePG is self-operated: after cutover the customer's team runs the cluster (backups, upgrades, failover drills), with no cloud database vendor behind it. **Features and capacity.** The comparison table on this page lists database features and high-availability differences. Check the target service's pricing and capacity limits when sizing the deployment. [Service Catalog](/service-adapters/catalog). # S3 Source: https://docs.tensor9.com/service-adapters/aws/databases-storage/s3 AWS S3. Object storage addressed by bucket and key, with versioning, lifecycle rules and strong read-after-write consistency on every object. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of S3 with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | S3 | Google Cloud | Azure | OCI | | ------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | Read consistency · after write | strong read-after-write | strong read-after-write | strong read-after-write | strong read-after-write | | Durability · designed-for | 11 nines (3 AZs) | 11 nines (erasure-coded, all classes) | up to 16 nines (GRS) | 11 nines (3 ADs / 3 FDs) | | Versioning | Yes | Yes - object versioning (noncurrent versions retained) | Yes - blob versioning (enabled on the account) | Yes - bucket versioning (Enabled / Suspended) | | Lifecycle policies | Yes | Partial - prefix-scoped expiration by relative age; transitions only to STANDARD\_IA / ONEZONE\_IA | - | Yes - archive / infrequent-access transitions, delete, and abort-incomplete-multipart | | Event notifications | Yes - one config fans out to SNS / SQS / Lambda / EventBridge | Partial - one Pub/Sub topic per notification config | Yes - one Event Grid subscription → Queue / Function / Event Hub / Service Bus / webhook | Partial - bucket object-events → an OCI Events rule → ONS / Streaming / Functions | | Server-side encryption | Yes - SSE-S3 / SSE-KMS / SSE-C / DSSE-KMS | Partial - Google-managed, customer-managed (Cloud KMS CMEK), and customer-supplied (CSEK) | - | Yes - Oracle-managed, customer-managed (Vault CMK), and SSE-C | | Replication | Yes - SRR / CRR rule engine, asynchronous cross-bucket copy | Partial - dual / multi-region placement + turbo-replication RPO; no per-rule engine | - | - | | Object lock / WORM | Yes - GOVERNANCE / COMPLIANCE modes + independent legal hold | Yes - native Bucket Lock retention and holds; runtime S3 retention/legal-hold calls are not served | Partial - native container immutability; runtime S3 retention/legal-hold calls are not served; legal-hold is not Terraform-settable | Partial - native lockable retention rules; runtime S3 retention/legal-hold calls are not served | | Storage tiers | Yes - per-object classes (Standard … Deep Archive) + Intelligent-Tiering | Yes - Standard / Nearline / Coldline / Archive + Autoclass | - | - | | CORS | Yes | Yes - native per-bucket CORS configuration; runtime S3 CORS calls are not served | Yes - native account-level Blob CORS; runtime S3 CORS calls are not served; no CORS on the website endpoint | No - fixed allow-all, not configurable | | Static website hosting | Yes | Partial - index / 404 website config, but no S3-style per-bucket website endpoint host | Partial - index / error documents on the account; no request-routing rules, no CORS | No | | Requester pays | Yes | Yes - native Requester Pays flag; runtime S3 request-payment configuration is not served | No | No | | Object tagging · granularity | per-object tags (usable in lifecycle / IAM conditions) | stored in reserved object metadata | - | no object tagging; native defined/freeform tags apply only to buckets | | Multipart upload | Yes | Partial - served by compose-staging; ListMultipartUploads and UploadPartCopy decline | Yes - mapped to Put Block / Put Block List | - | | Presigned access · time-boxed URLs | presigned URLs (SDK) | outside the adapter (GCS V4 signed URLs, out of band) | outside the adapter (Azure SAS tokens, out of band) | - | | API coverage | full | partial | partial | partial | | Bucket model · structural | the bucket is the unit of configuration | - | two-level account → container → blob | - | | Lifecycle policies | Yes - tier transitions + expiration + noncurrent + abort-incomplete-multipart | - | Partial - tier transitions + expiration + noncurrent, but no abort-incomplete-multipart rule | - | | Server-side encryption | Yes - SSE-S3 / SSE-KMS / SSE-C / DSSE-KMS + Bucket Keys | - | Yes - Microsoft-managed, customer-managed (Key Vault CMK), infrastructure double-encryption, and per-request customer-provided keys | - | | Replication | Yes | - | Yes - object replication (async, container → container) | Yes - cross-region replication policy | | Storage tiers | Yes - per-object classes (Standard … Deep Archive) + Intelligent-Tiering + One-Zone | - | Partial - Hot / Cool / Cold / Archive access tiers; no One-Zone (single-AZ) analog | - | | Object tagging · granularity | per-object tags (Terraform-settable, usable in lifecycle / IAM conditions) | - | blob index tags (filter / query only; not Terraform-settable) | - | | Integrity / ETag · translator ceiling | ETag = content-MD5 for single-part uploads | - | same: content-MD5, synthesized and persisted | - | | Presigned access · time-boxed URLs | presigned URLs (SDK, 7-day max) | - | - | outside the adapter (OCI Pre-Authenticated Requests, out of band) | ### Infrastructure-only adaptation | Capability | S3 | Private Kubernetes | | ---------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- | | Read consistency · after write | strong read-after-write | strict read-after-write + list-after-write | | Durability · designed-for | 11 nines (3 AZs) | deployment-specific erasure-coding protection | | Versioning | Yes | Yes - bucket versioning (Enabled / Suspended) | | Lifecycle policies | Yes | Yes - expiration, noncurrent-version expiration, and abort-incomplete-multipart | | Event notifications | Yes - one config fans out to SNS / SQS / Lambda / EventBridge | Partial - bucket events to webhook / Kafka / AMQP / NATS / Redis / PostgreSQL / MySQL / Elasticsearch targets | | Server-side encryption | Yes - SSE-S3 / SSE-KMS / SSE-C / DSSE-KMS | Partial - SSE-S3 and SSE-KMS (via KES) plus SSE-C | | Replication | Yes - same-region and cross-region bucket replication rules | Yes - active-active bucket replication + site replication (whole deployment, incl. IAM / users / policies) | | Object lock / WORM | Yes - GOVERNANCE / COMPLIANCE modes + retention + independent legal hold | Partial - native to MinIO, but the adapter does not forward the runtime retention / legal-hold verbs | | Storage tiers | Yes - per-object classes (Standard … Deep Archive) + Intelligent-Tiering | No | | CORS | Yes | No - Enterprise / AIStor-gated; Community returns NotImplemented | | Static website hosting | Yes | No | | Requester pays | Yes | No | | Object tagging | Yes | Yes - true per-object tags (get / put / delete) | | Multipart upload | Yes | Yes | | Presigned access · time-boxed URLs | presigned URLs (SDK) | outside the adapter (MinIO presign / `mc share`, out of band) | | API coverage | full | partial | ## On Google Cloud | Capability | Area | Support | Required tier | Operations | Notes | | --------------------------------------------------------------- | -------------- | ------------ | ------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Bucket policy, CORS, website, notification and replication APIs | Bucket config | Out of scope | - | - | These runtime S3 configuration APIs are outside the listed endpoint. Native target configuration and deployment mappings are described separately. | | Bucket versioning, tagging, lifecycle and encryption | Bucket config | Partial | Max | - | At Max, runtime S3 calls retain bucket settings and apply supported target mappings. Lifecycle filters, storage classes and account-level settings retain the limits described below. Simpler request forwarding configures these settings at deployment. | | Presigned URLs | Other features | Out of scope | - | - | S3 presigned URLs aren't honored: the adapter re-signs to the backend and does not validate the client's presigned signature, so its expiry/scope guarantees aren't enforced. | | Operation | Area | Support | Depth | Notes | | ----------------------- | -------------- | -------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | GetObjectAcl | Access control | Out of scope | Full surface | Object ACL requests are rejected. Configure access through target permissions or deployment configuration. | | PutObjectAcl | Access control | Out of scope | Full surface | Object ACL requests are rejected. Configure access through target permissions or deployment configuration. | | CreateBucket | Bucket config | Supported | Most usage | The adapter serves this bucket operation. | | DeleteBucket | Bucket config | Supported | Most usage | The adapter serves this bucket operation. | | HeadBucket | Bucket config | Supported | Most usage | The adapter serves this bucket operation. | | AbortMultipartUpload | Multipart | Supported | Most usage | served by compose-staging rather than by forwarding, because GCS's own XML multipart wire is not S3-identical: each part streams to a temp object and CompleteMultipartUpload composes them with the GCS JSON compose API, so an unmodified SDK's multipart upload works end to end | | CompleteMultipartUpload | Multipart | Supported | Most usage | served by compose-staging rather than by forwarding, because GCS's own XML multipart wire is not S3-identical: each part streams to a temp object and CompleteMultipartUpload composes them with the GCS JSON compose API, so an unmodified SDK's multipart upload works end to end | | CreateMultipartUpload | Multipart | Supported | Most usage | served by compose-staging rather than by forwarding, because GCS's own XML multipart wire is not S3-identical: each part streams to a temp object and CompleteMultipartUpload composes them with the GCS JSON compose API, so an unmodified SDK's multipart upload works end to end | | ListMultipartUploads | Multipart | Out of scope | Most usage | compose-staging keys an upload's parts under a per-key prefix and keeps no bucket-wide uploadId registry, so a bucket's in-progress uploads cannot be enumerated; the Azure translator declines it for the same reason | | ListParts | Multipart | Supported | Most usage | served by compose-staging rather than by forwarding, because GCS's own XML multipart wire is not S3-identical: each part streams to a temp object and CompleteMultipartUpload composes them with the GCS JSON compose API, so an unmodified SDK's multipart upload works end to end | | UploadPart | Multipart | Supported | Most usage | served by compose-staging rather than by forwarding, because GCS's own XML multipart wire is not S3-identical: each part streams to a temp object and CompleteMultipartUpload composes them with the GCS JSON compose API, so an unmodified SDK's multipart upload works end to end | | UploadPartCopy | Multipart | Out of scope | Full surface | the copy-source range form is declined on this backend | | GetObjectLegalHold | Object lock | Out of scope | Full surface | the object-lock sub-resources are rejected | | GetObjectRetention | Object lock | Out of scope | Full surface | the object-lock sub-resources are rejected | | PutObjectLegalHold | Object lock | Out of scope | Full surface | the object-lock sub-resources are rejected | | PutObjectRetention | Object lock | Out of scope | Full surface | the object-lock sub-resources are rejected | | CopyObject | Objects | Supported | Most usage | server-side copy via x-amz-copy-source | | DeleteObject | Objects | Supported | Common | - | | DeleteObjects | Objects | Supported | Common | batch delete is served: GCS has no \ batch endpoint, so the adapter fans the key list out to per-key deletes under a time budget and renders the S3 \/\ result rows; a missing key is a success, matching S3's own idempotency | | GetObject | Objects | Supported | Common | - | | HeadObject | Objects | Supported | Common | - | | ListObjectVersions | Objects | Supported | Most usage | The adapter renders S3 version listings from the GCS JSON API. VersionId identifies the native generation; delete markers record versionless deletes on versioned buckets. | | ListObjects | Objects | Supported | Common | the v1 listing form | | ListObjectsV2 | Objects | Supported | Common | - | | PutObject | Objects | Supported | Common | - | | GetObjectAttributes | Other features | Out of scope | Full surface | the ?attributes query is not served; use HeadObject | | RestoreObject | Other features | Out of scope | Full surface | the ?restore query is rejected | | SelectObjectContent | Other features | Out of scope | Full surface | the ?select query is rejected | | DeleteObjectTagging | Tagging | Adapter-served | Most usage | Cloud Storage has no object-tag primitive of its own, so the tag set is stored in reserved custom metadata on the object and rendered back as an S3 tagging document on read. Put replaces the whole set, matching S3. Version-addressed tagging requests are declined; native lifecycle and IAM conditions cannot use these reserved metadata tags | | GetObjectTagging | Tagging | Adapter-served | Most usage | Cloud Storage has no object-tag primitive of its own, so the tag set is stored in reserved custom metadata on the object and rendered back as an S3 tagging document on read. Put replaces the whole set, matching S3. Version-addressed tagging requests are declined; native lifecycle and IAM conditions cannot use these reserved metadata tags | | PutObjectTagging | Tagging | Adapter-served | Most usage | Cloud Storage has no object-tag primitive of its own, so the tag set is stored in reserved custom metadata on the object and rendered back as an S3 tagging document on read. Put replaces the whole set, matching S3. Version-addressed tagging requests are declined; native lifecycle and IAM conditions cannot use these reserved metadata tags | #### S3 requests in the customer environment The application continues using the S3 API. The Tensor9 adapter runs in the customer environment and sends object requests to Google Cloud Storage. It also serves bucket creation, deletion, listing, and configuration through a persistent bucket store. Google Cloud Storage stores the objects; Tensor9 operates the adapter and its bucket-management service. #### Request flow Object requests use the target storage service. Bucket settings persist separately and are applied to the target resource.
The S3 application calls the Tensor9 adapter. Object requests go to the target object store. Bucket settings persist in the adapter's store and a configuration worker applies them to the target. The S3 application calls the Tensor9 adapter. Object requests go to the target object store. Bucket settings persist in the adapter's store and a configuration worker applies them to the target.
#### Object requests Core GetObject, HeadObject, PutObject, CopyObject, DeleteObject, and listing requests use GCS's S3-interoperable XML endpoint with target credentials. Batch deletion sends per-object deletes and returns S3 Deleted/Error entries. Object tags are encoded in reserved GCS object metadata and reconstructed as S3 tagging documents. The object data stays in GCS; the bucket store does not hold object bodies. #### Bucket creation and configuration Each S3 bucket has a durable record containing its name, owner, requested configuration, and target reference. For a bucket named `bucket-name`, the S3-facing identifier is `arn:aws:s3:::bucket-name`; the target resource is a GCS bucket. `ListBuckets` returns the caller's buckets, and configuration reads use the stored request state. A configuration worker applies the stored settings to the target. `CreateBucket` waits for the target bucket to be ready before returning success, so an immediate object write does not race bucket creation. `DeleteBucket` waits for removal and returns `BucketNotEmpty` if objects prevent deletion. A lifecycle update also waits for target application. For other configuration changes, a successful read of the new setting describes stored configuration; it does not imply every target-side change has already completed. #### Versions and multipart uploads GCS multipart uploads use temporary objects for individual parts. Completion calls the GCS JSON compose API to form the final object. At Max, the adapter retains part checksums to supply S3's composite multipart ETag and maintains the S3 version history and delete markers that differ from native GCS generations. A deployment that only forwards object requests instead exposes the native generation model and compose ETag. ListMultipartUploads and UploadPartCopy remain outside the listed mapping. #### Target configuration Bucket versioning, tagging, lifecycle and encryption settings are managed through runtime S3 calls. Lifecycle translation supports the rules listed in the feature comparison: prefix-scoped, relative-age expiration and transitions to STANDARD\_IA or ONEZONE\_IA, both mapped to Nearline. Unsupported date, filter, noncurrent-version and archival-transition forms return an error rather than a different retention or cost policy. Encryption uses Google-managed keys, Cloud KMS customer-managed keys or customer-supplied keys as applicable; DSSE-KMS and S3 Bucket Keys have no direct equivalent. #### Placement and operation GCS provides strong read-after-write consistency and designs for 99.999999999% annual durability. Region, dual-region or multi-region placement controls where the provider stores the data. Cross-bucket replication uses Storage Transfer Service rather than the S3 replication-rule engine. Object notifications go to Pub/Sub; routing to other destination types requires downstream integration. Tensor9 operates the bucket-management adapter, while Google operates object storage. #### Compatibility limits Bucket policy is unsupported at every adaptation tier. Object ACL, retention/legal-hold, S3 Select and RestoreObject APIs remain outside the listed S3 endpoint. Native GCS features such as Bucket Lock, Requester Pays and Autoclass must be assessed with their own configuration and semantics; native availability does not turn them into unrestricted S3 API equivalents. #### Migration and leaving the adapter A newly provisioned target bucket starts empty. Copy existing objects and required versions with a migration tool or a coordinated dual-write, then verify object data and application reads before switching. Preserve version history through the S3 API when the application depends on S3 version identifiers or delete markers. Leaving the Max adapter requires moving bucket configuration as well as object data. Apply the retained settings to native target resources, resolve any S3-specific version metadata, and move clients to the target's own API. Keep the adapter and its persistent state until the application no longer depends on those S3 behaviors. ## On Azure | Capability | Area | Support | Required tier | Operations | Notes | | --------------------------------------------------------------- | -------------- | ------------ | ------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Bucket policy, CORS, website, notification and replication APIs | Bucket config | Out of scope | - | - | These runtime S3 configuration APIs are outside the listed endpoint. Native target configuration and deployment mappings are described separately. | | Bucket versioning, tagging, lifecycle and encryption | Bucket config | Partial | Max | - | At Max, runtime S3 calls retain bucket settings and apply supported target mappings. Lifecycle filters, storage classes and account-level settings retain the limits described below. Simpler request forwarding configures these settings at deployment. | | Presigned URLs | Other features | Out of scope | - | - | S3 presigned URLs aren't honored: the adapter re-signs to the backend and does not validate the client's presigned signature, so its expiry/scope guarantees aren't enforced. | | Operation | Area | Support | Depth | Notes | | ----------------------- | -------------- | -------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | GetObjectAcl | Access control | Out of scope | Full surface | Object ACL requests are rejected. Configure access through target permissions or deployment configuration. | | PutObjectAcl | Access control | Out of scope | Full surface | Object ACL requests are rejected. Configure access through target permissions or deployment configuration. | | CreateBucket | Bucket config | Supported | Most usage | the container is the bucket | | DeleteBucket | Bucket config | Supported | Most usage | the container is the bucket | | HeadBucket | Bucket config | Supported | Most usage | the container is the bucket | | AbortMultipartUpload | Multipart | Supported | Most usage | accepted; uncommitted blocks are left to Azure's 7-day cleanup | | CompleteMultipartUpload | Multipart | Supported | Most usage | Put Block List | | CreateMultipartUpload | Multipart | Supported | Most usage | mapped to Put Block / Put Block List | | ListMultipartUploads | Multipart | Out of scope | Most usage | Azure has no UploadId / cross-blob in-progress-upload listing, so the adapter returns 501 | | ListParts | Multipart | Partial | Most usage | the uncommitted block list: PartNumber + Size only, no per-part ETag / LastModified | | UploadPart | Multipart | Supported | Most usage | Put Block | | GetObjectLegalHold | Object lock | Out of scope | Full surface | the object-lock sub-resources are rejected | | GetObjectRetention | Object lock | Out of scope | Full surface | the object-lock sub-resources are rejected | | PutObjectLegalHold | Object lock | Out of scope | Full surface | the object-lock sub-resources are rejected | | PutObjectRetention | Object lock | Out of scope | Full surface | the object-lock sub-resources are rejected | | CopyObject | Objects | Partial | Most usage | download-then-upload, not server-side; copy-source conditionals are not honored | | DeleteObject | Objects | Supported | Common | idempotent | | DeleteObjects | Objects | Partial | Common | the batch form is served as per-key deletes (N round-trips) | | GetObject | Objects | Supported | Common | bodies stream; start/open-ended byte ranges honored (suffix and multi-range fall back to a full download) | | HeadObject | Objects | Supported | Common | metadata / size / ETag without a body | | ListObjectVersions | Objects | Partial | Most usage | Native Blob versions use timestamp identifiers and continuation markers. Max adds S3 version metadata and delete-marker behavior; native account-level versioning settings still apply. | | ListObjects | Objects | Supported | Common | the v1 listing form | | ListObjectsV2 | Objects | Supported | Common | flat listing with prefix + paging; delimiter/CommonPrefixes hierarchical listing is not served | | PutObject | Objects | Supported | Common | single-shot to 5000 MiB. The ETag a single-part write returns is S3's: the hex MD5 of the body, computed on the write and persisted as the blob's native Content-MD5 so reads render the same value. Azure's own blob ETag is an opaque change-counter and is deliberately not what a client sees | | GetObjectAttributes | Other features | Out of scope | Full surface | the ?attributes query is not served; use HeadObject | | RestoreObject | Other features | Out of scope | Full surface | the ?restore query is rejected | | SelectObjectContent | Other features | Out of scope | Full surface | the ?select query is rejected | | DeleteObjectTagging | Tagging | Adapter-served | Most usage | via Blob Index Tags | | GetObjectTagging | Tagging | Adapter-served | Most usage | via Blob Index Tags | | PutObjectTagging | Tagging | Adapter-served | Most usage | via Blob Index Tags; rejects @ / non-ASCII per Azure's tag charset | #### S3 requests in the customer environment The application continues using the S3 API. The Tensor9 adapter runs in the customer environment and sends object requests to Azure Blob Storage. It also serves bucket creation, deletion, listing, and configuration through a persistent bucket store. Azure Blob Storage stores the objects; Tensor9 operates the adapter and its bucket-management service. #### Request flow Object requests use the target storage service. Bucket settings persist separately and are applied to the target resource.
The S3 application calls the Tensor9 adapter. Object requests go to the target object store. Bucket settings persist in the adapter's store and a configuration worker applies them to the target. The S3 application calls the Tensor9 adapter. Object requests go to the target object store. Bucket settings persist in the adapter's store and a configuration worker applies them to the target.
#### Object requests Azure Blob uses its own API, so the adapter translates S3 requests, headers, XML results and errors into Blob operations. It streams reads and writes, maps object tags to Blob Index Tags, and handles batch deletion as individual blob deletes. CopyObject downloads and uploads the object rather than using a server-side copy. Flat listing supports prefixes and pagination; delimiter/CommonPrefixes grouping is outside the listed mapping. #### Bucket creation and configuration Each S3 bucket has a durable record containing its name, owner, requested configuration, and target reference. For a bucket named `bucket-name`, the S3-facing identifier is `arn:aws:s3:::bucket-name`; the target resource is a Blob container in a selected storage account. `ListBuckets` returns the caller's buckets, and configuration reads use the stored request state. A configuration worker applies the stored settings to the target. `CreateBucket` waits for the target bucket to be ready before returning success, so an immediate object write does not race bucket creation. `DeleteBucket` waits for removal and returns `BucketNotEmpty` if objects prevent deletion. A lifecycle update also waits for target application. For other configuration changes, a successful read of the new setting describes stored configuration; it does not imply every target-side change has already completed. #### Versions and multipart uploads Multipart uploads store parts as Azure blocks and commit a block list at completion. Max retains the part checksums and S3 version metadata needed for composite multipart ETags, S3 version IDs and delete markers. The simpler request-translation path uses Azure's native version and completion-ETag behavior instead. Single-part writes compute the content MD5 and persist it as Blob Content-MD5. Azure tag-character restrictions still apply, including rejection of @ and non-ASCII characters. #### Target configuration The adapter stores per-bucket configuration and reconciles each bucket to a container. Several Azure settings belong to the storage account, including region, redundancy, native versioning and default encryption; a container cannot independently choose them. Shared-account configuration must be planned for all buckets using that account. S3 lifecycle settings use the supported target mappings, and unsupported conditions must not change retention behavior silently. Max supplies S3 metadata where native account-level behavior alone is insufficient. #### Placement and operation Azure provides strong consistency. Its documented durability depends on redundancy: 11 nines for LRS, 12 for ZRS, and 16 for GRS or GZRS. Choose the account region and redundancy to meet the customer's placement and recovery needs. A bucket's S3 location field does not relocate the storage account. Tensor9 operates the adapter and persistent bucket state; Microsoft operates Blob Storage. #### Compatibility limits Bucket policy remains unsupported. The listed object API still has copy-source conditional and hierarchical-listing limits, and suffix or multiple byte ranges use a full-download fallback. A single PutObject is limited to 5000 MiB on this path. Azure has no S3 Requester Pays or direct One Zone storage-class equivalent. Object retention and legal-hold capabilities must be evaluated separately from the runtime APIs this mapping serves. #### Migration and leaving the adapter A newly provisioned target bucket starts empty. Copy existing objects and required versions with a migration tool or a coordinated dual-write, then verify object data and application reads before switching. Preserve version history through the S3 API when the application depends on S3 version identifiers or delete markers. Leaving the Max adapter requires moving bucket configuration as well as object data. Apply the retained settings to native target resources, resolve any S3-specific version metadata, and move clients to the target's own API. Keep the adapter and its persistent state until the application no longer depends on those S3 behaviors. ## On OCI | Capability | Area | Support | Required tier | Operations | Notes | | --------------------------------------------------------------- | -------------- | ------------ | ------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Bucket policy, CORS, website, notification and replication APIs | Bucket config | Out of scope | - | - | These runtime S3 configuration APIs are outside the listed endpoint. Native target configuration and deployment mappings are described separately. | | Bucket versioning, tagging, lifecycle and encryption | Bucket config | Partial | Max | - | At Max, runtime S3 calls retain bucket settings and apply supported target mappings. Lifecycle filters, storage classes and account-level settings retain the limits described below. Simpler request forwarding configures these settings at deployment. | | Presigned URLs | Other features | Out of scope | - | - | S3 presigned URLs aren't honored: the adapter re-signs to the backend and does not validate the client's presigned signature, so its expiry/scope guarantees aren't enforced. | | Operation | Area | Support | Depth | Notes | | ----------------------- | -------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------- | | GetObjectAcl | Access control | Out of scope | Full surface | Object ACL requests are rejected. Configure access through target permissions or deployment configuration. | | PutObjectAcl | Access control | Out of scope | Full surface | Object ACL requests are rejected. Configure access through target permissions or deployment configuration. | | CreateBucket | Bucket config | Supported | Most usage | The adapter serves this bucket operation. | | DeleteBucket | Bucket config | Supported | Most usage | The adapter serves this bucket operation. | | HeadBucket | Bucket config | Supported | Most usage | The adapter serves this bucket operation. | | AbortMultipartUpload | Multipart | Supported | Most usage | - | | CompleteMultipartUpload | Multipart | Supported | Most usage | - | | CreateMultipartUpload | Multipart | Supported | Most usage | - | | ListMultipartUploads | Multipart | Supported | Most usage | - | | ListParts | Multipart | Supported | Most usage | - | | UploadPart | Multipart | Supported | Most usage | - | | GetObjectLegalHold | Object lock | Out of scope | Full surface | the object-lock sub-resources are rejected | | GetObjectRetention | Object lock | Out of scope | Full surface | the object-lock sub-resources are rejected | | PutObjectLegalHold | Object lock | Out of scope | Full surface | the object-lock sub-resources are rejected | | PutObjectRetention | Object lock | Out of scope | Full surface | the object-lock sub-resources are rejected | | CopyObject | Objects | Supported | Most usage | server-side copy via x-amz-copy-source | | DeleteObject | Objects | Supported | Common | - | | DeleteObjects | Objects | Supported | Common | batch delete (OCI BulkDelete) | | GetObject | Objects | Supported | Common | bodies + byte ranges | | HeadObject | Objects | Supported | Common | - | | ListObjectVersions | Objects | Out of scope | Most usage | Version-addressed reads and deletes are supported, but ListObjectVersions is outside this mapping, including at Max. | | ListObjects | Objects | Supported | Common | the v1 listing form | | ListObjectsV2 | Objects | Supported | Common | - | | PutObject | Objects | Supported | Common | - | | GetObjectAttributes | Other features | Out of scope | Full surface | the ?attributes query is not served; use HeadObject | | RestoreObject | Other features | Out of scope | Full surface | the ?restore query is rejected | | SelectObjectContent | Other features | Out of scope | Full surface | the ?select query is rejected | | DeleteObjectTagging | Tagging | Out of scope | Most usage | OCI's S3-compat API has only bucket tagging, not object tagging | | GetObjectTagging | Tagging | Out of scope | Most usage | OCI's S3-compat API has only bucket tagging, not object tagging | | PutObjectTagging | Tagging | Out of scope | Most usage | OCI's S3-compat API has only bucket tagging, not object tagging | #### S3 requests in the customer environment The application continues using the S3 API. The Tensor9 adapter runs in the customer environment and sends object requests to OCI Object Storage. It also serves bucket creation, deletion, listing, and configuration through a persistent bucket store. OCI Object Storage stores the objects; Tensor9 operates the adapter and its bucket-management service. #### Request flow Object requests use the target storage service. Bucket settings persist separately and are applied to the target resource.
The S3 application calls the Tensor9 adapter. Object requests go to the target object store. Bucket settings persist in the adapter's store and a configuration worker applies them to the target. The S3 application calls the Tensor9 adapter. Object requests go to the target object store. Bucket settings persist in the adapter's store and a configuration worker applies them to the target.
#### Object requests Object requests use OCI's Amazon S3 Compatibility endpoint. The adapter changes the endpoint and signs with the customer's OCI S3-compatible access and secret keys. Core reads, writes, copies, deletes and multipart uploads use the provider API. Object bodies remain in OCI Object Storage, while the adapter stores bucket configuration separately. #### Bucket creation and configuration Each S3 bucket has a durable record containing its name, owner, requested configuration, and target reference. For a bucket named `bucket-name`, the S3-facing identifier is `arn:aws:s3:::bucket-name`; the target resource is an OCI Object Storage bucket. `ListBuckets` returns the caller's buckets, and configuration reads use the stored request state. A configuration worker applies the stored settings to the target. `CreateBucket` waits for the target bucket to be ready before returning success, so an immediate object write does not race bucket creation. `DeleteBucket` waits for removal and returns `BucketNotEmpty` if objects prevent deletion. A lifecycle update also waits for target application. For other configuration changes, a successful read of the new setting describes stored configuration; it does not imply every target-side change has already completed. #### Versions and multipart uploads OCI provides native multipart uploads and accepts a versionId on object reads, metadata reads and deletion. Its compatibility endpoint does not provide the S3 version-listing behavior used by the application. At Max, the adapter retains the version and deletion metadata needed by S3-addressed object operations. ListObjectVersions remains outside this mapping; maintaining version metadata does not add that API. Native multipart operations retain the provider's behavior. #### Target configuration Runtime bucket configuration is stored by the adapter and applied through the target bucket-management design. Native OCI bucket settings provide lifecycle, encryption, replication and retention. Encryption can use provider-managed, Vault-managed or customer-supplied keys; DSSE-KMS and S3 Bucket Keys have no direct equivalent. OCI tags apply to buckets; object tagging is outside the listed mapping. Retention rules combine time-based retention with optional locking and do not provide an independent S3 legal-hold object. #### Placement and operation OCI provides strong read-after-write consistency and designs for eleven-nines durability. It replicates across three availability domains, or three fault domains in a single-domain region. Cross-region replication targets a preexisting destination bucket. Object events use OCI Events. Tensor9 operates the bucket-management adapter, and Oracle operates the object store. #### Compatibility limits Bucket policy is unsupported at every tier. OCI's compatibility endpoint has no configurable CORS; its fixed response is not an S3 CORS rule set. Website hosting and Requester Pays have no direct OCI Object Storage equivalent. Presigned access uses OCI Pre-Authenticated Requests outside this S3 adapter path. The API table identifies operations that remain unavailable despite native storage features. #### Migration and leaving the adapter A newly provisioned target bucket starts empty. Copy existing objects and required versions with a migration tool or a coordinated dual-write, then verify object data and application reads before switching. Preserve version history through the S3 API when the application depends on S3 version identifiers or delete markers. Leaving the Max adapter requires moving bucket configuration as well as object data. Apply the retained settings to native target resources, resolve any S3-specific version metadata, and move clients to the target's own API. Keep the adapter and its persistent state until the application no longer depends on those S3 behaviors. ## On Private Kubernetes | Capability | Area | Support | Required tier | Operations | Notes | | ----------------------------------------------------------------------------------------------------------------- | -------------- | ------------ | ------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Bucket sub-resources (versioning / lifecycle / encryption / policy / CORS / website / notification / replication) | Bucket config | Out of scope | - | - | provisioned at deploy time, not served as runtime S3 control calls | | Presigned URLs | Other features | Out of scope | - | - | S3 presigned URLs aren't honored: the adapter re-signs to the backend and does not validate the client's presigned signature, so its expiry/scope guarantees aren't enforced. | | Operation | Area | Support | Depth | Notes | | ----------------------- | -------------- | ------------ | ------------ | ---------------------------------------------------------------------------------------------------------- | | GetObjectAcl | Access control | Out of scope | Full surface | Object ACL requests are rejected. Configure access through target permissions or deployment configuration. | | PutObjectAcl | Access control | Out of scope | Full surface | Object ACL requests are rejected. Configure access through target permissions or deployment configuration. | | CreateBucket | Bucket config | Supported | Most usage | The adapter serves this bucket operation. | | DeleteBucket | Bucket config | Supported | Most usage | The adapter serves this bucket operation. | | HeadBucket | Bucket config | Supported | Most usage | The adapter serves this bucket operation. | | AbortMultipartUpload | Multipart | Supported | Most usage | - | | CompleteMultipartUpload | Multipart | Supported | Most usage | - | | CreateMultipartUpload | Multipart | Supported | Most usage | - | | ListMultipartUploads | Multipart | Supported | Most usage | - | | ListParts | Multipart | Supported | Most usage | - | | UploadPart | Multipart | Supported | Most usage | - | | GetObjectLegalHold | Object lock | Out of scope | Full surface | the object-lock sub-resources are rejected | | GetObjectRetention | Object lock | Out of scope | Full surface | the object-lock sub-resources are rejected | | PutObjectLegalHold | Object lock | Out of scope | Full surface | the object-lock sub-resources are rejected | | PutObjectRetention | Object lock | Out of scope | Full surface | the object-lock sub-resources are rejected | | CopyObject | Objects | Supported | Most usage | server-side copy via x-amz-copy-source | | DeleteObject | Objects | Supported | Common | - | | DeleteObjects | Objects | Supported | Common | batch delete | | GetObject | Objects | Supported | Common | - | | HeadObject | Objects | Supported | Common | - | | ListObjectVersions | Objects | Supported | Most usage | when bucket versioning is enabled | | ListObjects | Objects | Supported | Common | the v1 listing form | | ListObjectsV2 | Objects | Supported | Common | - | | PutObject | Objects | Supported | Common | - | | GetObjectAttributes | Other features | Out of scope | Full surface | the ?attributes query is not served; use HeadObject | | RestoreObject | Other features | Out of scope | Full surface | the ?restore query is rejected | | SelectObjectContent | Other features | Out of scope | Full surface | the ?select query is rejected | | DeleteObjectTagging | Tagging | Supported | Most usage | - | | GetObjectTagging | Tagging | Supported | Most usage | - | | PutObjectTagging | Tagging | Supported | Most usage | - | #### S3 on the customer's own storage The application uses its S3 client to reach the Tensor9 adapter. The adapter signs requests with the customer's MinIO credentials and forwards the supported operations to MinIO's S3 endpoint. Tensor9 deploys and operates MinIO inside the appliance, including in disconnected environments. Objects remain on the customer's disks.
The application sends S3 requests through the Tensor9 adapter to MinIO in the customer environment. The adapter signs requests for MinIO; MinIO stores the objects on customer disks. The application sends S3 requests through the Tensor9 adapter to MinIO in the customer environment. The adapter signs requests for MinIO; MinIO stores the objects on customer disks.
#### Object operations The adapter forwards the listed reads, writes, listings, deletes, server-side copies and multipart uploads. Versioned buckets also support version listing. Object tags are stored as MinIO object tags, so MinIO can use them in its lifecycle and access-policy conditions. MinIO's broader API does not determine what this endpoint accepts. The adapter rejects object ACL requests, bucket-policy requests, S3 Select, Glacier restore and runtime retention or legal-hold calls. MinIO implements Object Lock, including GOVERNANCE and COMPLIANCE retention and independent legal holds; configure those features directly on MinIO. #### Consistency and recovery MinIO provides strict read-after-write and list-after-write consistency. A completed write is visible to subsequent reads and listings. MinIO splits objects into data and parity shards across drives. Parity lets it reconstruct missing or damaged shards while enough drives remain available. Failure tolerance depends on the configured parity and the placement of drives across nodes. Read and write availability have separate quorum requirements: recovering data after an outage does not guarantee that writes can continue throughout it. MinIO checks reads with HighwayHash and can repair detected corruption from surviving shards. Tensor9 operates recovery and monitors the deployment; durability depends on its hardware and layout. There is no MinIO durability percentage or availability SLA to substitute for that design. See [MinIO's erasure-coding documentation](https://min.io/docs/minio/linux/operations/concepts/erasure-coding.html) for parity and quorum requirements.
Illustrative erasure-coding layout: four data shards and two parity shards. Two missing shards can be reconstructed from the four remaining shards. Production recovery and write availability depend on the configured parity and placement. Illustrative erasure-coding layout: four data shards and two parity shards. Two missing shards can be reconstructed from the four remaining shards. Production recovery and write availability depend on the configured parity and placement.
#### Bucket configuration Configure bucket features at deployment or directly on MinIO. The described adapter does not forward runtime S3 bucket-configuration requests. MinIO supports versioning, expiration of current and noncurrent objects, and cleanup of incomplete multipart uploads. Encryption supports SSE-S3, SSE-C and SSE-KMS through the KES key server; DSSE-KMS and S3 Bucket Keys have no equivalent in this mapping. Bucket replication copies objects between configured buckets. Site replication also replicates buckets and identity configuration, including users, groups and policies, across MinIO deployments. Choose the replication mode and recovery destinations for the application. MinIO can send object events to webhooks, Kafka, AMQP, NATS, Redis and several databases. Configure these destinations on MinIO; the adapter rejects PutBucketNotificationConfiguration. #### Storage and website limitations MinIO's STANDARD and REDUCED\_REDUNDANCY classes select erasure-coding parity, not storage cost tiers. Lifecycle rules can move data to an external object store, but this mapping has no on-cluster Glacier-style class or Requester Pays billing mode. The Community server does not implement configurable CORS in this mapping; verify support in the MinIO edition deployed. MinIO also has no S3 index/error-document website mode. A web server or reverse proxy can serve static content from the bucket. S3 analytics, inventory, Intelligent-Tiering and metrics configurations are outside this mapping. #### Operations and migration The customer supplies the disks and nodes, and Tensor9 operates the store as part of the appliance. Size usable capacity after accounting for parity and replication, and plan expansion and recovery around the actual drive and node layout. New buckets start empty. Copy existing S3 objects and required versions before switching clients, then verify reads and version-dependent behavior. MinIO site replication can seed another MinIO deployment, including its identity configuration. This adapter does not enforce S3 presigned-URL signatures and expiry. Use MinIO's native presigning or mc share for time-limited direct access. Moving away from the adapter requires changing client endpoints and credentials and preserving the bucket settings the application uses. [Service Catalog](/service-adapters/catalog). # S3 Glacier Source: https://docs.tensor9.com/service-adapters/aws/databases-storage/s3-glacier AWS S3 Glacier. The Glacier vault and archive API: stores archives in vaults and retrieves archive data or inventory through jobs. This is separate from S3 bucket storage classes. ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## Mapping and limitations Tensor9 provides a bounded Max adaptation of the Glacier vault/archive API over MinIO. It supports vault creation, deletion, description and listing; single-request archive uploads up to 256 MiB and archive deletion; and archive or JSON inventory retrieval through InitiateJob, DescribeJob and GetJobOutput. Retrieval jobs complete immediately: AWS cold-storage retrieval timing and pricing are not reproduced. Multipart uploads, ListJobs, vault locks, notifications, access policies, provisioned capacity and tags are unsupported. Jobs reject SNSTopic and OutputLocation; archive retrieval rejects RetrievalByteRange, and inventory retrieval rejects InventoryRetrievalParameters. These target services provide the mapping described above, within its stated limits. | Cloud | Target service | | ------------------ | -------------- | | Google Cloud | MinIO | | Azure | MinIO | | OCI | MinIO | | Private Kubernetes | MinIO | [Service Catalog](/service-adapters/catalog). # Amazon MQ RabbitMQ Cluster Source: https://docs.tensor9.com/service-adapters/aws/messaging-streaming/amazon-mq-rabbitmq-cluster AWS Amazon MQ RabbitMQ Cluster. A RabbitMQ cluster deployment with three broker nodes across availability zones. Replicated queues maintain availability during a node failure; replication depends on the queue type and broker version. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud, Azure, OCI, and Private Kubernetes](#on-google-cloud-azure-oci-and-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of Amazon MQ RabbitMQ Cluster with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | Amazon MQ RabbitMQ Cluster | Google Cloud, Azure, OCI, and Private Kubernetes | | ------------------- | ----------------------------------------- | ------------------------------------------------------------------------------ | | AMQP protocol | AMQP 0-9-1 (RabbitMQ) | native; same broker engine, client unchanged | | Broker topology | three-node cluster (multi-AZ) | 3-node cluster | | High availability | AWS-managed three-node cluster (multi-AZ) | quorum queues replicated across 3 nodes | | Managed broker | Yes | No - you self-manage the broker on Kubernetes | | Management API + UI | Yes | Yes - RabbitMQ management plugin (console URL differs) | | TLS | Yes | Yes - self-signed / cert-manager issuer (trust chain differs from AWS-managed) | | API coverage | full | high | ## On Google Cloud, Azure, OCI, and Private Kubernetes | Operation | Area | Support | Depth | Notes | | ---------------------------------------------------------------------------------- | ----------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | CloudWatch logs / KMS / maintenance window / VPC placement / minor-version upgrade | AWS control plane | Out of scope | Full surface | these AWS-managed settings are omitted. Configure logging, monitoring, encryption at rest and upgrades with the target cluster's tools | | Management API + UI | Admin | Supported | Most usage | the RabbitMQ management plugin is enabled (the same HTTP API Amazon MQ exposes on 443/15671); the management console URL differs from Amazon MQ's | | Plugins (Shovel / Federation / Consistent-Hash / Prometheus / OAuth2) | Admin | Supported | Most usage | because you operate the broker, you can enable any RabbitMQ plugin, a wider set than Amazon MQ's curated list (Management, dynamic Shovel, Federation, Consistent-Hash-Exchange, OAuth2, LDAP, Prometheus). Amazon MQ's private-broker Shovel/Federation restriction does not apply on the target cluster | | Consumer acks / QoS prefetch | Delivery | Supported | Most usage | RabbitMQ handles acknowledgements and prefetch directly; consumer\_timeout sets the delivery acknowledgement timeout | | Dead-letter exchanges / TTL | Delivery | Supported | Most usage | native RabbitMQ dead-letter exchanges and message/queue TTL | | Publisher confirms | Delivery | Supported | Most usage | native RabbitMQ publisher confirms | | Transactions (Tx.Select / Tx.Commit / Tx.Rollback) | Delivery | Supported | Full surface | native AMQP 0-9-1 transactions on the same broker engine; publisher confirms are the recommended higher-throughput path | | ActiveMQ engine | Engine | Out of scope | Full surface | Amazon MQ's ActiveMQ engine is a different protocol and is out of scope; only RabbitMQ is supported | | Bindings + routing keys | Messaging | Supported | Common | native RabbitMQ routing, unchanged | | Exchanges (direct / fanout / topic / headers) | Messaging | Supported | Common | native RabbitMQ; every exchange type works unchanged | | Queues (classic / quorum / lazy) | Messaging | Supported | Common | RabbitMQ quorum queues replicate across the 3-node cluster for high availability | | Streams (RabbitMQ 3.9+) | Messaging | Supported | Full surface | The target supports RabbitMQ Streams as an optional feature beyond the Amazon MQ queue mapping. Core streams can use AMQP; the dedicated Stream protocol needs its plugin, listener and compatible client. Configure and validate Streams separately. | | Authentication (authentication\_strategy=SIMPLE) | Security | Supported | Most usage | SIMPLE is the default strategy. The broker's declared user block (username/password) is written into a Kubernetes secret, and RabbitMQ's internal user backend authenticates against it unchanged | | LDAP authentication | Security | Partial | Full surface | RabbitMQ's LDAP auth-backend plugin (rabbitmq\_auth\_backend\_ldap) is available on the self-operated broker; enable it and point it at your directory. Configure the LDAP server connection on RabbitMQ directly | | TLS | Security | Supported | Most usage | TLS is on (Amazon MQ RabbitMQ is always-TLS); the certificate is self-signed or cert-manager-issued rather than AWS-managed, so the trust chain differs. Pin or trust the cluster's issuer | | Connect (AMQP 0-9-1) | Wire protocol | Supported | Common | your AMQP client (pika, amqplib, the RabbitMQ Java client) connects without changes. Bitnami RabbitMQ runs the same broker engine as Amazon MQ, so the protocol is native; Tensor9 writes the target broker endpoint into the generated Amazon MQ resource outputs | #### How it works Tensor9 deploys the Bitnami RabbitMQ Helm chart on the customer's Kubernetes cluster and sets the broker host and port in the generated Amazon MQ outputs. Your application connects directly to that broker over AMQP 0-9-1. It keeps its RabbitMQ client library; clients that pin the AWS certificate chain must trust the target issuer. The deployment uses 3 broker pods, persistent storage and a Kubernetes Service. The customer platform team operates the broker, storage, upgrades and recovery. A disconnected deployment also needs its container images, chart, certificates and other dependencies available locally. #### Queues and broker configuration RabbitMQ handles exchanges, bindings, routing keys, publisher confirms, transactions, consumer acknowledgments, prefetch, dead-letter exchanges and TTL. Check queue types and enabled plugins against the selected broker version when importing definitions. Configure quorum queues explicitly, or set the broker's default queue type. Upstream RabbitMQ defaults to classic queues unless that default is overridden; running RabbitMQ 4.x alone does not select quorum queues. The management HTTP API and console provide access to broker definitions and operational state. Use the Helm chart's configuration values for broker settings, credentials, TLS and plugin enablement. This mapping does not require a RabbitmqCluster custom resource or the RabbitMQ Cluster Operator. #### Three-node deployment and replication The cluster uses three broker pods. A quorum queue replicates its messages using Raft, a protocol that requires agreement from a majority of the queue's members. With three members, it can continue after losing one, provided the remaining members can communicate and access their storage. Publisher confirms and durable queue configuration are part of this guarantee. Place replicas on separate nodes and failure domains. Three pods sharing one node do not protect against that node failing. Classic queues do not gain replication merely by running on a three-node broker; declare the required queue type explicitly. Size each persistent volume for the retained backlog and recovery requirements. Storage availability and backups remain separate from broker replication: a replicated queue is not a backup against deletion or an incorrect retention policy.
The Bitnami RabbitMQ Helm chart deploys 3 broker pods, each with a persistent volume. Quorum queues replicate across their members. The Bitnami RabbitMQ Helm chart deploys 3 broker pods, each with a persistent volume. Quorum queues replicate across their members.
#### Limitations △ Operational differences * **TLS trust changes.** The target uses a self-signed or cert-manager-issued certificate. Configure client trust and certificate renewal before switching endpoints. * **AWS broker management does not move with the messages.** Use the target's monitoring, storage encryption and upgrade scheduling in place of CloudWatch, KMS and the Amazon MQ maintenance window. Broker credentials and TLS configuration are part of the Kubernetes deployment. * **Streams are an optional target feature.** Self-operated RabbitMQ supports Streams, but they are not an Amazon MQ queue migration. Core stream queues can use AMQP; the dedicated RabbitMQ Stream protocol additionally needs its plugin, listener and compatible client. Configure and validate that path separately. * **ActiveMQ is outside this mapping.** This adapter covers the RabbitMQ engine. Amazon MQ's ActiveMQ engine uses a different broker and protocols. #### Cutover and ongoing operation Export exchanges, queues, bindings, users, virtual hosts and policies through the source management API as definitions.json, then import the supported definitions into the target. Applications that declare their own topology can recreate those definitions when they connect. Definitions do not contain the message backlog. Drain the old broker or use Shovel or Federation for the required continuity. Account for deliveries in flight at cutover and test consumer retry behavior; they are not transferred automatically. The broker and its provisioned volumes run continuously. Plan capacity from backlog, message sizes, publish and consume rates, and recovery tests on the actual cluster. Monitor disk capacity, queue growth and consumer progress after cutover. Provider references: [quorum queues](https://www.rabbitmq.com/docs/quorum-queues), [default queue type](https://www.rabbitmq.com/docs/vhosts), and [core Streams and the Stream plugin](https://www.rabbitmq.com/docs/stream-core-plugin-comparison). [Service Catalog](/service-adapters/catalog). # Amazon MQ RabbitMQ Single Source: https://docs.tensor9.com/service-adapters/aws/messaging-streaming/amazon-mq-rabbitmq-single AWS Amazon MQ RabbitMQ Single. A single-instance RabbitMQ broker running on one node in one availability zone, which restarts in place rather than failing over. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud, Azure, OCI, and Private Kubernetes](#on-google-cloud-azure-oci-and-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of Amazon MQ RabbitMQ Single with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | Amazon MQ RabbitMQ Single | Google Cloud, Azure, OCI, and Private Kubernetes | | ------------------- | ----------------------------------------- | ------------------------------------------------------------------------------ | | AMQP protocol | AMQP 0-9-1 (RabbitMQ) | native; same broker engine, client unchanged | | Broker topology | single-instance | single node | | High availability | none; Amazon MQ single-instance is non-HA | none (single node; size the cluster topology for HA) | | Managed broker | Yes | No - you self-manage the broker on Kubernetes | | Management API + UI | Yes | Yes - RabbitMQ management plugin (console URL differs) | | TLS | Yes | Yes - self-signed / cert-manager issuer (trust chain differs from AWS-managed) | | API coverage | full | high | ## On Google Cloud, Azure, OCI, and Private Kubernetes | Operation | Area | Support | Depth | Notes | | ---------------------------------------------------------------------------------- | ----------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | CloudWatch logs / KMS / maintenance window / VPC placement / minor-version upgrade | AWS control plane | Out of scope | Full surface | these AWS-managed settings are omitted. Configure logging, monitoring, encryption at rest and upgrades with the target cluster's tools | | Management API + UI | Admin | Supported | Most usage | the RabbitMQ management plugin is enabled (the same HTTP API Amazon MQ exposes on 443/15671); the management console URL differs from Amazon MQ's | | Plugins (Shovel / Federation / Consistent-Hash / Prometheus / OAuth2) | Admin | Supported | Most usage | because you operate the broker, you can enable any RabbitMQ plugin, a wider set than Amazon MQ's curated list (Management, dynamic Shovel, Federation, Consistent-Hash-Exchange, OAuth2, LDAP, Prometheus). Amazon MQ's private-broker Shovel/Federation restriction does not apply on the target cluster | | Consumer acks / QoS prefetch | Delivery | Supported | Most usage | RabbitMQ handles acknowledgements and prefetch directly; consumer\_timeout sets the delivery acknowledgement timeout | | Dead-letter exchanges / TTL | Delivery | Supported | Most usage | native RabbitMQ dead-letter exchanges and message/queue TTL | | Publisher confirms | Delivery | Supported | Most usage | native RabbitMQ publisher confirms | | Transactions (Tx.Select / Tx.Commit / Tx.Rollback) | Delivery | Supported | Full surface | native AMQP 0-9-1 transactions on the same broker engine; publisher confirms are the recommended higher-throughput path | | ActiveMQ engine | Engine | Out of scope | Full surface | Amazon MQ's ActiveMQ engine is a different protocol and is out of scope; only RabbitMQ is supported | | Bindings + routing keys | Messaging | Supported | Common | native RabbitMQ routing, unchanged | | Exchanges (direct / fanout / topic / headers) | Messaging | Supported | Common | native RabbitMQ; every exchange type works unchanged | | Queues (classic / quorum / lazy) | Messaging | Supported | Common | RabbitMQ queue types run on one node; replication across nodes requires a cluster | | Streams (RabbitMQ 3.9+) | Messaging | Supported | Full surface | The target supports RabbitMQ Streams as an optional feature beyond the Amazon MQ queue mapping. Core streams can use AMQP; the dedicated Stream protocol needs its plugin, listener and compatible client. Configure and validate Streams separately. | | Authentication (authentication\_strategy=SIMPLE) | Security | Supported | Most usage | SIMPLE is the default strategy. The broker's declared user block (username/password) is written into a Kubernetes secret, and RabbitMQ's internal user backend authenticates against it unchanged | | LDAP authentication | Security | Partial | Full surface | RabbitMQ's LDAP auth-backend plugin (rabbitmq\_auth\_backend\_ldap) is available on the self-operated broker; enable it and point it at your directory. Configure the LDAP server connection on RabbitMQ directly | | TLS | Security | Supported | Most usage | TLS is on (Amazon MQ RabbitMQ is always-TLS); the certificate is self-signed or cert-manager-issued rather than AWS-managed, so the trust chain differs. Pin or trust the cluster's issuer | | Connect (AMQP 0-9-1) | Wire protocol | Supported | Common | your AMQP client (pika, amqplib, the RabbitMQ Java client) connects without changes. Bitnami RabbitMQ runs the same broker engine as Amazon MQ, so the protocol is native; Tensor9 writes the target broker endpoint into the generated Amazon MQ resource outputs | #### How it works Tensor9 deploys the Bitnami RabbitMQ Helm chart on the customer's Kubernetes cluster and sets the broker host and port in the generated Amazon MQ outputs. Your application connects directly to that broker over AMQP 0-9-1. It keeps its RabbitMQ client library; clients that pin the AWS certificate chain must trust the target issuer. The deployment uses 1 broker pod, persistent storage and a Kubernetes Service. The customer platform team operates the broker, storage, upgrades and recovery. A disconnected deployment also needs its container images, chart, certificates and other dependencies available locally. #### Queues and broker configuration RabbitMQ handles exchanges, bindings, routing keys, publisher confirms, transactions, consumer acknowledgments, prefetch, dead-letter exchanges and TTL. Check queue types and enabled plugins against the selected broker version when importing definitions. Configure quorum queues explicitly, or set the broker's default queue type. Upstream RabbitMQ defaults to classic queues unless that default is overridden; running RabbitMQ 4.x alone does not select quorum queues. The management HTTP API and console provide access to broker definitions and operational state. Use the Helm chart's configuration values for broker settings, credentials, TLS and plugin enablement. This mapping does not require a RabbitmqCluster custom resource or the RabbitMQ Cluster Operator. #### Single-node deployment and recovery This target runs one broker pod. Its persistent volume retains messages across pod replacement, but there is no second broker to serve clients while it is unavailable. A single-member quorum queue loses availability if its only node fails. Use the clustered mapping when the application needs broker failover. Size each persistent volume for the retained backlog and recovery requirements. Storage availability and backups remain separate from broker replication: a replicated queue is not a backup against deletion or an incorrect retention policy.
The Bitnami RabbitMQ Helm chart deploys 1 broker pod with a persistent volume. There is no second broker or replicated queue member to take over. The Bitnami RabbitMQ Helm chart deploys 1 broker pod with a persistent volume. There is no second broker or replicated queue member to take over.
#### Limitations △ Operational differences * **TLS trust changes.** The target uses a self-signed or cert-manager-issued certificate. Configure client trust and certificate renewal before switching endpoints. * **AWS broker management does not move with the messages.** Use the target's monitoring, storage encryption and upgrade scheduling in place of CloudWatch, KMS and the Amazon MQ maintenance window. Broker credentials and TLS configuration are part of the Kubernetes deployment. * **Streams are an optional target feature.** Self-operated RabbitMQ supports Streams, but they are not an Amazon MQ queue migration. Core stream queues can use AMQP; the dedicated RabbitMQ Stream protocol additionally needs its plugin, listener and compatible client. Configure and validate that path separately. * **ActiveMQ is outside this mapping.** This adapter covers the RabbitMQ engine. Amazon MQ's ActiveMQ engine uses a different broker and protocols. #### Cutover and ongoing operation Export exchanges, queues, bindings, users, virtual hosts and policies through the source management API as definitions.json, then import the supported definitions into the target. Applications that declare their own topology can recreate those definitions when they connect. Definitions do not contain the message backlog. Drain the old broker or use Shovel or Federation for the required continuity. Account for deliveries in flight at cutover and test consumer retry behavior; they are not transferred automatically. The broker and its provisioned volumes run continuously. Plan capacity from backlog, message sizes, publish and consume rates, and recovery tests on the actual cluster. Monitor disk capacity, queue growth and consumer progress after cutover. Provider references: [quorum queues](https://www.rabbitmq.com/docs/quorum-queues), [default queue type](https://www.rabbitmq.com/docs/vhosts), and [core Streams and the Stream plugin](https://www.rabbitmq.com/docs/stream-core-plugin-comparison). [Service Catalog](/service-adapters/catalog). # EventBridge Source: https://docs.tensor9.com/service-adapters/aws/messaging-streaming/eventbridge AWS EventBridge, an event bus that matches JSON events against rules and delivers them to targets such as Lambda, SQS and Step Functions. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [On Google Cloud](#on-google-cloud) * [Via Google Pub/Sub](#via-google-pub/sub) * [On Google Cloud, Azure, OCI, and Private Kubernetes](#on-google-cloud-azure-oci-and-private-kubernetes) * [Via CloudNativePG](#via-cloudnativepg) * [On Azure](#on-azure) * [Via Azure Event Grid](#via-azure-event-grid) * [Via Container Apps](#via-container-apps) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of EventBridge with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | EventBridge | Google Cloud · Google Pub/Sub | Google Cloud, Azure, OCI, and Private Kubernetes · CloudNativePG | Azure · Azure Event Grid | Azure · Container Apps | | ----------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | Event bus + PutEvents | native | Google Cloud Pub/Sub topic | EventBridge bus and publish API served by the adapter | Azure Event Grid topic | - | | Event-pattern matching | Yes | No - a Pub/Sub subscription attribute filter, a subset of EventBridge's grammar with no nested detail matching | Partial - EventBridge content filtering evaluated in the adapter, including nested detail matching. NOT cidr and NOT \$or - both fail closed, so a pattern using either never matches and raises nothing. Validate the application's operators | No - Event Grid advanced filters, the closest native match, still a subset (filter count and nesting limits) | - | | Scheduled rules · cron / rate | native | Google Cloud Scheduler | rate(...) fires from the durable store; cron(...) is accepted and never fires (it is not rejected at deploy) | a Container Apps Job | a Container Apps Job schedule trigger (translated cron; inexpressible crons are rejected at deploy) | | Rule targets · fan-out | SQS / SNS / Lambda / … | a pull-bridge re-emits to each target's own equivalent | a durable outbox row per target, re-delivered at-least-once to https/http destinations, and to SQS when the cfg names an SQS endpoint (nothing stamps that key today); an SNS or Lambda target has no egress and fails permanently | event handlers re-emit to each target's own equivalent | - | | Target DLQ + retry | Yes | No - a Pub/Sub dead-letter topic plus maxDeliveryAttempts (attempt-count only; the age cap maps only to Pub/Sub's coarse 10-minute minimum message retention) | Partial - durable outbox retry with exponential backoff (Tensor9-owned) under ONE adapter-wide attempt cap: the target record has no dead\_letter\_config and no retry\_policy, so maximum\_retry\_attempts and maximum\_event\_age\_in\_seconds are not read. No DLQ table - a row past the cap is marked dead in the same outbox and the reaper deletes it past grace | No - Event Grid dead-lettering to Storage plus native retry (both caps: maxDeliveryAttempts and eventTimeToLive) | - | | API coverage | full | minimal | high | minimal | partial | | Event-pattern rules | Yes | - | - | - | No - this target supports scheduled rules only; use the Event Grid or Postgres tier for event-pattern rules | | Rule target · delivery | SQS / SNS / Lambda / … | - | - | - | the Job runs Microsoft's sample image and delivers nothing; an SQS target maps to SendMessage into the SQS-equivalent | ## On Google Cloud ### Via Google Pub/Sub | Operation | Area | Support | Depth | Notes | | --------------------------------------------------- | --------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Cross-account / cross-region buses | Bus | Out of scope | Full surface | a bus spans one deployment; cross-account and cross-region event routing is out of scope | | Event bus (default + custom) | Bus | Out of scope | Common | each bus maps to a Pub/Sub topic, provisioned at apply-time | | Event bus SSE (customer KMS key) | Bus | Out of scope | Full surface | a bus-level customer-managed KMS key is not supported; encryption at rest follows the backing store's own model (target-managed), not a per-bus customer key | | Schema registry / EventBridge Pipes | Ecosystem | Out of scope | Full surface | the schema registry and Pipes (source → filter → enrich → target) are distinct products, out of scope | | PutEvents | Publish | Out of scope | Common | PutEvents maps to a publish on the bus's Pub/Sub topic | | Archive + replay | Replay | Out of scope | Full surface | event archive and time-range replay have no analog here (out of scope for this equivalence) | | Rule: event pattern (JSON matcher) | Rules | Out of scope | Common | an event pattern maps to a Pub/Sub subscription attribute filter: source and detail-type are promoted to attributes and matched natively. Deep nested-detail matching is a subset of EventBridge's grammar; the CloudNativePG target evaluates the full grammar in code | | Rule: scheduled (cron / rate) | Rules | Out of scope | Most usage | a scheduled rule rides Google Cloud Scheduler; the CloudNativePG target fires rate(...) schedules from its durable store | | DLQ + retry policy on targets | Targets | Out of scope | Most usage | a target DLQ maps to a Pub/Sub dead-letter topic, and the attempt cap (maximum\_retry\_attempts) to that policy's maxDeliveryAttempts (the cloud's own, range 5-100). The age cap (maximum\_event\_age\_in\_seconds) has no equivalent: Pub/Sub bounds redelivery only by the subscription's message retention, whose 10-minute minimum is too coarse to honor it | | Target input (constant / input\_path / transformer) | Targets | Out of scope | Full surface | a constant Input, a JSONPath selection or a template transformer shapes the delivered event | | Targets (fan a rule to N targets) | Targets | Out of scope | Common | a pull-bridge re-emits each matched event to the target's own equivalent | #### Bus, rules and destinations The CloudNativePG target is the one to use for a durable events store. The mapping takes an EventBridge bus to a Pub/Sub topic and PutEvents to topic publication. Rules would use Pub/Sub subscription attribute filters, and a delivery worker would pull matched messages and forward them to each rule's configured target through that target's selected adapter. Pub/Sub attribute filtering cannot directly evaluate nested EventBridge detail objects. Review each event pattern before selecting this target. Rules that depend on nested content need the PostgreSQL-backed matcher or a deliberate change to the event and filter design; similar field names do not make the filter languages equivalent. #### Delivery, scheduling and operations Google operates Pub/Sub storage and subscription redelivery. The adapter handles the EventBridge API and destination delivery. Broker acceptance, filter matching and target success are distinct stages: test all three and make target side effects safe to repeat after duplicate delivery. Scheduled rules use Google Cloud Scheduler rather than a Pub/Sub filter. Provision the schedule, topic, subscription and destination together, with the required identities and network access. Archive/replay, Pipes and the schema registry are outside this mapping. Switch producers after the rules and destinations are ready. Existing AWS history is not copied into the target. Size and validate the deployment with the customer's event sizes, rule count and destination behavior. ## On Google Cloud, Azure, OCI, and Private Kubernetes ### Via CloudNativePG | Operation | Area | Support | Depth | Notes | | --------------------------------------------------- | --------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Cross-account / cross-region buses | Bus | Out of scope | Full surface | a bus spans one deployment; cross-account and cross-region event routing is out of scope | | Event bus (default + custom) | Bus | Supported | Common | a bus is created by the CreateEventBus API call and stored; the default bus exists per account without one. NOT PROVISIONED AT APPLY TIME: the Go provider defines no events resources, so a bus declared in Terraform does not reach this adapter -- see events-provider-entities-missing | | Event bus SSE (customer KMS key) | Bus | Out of scope | Full surface | a bus-level customer-managed KMS key is not supported; encryption at rest follows the backing store's own model (target-managed), not a per-bus customer key | | Schema registry / EventBridge Pipes | Ecosystem | Out of scope | Full surface | the schema registry and Pipes (source → filter → enrich → target) are distinct products, out of scope | | PutEvents | Publish | Supported | Common | a custom event is published to the bus with its source and detail-type, committed with its matched target deliveries in one transaction before the call returns | | Archive + replay | Replay | Out of scope | Full surface | event archive and time-range replay have no analog here (out of scope for this equivalence) | | Rule: event pattern (JSON matcher) | Rules | Partial | Common | The adapter stores and evaluates event patterns in its own code, including prefix, suffix, equals-ignore-case, wildcard, anything-but, numeric ranges, exists and nested detail matching - a wider grammar than any cloud filter dialect. TWO MATCHERS ARE NOT EVALUATED and both FAIL CLOSED, so a pattern using either is accepted at PutRule and then silently never matches: `cidr`, which the matcher's own module doc calls the one documented matcher not yet implemented, and `$or`, which appears nowhere in the matcher at all and is therefore read as an ordinary field name. Check the pattern operators the application uses. | | Rule: scheduled (cron / rate) | Rules | Partial | Most usage | rate(N minutes\|hours\|days) rules fire: the schedule driver claims due rules from the store and writes the same delivery rows a matched event would. cron(...) is PARSED AND COUNTED BUT NOT EVALUATED, so a cron rule never fires - and nothing rejects it at PutRule or at deploy, so it is accepted silently. Use rate(...) here, or the Azure Container Apps tier for cron | | DLQ + retry policy on targets | Targets | Partial | Most usage | a failed target delivery retries from the durable outbox with exponential backoff (Tensor9-owned), and delivery is at-least-once. THE POLICY IS GLOBAL, NOT PER-TARGET: the adapter's target record has no dead\_letter\_config and no retry\_policy, so maximum\_retry\_attempts and maximum\_event\_age\_in\_seconds are not read -- one adapter-wide attempt cap applies instead, and there is no age cap. There is also no DLQ table: a row past the cap is marked `dead` in the same outbox and the retention reaper deletes it past its grace window, so a customer DLQ cannot be drained | | Target input (constant / input\_path / transformer) | Targets | Partial | Full surface | a CONSTANT Input is applied and replaces the event body on delivery. input\_path and input\_transformer are NOT applied: the adapter's target record stores only the constant, so a rule declaring a JSONPath selection or an input\_paths/input\_template transformer is accepted and the target receives the unmodified envelope instead | | Targets (fan a rule to N targets) | Targets | Partial | Common | a matched rule fans out to every target, and each delivery is attempted through the dispatcher. DESTINATION KINDS ARE A SUBSET: https/http targets deliver, and an SQS target delivers when the cfg names an SQS endpoint to send to. An SNS or Lambda target has no egress and fails Permanent (dead-lettered, loudly) rather than being dropped - use an https target, or SQS, until those egresses exist | #### Accepting and matching an event A deployment does not select this tier today. The Tensor9 adapter serves EventBridge requests using PostgreSQL for buses, rules, targets, events and pending deliveries. For each event, it evaluates enabled event-pattern rules on the destination bus. It commits the event and delivery records for matched rule-target pairs in one database transaction. This transaction is per event; do not treat a PutEvents batch as one application transaction. The adapter evaluates patterns in code instead of translating them to a broker's attribute filter. Nested objects are matched recursively. Fields in an object must all match, while a field's candidate values are alternatives. This supports rules over nested detail data as well as the envelope. The matcher supports prefix, suffix, ASCII case-insensitive equality, numeric comparisons, exists, \* wildcards and the documented anything-but forms. CIDR matching is outside this matcher scope. Test matching and nonmatching examples for each operator used by the application, including combinations of operators. #### Delivering matched events A worker claims pending deliveries with a temporary lease and sends each to its resolved target. The delivered body is the EventBridge envelope, or the target's static Input when supplied. The envelope includes id, source, detail-type, time, region, account, resources and detail. Input transformation is a separate feature and must be checked against the profile's limits. A delivery accepted by the target can be repeated if the worker fails before recording success. Targets must tolerate duplicates. Temporary failures are retried with increasing delay and an attempt cap; permanent failures and exhausted attempts become terminal. Removing a target also makes its remaining deliveries terminal. Monitor backlog and terminal failures independently of PutEvents success. #### Rules, targets and migration Provision buses, rules and targets before switching producers, then test representative matching and nonmatching events. Confirm each target's selected service adapter and network access. A matching rule is not enough if its destination cannot accept the delivered event. Scheduled rules need the scheduler described by the deployment, separately from event-pattern evaluation. Existing AWS event history is not copied into the database. Archive/replay, Pipes and the schema registry remain outside this mapping. A stored event used for pending delivery is not an EventBridge archive or an application-facing replay facility. ## On Azure ### Via Azure Event Grid | Operation | Area | Support | Depth | Notes | | --------------------------------------------------- | --------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Cross-account / cross-region buses | Bus | Out of scope | Full surface | a bus spans one deployment; cross-account and cross-region event routing is out of scope | | Event bus (default + custom) | Bus | Out of scope | Common | each bus maps to an Event Grid topic, provisioned at apply-time | | Event bus SSE (customer KMS key) | Bus | Out of scope | Full surface | a bus-level customer-managed KMS key is not supported; encryption at rest follows the backing store's own model (target-managed), not a per-bus customer key | | Schema registry / EventBridge Pipes | Ecosystem | Out of scope | Full surface | the schema registry and Pipes (source → filter → enrich → target) are distinct products, out of scope | | PutEvents | Publish | Out of scope | Common | PutEvents maps to a publish on the bus's Event Grid topic | | Archive + replay | Replay | Out of scope | Full surface | event archive and time-range replay have no analog here (out of scope for this equivalence) | | Rule: event pattern (JSON matcher) | Rules | Out of scope | Common | an event pattern maps to Event Grid advanced filters, the closest native match to EventBridge content filtering (numeric ranges, string contains, nested keys). They are capped below the full grammar; the CloudNativePG target evaluates it in code | | Rule: scheduled (cron / rate) | Rules | Out of scope | Most usage | a scheduled rule rides a Container Apps Job, since Event Grid has no native scheduler; the standalone Azure Container Apps target is a separate equivalent that also runs them | | DLQ + retry policy on targets | Targets | Out of scope | Most usage | a target DLQ maps to Event Grid dead-lettering to Storage, with both retry caps native: the attempt cap (maximum\_retry\_attempts) to maxDeliveryAttempts (1-30) and the age cap (maximum\_event\_age\_in\_seconds) to eventTimeToLive (1-1440 min) | | Target input (constant / input\_path / transformer) | Targets | Out of scope | Full surface | a constant Input, a JSONPath selection or a template transformer shapes the delivered event | | Targets (fan a rule to N targets) | Targets | Out of scope | Common | event handlers re-emit each matched event to the target's own equivalent | #### How it works A Tensor9 adapter in the customer environment accepts your application's EventBridge `PutEvents` calls. Each event bus maps to an Event Grid topic; events are published in the CloudEvents format, and Event Grid advanced filters match the rules. Event handlers deliver matches to each target's replacement service, such as the SQS or SNS adapter. Microsoft operates Event Grid's storage, delivery, retry and dead-lettering. The adapter uses the appliance's workload identity to authenticate without a stored key.
Before: on AWS a producer calls PutEvents on an EventBridge bus, rules match events by pattern and deliver to targets. After: on Azure the same PutEvents call is served by a Tensor9 adapter onto an Event Grid topic as CloudEvents, advanced filters match, and event handlers re-emit each matched event to the target's own equivalent. Before: on AWS a producer calls PutEvents on an EventBridge bus, rules match events by pattern and deliver to targets. After: on Azure the same PutEvents call is served by a Tensor9 adapter onto an Event Grid topic as CloudEvents, advanced filters match, and event handlers re-emit each matched event to the target's own equivalent.
#### Filter support and the PostgreSQL alternative Event Grid advanced filters support numeric ranges, string containment and nested keys, but cover only part of EventBridge's filter syntax. Limits on filter count and nesting can prevent a rule from being translated. Check the event patterns your application uses before choosing this target. With Event Grid, Microsoft operates event matching and delivery. The PostgreSQL target evaluates nested detail fields and its supported pattern operators in the adapter. Compare those operators with your rules when Event Grid cannot express them; it does not cover every EventBridge pattern form. The customer also operates the PostgreSQL database used for event and delivery state. #### Scheduled rules use Container Apps Event Grid has no scheduler. Scheduled EventBridge rules use a Container Apps Job that scales to zero between runs. A stack containing both event-pattern rules and scheduled rules therefore provisions both Event Grid and Container Apps resources. Check the Container Apps section for its schedule and target restrictions. #### Limitations △ Event Grid limitations * **Content filtering is capped below EventBridge's grammar.** Advanced filters cover numeric ranges, string containment and nested keys, with limits on filter count and nesting. Check each required pattern against Event Grid and the PostgreSQL adapter's supported operators before selecting a target. * **Archive and replay are out of scope.** EventBridge can archive events and replay them later. Nothing here reproduces that, so a recovery or backfill procedure built on replay does not port and needs another design. * **Pipes and the schema registry are out of scope.** EventBridge Pipes and the schema registry are unsupported by this target. * **Scheduled rules use Container Apps Jobs.** Event Grid has no scheduler, so a cron or rate rule is served by a Container Apps Job instead. Expect a mixed stack to provision both, and review the scheduled-rule section for its constraints. #### Other considerations * **Allow pending AWS deliveries to finish.** Provision topics, filters and handlers before switching producers. Pending EventBridge deliveries and retries are not copied to Event Grid. Keep the old destinations available while those deliveries finish, and handle duplicate events during cutover. * **Review Event Grid retry limits.** Retry and dead-lettering are Event Grid's, with its own caps on delivery attempts and event time-to-live, and dead-lettered events land in Storage. If your operational runbooks assume EventBridge's retry behaviour, those numbers are the ones to re-read. * **Targets resolve to their own equivalents.** An SQS target sends a message through the SQS adapter selected for this appliance. Review each target service's support and limitations alongside the EventBridge rules. ### Via Container Apps | Operation | Area | Support | Depth | Notes | | ---------------------------------------------------------- | --------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Event bus + PutEvents | Bus | Out of scope | Common | this target supports scheduled rules only; it provisions no event bus and no publish path. Use the Event Grid or the durable Postgres tier for a bus + PutEvents | | Archive + replay / Pipes / schema registry / cross-account | Ecosystem | Out of scope | Full surface | unsupported for scheduled jobs | | Rule: event pattern (JSON matcher) | Rules | Out of scope | Common | an event-pattern (bus-matching) rule is caught at deploy here; use Event Grid or the PostgreSQL adapter for event-pattern rules | | Rule: scheduled (cron / rate) | Rules | Supported | Common | a scheduled Container Apps Job runs the translated AWS 6-field cron or rate schedule and scales to zero between runs; deployment rejects schedules that cannot be represented | | DLQ + retry policy on targets | Targets | Out of scope | Most usage | depends on the target delivery above; delivery uses the selected SQS adapter and inherits its storage and dead-letter behaviour | | Target input (constant / input\_path / transformer) | Targets | Out of scope | Full surface | for scheduled jobs the job runs on its cron and receives the rule's scheduled event; a target input override (a constant Input, an input\_path selector, or an input\_transformer) is caught at deploy, so set the job's payload in the Container Apps Job definition instead | | Target: SNS / Lambda / non-SQS | Targets | Out of scope | Full surface | only SQS destinations are supported; deployment rejects other destination types | | Target: SQS | Targets | Out of scope | Common | The Job fires on schedule and delivers nothing to the target: it runs `mcr.microsoft.com/k8se/quickstart-jobs:latest`, Microsoft's sample image, and the PID-1 shim that would read EB\_TARGET\_QUEUE/EB\_BODY\_TEMPLATE and issue SendMessage does not exist, and neither env var has a consumer anywhere in the repository. So a job fires on its cron and delivers nothing. The compiler already stamps the target queue and body template for it; what is missing is the image that reads them | #### How it works A scheduled EventBridge rule maps to a **Container Apps Job with a schedule trigger**. The job scales to zero between runs, and the schedule half is real: the translated cron fires on time. **The Job fires on schedule and delivers nothing to the target.** The applied job runs `mcr.microsoft.com/k8se/quickstart-jobs:latest`, Microsoft's sample image. The compiler stamps the target queue and the body template into the job's environment, and nothing in that image reads them to issue `SendMessage`. Use the durable PostgreSQL tier for a scheduled rule whose event has to arrive.
Before: on AWS a scheduled EventBridge rule fires on a cron or rate schedule and delivers an event to its target. After: on Azure a Container Apps Job with a schedule trigger fires the translated cron and scales to zero between runs, and delivers nothing to the target, because the applied Job runs a placeholder image rather than one that issues SendMessage. Before: on AWS a scheduled EventBridge rule fires on a cron or rate schedule and delivers an event to its target. After: on Azure a Container Apps Job with a schedule trigger fires the translated cron and scales to zero between runs, and delivers nothing to the target, because the applied Job runs a placeholder image rather than one that issues SendMessage.
#### Cron translation AWS cron expressions have six fields; standard cron has five and uses a different day-of-week convention. Tensor9 translates expressions that preserve the schedule. If an expression cannot be represented, deployment fails with an error identifying it. Revise that schedule before deployment. #### Supported rules and targets This target supports scheduled rules with SQS destinations. Deployment rejects event-pattern rules because the job provides neither an event bus nor `PutEvents`. Use Event Grid for pattern rules, or compare the PostgreSQL adapter's supported operators when your rules need nested detail matching that Event Grid cannot express. Non-SQS destinations are also rejected. A scheduled rule that invokes a function or sends to another service needs a different implementation. #### Limitations △ Scheduled-rule limitations * **Only scheduled rules are served here.** Deployment rejects event-pattern rules for this target. Use Event Grid or the PostgreSQL adapter after checking its supported pattern operators. * **No target is served here.** The Job fires on schedule and delivers nothing to the target: the applied job runs a placeholder image rather than one that issues SendMessage through the SQS adapter. Other target types are rejected at deploy, so a scheduled rule that invokes a function or posts to an API is not served by this target. * **Some AWS cron expressions cannot be expressed at all.** AWS's six-field cron is more expressive than standard cron in places. An untranslatable expression is a deploy-time error, which means a rule you rely on may need its schedule restated in a form standard cron can express. * **Allow for job startup time.** The job starts a new instance on each trigger. Measure the delay before processing begins if your application has a deadline after the scheduled time. #### Other considerations * **Estimate cost from job runs.** Job compute is billed while the job runs. Estimate it from the schedule frequency, run duration and resources required. * **Check cron translation before deployment.** The translation is decided at deploy, so the fastest way to find an untranslatable expression is to look at your rules' schedules first. Six-field expressions and unusual day-of-week forms are where the refusals land. * **A mixed stack provisions both mechanisms.** If some rules are scheduled and others match patterns, expect Container Apps resources alongside Event Grid ones. On AWS they were entries on one bus; here they are two different services, and both appear in the plan. [Service Catalog](/service-adapters/catalog). # Kinesis Source: https://docs.tensor9.com/service-adapters/aws/messaging-streaming/kinesis AWS Kinesis. An ordered, sharded record stream with a retention window, where each consumer tracks its own position and can replay past records. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [On Google Cloud](#on-google-cloud) * [Via Google Pub/Sub](#via-google-pub/sub) * [Via Managed Service for Apache Kafka](#via-managed-service-for-apache-kafka) * [On Google Cloud and Private Kubernetes](#on-google-cloud-and-private-kubernetes) * [Via Strimzi Kafka](#via-strimzi-kafka) * [On Azure](#on-azure) * [On OCI](#on-oci) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of Kinesis with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation #### Runtime surface | Capability | Kinesis | Google Cloud · Google Pub/Sub | Google Cloud and Private Kubernetes · Strimzi Kafka | Google Cloud · Managed Service for Apache Kafka | Azure | OCI | | -------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | Ordered ingest · partition key → shard | shard (hash-key ranges) | an ordering key (per-key order preserved; no fixed addressable shard count) | partition (the layer replicates Kinesis's hash-key partitioner) | partition (the layer replicates Kinesis's hash-key partitioner) | partition (the layer replicates Kinesis's hash-key partitioner) | partition (the layer replicates Kinesis's hash-key partitioner) | | Sequence numbers · replay position | native | generated in order within each ordering key; replay can seek to a timestamp or snapshot, not an exact message offset | synthesized, per-shard monotonic, decode to a seek position | synthesized, per-shard monotonic, decode to a seek position | synthesized, per-shard monotonic, decode to a seek position | synthesized, per-shard monotonic, decode to a seek position | | KCL checkpointing · consumer position | DynamoDB lease table | Pub/Sub subscription acks (not consumer-group offsets) + a co-deployed DynamoDB adapter for the KCL lease table | target consumer-group offsets + a co-deployed DynamoDB adapter for the lease table | target consumer-group offsets + a co-deployed DynamoDB adapter for the lease table | target consumer-group offsets + a co-deployed DynamoDB adapter for the lease table | target consumer-group offsets + a co-deployed DynamoDB adapter for the lease table | | Retention | 24 h - 365 d | up to Pub/Sub's \~31-day cap, short of Kinesis's 365 days | up to Kinesis's 365-day ceiling (broker retention.ms; storage cost is the broker's) | up to Kinesis's 365-day ceiling (broker retention.ms; storage cost is the broker's) | up to Event Hubs's ceiling (\~90 days on Premium/Dedicated), short of Kinesis's 365 days | OCI Streaming's managed retention window (under OCI quotas) | | Resharding | Yes | No - no analog; Pub/Sub auto-scales, out of scope | Partial - add partitions (UpdateShardCount up); split/merge/reduce out of scope | Partial - add partitions (UpdateShardCount up); split/merge/reduce out of scope | Partial - add partitions (UpdateShardCount up); split/merge/reduce out of scope | Partial - add partitions (UpdateShardCount up); split/merge/reduce out of scope | | Enhanced fan-out | Yes | Yes - a Pub/Sub subscription per consumer, each receiving its own copy of the stream | Partial - a consumer group per enhanced-fan-out consumer; dedicated-throughput SLA becomes broker-limited | Partial - a consumer group per enhanced-fan-out consumer; dedicated-throughput SLA becomes broker-limited | Partial - a consumer group per enhanced-fan-out consumer; dedicated-throughput SLA becomes broker-limited | Partial - a consumer group per enhanced-fan-out consumer; dedicated-throughput SLA becomes broker-limited | | API coverage | full | high | high | high | high | high | #### Limits | Capability | Kinesis | Google Cloud · Google Pub/Sub | Google Cloud and Private Kubernetes · Strimzi Kafka | Google Cloud · Managed Service for Apache Kafka | Azure | OCI | | ---------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Capacity mode · PROVISIONED / ON\_DEMAND | Yes | Partial - Pub/Sub scales automatically; a PROVISIONED shard count does not create a fixed set of addressable shards | Partial - PROVISIONED → a fixed partition count; ON\_DEMAND (auto-scaled shards) has no elastic analog on a fixed-partition broker, served at a provisioned partition count | Partial - PROVISIONED → a fixed partition count; ON\_DEMAND (auto-scaled shards) has no elastic analog on a fixed-partition broker, served at a provisioned partition count | Partial - PROVISIONED → a fixed partition count; ON\_DEMAND (auto-scaled shards) has no elastic analog on a fixed-partition broker, served at a provisioned partition count | Partial - PROVISIONED → a fixed partition count; ON\_DEMAND (auto-scaled shards) has no elastic analog on a fixed-partition broker, served at a provisioned partition count | ## On Google Cloud ### Via Google Pub/Sub | Operation | Area | Support | Depth | Notes | | -------------------------------------------------------------------------------------- | ------------------ | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Capacity mode (PROVISIONED / ON\_DEMAND) | Capacity | Partial | Common | Pub/Sub scales automatically, matching ON\_DEMAND behavior. It cannot provide the fixed, addressable shard set of PROVISIONED mode; a supplied shard count does not control capacity | | GetRecords (Limit / NextShardIterator / MillisBehindLatest) | Consume | Partial | Most usage | GetRecords maps to a StreamingPull drain of the subscription (the same receive path as the SQS→Pub/Sub adapter); AT\_TIMESTAMP maps to a Pub/Sub Seek, but replay to an exact sequence position is Partial: Seek is timestamp/snapshot-granular, not per-message offset | | GetShardIterator (TRIM\_HORIZON / LATEST / AT\_TIMESTAMP / AT/AFTER\_SEQUENCE\_NUMBER) | Consume | Supported | Most usage | TRIM\_HORIZON maps to the start, LATEST to the tail, and AT\_TIMESTAMP to a Pub/Sub Seek; AT/AFTER\_SEQUENCE\_NUMBER has no exact analog because Pub/Sub has no per-message offset, so at-sequence positioning is Seek-granular (timestamp/snapshot), not per-message | | Sequence numbers | Consume | Supported | Most usage | the adapter generates sequence numbers that increase within each ordering key. Pub/Sub has no per-message offset, so a number cannot select an exact message for replay; seeking uses a timestamp or snapshot | | KCL (checkpoint via a DynamoDB lease table) | Consumer framework | Partial | Full surface | the Kinesis Client Library (KCL) uses Pub/Sub subscriptions and acknowledgements. For its separate checkpoint lease table, deploy the DynamoDB adapter or another DynamoDB-compatible table alongside Kinesis | | CreateStream / DeleteStream / DescribeStream | Control plane | Supported | Common | Terraform creates a Pub/Sub topic. A supplied shard count does not create addressable shards; DescribeStream reports the logical stream without a fixed shard set | | Enhanced fan-out (RegisterStreamConsumer / SubscribeToShard) | Fan-out | Supported | Full surface | each enhanced-fan-out consumer maps to its own Pub/Sub subscription, each receiving its own copy of the stream, though the dedicated per-consumer throughput SLA becomes Pub/Sub's own | | PutRecord (partitionKey → shard) | Ingest | Supported | Common | PutRecord publishes with the partition key as Pub/Sub's ordering key, so 'same key → same order' holds (the core guarantee a shard provides); Pub/Sub has no shards and no hash-key ranges, so 'same key → same shard' and ExplicitHashKey have no shardless analog and are out of scope | | PutRecords (batch) | Ingest | Supported | Common | returns a PutRecordsResultEntry for each record in the batch | | Shard-level metrics (EnableEnhancedMonitoring, per-shard CloudWatch) | Observability | Out of scope | Full surface | Kinesis's enhanced per-shard CloudWatch metrics (per-shard IncomingBytes/Records, OutgoingBytes/Records, IteratorAgeMilliseconds) are AWS-specific; no target exposes the Kinesis-named CloudWatch series or the enhanced-monitoring toggle; the broker/topic publishes its own metrics under its own names (Kafka and Event Hubs do expose per-partition metrics), so observability moves to the target's native monitoring | | Resharding (SplitShard / MergeShards / UpdateShardCount) | Resharding | Out of scope | Full surface | Pub/Sub auto-scales and has no addressable shard; SplitShard/MergeShards/UpdateShardCount have no analog, so design for Pub/Sub's own elasticity rather than a fixed shard count | | Server-side encryption (KMS) | Security | Supported | Most usage | at-rest encryption is the target's own: the managed targets always encrypt at rest (so a request for no encryption has no representation there), while a self-hosted broker encrypts per its storage/volume configuration; a customer-managed key maps where the target offers one (CMEK on Pub/Sub and managed Kafka; tier-gated on Event Hubs; the storage layer's key on a self-hosted broker), not necessarily per-stream | | Retention | Storage | Supported | Most usage | retention maps to Pub/Sub's message retention, which caps at \~31 days, well short of Kinesis's 365-day ceiling, so multi-month replay is out of reach on this target | #### Ordered messages without addressable shards The adapter maps the Kinesis stream to a Pub/Sub topic and uses the partition key as an ordering key. This preserves the intended order within a key, but Pub/Sub does not expose Kinesis hash ranges or a fixed set of addressable shards. ExplicitHashKey and split/merge operations cannot be represented by that ordering-key mapping. Reads drain a Pub/Sub subscription. Pub/Sub can seek by timestamp or snapshot; it does not expose a Kafka-style offset for each message. A returned sequence number therefore cannot promise exact-message replay. Review any consumer that persists a sequence number and later expects to resume at exactly that record. #### Consumers, retention and cutover Separate subscriptions give fan-out consumers separate copies and delivery state. Google controls subscription delivery and capacity; the Kinesis dedicated-throughput guarantee does not apply to this target. Kinesis Client Library applications still need their separate DynamoDB lease table through a compatible adapter. Pub/Sub retention is limited to the profile's stated window. Provision subscriptions before publication, test restart and replay behavior, and verify that the consumer can work without addressable shard positions. Existing AWS records and checkpoint positions are not copied. Measure adapter latency, backlog and consumer progress together; topic acceptance alone does not establish end-to-end processing speed. ### Via Managed Service for Apache Kafka | Operation | Area | Support | Depth | Notes | | -------------------------------------------------------------------------------------- | ------------------ | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Capacity mode (PROVISIONED / ON\_DEMAND) | Capacity | Partial | Common | PROVISIONED sets a fixed partition count at deployment. ON\_DEMAND also uses a fixed count on this target; the broker does not automatically scale partitions as Kinesis scales shards | | GetRecords (Limit / NextShardIterator / MillisBehindLatest) | Consume | Partial | Most usage | GetRecords maps to a broker poll from the \{shard, offset} position; NextShardIterator advances the offset and MillisBehindLatest comes from log-end lag; Kinesis's 5-calls/s-per-shard and 2 MB/s-per-shard read quotas are relaxed to the broker's own limits | | GetShardIterator (TRIM\_HORIZON / LATEST / AT\_TIMESTAMP / AT/AFTER\_SEQUENCE\_NUMBER) | Consume | Supported | Most usage | each iterator type maps to a seek: TRIM\_HORIZON to the start, LATEST to the tail, AT\_TIMESTAMP to the timestamp, AT/AFTER\_SEQUENCE\_NUMBER to the offset decoded from the sequence number; the iterator is an opaque token | | Sequence numbers | Consume | Supported | Most usage | the adapter synthesizes Kinesis-shaped sequence numbers that are per-shard monotonic and decode back to a seek position; review applications that compare sequence numbers across shards or use Kinesis Producer Library (KPL) aggregated-record sub-sequence numbers | | KCL (checkpoint via a DynamoDB lease table) | Consumer framework | Partial | Full surface | the Kinesis Client Library (KCL) uses target consumer groups for shard assignment and offset commits. It also requires a DynamoDB lease table for checkpoints; deploy the DynamoDB adapter or another compatible table alongside it | | CreateStream / DeleteStream / DescribeStream | Control plane | Supported | Common | Terraform provisions the stream and shard count; DescribeStream returns that stream configuration | | Enhanced fan-out (RegisterStreamConsumer / SubscribeToShard) | Fan-out | Partial | Full surface | each enhanced-fan-out consumer maps to its own consumer group; SubscribeToShard's server-push becomes a long-lived poll, and the dedicated 2 MB/s-per-shard-per-consumer SLA becomes broker-limited | | PutRecord (partitionKey → shard) | Ingest | Supported | Common | the adapter replicates Kinesis's own partition-key hashing (MD5 into the 128-bit hash-key ranges), so 'same partition key → same shard' holds and ExplicitHashKey is honored | | PutRecords (batch) | Ingest | Supported | Common | returns a PutRecordsResultEntry for each record in the batch | | Shard-level metrics (EnableEnhancedMonitoring, per-shard CloudWatch) | Observability | Out of scope | Full surface | Kinesis's enhanced per-shard CloudWatch metrics (per-shard IncomingBytes/Records, OutgoingBytes/Records, IteratorAgeMilliseconds) are AWS-specific; no target exposes the Kinesis-named CloudWatch series or the enhanced-monitoring toggle; the broker/topic publishes its own metrics under its own names (Kafka and Event Hubs do expose per-partition metrics), so observability moves to the target's native monitoring | | Resharding (SplitShard / MergeShards / UpdateShardCount) | Resharding | Partial | Full surface | UpdateShardCount up maps to adding partitions (Partial: it breaks the hash-range continuity Kinesis preserves across a split); SplitShard/MergeShards and reducing shard count have no analog and are out of scope | | Server-side encryption (KMS) | Security | Supported | Most usage | at-rest encryption is the target's own: the managed targets always encrypt at rest (so a request for no encryption has no representation there), while a self-hosted broker encrypts per its storage/volume configuration; a customer-managed key maps where the target offers one (CMEK on Pub/Sub and managed Kafka; tier-gated on Event Hubs; the storage layer's key on a self-hosted broker), not necessarily per-stream | | Retention | Storage | Supported | Most usage | retention maps to the topic's retention.ms/bytes; Kinesis's 24 h - 365 d range is expressible (365-day retention is a long-retention config, and the storage cost is the broker's) | #### From a shard to a target partition The application calls the Kinesis API on the Tensor9 adapter; the adapter uses Google's Managed Service for Apache Kafka for the retained record log. The mapping assigns each logical Kinesis shard to a target partition. Partition-key hashing chooses a shard, and the target partition supplies the ordered position used to construct the returned sequence number. Consumers receive an opaque shard iterator that records the partition and read position. Advancing the iterator reads later records; obtaining an iterator at a retained position supports replay. These positions belong to the new stream. Existing AWS sequence numbers and checkpoints do not identify positions in a newly provisioned target log. #### Consumer state and capacity The retained log and the consumer's progress are separate state. Kinesis Client Library applications also use a DynamoDB table for leases and checkpoints. Deploy a compatible DynamoDB adapter with this stream and test the consumer's restart and reassignment behavior. Keeping the Kinesis endpoint alone does not provide that table. A fixed target partition count does not reproduce Kinesis split/merge history. Adding partitions changes routing for some keys; plan that change with the consumers instead of assuming existing shard positions remain valid. Enhanced fan-out uses target consumer groups, so the target's capacity replaces Kinesis's dedicated per-consumer throughput guarantee. #### Operating and moving the stream Retention is configured on the target and is bounded by the provider limits in the comparison table. Record size, partition imbalance, consumer lag and retained storage all affect capacity. Measure adapter request latency and sustained throughput with the application's record sizes and partition distribution. Provision the target stream and consumer checkpoint store before cutover. Drain the old stream, or coordinate publication to both systems while consumers catch up. The move does not copy retained AWS records. Use target partition and adapter metrics to monitor reads, writes and lag; AWS CloudWatch series do not move with the stream. ## On Google Cloud and Private Kubernetes ### Via Strimzi Kafka | Operation | Area | Support | Depth | Notes | | -------------------------------------------------------------------------------------- | ------------------ | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Capacity mode (PROVISIONED / ON\_DEMAND) | Capacity | Partial | Common | PROVISIONED sets a fixed partition count at deployment. ON\_DEMAND also uses a fixed count on this target; the broker does not automatically scale partitions as Kinesis scales shards | | GetRecords (Limit / NextShardIterator / MillisBehindLatest) | Consume | Partial | Most usage | GetRecords maps to a broker poll from the \{shard, offset} position; NextShardIterator advances the offset and MillisBehindLatest comes from log-end lag; Kinesis's 5-calls/s-per-shard and 2 MB/s-per-shard read quotas are relaxed to the broker's own limits | | GetShardIterator (TRIM\_HORIZON / LATEST / AT\_TIMESTAMP / AT/AFTER\_SEQUENCE\_NUMBER) | Consume | Supported | Most usage | each iterator type maps to a seek: TRIM\_HORIZON to the start, LATEST to the tail, AT\_TIMESTAMP to the timestamp, AT/AFTER\_SEQUENCE\_NUMBER to the offset decoded from the sequence number; the iterator is an opaque token | | Sequence numbers | Consume | Supported | Most usage | the adapter synthesizes Kinesis-shaped sequence numbers that are per-shard monotonic and decode back to a seek position; review applications that compare sequence numbers across shards or use Kinesis Producer Library (KPL) aggregated-record sub-sequence numbers | | KCL (checkpoint via a DynamoDB lease table) | Consumer framework | Partial | Full surface | the Kinesis Client Library (KCL) uses target consumer groups for shard assignment and offset commits. It also requires a DynamoDB lease table for checkpoints; deploy the DynamoDB adapter or another compatible table alongside it | | CreateStream / DeleteStream / DescribeStream | Control plane | Supported | Common | Terraform provisions the stream and shard count; DescribeStream returns that stream configuration | | Enhanced fan-out (RegisterStreamConsumer / SubscribeToShard) | Fan-out | Partial | Full surface | each enhanced-fan-out consumer maps to its own consumer group; SubscribeToShard's server-push becomes a long-lived poll, and the dedicated 2 MB/s-per-shard-per-consumer SLA becomes broker-limited | | PutRecord (partitionKey → shard) | Ingest | Supported | Common | the adapter replicates Kinesis's own partition-key hashing (MD5 into the 128-bit hash-key ranges), so 'same partition key → same shard' holds and ExplicitHashKey is honored | | PutRecords (batch) | Ingest | Supported | Common | returns a PutRecordsResultEntry for each record in the batch | | Shard-level metrics (EnableEnhancedMonitoring, per-shard CloudWatch) | Observability | Out of scope | Full surface | Kinesis's enhanced per-shard CloudWatch metrics (per-shard IncomingBytes/Records, OutgoingBytes/Records, IteratorAgeMilliseconds) are AWS-specific; no target exposes the Kinesis-named CloudWatch series or the enhanced-monitoring toggle; the broker/topic publishes its own metrics under its own names (Kafka and Event Hubs do expose per-partition metrics), so observability moves to the target's native monitoring | | Resharding (SplitShard / MergeShards / UpdateShardCount) | Resharding | Partial | Full surface | UpdateShardCount up maps to adding partitions (Partial: it breaks the hash-range continuity Kinesis preserves across a split); SplitShard/MergeShards and reducing shard count have no analog and are out of scope | | Server-side encryption (KMS) | Security | Supported | Most usage | at-rest encryption is the target's own: the managed targets always encrypt at rest (so a request for no encryption has no representation there), while a self-hosted broker encrypts per its storage/volume configuration; a customer-managed key maps where the target offers one (CMEK on Pub/Sub and managed Kafka; tier-gated on Event Hubs; the storage layer's key on a self-hosted broker), not necessarily per-stream | | Retention | Storage | Supported | Most usage | retention maps to the topic's retention.ms/bytes; Kinesis's 24 h - 365 d range is expressible (365-day retention is a long-retention config, and the storage cost is the broker's) | #### From a shard to a target partition The application calls the Kinesis API on the Tensor9 adapter; the adapter uses Apache Kafka (Strimzi) on the target cluster for the retained record log. The mapping assigns each logical Kinesis shard to a target partition. Partition-key hashing chooses a shard, and the target partition supplies the ordered position used to construct the returned sequence number. Consumers receive an opaque shard iterator that records the partition and read position. Advancing the iterator reads later records; obtaining an iterator at a retained position supports replay. These positions belong to the new stream. Existing AWS sequence numbers and checkpoints do not identify positions in a newly provisioned target log. #### Consumer state and capacity The retained log and the consumer's progress are separate state. Kinesis Client Library applications also use a DynamoDB table for leases and checkpoints. Deploy a compatible DynamoDB adapter with this stream and test the consumer's restart and reassignment behavior. Keeping the Kinesis endpoint alone does not provide that table. A fixed target partition count does not reproduce Kinesis split/merge history. Adding partitions changes routing for some keys; plan that change with the consumers instead of assuming existing shard positions remain valid. Enhanced fan-out uses target consumer groups, so the target's capacity replaces Kinesis's dedicated per-consumer throughput guarantee. #### Operating and moving the stream Retention is configured on the target and is bounded by the provider limits in the comparison table. Record size, partition imbalance, consumer lag and retained storage all affect capacity. Measure adapter request latency and sustained throughput with the application's record sizes and partition distribution. Provision the target stream and consumer checkpoint store before cutover. Drain the old stream, or coordinate publication to both systems while consumers catch up. The move does not copy retained AWS records. Use target partition and adapter metrics to monitor reads, writes and lag; AWS CloudWatch series do not move with the stream. ## On Azure | Operation | Area | Support | Depth | Notes | | -------------------------------------------------------------------------------------- | ------------------ | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Capacity mode (PROVISIONED / ON\_DEMAND) | Capacity | Partial | Common | PROVISIONED sets a fixed partition count at deployment. ON\_DEMAND also uses a fixed count on this target; the broker does not automatically scale partitions as Kinesis scales shards | | GetRecords (Limit / NextShardIterator / MillisBehindLatest) | Consume | Partial | Most usage | GetRecords maps to a broker poll from the \{shard, offset} position; NextShardIterator advances the offset and MillisBehindLatest comes from log-end lag; Kinesis's 5-calls/s-per-shard and 2 MB/s-per-shard read quotas are relaxed to the broker's own limits | | GetShardIterator (TRIM\_HORIZON / LATEST / AT\_TIMESTAMP / AT/AFTER\_SEQUENCE\_NUMBER) | Consume | Supported | Most usage | each iterator type maps to a seek: TRIM\_HORIZON to the start, LATEST to the tail, AT\_TIMESTAMP to the timestamp, AT/AFTER\_SEQUENCE\_NUMBER to the offset decoded from the sequence number; the iterator is an opaque token | | Sequence numbers | Consume | Supported | Most usage | the adapter synthesizes Kinesis-shaped sequence numbers that are per-shard monotonic and decode back to a seek position; review applications that compare sequence numbers across shards or use Kinesis Producer Library (KPL) aggregated-record sub-sequence numbers | | KCL (checkpoint via a DynamoDB lease table) | Consumer framework | Partial | Full surface | the Kinesis Client Library (KCL) uses target consumer groups for shard assignment and offset commits. It also requires a DynamoDB lease table for checkpoints; deploy the DynamoDB adapter or another compatible table alongside it | | CreateStream / DeleteStream / DescribeStream | Control plane | Supported | Common | Terraform provisions the stream and shard count; DescribeStream returns that stream configuration | | Enhanced fan-out (RegisterStreamConsumer / SubscribeToShard) | Fan-out | Partial | Full surface | each enhanced-fan-out consumer maps to its own consumer group; SubscribeToShard's server-push becomes a long-lived poll, and the dedicated 2 MB/s-per-shard-per-consumer SLA becomes broker-limited | | PutRecord (partitionKey → shard) | Ingest | Supported | Common | the adapter replicates Kinesis's own partition-key hashing (MD5 into the 128-bit hash-key ranges), so 'same partition key → same shard' holds and ExplicitHashKey is honored | | PutRecords (batch) | Ingest | Supported | Common | returns a PutRecordsResultEntry for each record in the batch | | Shard-level metrics (EnableEnhancedMonitoring, per-shard CloudWatch) | Observability | Out of scope | Full surface | Kinesis's enhanced per-shard CloudWatch metrics (per-shard IncomingBytes/Records, OutgoingBytes/Records, IteratorAgeMilliseconds) are AWS-specific; no target exposes the Kinesis-named CloudWatch series or the enhanced-monitoring toggle; the broker/topic publishes its own metrics under its own names (Kafka and Event Hubs do expose per-partition metrics), so observability moves to the target's native monitoring | | Resharding (SplitShard / MergeShards / UpdateShardCount) | Resharding | Partial | Full surface | UpdateShardCount up maps to adding partitions (Partial: it breaks the hash-range continuity Kinesis preserves across a split); SplitShard/MergeShards and reducing shard count have no analog and are out of scope | | Server-side encryption (KMS) | Security | Supported | Most usage | at-rest encryption is the target's own: the managed targets always encrypt at rest (so a request for no encryption has no representation there), while a self-hosted broker encrypts per its storage/volume configuration; a customer-managed key maps where the target offers one (CMEK on Pub/Sub and managed Kafka; tier-gated on Event Hubs; the storage layer's key on a self-hosted broker), not necessarily per-stream | | Retention | Storage | Supported | Most usage | retention maps to Event Hubs's own retention; the Kinesis 365-day ceiling is capped: Event Hubs retains up to \~90 days (Premium/Dedicated), so a workload relying on multi-month replay needs review for this target | #### From a shard to a target partition The application calls the Kinesis API on the Tensor9 adapter; the adapter uses Azure Event Hubs for the retained record log. The mapping assigns each logical Kinesis shard to a target partition. Partition-key hashing chooses a shard, and the target partition supplies the ordered position used to construct the returned sequence number. Consumers receive an opaque shard iterator that records the partition and read position. Advancing the iterator reads later records; obtaining an iterator at a retained position supports replay. These positions belong to the new stream. Existing AWS sequence numbers and checkpoints do not identify positions in a newly provisioned target log. #### Consumer state and capacity The retained log and the consumer's progress are separate state. Kinesis Client Library applications also use a DynamoDB table for leases and checkpoints. Deploy a compatible DynamoDB adapter with this stream and test the consumer's restart and reassignment behavior. Keeping the Kinesis endpoint alone does not provide that table. A fixed target partition count does not reproduce Kinesis split/merge history. Adding partitions changes routing for some keys; plan that change with the consumers instead of assuming existing shard positions remain valid. Enhanced fan-out uses target consumer groups, so the target's capacity replaces Kinesis's dedicated per-consumer throughput guarantee. #### Operating and moving the stream Retention is configured on the target and is bounded by the provider limits in the comparison table. Record size, partition imbalance, consumer lag and retained storage all affect capacity. Measure adapter request latency and sustained throughput with the application's record sizes and partition distribution. Provision the target stream and consumer checkpoint store before cutover. Drain the old stream, or coordinate publication to both systems while consumers catch up. The move does not copy retained AWS records. Use target partition and adapter metrics to monitor reads, writes and lag; AWS CloudWatch series do not move with the stream. ## On OCI | Operation | Area | Support | Depth | Notes | | -------------------------------------------------------------------------------------- | ------------------ | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Capacity mode (PROVISIONED / ON\_DEMAND) | Capacity | Partial | Common | PROVISIONED sets a fixed partition count at deployment. ON\_DEMAND also uses a fixed count on this target; the broker does not automatically scale partitions as Kinesis scales shards | | GetRecords (Limit / NextShardIterator / MillisBehindLatest) | Consume | Partial | Most usage | GetRecords maps to a broker poll from the \{shard, offset} position; NextShardIterator advances the offset and MillisBehindLatest comes from log-end lag; Kinesis's 5-calls/s-per-shard and 2 MB/s-per-shard read quotas are relaxed to the broker's own limits | | GetShardIterator (TRIM\_HORIZON / LATEST / AT\_TIMESTAMP / AT/AFTER\_SEQUENCE\_NUMBER) | Consume | Supported | Most usage | each iterator type maps to a seek: TRIM\_HORIZON to the start, LATEST to the tail, AT\_TIMESTAMP to the timestamp, AT/AFTER\_SEQUENCE\_NUMBER to the offset decoded from the sequence number; the iterator is an opaque token | | Sequence numbers | Consume | Supported | Most usage | the adapter synthesizes Kinesis-shaped sequence numbers that are per-shard monotonic and decode back to a seek position; review applications that compare sequence numbers across shards or use Kinesis Producer Library (KPL) aggregated-record sub-sequence numbers | | KCL (checkpoint via a DynamoDB lease table) | Consumer framework | Partial | Full surface | the Kinesis Client Library (KCL) uses target consumer groups for shard assignment and offset commits. It also requires a DynamoDB lease table for checkpoints; deploy the DynamoDB adapter or another compatible table alongside it | | CreateStream / DeleteStream / DescribeStream | Control plane | Supported | Common | Terraform provisions the stream and shard count; DescribeStream returns that stream configuration | | Enhanced fan-out (RegisterStreamConsumer / SubscribeToShard) | Fan-out | Partial | Full surface | each enhanced-fan-out consumer maps to its own consumer group; SubscribeToShard's server-push becomes a long-lived poll, and the dedicated 2 MB/s-per-shard-per-consumer SLA becomes broker-limited | | PutRecord (partitionKey → shard) | Ingest | Supported | Common | the adapter replicates Kinesis's own partition-key hashing (MD5 into the 128-bit hash-key ranges), so 'same partition key → same shard' holds and ExplicitHashKey is honored | | PutRecords (batch) | Ingest | Supported | Common | returns a PutRecordsResultEntry for each record in the batch | | Shard-level metrics (EnableEnhancedMonitoring, per-shard CloudWatch) | Observability | Out of scope | Full surface | Kinesis's enhanced per-shard CloudWatch metrics (per-shard IncomingBytes/Records, OutgoingBytes/Records, IteratorAgeMilliseconds) are AWS-specific; no target exposes the Kinesis-named CloudWatch series or the enhanced-monitoring toggle; the broker/topic publishes its own metrics under its own names (Kafka and Event Hubs do expose per-partition metrics), so observability moves to the target's native monitoring | | Resharding (SplitShard / MergeShards / UpdateShardCount) | Resharding | Partial | Full surface | UpdateShardCount up maps to adding partitions (Partial: it breaks the hash-range continuity Kinesis preserves across a split); SplitShard/MergeShards and reducing shard count have no analog and are out of scope | | Server-side encryption (KMS) | Security | Supported | Most usage | at-rest encryption is the target's own: the managed targets always encrypt at rest (so a request for no encryption has no representation there), while a self-hosted broker encrypts per its storage/volume configuration; a customer-managed key maps where the target offers one (CMEK on Pub/Sub and managed Kafka; tier-gated on Event Hubs; the storage layer's key on a self-hosted broker), not necessarily per-stream | | Retention | Storage | Supported | Most usage | retention maps to OCI Streaming's own retention window, under OCI quotas; multi-month replay beyond OCI's ceiling needs review for this target | #### From a shard to a target partition The application calls the Kinesis API on the Tensor9 adapter; the adapter uses OCI Streaming for the retained record log. The mapping assigns each logical Kinesis shard to a target partition. Partition-key hashing chooses a shard, and the target partition supplies the ordered position used to construct the returned sequence number. Consumers receive an opaque shard iterator that records the partition and read position. Advancing the iterator reads later records; obtaining an iterator at a retained position supports replay. These positions belong to the new stream. Existing AWS sequence numbers and checkpoints do not identify positions in a newly provisioned target log. #### Consumer state and capacity The retained log and the consumer's progress are separate state. Kinesis Client Library applications also use a DynamoDB table for leases and checkpoints. Deploy a compatible DynamoDB adapter with this stream and test the consumer's restart and reassignment behavior. Keeping the Kinesis endpoint alone does not provide that table. A fixed target partition count does not reproduce Kinesis split/merge history. Adding partitions changes routing for some keys; plan that change with the consumers instead of assuming existing shard positions remain valid. Enhanced fan-out uses target consumer groups, so the target's capacity replaces Kinesis's dedicated per-consumer throughput guarantee. #### Operating and moving the stream Retention is configured on the target and is bounded by the provider limits in the comparison table. Record size, partition imbalance, consumer lag and retained storage all affect capacity. Measure adapter request latency and sustained throughput with the application's record sizes and partition distribution. Provision the target stream and consumer checkpoint store before cutover. Drain the old stream, or coordinate publication to both systems while consumers catch up. The move does not copy retained AWS records. Use target partition and adapter metrics to monitor reads, writes and lag; AWS CloudWatch series do not move with the stream. [Service Catalog](/service-adapters/catalog). # Kinesis Data Firehose Source: https://docs.tensor9.com/service-adapters/aws/messaging-streaming/kinesis-data-firehose AWS Kinesis Data Firehose. A delivery stream that buffers incoming records and writes them into S3, Redshift, OpenSearch or an HTTP endpoint, with optional format conversion. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [On Google Cloud](#on-google-cloud) * [Via Google Pub/Sub](#via-google-pub/sub) * [On Google Cloud and Private Kubernetes](#on-google-cloud-and-private-kubernetes) * [Via Strimzi Kafka](#via-strimzi-kafka) * [On Azure](#on-azure) * [On OCI](#on-oci) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of Kinesis Data Firehose with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | Kinesis Data Firehose | Google Cloud · Google Pub/Sub | Google Cloud and Private Kubernetes · Strimzi Kafka | Azure | OCI | | ---------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | Ingest (Direct PUT) | PutRecord / PutRecordBatch | buffered onto Google Cloud Pub/Sub | buffered onto Apache Kafka (Strimzi) on the target cluster | buffered onto Azure Event Hubs | buffered onto OCI Streaming | | Record transformation · per-batch | Lambda | the Lambda equivalent | the Lambda equivalent | the Lambda equivalent | the Lambda equivalent | | Delivery sink · destination API | S3 / Redshift / OpenSearch / Snowflake / Splunk / Datadog / HTTP | Records are delivered through destination APIs; warehouse delivery is a record-loading integration, not a Redshift service replacement. | Records are delivered through destination APIs; warehouse delivery is a record-loading integration, not a Redshift service replacement. | Records are delivered through destination APIs; warehouse delivery is a record-loading integration, not a Redshift service replacement. | Records are delivered through destination APIs; warehouse delivery is a record-loading integration, not a Redshift service replacement. | | Format conversion | Yes | Partial - JSON→Parquet served against a default/inferred schema; the declared Glue Data Catalog schema is AWS-specific and has no target counterpart | Partial - JSON→Parquet served against a default/inferred schema; the declared Glue Data Catalog schema is AWS-specific and has no target counterpart | Partial - JSON→Parquet served against a default/inferred schema; the declared Glue Data Catalog schema is AWS-specific and has no target counterpart | Partial - JSON→Parquet served against a default/inferred schema; the declared Glue Data Catalog schema is AWS-specific and has no target counterpart | | Snowflake / Splunk / Datadog sinks | Yes | Yes - delivered to Snowpipe / HEC / intake endpoints from the target cloud | Yes - delivered to Snowpipe / HEC / intake endpoints from the target cloud | Yes - delivered to Snowpipe / HEC / intake endpoints from the target cloud | Yes - delivered to Snowpipe / HEC / intake endpoints from the target cloud | | Redshift sink | Yes | Partial - A retained Redshift destination or a destination-specific warehouse loader; no database, schema or SQL migration | Partial - A retained Redshift destination or a destination-specific warehouse loader; no database, schema or SQL migration | Partial - A retained Redshift destination or a destination-specific warehouse loader; no database, schema or SQL migration | Partial - A retained Redshift destination or a destination-specific warehouse loader; no database, schema or SQL migration | | API coverage | full | partial | partial | partial | partial | ## On Google Cloud ### Via Google Pub/Sub | Operation | Area | Support | Depth | Notes | | ------------------------------------------- | --------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Buffering (size / interval) | Delivery | Supported | Most usage | the buffer size and interval hints drive when a batch flushes to the sink | | Dynamic partitioning | Delivery | Partial | Full surface | key-based dynamic partitioning of the delivered objects is a subset of Firehose's grammar | | CreateDeliveryStream / DeleteDeliveryStream | Delivery stream | Supported | Common | the delivery stream, its buffering policy, and its sink are provisioned at apply-time | | PutRecord / PutRecordBatch (Direct PUT) | Ingest | Supported | Common | records are buffered onto Google Cloud Pub/Sub; a Kinesis-stream source maps to the Kinesis adapter | | Delivery to destination APIs | Sink | Supported | Common | the delivery agent sends records to each configured destination through that service's API, preserving the destination's delivery behavior | | Sink: Apache Iceberg tables | Sink | Out of scope | Full surface | an Apache Iceberg destination is not offered: Iceberg tables register in the AWS Glue Data Catalog, which is AWS-specific and has no target-native counterpart on these backends | | Sink: OpenSearch Serverless | Sink | Partial | Full surface | the AWS serverless collection endpoint is replaced by the target's OpenSearch or search service, using the same delivery mechanism as the OpenSearch destination | | Sink: Redshift (AWS warehouse) | Sink | Partial | Full surface | A retained Redshift destination requires a delivery integration. Moving record delivery to BigQuery, Synapse or ClickHouse requires a destination-specific object-storage load; it does not migrate Redshift schemas, SQL or warehouse behavior. | | Sink: S3 / object store, HTTP, OpenSearch | Sink | Supported | Common | the S3 sink bridges to Google Cloud Storage (the target object store); an HTTP-endpoint sink forwards natively; an OpenSearch sink bridges to the target's OpenSearch/Elasticsearch equivalent | | Sink: Snowflake / Splunk / Datadog (SaaS) | Sink | Supported | Most usage | records are sent to Snowflake Snowpipe, Splunk HTTP Event Collector (HEC), or Datadog's intake API. These endpoints can receive records from the target cloud | | Format conversion (JSON → Parquet) | Transform | Partial | Full surface | JSON→Parquet conversion uses a default or inferred schema. The declared AWS Glue Data Catalog schema is not translated, so conversion does not use your Glue table definition | | Record transformation (Lambda) | Transform | Supported | Most usage | a transform runs each batch through the Lambda equivalent before delivery | #### Acceptance, buffering and delivery The application sends Firehose PutRecord or PutRecordBatch requests to the Tensor9 adapter. The mapping buffers accepted records on Google Cloud Pub/Sub, then a delivery worker sends batches to the configured destination. The object-storage destination for this mapping is Google Cloud Storage. Success from the ingest API means acceptance into the buffer, not confirmation from the final destination. Inspect individual results when a batch has both successful and failed records. Size and interval settings control when a batch is ready for delivery. A low-volume stream may wait for the interval; a busy stream may reach the size threshold first. Worker scheduling, optional transformation and destination response time also affect delivery latency, so the buffering interval is not an end-to-end latency guarantee. #### Transformation and destination setup The intended delivery path can run a batch through the selected Lambda adapter before writing its output. JSON-to-Parquet conversion uses a default or inferred schema; it does not import the declared Glue table schema. Validate field types and the resulting files with their readers. HTTP, search and SaaS destinations require their own credentials, reachable endpoints and delivery formats. A warehouse loader delivers records to the selected warehouse. Configure its destination tables and load format separately; this mapping does not migrate a Redshift database, schema, SQL queries or warehouse workload. #### Failure recovery and migration Delivery must tolerate a destination accepting a batch before the worker records success. A retry can therefore repeat records or create a duplicate object. Downstream processing should identify duplicates. Monitor pending work, retry failures and destination delivery, rather than using successful PutRecord calls as a delivery-health signal. Provision destination storage, credentials and transformation dependencies before switching producers. Allow the old AWS stream to finish delivering its buffered records; those pending records are not copied into the new buffer. Test the selected destination with representative batches, including destination failures and retries. Measure delivery delay and sustained throughput with the actual transform, batch settings and destination. ## On Google Cloud and Private Kubernetes ### Via Strimzi Kafka | Operation | Area | Support | Depth | Notes | | ------------------------------------------- | --------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Buffering (size / interval) | Delivery | Supported | Most usage | the buffer size and interval hints drive when a batch flushes to the sink | | Dynamic partitioning | Delivery | Partial | Full surface | key-based dynamic partitioning of the delivered objects is a subset of Firehose's grammar | | CreateDeliveryStream / DeleteDeliveryStream | Delivery stream | Supported | Common | the delivery stream, its buffering policy, and its sink are provisioned at apply-time | | PutRecord / PutRecordBatch (Direct PUT) | Ingest | Supported | Common | records are buffered onto Apache Kafka (Strimzi) on the target cluster; a Kinesis-stream source maps to the Kinesis adapter | | Delivery to destination APIs | Sink | Supported | Common | the delivery agent sends records to each configured destination through that service's API, preserving the destination's delivery behavior | | Sink: Apache Iceberg tables | Sink | Out of scope | Full surface | an Apache Iceberg destination is not offered: Iceberg tables register in the AWS Glue Data Catalog, which is AWS-specific and has no target-native counterpart on these backends | | Sink: OpenSearch Serverless | Sink | Partial | Full surface | the AWS serverless collection endpoint is replaced by the target's OpenSearch or search service, using the same delivery mechanism as the OpenSearch destination | | Sink: Redshift (AWS warehouse) | Sink | Partial | Full surface | A retained Redshift destination requires a delivery integration. Moving record delivery to BigQuery, Synapse or ClickHouse requires a destination-specific object-storage load; it does not migrate Redshift schemas, SQL or warehouse behavior. | | Sink: S3 / object store, HTTP, OpenSearch | Sink | Supported | Common | the S3 sink bridges to the appliance object store (MinIO) (the target object store); an HTTP-endpoint sink forwards natively; an OpenSearch sink bridges to the target's OpenSearch/Elasticsearch equivalent | | Sink: Snowflake / Splunk / Datadog (SaaS) | Sink | Supported | Most usage | records are sent to Snowflake Snowpipe, Splunk HTTP Event Collector (HEC), or Datadog's intake API. These endpoints can receive records from the target cloud | | Format conversion (JSON → Parquet) | Transform | Partial | Full surface | JSON→Parquet conversion uses a default or inferred schema. The declared AWS Glue Data Catalog schema is not translated, so conversion does not use your Glue table definition | | Record transformation (Lambda) | Transform | Supported | Most usage | a transform runs each batch through the Lambda equivalent before delivery | #### Acceptance, buffering and delivery The application sends Firehose PutRecord or PutRecordBatch requests to the Tensor9 adapter. The mapping buffers accepted records on Apache Kafka (Strimzi) on the target cluster, then a delivery worker sends batches to the configured destination. The object-storage destination for this mapping is the appliance object store (MinIO). Success from the ingest API means acceptance into the buffer, not confirmation from the final destination. Inspect individual results when a batch has both successful and failed records. Size and interval settings control when a batch is ready for delivery. A low-volume stream may wait for the interval; a busy stream may reach the size threshold first. Worker scheduling, optional transformation and destination response time also affect delivery latency, so the buffering interval is not an end-to-end latency guarantee. #### Transformation and destination setup The intended delivery path can run a batch through the selected Lambda adapter before writing its output. JSON-to-Parquet conversion uses a default or inferred schema; it does not import the declared Glue table schema. Validate field types and the resulting files with their readers. HTTP, search and SaaS destinations require their own credentials, reachable endpoints and delivery formats. A warehouse loader delivers records to the selected warehouse. Configure its destination tables and load format separately; this mapping does not migrate a Redshift database, schema, SQL queries or warehouse workload. #### Failure recovery and migration Delivery must tolerate a destination accepting a batch before the worker records success. A retry can therefore repeat records or create a duplicate object. Downstream processing should identify duplicates. Monitor pending work, retry failures and destination delivery, rather than using successful PutRecord calls as a delivery-health signal. Provision destination storage, credentials and transformation dependencies before switching producers. Allow the old AWS stream to finish delivering its buffered records; those pending records are not copied into the new buffer. Test the selected destination with representative batches, including destination failures and retries. Measure delivery delay and sustained throughput with the actual transform, batch settings and destination. ## On Azure | Operation | Area | Support | Depth | Notes | | ------------------------------------------- | --------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Buffering (size / interval) | Delivery | Supported | Most usage | the buffer size and interval hints drive when a batch flushes to the sink | | Dynamic partitioning | Delivery | Partial | Full surface | key-based dynamic partitioning of the delivered objects is a subset of Firehose's grammar | | CreateDeliveryStream / DeleteDeliveryStream | Delivery stream | Supported | Common | the delivery stream, its buffering policy, and its sink are provisioned at apply-time | | PutRecord / PutRecordBatch (Direct PUT) | Ingest | Supported | Common | records are buffered onto Azure Event Hubs; a Kinesis-stream source maps to the Kinesis adapter | | Delivery to destination APIs | Sink | Supported | Common | the delivery agent sends records to each configured destination through that service's API, preserving the destination's delivery behavior | | Sink: Apache Iceberg tables | Sink | Out of scope | Full surface | an Apache Iceberg destination is not offered: Iceberg tables register in the AWS Glue Data Catalog, which is AWS-specific and has no target-native counterpart on these backends | | Sink: OpenSearch Serverless | Sink | Partial | Full surface | the AWS serverless collection endpoint is replaced by the target's OpenSearch or search service, using the same delivery mechanism as the OpenSearch destination | | Sink: Redshift (AWS warehouse) | Sink | Partial | Full surface | A retained Redshift destination requires a delivery integration. Moving record delivery to BigQuery, Synapse or ClickHouse requires a destination-specific object-storage load; it does not migrate Redshift schemas, SQL or warehouse behavior. | | Sink: S3 / object store, HTTP, OpenSearch | Sink | Supported | Common | the S3 sink bridges to Azure Blob Storage (the target object store); an HTTP-endpoint sink forwards natively; an OpenSearch sink bridges to the target's OpenSearch/Elasticsearch equivalent | | Sink: Snowflake / Splunk / Datadog (SaaS) | Sink | Supported | Most usage | records are sent to Snowflake Snowpipe, Splunk HTTP Event Collector (HEC), or Datadog's intake API. These endpoints can receive records from the target cloud | | Format conversion (JSON → Parquet) | Transform | Partial | Full surface | JSON→Parquet conversion uses a default or inferred schema. The declared AWS Glue Data Catalog schema is not translated, so conversion does not use your Glue table definition | | Record transformation (Lambda) | Transform | Supported | Most usage | a transform runs each batch through the Lambda equivalent before delivery | #### Acceptance, buffering and delivery The application sends Firehose PutRecord or PutRecordBatch requests to the Tensor9 adapter. The mapping buffers accepted records on Azure Event Hubs, then a delivery worker sends batches to the configured destination. The object-storage destination for this mapping is Azure Blob Storage. Success from the ingest API means acceptance into the buffer, not confirmation from the final destination. Inspect individual results when a batch has both successful and failed records. Size and interval settings control when a batch is ready for delivery. A low-volume stream may wait for the interval; a busy stream may reach the size threshold first. Worker scheduling, optional transformation and destination response time also affect delivery latency, so the buffering interval is not an end-to-end latency guarantee. #### Transformation and destination setup The intended delivery path can run a batch through the selected Lambda adapter before writing its output. JSON-to-Parquet conversion uses a default or inferred schema; it does not import the declared Glue table schema. Validate field types and the resulting files with their readers. HTTP, search and SaaS destinations require their own credentials, reachable endpoints and delivery formats. A warehouse loader delivers records to the selected warehouse. Configure its destination tables and load format separately; this mapping does not migrate a Redshift database, schema, SQL queries or warehouse workload. #### Failure recovery and migration Delivery must tolerate a destination accepting a batch before the worker records success. A retry can therefore repeat records or create a duplicate object. Downstream processing should identify duplicates. Monitor pending work, retry failures and destination delivery, rather than using successful PutRecord calls as a delivery-health signal. Provision destination storage, credentials and transformation dependencies before switching producers. Allow the old AWS stream to finish delivering its buffered records; those pending records are not copied into the new buffer. Test the selected destination with representative batches, including destination failures and retries. Measure delivery delay and sustained throughput with the actual transform, batch settings and destination. ## On OCI | Operation | Area | Support | Depth | Notes | | ------------------------------------------- | --------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Buffering (size / interval) | Delivery | Supported | Most usage | the buffer size and interval hints drive when a batch flushes to the sink | | Dynamic partitioning | Delivery | Partial | Full surface | key-based dynamic partitioning of the delivered objects is a subset of Firehose's grammar | | CreateDeliveryStream / DeleteDeliveryStream | Delivery stream | Supported | Common | the delivery stream, its buffering policy, and its sink are provisioned at apply-time | | PutRecord / PutRecordBatch (Direct PUT) | Ingest | Supported | Common | records are buffered onto OCI Streaming; a Kinesis-stream source maps to the Kinesis adapter | | Delivery to destination APIs | Sink | Supported | Common | the delivery agent sends records to each configured destination through that service's API, preserving the destination's delivery behavior | | Sink: Apache Iceberg tables | Sink | Out of scope | Full surface | an Apache Iceberg destination is not offered: Iceberg tables register in the AWS Glue Data Catalog, which is AWS-specific and has no target-native counterpart on these backends | | Sink: OpenSearch Serverless | Sink | Partial | Full surface | the AWS serverless collection endpoint is replaced by the target's OpenSearch or search service, using the same delivery mechanism as the OpenSearch destination | | Sink: Redshift (AWS warehouse) | Sink | Partial | Full surface | A retained Redshift destination requires a delivery integration. Moving record delivery to BigQuery, Synapse or ClickHouse requires a destination-specific object-storage load; it does not migrate Redshift schemas, SQL or warehouse behavior. | | Sink: S3 / object store, HTTP, OpenSearch | Sink | Supported | Common | the S3 sink bridges to OCI Object Storage (the target object store); an HTTP-endpoint sink forwards natively; an OpenSearch sink bridges to the target's OpenSearch/Elasticsearch equivalent | | Sink: Snowflake / Splunk / Datadog (SaaS) | Sink | Supported | Most usage | records are sent to Snowflake Snowpipe, Splunk HTTP Event Collector (HEC), or Datadog's intake API. These endpoints can receive records from the target cloud | | Format conversion (JSON → Parquet) | Transform | Partial | Full surface | JSON→Parquet conversion uses a default or inferred schema. The declared AWS Glue Data Catalog schema is not translated, so conversion does not use your Glue table definition | | Record transformation (Lambda) | Transform | Supported | Most usage | a transform runs each batch through the Lambda equivalent before delivery | #### Acceptance, buffering and delivery The application sends Firehose PutRecord or PutRecordBatch requests to the Tensor9 adapter. The mapping buffers accepted records on OCI Streaming, then a delivery worker sends batches to the configured destination. The object-storage destination for this mapping is OCI Object Storage. Success from the ingest API means acceptance into the buffer, not confirmation from the final destination. Inspect individual results when a batch has both successful and failed records. Size and interval settings control when a batch is ready for delivery. A low-volume stream may wait for the interval; a busy stream may reach the size threshold first. Worker scheduling, optional transformation and destination response time also affect delivery latency, so the buffering interval is not an end-to-end latency guarantee. #### Transformation and destination setup The intended delivery path can run a batch through the selected Lambda adapter before writing its output. JSON-to-Parquet conversion uses a default or inferred schema; it does not import the declared Glue table schema. Validate field types and the resulting files with their readers. HTTP, search and SaaS destinations require their own credentials, reachable endpoints and delivery formats. A warehouse loader delivers records to the selected warehouse. Configure its destination tables and load format separately; this mapping does not migrate a Redshift database, schema, SQL queries or warehouse workload. #### Failure recovery and migration Delivery must tolerate a destination accepting a batch before the worker records success. A retry can therefore repeat records or create a duplicate object. Downstream processing should identify duplicates. Monitor pending work, retry failures and destination delivery, rather than using successful PutRecord calls as a delivery-health signal. Provision destination storage, credentials and transformation dependencies before switching producers. Allow the old AWS stream to finish delivering its buffered records; those pending records are not copied into the new buffer. Test the selected destination with representative batches, including destination failures and retries. Measure delivery delay and sustained throughput with the actual transform, batch settings and destination. [Service Catalog](/service-adapters/catalog). # MSK (Kafka) Source: https://docs.tensor9.com/service-adapters/aws/messaging-streaming/msk-kafka A Kafka cluster whose brokers, storage and coordination AWS operates, with topics, partitions and consumer groups behaving as in open source Kafka. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud and Private Kubernetes](#on-google-cloud-and-private-kubernetes) * [Via Strimzi Kafka](#via-strimzi-kafka) * [Via Bitnami Kafka](#via-bitnami-kafka) * [On Google Cloud](#on-google-cloud) * [Via Managed Service for Apache Kafka](#via-managed-service-for-apache-kafka) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) * [Via Apache Kafka](#via-apache-kafka) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of MSK (Kafka) with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | MSK (Kafka) | Google Cloud and Private Kubernetes · Strimzi Kafka | Google Cloud and Private Kubernetes · Bitnami Kafka | Google Cloud · Managed Service for Apache Kafka | Azure | OCI | Private Kubernetes · Apache Kafka | | ------------------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | Kafka wire protocol | Apache Kafka | native: producer/consumer unchanged | native: producer/consumer unchanged | native: producer/consumer unchanged | native: producer/consumer unchanged | native: producer/consumer unchanged | native: producer/consumer unchanged | | Durability / replication / HA · who operates the broker | AWS-managed (MSK) | you (self-managed on the cluster) | you (self-managed on the cluster) | Google (managed Apache Kafka) | Microsoft (managed Event Hubs) | Oracle (OCI-managed stream pool) | you (self-managed on the cluster) | | Authentication · broker auth | SASL/SCRAM, mTLS, IAM | SASL/SCRAM + mTLS (MSK IAM has no analog) | SASL/SCRAM + mTLS (MSK IAM has no analog) | Google Cloud IAM over SASL\_SSL/OAUTHBEARER (MSK IAM, SASL/SCRAM and mTLS have no analog) | Shared Access Signatures / Microsoft Entra (not Kafka SASL or MSK IAM) | OCI auth tokens (not MSK SASL/SCRAM/mTLS/IAM) | SASL/SCRAM + mTLS (MSK IAM has no analog) | | Retention / storage | broker log + tiered storage | the broker's own local storage (no tiered storage) | the broker's own local storage (no tiered storage) | the managed broker's own storage, tuned per topic | Event Hubs managed retention (capacity sized in throughput/processing units, separate from the partition count) | the stream pool's managed storage, under OCI quotas | the broker's own local storage (no tiered storage) | | Encryption in transit | Yes | Yes - client-broker TLS terminates on the target listener's TLS | Yes - client-broker TLS terminates on the target listener's TLS | Yes - client-broker TLS terminates on the target listener's TLS | Yes - client-broker TLS terminates on the target listener's TLS | Yes - client-broker TLS terminates on the target listener's TLS | Yes - client-broker TLS terminates on the target listener's TLS | | Encryption at rest | KMS key ARN | the target's own storage / managed encryption (the KMS key ARN has no equivalent) | the target's own storage / managed encryption (the KMS key ARN has no equivalent) | the target's own storage / managed encryption (the KMS key ARN has no equivalent) | the target's own storage / managed encryption (the KMS key ARN has no equivalent) | the target's own storage / managed encryption (the KMS key ARN has no equivalent) | the target's own storage / managed encryption (the KMS key ARN has no equivalent) | | API coverage | full | high | high | high | partial | partial | high | ## On Google Cloud and Private Kubernetes ### Via Strimzi Kafka | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------------------- | ------------------ | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Monitoring / broker-log delivery / at-rest KMS / VPC placement | AWS control plane | Out of scope | Full surface | Configure target monitoring and broker logs in place of MSK's CloudWatch, Prometheus, S3 and Firehose settings. Storage encryption uses the target's key and storage configuration. Select target subnets and failure domains separately from the supplied bootstrap endpoint. | | Broker config (custom broker-config revision) | Admin | Partial | Most usage | the target broker natively accepts custom broker config (via the Strimzi or Bitnami chart's config values, or the broker's own config for a raw Apache deployment); MSK's custom broker-config revision has no counterpart, so broker config is set on the target directly | | Partition semantics | Admin | Supported | Most usage | partitions are the real broker's own; set and grow them via the AdminClient as on Kafka | | Topic admin (create / delete / configs) | Admin | Supported | Most usage | the Kafka AdminClient talks to the real broker: create/delete topics, alter configs, and manage partitions natively | | ACLs / SASL auth | Auth | Partial | Full surface | SASL/SCRAM and mutual TLS are supported. Replace MSK IAM authentication and configure access-control lists on the broker. | | Offset commit (\_\_consumer\_offsets) | Consume | Supported | Most usage | offsets commit to the broker's own \_\_consumer\_offsets, exactly as on Kafka | | Glue Schema Registry | Ecosystem | Out of scope | Full surface | AWS Glue Schema Registry has no target-side analog; run a Kafka-native schema registry (Confluent/Apicurio) alongside the broker if you need one | | MSK Connect | Ecosystem | Out of scope | Full surface | MSK Connect (managed Kafka Connect) is not provisioned; run Kafka Connect yourself against the broker if you need connectors | | acks / idempotence / transactions | Producer semantics | Supported | Most usage | acks, idempotence, and transactions are the real broker's own, with full exactly-once semantics | | Broker sizing (MSK broker instance type → CPU / memory / storage) | Sizing | Supported | Most usage | the MSK broker instance type is normalized to CPU/memory and emitted as the broker pod's resource requests/limits (kafka.m5.large → 2 vCPU / 8 GiB), and the storage volume size becomes the broker's persistent-volume size, so sizing follows through to a broker you own | | Tiered storage | Storage | Out of scope | Full surface | MSK's tiered storage is not configured on the target; a self-hosted Kafka broker has native tiered storage (KIP-405, production-ready in Kafka 3.9+) you can enable yourself with a remote-store backend, and the managed targets expose no equivalent knob; retention is the target broker's own local or managed storage | | Consume (consumer groups) | Wire protocol | Supported | Common | your Kafka consumer and consumer-group membership run unchanged over the wire | | Produce (Kafka wire) | Wire protocol | Supported | Common | your Kafka producer runs unchanged; the adapter supplies the target bootstrap-broker endpoint to the target | #### Connecting Kafka clients to Apache Kafka on Kubernetes cluster (via the Strimzi operator) Producers and consumers connect directly to Apache Kafka on Kubernetes cluster (via the Strimzi operator) using the Kafka protocol. Tensor9 supplies the target bootstrap endpoint; there is no Tensor9 message proxy between client and broker. Broker metadata can return additional advertised addresses, so clients need network access and TLS trust for those addresses as well as the bootstrap address. Authentication changes with the broker: SASL/SCRAM and mutual TLS are supported. Replace MSK IAM authentication and manage the broker's access-control lists. Check producer acknowledgment, idempotence and transaction settings against the target-specific support table. A Kafka-compatible endpoint does not by itself guarantee every Kafka administrative or transactional feature. #### State and operating responsibility You configure the replication factor and minimum in-sync replicas (ISR), monitor replica health, and operate storage, recovery and upgrades. Topic partitions hold the retained log and consumer-group offsets record progress. A new broker does not contain the source records, offsets or consumer-group state. Coordinate producer and consumer cutover, and use a separately planned replication process if retained history must move. Source offsets cannot be applied blindly to a different log. Review topic creation, retention, partition counts and broker configuration on the target. AWS IAM authentication, MSK Connect and Glue Schema Registry require separate decisions; changing bootstrap servers does not replace those dependencies. Configure the target's monitoring, backup or recovery procedures and certificate rotation before production cutover. Performance depends on the chosen broker, storage and workload; this profile has no Tensor9 broker benchmark. ### Via Bitnami Kafka | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------------------- | ------------------ | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Monitoring / broker-log delivery / at-rest KMS / VPC placement | AWS control plane | Out of scope | Full surface | Configure target monitoring and broker logs in place of MSK's CloudWatch, Prometheus, S3 and Firehose settings. Storage encryption uses the target's key and storage configuration. Select target subnets and failure domains separately from the supplied bootstrap endpoint. | | Broker config (custom broker-config revision) | Admin | Partial | Most usage | the target broker natively accepts custom broker config (via the Strimzi or Bitnami chart's config values, or the broker's own config for a raw Apache deployment); MSK's custom broker-config revision has no counterpart, so broker config is set on the target directly | | Partition semantics | Admin | Supported | Most usage | partitions are the real broker's own; set and grow them via the AdminClient as on Kafka | | Topic admin (create / delete / configs) | Admin | Supported | Most usage | the Kafka AdminClient talks to the real broker: create/delete topics, alter configs, and manage partitions natively | | ACLs / SASL auth | Auth | Partial | Full surface | SASL/SCRAM and mutual TLS are supported. Replace MSK IAM authentication and configure access-control lists on the broker. | | Offset commit (\_\_consumer\_offsets) | Consume | Supported | Most usage | offsets commit to the broker's own \_\_consumer\_offsets, exactly as on Kafka | | Glue Schema Registry | Ecosystem | Out of scope | Full surface | AWS Glue Schema Registry has no target-side analog; run a Kafka-native schema registry (Confluent/Apicurio) alongside the broker if you need one | | MSK Connect | Ecosystem | Out of scope | Full surface | MSK Connect (managed Kafka Connect) is not provisioned; run Kafka Connect yourself against the broker if you need connectors | | acks / idempotence / transactions | Producer semantics | Supported | Most usage | acks, idempotence, and transactions are the real broker's own, with full exactly-once semantics | | Broker sizing (MSK broker instance type → CPU / memory / storage) | Sizing | Supported | Most usage | the MSK broker instance type is normalized to CPU/memory and emitted as the broker pod's resource requests/limits (kafka.m5.large → 2 vCPU / 8 GiB), and the storage volume size becomes the broker's persistent-volume size, so sizing follows through to a broker you own | | Tiered storage | Storage | Out of scope | Full surface | MSK's tiered storage is not configured on the target; a self-hosted Kafka broker has native tiered storage (KIP-405, production-ready in Kafka 3.9+) you can enable yourself with a remote-store backend, and the managed targets expose no equivalent knob; retention is the target broker's own local or managed storage | | Consume (consumer groups) | Wire protocol | Supported | Common | your Kafka consumer and consumer-group membership run unchanged over the wire | | Produce (Kafka wire) | Wire protocol | Supported | Common | your Kafka producer runs unchanged; the adapter supplies the target bootstrap-broker endpoint to the target | #### Connecting Kafka clients to Apache Kafka on Kubernetes cluster (via the Bitnami Kafka chart) Producers and consumers connect directly to Apache Kafka on Kubernetes cluster (via the Bitnami Kafka chart) using the Kafka protocol. Tensor9 supplies the target bootstrap endpoint; there is no Tensor9 message proxy between client and broker. Broker metadata can return additional advertised addresses, so clients need network access and TLS trust for those addresses as well as the bootstrap address. Authentication changes with the broker: SASL/SCRAM and mutual TLS are supported. Replace MSK IAM authentication and manage the broker's access-control lists. Check producer acknowledgment, idempotence and transaction settings against the target-specific support table. A Kafka-compatible endpoint does not by itself guarantee every Kafka administrative or transactional feature. #### State and operating responsibility You configure the replication factor and minimum in-sync replicas (ISR), monitor replica health, and operate storage, recovery and upgrades. Topic partitions hold the retained log and consumer-group offsets record progress. A new broker does not contain the source records, offsets or consumer-group state. Coordinate producer and consumer cutover, and use a separately planned replication process if retained history must move. Source offsets cannot be applied blindly to a different log. Review topic creation, retention, partition counts and broker configuration on the target. AWS IAM authentication, MSK Connect and Glue Schema Registry require separate decisions; changing bootstrap servers does not replace those dependencies. Configure the target's monitoring, backup or recovery procedures and certificate rotation before production cutover. Performance depends on the chosen broker, storage and workload; this profile has no Tensor9 broker benchmark. ## On Google Cloud ### Via Managed Service for Apache Kafka | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------------------- | ------------------ | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Monitoring / broker-log delivery / at-rest KMS / VPC placement | AWS control plane | Out of scope | Full surface | Configure target monitoring and broker logs in place of MSK's CloudWatch, Prometheus, S3 and Firehose settings. Storage encryption uses the target's key and storage configuration. Select target subnets and failure domains separately from the supplied bootstrap endpoint. | | Broker config (custom broker-config revision) | Admin | Partial | Most usage | Google manages the broker configuration; per-topic settings (retention, partition count) are yours to set, but MSK's custom broker-config revision has no equivalent | | Partition semantics | Admin | Supported | Most usage | partitions are a real Kafka broker's own; create and grow them via the AdminClient as on Kafka | | Topic admin (create / delete / configs) | Admin | Supported | Most usage | the Kafka AdminClient talks to a real Apache Kafka broker, so topic, config, and partition admin all work natively | | ACLs / SASL auth | Auth | Partial | Full surface | clients authenticate through Google Cloud IAM over SASL\_SSL/OAUTHBEARER with Google's login callback handler; MSK's IAM auth has no analog, and SASL/SCRAM credentials and mTLS client certificates have no equivalent, so producers and consumers swap their callback handler and credentials once | | Offset commit (\_\_consumer\_offsets) | Consume | Supported | Most usage | offsets commit to the real broker's \_\_consumer\_offsets, exactly as on Kafka | | Glue Schema Registry | Ecosystem | Out of scope | Full surface | AWS Glue Schema Registry has no target-side analog; run a Kafka-native schema registry (Confluent/Apicurio) alongside the broker if you need one | | MSK Connect | Ecosystem | Out of scope | Full surface | MSK Connect (managed Kafka Connect) is not provisioned; run Kafka Connect yourself against the broker if you need connectors | | acks / idempotence / transactions | Producer semantics | Supported | Most usage | acks, idempotence, and transactions are a real Apache Kafka broker's own, because Google runs Kafka itself rather than a Kafka-compatible surface | | Broker sizing (MSK broker instance type → CPU / memory / storage) | Sizing | Partial | Most usage | Managed Kafka sizes the cluster (vCPU and memory across the whole fleet) rather than a per-broker instance type, so the source instance capacity is multiplied by broker count to estimate cluster capacity and the per-broker EBS volume becomes the managed per-broker disk | | Tiered storage | Storage | Out of scope | Full surface | MSK's tiered storage is not configured on the target; a self-hosted Kafka broker has native tiered storage (KIP-405, production-ready in Kafka 3.9+) you can enable yourself with a remote-store backend, and the managed targets expose no equivalent knob; retention is the target broker's own local or managed storage | | Consume (consumer groups) | Wire protocol | Supported | Common | your Kafka consumer and consumer-group membership run unchanged over the wire | | Produce (Kafka wire) | Wire protocol | Supported | Common | your Kafka producer runs unchanged; the adapter supplies the target bootstrap-broker endpoint to the target | #### Connecting Kafka clients to Google Managed Service for Apache Kafka Producers and consumers connect directly to Google Managed Service for Apache Kafka using the Kafka protocol. Tensor9 supplies the target bootstrap endpoint; there is no Tensor9 message proxy between client and broker. Broker metadata can return additional advertised addresses, so clients need network access and TLS trust for those addresses as well as the bootstrap address. Authentication changes with the broker: clients authenticate through Google Cloud IAM over SASL\_SSL/OAUTHBEARER; MSK IAM, SASL/SCRAM, and mTLS each need a one-time client-side handler and credential swap. Check producer acknowledgment, idempotence and transaction settings against the target-specific support table. A Kafka-compatible endpoint does not by itself guarantee every Kafka administrative or transactional feature. #### State and operating responsibility Google operates durability and HA; capacity is cluster-wide vCPU/memory rather than a broker instance type you pick, and the cluster is reachable only inside your VPC. Topic partitions hold the retained log and consumer-group offsets record progress. A new broker does not contain the source records, offsets or consumer-group state. Coordinate producer and consumer cutover, and use a separately planned replication process if retained history must move. Source offsets cannot be applied blindly to a different log. Review topic creation, retention, partition counts and broker configuration on the target. AWS IAM authentication, MSK Connect and Glue Schema Registry require separate decisions; changing bootstrap servers does not replace those dependencies. Configure the target's monitoring, backup or recovery procedures and certificate rotation before production cutover. Performance depends on the chosen broker, storage and workload; this profile has no Tensor9 broker benchmark. ## On Azure | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------------------- | ------------------ | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Monitoring / broker-log delivery / at-rest KMS / VPC placement | AWS control plane | Out of scope | Full surface | Configure target monitoring and broker logs in place of MSK's CloudWatch, Prometheus, S3 and Firehose settings. Storage encryption uses the target's key and storage configuration. Select target subnets and failure domains separately from the supplied bootstrap endpoint. | | Broker config (custom broker-config revision) | Admin | Out of scope | Most usage | Event Hubs does not expose Kafka broker config, so MSK's custom broker-config revision has no analog | | Partition semantics | Admin | Partial | Most usage | on Standard the partition count is set at creation and cannot be changed; Premium and Dedicated allow increasing (but not decreasing) it after creation, as Kafka itself allows; capacity is sized on a separate axis (throughput units on Standard, processing units on Premium), independent of the partition count | | Topic admin (create / delete / configs) | Admin | Partial | Most usage | topics are Event Hubs, created up front; the Kafka broker-admin/ACL surface differs, so it is not full AdminClient parity | | ACLs / SASL auth | Auth | Partial | Full surface | authentication is Shared Access Signatures or Microsoft Entra, not Kafka SASL/SCRAM or MSK IAM; your Kafka clients configure the Event Hubs connection string instead | | Offset commit (\_\_consumer\_offsets) | Consume | Partial | Most usage | Event Hubs keeps its own consumer-group offset store; the semantics differ slightly from Kafka's \_\_consumer\_offsets | | Glue Schema Registry | Ecosystem | Out of scope | Full surface | AWS Glue Schema Registry has no target-side analog; run a Kafka-native schema registry (Confluent/Apicurio) alongside the broker if you need one | | MSK Connect | Ecosystem | Out of scope | Full surface | MSK Connect (managed Kafka Connect) is not provisioned; run Kafka Connect yourself against the broker if you need connectors | | acks / idempotence / transactions | Producer semantics | Partial | Most usage | produce over the Kafka wire is unchanged; full transactions/idempotence are not complete on the Event Hubs Kafka surface | | Broker sizing (MSK broker instance type → CPU / memory / storage) | Sizing | Partial | Most usage | capacity is throughput units (Standard) or processing units (Premium), sized on a separate axis from the MSK broker instance type; the partition count is fixed at creation on Standard, while Premium and Dedicated allow increasing it | | Tiered storage | Storage | Out of scope | Full surface | MSK's tiered storage is not configured on the target; a self-hosted Kafka broker has native tiered storage (KIP-405, production-ready in Kafka 3.9+) you can enable yourself with a remote-store backend, and the managed targets expose no equivalent knob; retention is the target broker's own local or managed storage | | Consume (consumer groups) | Wire protocol | Supported | Common | your Kafka consumer and consumer-group membership run unchanged over the wire | | Produce (Kafka wire) | Wire protocol | Supported | Common | your Kafka producer runs unchanged; the adapter supplies the target bootstrap-broker endpoint to the target | #### How it works Your Kafka client connects directly to Azure Event Hubs at `.servicebus.windows.net:9093`. Standard and Premium tiers expose this Kafka endpoint. Produce and consume code remain unchanged; configure the new bootstrap address and credentials. Tensor9 does not proxy these connections. Event Hubs differs from Kafka in administration, transactions, authentication and capacity settings. Review those differences below before selecting this target.
Before: on AWS the application's Kafka client connects to MSK bootstrap brokers over SASL_SSL. After: in the target Azure subscription the same client and the same Kafka protocol connect to an Event Hubs namespace's Kafka endpoint on port 9093, with no Tensor9 adapter in the data path. Before: on AWS the application's Kafka client connects to MSK bootstrap brokers over SASL_SSL. After: in the target Azure subscription the same client and the same Kafka protocol connect to an Event Hubs namespace's Kafka endpoint on port 9093, with no Tensor9 adapter in the data path.
#### Estimating namespace capacity MSK capacity is declared as a broker count and instance type, with storage and placement per broker. Event Hubs capacity is set on a namespace using throughput units or processing units. Tensor9 estimates namespace capacity from broker count multiplied by vCPUs per broker. Brokers with at least 16 vCPUs each, such as `kafka.m5.4xlarge`, select Premium processing units. Smaller brokers select Standard throughput units. Basic is excluded because it has no Kafka endpoint. The conversion is reported as a lossy translation because machine size does not determine your workload's messaging throughput. Load-test the generated capacity and adjust it using measured traffic. #### Provision topics as Event Hubs The generated Terraform creates the namespace. It creates no Event Hubs because an MSK cluster resource does not declare topics: Kafka topics are created by producers or administrators at runtime. `aws_msk_configuration` contains `server.properties` settings, not a topic list. Each Kafka topic needs an Event Hub inside the namespace. Provision these before producing messages; this target does not create them automatically on first use. #### Configure authentication Configure clients to use an Event Hubs Shared Access Signature (SAS) policy or Microsoft Entra identity. MSK's SASL/SCRAM, mutual TLS and IAM configurations do not transfer directly. Namespace authentication is configured separately from the generated stack. Credentials use the appliance's secrets mechanism; the generated Terraform contains no SAS key or connection string. Event Hubs requires TLS and encrypts stored data by default, so MSK's `encryption_info` is not translated. Configure a customer-managed encryption key separately on Azure if required. #### Limitations △ Where MSK and Event Hubs diverge, read before you adopt * **Transactions and exactly-once are not complete.** Produce and consume over the Kafka wire are unchanged, but the Event Hubs Kafka surface does not offer full Kafka transaction and idempotent-producer semantics. An application that relies on exactly-once delivery through Kafka transactions needs these limitations resolved before it can use this target. * **Partition count is fixed at creation on Standard.** On Standard, an Event Hub's partition count is set when it is created and cannot be changed afterwards. Premium and Dedicated allow increasing it (but never decreasing), which is what Kafka itself allows. Size partitions for the consumer parallelism you expect to need, because on Standard the only way to change your mind is a new hub. * **Capacity and partitions are sized independently.** On Kafka, partitions set both parallelism and much of your throughput headroom. On Event Hubs, throughput is bought separately as throughput units (Standard) or processing units (Premium), independent of partition count. Sizing one does not size the other, and a partition count copied across from MSK does not bring its throughput with it. * **Consumer-group offsets live in a different store.** Event Hubs keeps its own consumer-group offset store rather than Kafka's `__consumer_offsets` topic. Committing and reading offsets works through the Kafka client as usual; tooling that reads the offsets topic directly, or that reasons about its retention and compaction, does not port. * **Kafka administration is only partly supported.** Topics are Event Hubs, created up front, and the broker-administration and ACL surface differs from Kafka's. Code that provisions topics or manages ACLs through AdminClient at runtime should be treated as needing rework rather than assumed to work. * **There is no Kafka version to pin.** MSK's `kafka_version` has no counterpart: Event Hubs tracks its own supported Kafka protocol version and you do not choose or freeze it. A stack that pins a version for compatibility reasons is relying on something this target cannot promise. * **Custom server.properties do not transfer.** MSK's `configuration_info` (retention hours, default partition counts and the rest of `server.properties`) has no namespace-level equivalent. The settings that do exist live per Event Hub and are tuned after provisioning, so a carefully tuned cluster configuration is re-expressed rather than migrated. * **Broker placement across availability zones has no analog.** An Event Hubs namespace is a regional service with no brokers to place, so MSK's per-broker subnet and availability-zone distribution has nothing to map onto. If your MSK topology was chosen for zone-level placement, that reasoning does not transfer. #### Other considerations * **Update client endpoint and authentication.** Repoint the bootstrap servers at the namespace's Kafka endpoint on port 9093 and switch the SASL configuration to a SAS policy or a Microsoft Entra identity. Produce and consume paths, serializers and partitioning logic remain unchanged. * **Data does not migrate; the namespace starts empty.** Provisioning creates a fresh namespace. Messages still in MSK do not move, and Kafka's retention means the window is finite anyway, so plan a cutover in which producers switch over and consumers drain the old cluster, rather than expecting a copy. * **Capacity is the number to revisit after launch.** The emitted SKU and capacity are estimated from your broker fleet, not measured from your traffic. Watch throughput-unit utilisation once real load arrives; Standard's throughput units and Premium's processing units are both adjustable. * **Naming is deterministic, so re-planning is stable.** The namespace name derives from the stack's persisted logical identity and the original cluster name, so the same stack compiles to the same namespace every time. A re-plan does not propose replacing infrastructure that has not changed. ## On OCI | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------------------- | ------------------ | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Monitoring / broker-log delivery / at-rest KMS / VPC placement | AWS control plane | Out of scope | Full surface | Configure target monitoring and broker logs in place of MSK's CloudWatch, Prometheus, S3 and Firehose settings. Storage encryption uses the target's key and storage configuration. Select target subnets and failure domains separately from the supplied bootstrap endpoint. | | Broker config (custom broker-config revision) | Admin | Partial | Most usage | OCI exposes a subset of broker configuration settings; MSK's custom broker-config revision has no counterpart | | Partition semantics | Admin | Partial | Most usage | partitions are the stream pool's, under OCI quotas | | Topic admin (create / delete / configs) | Admin | Partial | Most usage | topics are created explicitly (auto-create is off); OCI exposes a subset of Kafka's broker configuration settings, not the full broker admin surface | | ACLs / SASL auth | Auth | Partial | Full surface | authentication is OCI auth tokens at the stream-pool endpoint, not MSK's SASL/SCRAM, mTLS, or IAM; encryption and monitoring move to OCI-managed | | Offset commit (\_\_consumer\_offsets) | Consume | Partial | Most usage | consumer-group offsets use OCI Streaming's managed offset store; the semantics track Kafka's with OCI's quotas | | Glue Schema Registry | Ecosystem | Out of scope | Full surface | AWS Glue Schema Registry has no target-side analog; run a Kafka-native schema registry (Confluent/Apicurio) alongside the broker if you need one | | MSK Connect | Ecosystem | Out of scope | Full surface | MSK Connect (managed Kafka Connect) is not provisioned; run Kafka Connect yourself against the broker if you need connectors | | acks / idempotence / transactions | Producer semantics | Partial | Most usage | produce over the Kafka wire runs unchanged, but full idempotence/transactions are not complete on OCI Streaming's Kafka-compatible API, and acks and throughput are bounded by OCI's quotas rather than a broker you tune | | Broker sizing (MSK broker instance type → CPU / memory / storage) | Sizing | Partial | Most usage | the stream pool is not a broker you size; capacity is OCI's partitions and quotas, not an instance type or a volume you set | | Tiered storage | Storage | Out of scope | Full surface | MSK's tiered storage is not configured on the target; a self-hosted Kafka broker has native tiered storage (KIP-405, production-ready in Kafka 3.9+) you can enable yourself with a remote-store backend, and the managed targets expose no equivalent knob; retention is the target broker's own local or managed storage | | Consume (consumer groups) | Wire protocol | Supported | Common | your Kafka consumer and consumer-group membership run unchanged over the wire | | Produce (Kafka wire) | Wire protocol | Supported | Common | your Kafka producer runs unchanged; the adapter supplies the target bootstrap-broker endpoint to the target | #### Connecting Kafka clients to OCI Streaming Producers and consumers connect directly to OCI Streaming using the Kafka protocol. Tensor9 supplies the target bootstrap endpoint; there is no Tensor9 message proxy between client and broker. Broker metadata can return additional advertised addresses, so clients need network access and TLS trust for those addresses as well as the bootstrap address. Authentication changes with the broker: authentication is OCI auth tokens at the stream-pool endpoint, not MSK's SASL/SCRAM, mTLS, or IAM; your Kafka clients configure OCI's auth instead. Check producer acknowledgment, idempotence and transaction settings against the target-specific support table. A Kafka-compatible endpoint does not by itself guarantee every Kafka administrative or transactional feature. #### State and operating responsibility Oracle operates durability and HA; partitions and retention live under OCI's quotas rather than a broker you tune directly. Topic partitions hold the retained log and consumer-group offsets record progress. A new broker does not contain the source records, offsets or consumer-group state. Coordinate producer and consumer cutover, and use a separately planned replication process if retained history must move. Source offsets cannot be applied blindly to a different log. Review topic creation, retention, partition counts and broker configuration on the target. AWS IAM authentication, MSK Connect and Glue Schema Registry require separate decisions; changing bootstrap servers does not replace those dependencies. Configure the target's monitoring, backup or recovery procedures and certificate rotation before production cutover. Performance depends on the chosen broker, storage and workload; this profile has no Tensor9 broker benchmark. ## On Private Kubernetes ### Via Apache Kafka | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------------------- | ------------------ | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Monitoring / broker-log delivery / at-rest KMS / VPC placement | AWS control plane | Out of scope | Full surface | Configure target monitoring and broker logs in place of MSK's CloudWatch, Prometheus, S3 and Firehose settings. Storage encryption uses the target's key and storage configuration. Select target subnets and failure domains separately from the supplied bootstrap endpoint. | | Broker config (custom broker-config revision) | Admin | Partial | Most usage | the target broker natively accepts custom broker config (via the Strimzi or Bitnami chart's config values, or the broker's own config for a raw Apache deployment); MSK's custom broker-config revision has no counterpart, so broker config is set on the target directly | | Partition semantics | Admin | Supported | Most usage | partitions are the real broker's own; set and grow them via the AdminClient as on Kafka | | Topic admin (create / delete / configs) | Admin | Supported | Most usage | the Kafka AdminClient talks to the real broker: create/delete topics, alter configs, and manage partitions natively | | ACLs / SASL auth | Auth | Partial | Full surface | SASL/SCRAM and mutual TLS are supported. Replace MSK IAM authentication and configure access-control lists on the broker. | | Offset commit (\_\_consumer\_offsets) | Consume | Supported | Most usage | offsets commit to the broker's own \_\_consumer\_offsets, exactly as on Kafka | | Glue Schema Registry | Ecosystem | Out of scope | Full surface | AWS Glue Schema Registry has no target-side analog; run a Kafka-native schema registry (Confluent/Apicurio) alongside the broker if you need one | | MSK Connect | Ecosystem | Out of scope | Full surface | MSK Connect (managed Kafka Connect) is not provisioned; run Kafka Connect yourself against the broker if you need connectors | | acks / idempotence / transactions | Producer semantics | Supported | Most usage | acks, idempotence, and transactions are the real broker's own, with full exactly-once semantics | | Broker sizing (MSK broker instance type → CPU / memory / storage) | Sizing | Supported | Most usage | the MSK broker instance type is normalized to CPU/memory and emitted as the broker pod's resource requests/limits (kafka.m5.large → 2 vCPU / 8 GiB), and the storage volume size becomes the broker's persistent-volume size, so sizing follows through to a broker you own | | Tiered storage | Storage | Out of scope | Full surface | MSK's tiered storage is not configured on the target; a self-hosted Kafka broker has native tiered storage (KIP-405, production-ready in Kafka 3.9+) you can enable yourself with a remote-store backend, and the managed targets expose no equivalent knob; retention is the target broker's own local or managed storage | | Consume (consumer groups) | Wire protocol | Supported | Common | your Kafka consumer and consumer-group membership run unchanged over the wire | | Produce (Kafka wire) | Wire protocol | Supported | Common | your Kafka producer runs unchanged; the adapter supplies the target bootstrap-broker endpoint to the target | #### Connecting Kafka clients to Apache Kafka on Kubernetes cluster (as a raw Apache Kafka deployment) Producers and consumers connect directly to Apache Kafka on Kubernetes cluster (as a raw Apache Kafka deployment) using the Kafka protocol. Tensor9 supplies the target bootstrap endpoint; there is no Tensor9 message proxy between client and broker. Broker metadata can return additional advertised addresses, so clients need network access and TLS trust for those addresses as well as the bootstrap address. Authentication changes with the broker: SASL/SCRAM and mutual TLS are supported. Replace MSK IAM authentication and manage the broker's access-control lists. Check producer acknowledgment, idempotence and transaction settings against the target-specific support table. A Kafka-compatible endpoint does not by itself guarantee every Kafka administrative or transactional feature. #### State and operating responsibility You configure the replication factor and minimum in-sync replicas (ISR), monitor replica health, and operate storage, recovery and upgrades. Topic partitions hold the retained log and consumer-group offsets record progress. A new broker does not contain the source records, offsets or consumer-group state. Coordinate producer and consumer cutover, and use a separately planned replication process if retained history must move. Source offsets cannot be applied blindly to a different log. Review topic creation, retention, partition counts and broker configuration on the target. AWS IAM authentication, MSK Connect and Glue Schema Registry require separate decisions; changing bootstrap servers does not replace those dependencies. Configure the target's monitoring, backup or recovery procedures and certificate rotation before production cutover. Performance depends on the chosen broker, storage and workload; this profile has no Tensor9 broker benchmark. [Service Catalog](/service-adapters/catalog). # SNS Source: https://docs.tensor9.com/service-adapters/aws/messaging-streaming/sns AWS SNS. Publish-subscribe messaging where one topic fans a message out to many subscribers: queues, Lambda functions, HTTP endpoints, email and SMS. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [On Google Cloud](#on-google-cloud) * [Via Google Cloud Pub/Sub](#via-google-cloud-pub/sub) * [Via Google Cloud SQL for PostgreSQL](#via-google-cloud-sql-for-postgresql) * [On Azure, OCI, and Private Kubernetes](#on-azure-oci-and-private-kubernetes) * [Via PostgreSQL](#via-postgresql) * [On Azure](#on-azure) * [Via Azure Service Bus Premium](#via-azure-service-bus-premium) * [Via Azure Database for PostgreSQL Flexible Server](#via-azure-database-for-postgresql-flexible-server) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of SNS with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | SNS | Google Cloud · Google Cloud Pub/Sub | Google Cloud · Google Cloud SQL for PostgreSQL | Azure, OCI, and Private Kubernetes · PostgreSQL | Azure · Azure Service Bus Premium | Azure · Azure Database for PostgreSQL Flexible Server | | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------- | | Delivery guarantee | at least once | at least once | at least once | at least once | at least once | at least once | | Topic fan-out · one publication to independent subscriptions | native | native Google Pub/Sub fan-out | one transactional outbox row per selected subscription | one durable delivery row per selected subscription | native Service Bus topic with a broker subscription per binding | one transactional outbox row per selected subscription | | Delivery protocols · subscription endpoints | SQS / HTTP-S / email / SMS / Lambda / Firehose / application | SQS, HTTP, HTTPS and Lambda; other protocols are refused | SQS, HTTP, HTTPS and Lambda; other protocols are refused | SQS, HTTP, HTTPS and Lambda; other protocols are refused | SQS, HTTP, HTTPS and Lambda; other protocols are refused | SQS, HTTP, HTTPS and Lambda; other protocols are refused | | Subscription filtering | message attributes or JSON message body | both scopes with a bounded AWS-compatible grammar subset | both scopes with a bounded AWS-compatible grammar subset | both scopes with a bounded AWS-compatible grammar subset | both scopes with a bounded AWS-compatible grammar subset | both scopes with a bounded AWS-compatible grammar subset | | Raw message delivery | SQS and HTTP-S | SQS only; HTTP-S receives a signed envelope | SQS only; HTTP-S receives a signed envelope | SQS only; HTTP-S receives a signed envelope | SQS only; HTTP-S receives a signed envelope | SQS only; HTTP-S receives a signed envelope | | Message durability | AWS-managed | Google-operated Pub/Sub retention | Cloud SQL for PostgreSQL transaction before Publish returns | transactional PostgreSQL outbox | Microsoft-operated Service Bus retention | Flexible Server transaction before Publish returns | | Delivery retry | SNS-owned | Pub/Sub schedules broker redelivery; the adapter controls destination admission | the service delivery workers | the service delivery workers | Service Bus schedules broker redelivery; the adapter controls destination admission | the service delivery workers | | API coverage | full | partial | partial | partial | partial | partial | ## On Google Cloud ### Via Google Cloud Pub/Sub | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------ | -------- | ------------ | ------------- | ---------- | -------------------------------------------------------------------------------------------------------- | | At-least-once delivery | Delivery | Supported | - | - | subscriber outcomes are independent and an uncertain acknowledgement can produce a duplicate | | Raw message delivery | Delivery | Partial | - | - | SQS only, subject to the origin's raw-delivery attribute cap; HTTP/S raw delivery is refused | | Subscriber protocols | Delivery | Partial | - | - | SQS, HTTP, HTTPS and Lambda are supported; email, SMS, Firehose, mobile push and application are refused | | Subscription redrive | Delivery | Out of scope | - | - | RedrivePolicy is refused; bounded retries end in diagnostic state | | FIFO and ordering | Ordering | Out of scope | - | - | FIFO topics, deduplication and MessageGroupId are refused | | Subscription filter policies | Routing | Partial | - | - | message-attribute and JSON-body scopes use a bounded AWS-compatible grammar subset | | HTTP/S signature compatibility | Security | Partial | - | - | detached tenant JWS requires receiver trust setup and is not AWS SigningCertURL compatibility | | Operation | Area | Support | Depth | Notes | | ------------------------- | ------------- | ------------ | ------------ | --------------------------------------------------------------------------------------- | | AddPermission | Control plane | Out of scope | Full surface | caller and destination grants remain in the environment's existing identity system | | RemovePermission | Control plane | Out of scope | Full surface | caller and destination grants remain in the environment's existing identity system | | SetTopicAttributes | Control plane | Out of scope | Full surface | arbitrary topic policy, delivery policy and encryption changes are refused | | GetTopicAttributes | Discovery | Partial | Most usage | returns represented attributes without fabricating policy or delivery-policy values | | ListSubscriptions | Discovery | Supported | Most usage | tenant-authorized snapshot pages contain no confirmation secrets or receiver handles | | ListSubscriptionsByTopic | Discovery | Supported | Most usage | tenant-authorized snapshot pages for one topic | | ListTopics | Discovery | Supported | Common | tenant-authorized snapshot pages contain at most 100 topics | | Publish | Publish | Partial | Common | translates TopicArn publication to the corresponding native Pub/Sub topic | | PublishBatch | Publish | Partial | Most usage | at most ten independently admitted entries within the 256-KiB aggregate budget | | ConfirmSubscription | Subscriptions | Partial | Common | activates pending HTTP/S subscriptions through a bounded, one-purpose token | | GetSubscriptionAttributes | Subscriptions | Partial | Most usage | returns supported filter, scope and SQS raw-delivery attributes | | SetSubscriptionAttributes | Subscriptions | Partial | Most usage | supports FilterPolicy, FilterPolicyScope and SQS RawMessageDelivery; redrive is refused | | Subscribe | Subscriptions | Partial | Common | same-tenant, same-region SQS, HTTP, HTTPS and unqualified Lambda destinations | | Unsubscribe | Subscriptions | Supported | Common | authenticated, generation-aware removal blocks new attempts and retires the binding | | ListTagsForResource | Tags | Supported | Most usage | returns customer tags without internal ownership metadata | | TagResource | Tags | Supported | Most usage | customer tags cannot alter internal ownership | | UntagResource | Tags | Supported | Most usage | - | | CreateTopic | Topics | Partial | Common | creates an idempotent standard topic; unsupported attributes are refused | | DeleteTopic | Topics | Partial | Common | generation-aware deletion cannot remove a replacement topic | #### How it works This relation names the Google Cloud resource that replaces the SNS topic. Topic storage and fan-out are native Pub/Sub operations. SNS subscription identity, filtering, confirmation, signatures and delivery to SQS, HTTP, HTTPS and Lambda remain part of the AWS-compatible service surface. Google operates broker durability and scale. The customer operates destination reachability and receiver trust configuration. The service adapter uses the customer environment's workload identity.
An AWS SNS topic becomes a Google Pub/Sub topic. The application's unchanged SNS client calls a service adapter, which publishes to the target topic. Native Pub/Sub subscriptions retain copies while destination workers preserve SNS protocol behavior. An AWS SNS topic becomes a Google Pub/Sub topic. The application's unchanged SNS client calls a service adapter, which publishes to the target topic. Native Pub/Sub subscriptions retain copies while destination workers preserve SNS protocol behavior.

The target is a real Pub/Sub topic; SNS endpoint semantics remain in the service adapter.

#### Native resource, SNS behavior A Pub/Sub topic provides durable one-to-many storage, but it does not call an SQS queue, invoke a Lambda function or perform the selected HTTP/S confirmation and signature contract. Pull workers add those SNS behaviors. The broker owns copy retention and redelivery; current destination authorization still gates each external attempt. Filter policies are evaluated from the immutable SNS routing revision instead of translated into Pub/Sub filters. This keeps the same accepted grammar and update behavior across every target. #### Limitations * **Partial SNS surface.** Unsupported operations and fields are refused before they change state. * **No FIFO mapping.** Ordering, deduplication and MessageGroupId are not translated to Pub/Sub ordering keys. * **Protocol scope.** SQS, HTTP, HTTPS and Lambda are supported; email, SMS and remaining protocols are refused. * **Receiver changes for HTTP/S.** Tenant JWS is not compatible with an AWS-only certificate verifier. * **Existing work stays in AWS.** Cutover does not copy pending AWS notifications into Pub/Sub. #### Other considerations Measure broker acceptance and end-to-end subscriber receipt separately. The published June 2026 comparison covers Publish acceptance only: p50 about 57 ms through Pub/Sub versus about 9 ms on native SNS at 50 open-loop connections. ### Via Google Cloud SQL for PostgreSQL | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------ | -------- | ------------ | ------------- | ---------- | -------------------------------------------------------------------------------------------------------- | | At-least-once delivery | Delivery | Supported | - | - | subscriber outcomes are independent and an uncertain acknowledgement can produce a duplicate | | Raw message delivery | Delivery | Partial | - | - | SQS only, subject to the origin's raw-delivery attribute cap; HTTP/S raw delivery is refused | | Subscriber protocols | Delivery | Partial | - | - | SQS, HTTP, HTTPS and Lambda are supported; email, SMS, Firehose, mobile push and application are refused | | Subscription redrive | Delivery | Out of scope | - | - | RedrivePolicy is refused; bounded retries end in diagnostic state | | FIFO and ordering | Ordering | Out of scope | - | - | FIFO topics, deduplication and MessageGroupId are refused | | Subscription filter policies | Routing | Partial | - | - | message-attribute and JSON-body scopes use a bounded AWS-compatible grammar subset | | HTTP/S signature compatibility | Security | Partial | - | - | detached tenant JWS requires receiver trust setup and is not AWS SigningCertURL compatibility | | Operation | Area | Support | Depth | Notes | | ------------------------- | ------------- | ------------ | ------------ | --------------------------------------------------------------------------------------- | | AddPermission | Control plane | Out of scope | Full surface | caller and destination grants remain in the environment's existing identity system | | RemovePermission | Control plane | Out of scope | Full surface | caller and destination grants remain in the environment's existing identity system | | SetTopicAttributes | Control plane | Out of scope | Full surface | arbitrary topic policy, delivery policy and encryption changes are refused | | GetTopicAttributes | Discovery | Partial | Most usage | returns represented attributes without fabricating policy or delivery-policy values | | ListSubscriptions | Discovery | Supported | Most usage | tenant-authorized snapshot pages contain no confirmation secrets or receiver handles | | ListSubscriptionsByTopic | Discovery | Supported | Most usage | tenant-authorized snapshot pages for one topic | | ListTopics | Discovery | Supported | Common | tenant-authorized snapshot pages contain at most 100 topics | | Publish | Publish | Partial | Common | commits the message and selected deliveries to Cloud SQL in one transaction | | PublishBatch | Publish | Partial | Most usage | at most ten independently admitted entries within the 256-KiB aggregate budget | | ConfirmSubscription | Subscriptions | Partial | Common | activates pending HTTP/S subscriptions through a bounded, one-purpose token | | GetSubscriptionAttributes | Subscriptions | Partial | Most usage | returns supported filter, scope and SQS raw-delivery attributes | | SetSubscriptionAttributes | Subscriptions | Partial | Most usage | supports FilterPolicy, FilterPolicyScope and SQS RawMessageDelivery; redrive is refused | | Subscribe | Subscriptions | Partial | Common | same-tenant, same-region SQS, HTTP, HTTPS and unqualified Lambda destinations | | Unsubscribe | Subscriptions | Supported | Common | authenticated, generation-aware removal blocks new attempts and retires the binding | | ListTagsForResource | Tags | Supported | Most usage | returns customer tags without internal ownership metadata | | TagResource | Tags | Supported | Most usage | customer tags cannot alter internal ownership | | UntagResource | Tags | Supported | Most usage | - | | CreateTopic | Topics | Partial | Common | creates an idempotent standard topic; unsupported attributes are refused | | DeleteTopic | Topics | Partial | Common | generation-aware deletion cannot remove a replacement topic | #### How it works The SNS API is served from a PostgreSQL delivery outbox on Google Cloud SQL. A successful `Publish` means the message and its selected delivery rows committed together. It does not mean that every destination has received the notification. Filtering uses the subscription policy revision captured by the publication. A nonmatching row settles without a destination call. Matching rows move independently through SQS, HTTP, HTTPS or Lambda delivery, so a failing webhook does not delay a healthy queue or function.
An application calls the SNS API through a service adapter in Google Cloud. The adapter commits the message and one delivery row per selected subscription to Cloud SQL in one transaction. Workers send matching rows to SQS, HTTP, HTTPS, or Lambda. An application calls the SNS API through a service adapter in Google Cloud. The adapter commits the message and one delivery row per selected subscription to Cloud SQL in one transaction. Workers send matching rows to SQS, HTTP, HTTPS, or Lambda.

Cloud SQL holds the notification and every selected subscription delivery in one transaction.

#### Recovery and ownership Workers claim only available capacity and attach a monotonic generation to the attempt. Completion and rescheduling compare that generation, so a worker that resumes after its claim expired cannot overwrite a newer outcome. A crash after remote acceptance but before settlement can still repeat the notification. Google operates Cloud SQL availability, backups and database maintenance. The customer sizes database storage, connections and worker capacity. Database rows expose attempts, next-attempt time and sanitized failure state; service diagnostics join those facts to topic, subscription and destination acceptance.
Cloud SQL delivery recovery. A worker claims an available delivery with a generation, obtains current destination authority, sends once, and conditionally records success or retry. A stale worker cannot overwrite a newer claim. Cloud SQL delivery recovery. A worker claims an available delivery with a generation, obtains current destination authority, sends once, and conditionally records success or retry. A stale worker cannot overwrite a newer claim.

Claim generations make recovery safe; they do not remove at-least-once duplicates.

#### Limitations * **Standard topics only.** FIFO topics, ordering, deduplication and MessageGroupId are refused. * **Limited filter grammar.** Attribute and JSON-body scopes are supported, but wildcard forms and uncharacterized nested cases are refused. * **SQS raw delivery only.** SQS may receive the original body with at most ten attributes. HTTP/S always receives a signed envelope. * **Receiver integration.** SQS and Lambda calls require their local service adapters. HTTP/S requires confirmation and tenant JWS trust setup. * **No inherited AWS work.** Existing AWS delivery backlog is not transferred to Cloud SQL during cutover. #### Other considerations Use workload identity and private database connectivity for Cloud SQL. Monitor commit latency, oldest pending delivery, attempts, terminal failures and worker capacity. No Cloud SQL SNS performance result has been published; Pub/Sub measurements describe a different path. Cut over publishers only after topics, subscriptions, receiver grants and workers report ready. Drain or deliberately expire outstanding AWS deliveries separately. ## On Azure, OCI, and Private Kubernetes ### Via PostgreSQL | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------ | -------- | ------------ | ------------- | ---------- | -------------------------------------------------------------------------------------------------------- | | At-least-once delivery | Delivery | Supported | - | - | subscriber outcomes are independent and an uncertain acknowledgement can produce a duplicate | | Raw message delivery | Delivery | Partial | - | - | SQS only, subject to the origin's raw-delivery attribute cap; HTTP/S raw delivery is refused | | Subscriber protocols | Delivery | Partial | - | - | SQS, HTTP, HTTPS and Lambda are supported; email, SMS, Firehose, mobile push and application are refused | | Subscription redrive | Delivery | Out of scope | - | - | RedrivePolicy is refused; bounded retries end in diagnostic state | | FIFO and ordering | Ordering | Out of scope | - | - | FIFO topics, deduplication and MessageGroupId are refused | | Subscription filter policies | Routing | Partial | - | - | message-attribute and JSON-body scopes use a bounded AWS-compatible grammar subset | | HTTP/S signature compatibility | Security | Partial | - | - | detached tenant JWS requires receiver trust setup and is not AWS SigningCertURL compatibility | | Operation | Area | Support | Depth | Notes | | ------------------------- | ------------- | ------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | AddPermission | Control plane | Out of scope | Full surface | caller and destination grants remain in the environment's existing identity system | | RemovePermission | Control plane | Out of scope | Full surface | caller and destination grants remain in the environment's existing identity system | | SetTopicAttributes | Control plane | Out of scope | Full surface | arbitrary topic policy, delivery policy and encryption changes are refused | | GetTopicAttributes | Discovery | Partial | Most usage | returns represented attributes without fabricating policy or delivery-policy values | | ListSubscriptions | Discovery | Supported | Most usage | tenant-authorized snapshot pages contain no confirmation secrets or receiver handles | | ListSubscriptionsByTopic | Discovery | Supported | Most usage | tenant-authorized snapshot pages for one topic | | ListTopics | Discovery | Supported | Common | tenant-authorized snapshot pages contain at most 100 topics | | Publish | Publish | Partial | Common | commits the message and one row per selected subscription in one transaction; TopicArn publication, Subject and typed attributes are supported | | PublishBatch | Publish | Partial | Most usage | at most ten independently admitted entries within the 256-KiB aggregate budget | | ConfirmSubscription | Subscriptions | Partial | Common | activates pending HTTP/S subscriptions through a bounded, one-purpose token | | GetSubscriptionAttributes | Subscriptions | Partial | Most usage | returns supported filter, scope and SQS raw-delivery attributes | | SetSubscriptionAttributes | Subscriptions | Partial | Most usage | supports FilterPolicy, FilterPolicyScope and SQS RawMessageDelivery; redrive is refused | | Subscribe | Subscriptions | Partial | Common | same-tenant, same-region SQS, HTTP, HTTPS and unqualified Lambda destinations | | Unsubscribe | Subscriptions | Supported | Common | authenticated, generation-aware removal blocks new attempts and retires the binding | | ListTagsForResource | Tags | Supported | Most usage | returns customer tags without internal ownership metadata | | TagResource | Tags | Supported | Most usage | customer tags cannot alter internal ownership | | UntagResource | Tags | Supported | Most usage | - | | CreateTopic | Topics | Partial | Common | creates an idempotent standard topic; unsupported attributes are refused | | DeleteTopic | Topics | Partial | Common | generation-aware deletion cannot remove a replacement topic | #### How it works The application keeps its SNS client. Requests are routed to an adapter in the customer's environment. A publication commits the message and one row for every selected subscription in the same transaction. Workers claim rows concurrently and deliver each subscription independently. The captured routing revision fixes membership and filtering for that message. Current destination authority is checked again before every external attempt. A restart rebuilds worker progress from durable rows, without asking the application to subscribe again.
The SNS API is served in the target Kubernetes environment. PostgreSQL stores topic and subscription state, the message, and one delivery row per selected subscription. Workers call the SQS and Lambda service adapters or connect to confirmed HTTP and HTTPS endpoints. The SNS API is served in the target Kubernetes environment. PostgreSQL stores topic and subscription state, the message, and one delivery row per selected subscription. Workers call the SQS and Lambda service adapters or connect to confirmed HTTP and HTTPS endpoints.

PostgreSQL is both the topic registry and the durable delivery queue.

#### Delivery and recovery Temporary failures retry with bounded backoff. Permanent failures and exhausted work remain visible for the diagnostic retention period. A destination can accept a notification just before the worker loses its acknowledgement, so recipients must make repeated `MessageId` effects safe. The customer operates PostgreSQL capacity, backups and recovery for this target. Monitor commit latency, oldest pending delivery, connection pressure, attempt count and terminal failures.
Three PostgreSQL delivery rows proceed independently. The SQS row is accepted, the HTTPS row is scheduled for retry, and the Lambda row is accepted. A conditional update records each result without allowing a stale worker to replace a newer outcome. Three PostgreSQL delivery rows proceed independently. The SQS row is accepted, the HTTPS row is scheduled for retry, and the Lambda row is accepted. A conditional update records each result without allowing a stale worker to replace a newer outcome.

One slow destination does not block its siblings.

#### Limitations * **Standard topics only.** FIFO, ordering, deduplication and MessageGroupId are refused. * **Four subscriber protocols.** SQS, HTTP, HTTPS and Lambda are supported; email, SMS and other protocols are refused. * **Filter subset.** Both scopes are available, with explicit refusals for operators outside the bounded grammar. * **No subscription redrive.** RedrivePolicy is refused; terminal state remains in the service diagnostics instead. * **Customer-operated database.** Database availability, backup, storage and connection capacity are customer responsibilities. #### Other considerations Validate all four destination paths and restart recovery before publisher cutover. Outstanding AWS deliveries are not copied into PostgreSQL. No Postgres performance result exists for this SNS mapping. ## On Azure ### Via Azure Service Bus Premium | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------ | -------- | ------------ | ------------- | ---------- | -------------------------------------------------------------------------------------------------------- | | At-least-once delivery | Delivery | Supported | - | - | subscriber outcomes are independent and an uncertain acknowledgement can produce a duplicate | | Raw message delivery | Delivery | Partial | - | - | SQS only, subject to the origin's raw-delivery attribute cap; HTTP/S raw delivery is refused | | Subscriber protocols | Delivery | Partial | - | - | SQS, HTTP, HTTPS and Lambda are supported; email, SMS, Firehose, mobile push and application are refused | | Subscription redrive | Delivery | Out of scope | - | - | RedrivePolicy is refused; bounded retries end in diagnostic state | | FIFO and ordering | Ordering | Out of scope | - | - | FIFO topics, deduplication and MessageGroupId are refused | | Subscription filter policies | Routing | Partial | - | - | message-attribute and JSON-body scopes use a bounded AWS-compatible grammar subset | | HTTP/S signature compatibility | Security | Partial | - | - | detached tenant JWS requires receiver trust setup and is not AWS SigningCertURL compatibility | | Operation | Area | Support | Depth | Notes | | ------------------------- | ------------- | ------------ | ------------ | --------------------------------------------------------------------------------------- | | AddPermission | Control plane | Out of scope | Full surface | caller and destination grants remain in the environment's existing identity system | | RemovePermission | Control plane | Out of scope | Full surface | caller and destination grants remain in the environment's existing identity system | | SetTopicAttributes | Control plane | Out of scope | Full surface | arbitrary topic policy, delivery policy and encryption changes are refused | | GetTopicAttributes | Discovery | Partial | Most usage | returns represented attributes without fabricating policy or delivery-policy values | | ListSubscriptions | Discovery | Supported | Most usage | tenant-authorized snapshot pages contain no confirmation secrets or receiver handles | | ListSubscriptionsByTopic | Discovery | Supported | Most usage | tenant-authorized snapshot pages for one topic | | ListTopics | Discovery | Supported | Common | tenant-authorized snapshot pages contain at most 100 topics | | Publish | Publish | Partial | Common | publishes once to the Service Bus topic after capturing the immutable routing revision | | PublishBatch | Publish | Partial | Most usage | at most ten independently admitted entries within the 256-KiB aggregate budget | | ConfirmSubscription | Subscriptions | Partial | Common | activates pending HTTP/S subscriptions through a bounded, one-purpose token | | GetSubscriptionAttributes | Subscriptions | Partial | Most usage | returns supported filter, scope and SQS raw-delivery attributes | | SetSubscriptionAttributes | Subscriptions | Partial | Most usage | supports FilterPolicy, FilterPolicyScope and SQS RawMessageDelivery; redrive is refused | | Subscribe | Subscriptions | Partial | Common | same-tenant, same-region SQS, HTTP, HTTPS and unqualified Lambda destinations | | Unsubscribe | Subscriptions | Supported | Common | authenticated, generation-aware removal blocks new attempts and retires the binding | | ListTagsForResource | Tags | Supported | Most usage | returns customer tags without internal ownership metadata | | TagResource | Tags | Supported | Most usage | customer tags cannot alter internal ownership | | UntagResource | Tags | Supported | Most usage | - | | CreateTopic | Topics | Partial | Common | creates an idempotent standard topic; unsupported attributes are refused | | DeleteTopic | Topics | Partial | Common | generation-aware deletion cannot remove a replacement topic | #### How it works Each SNS topic maps to a Service Bus Premium topic, and each SNS subscription has a durable broker subscription. One publication fans out natively. Workers reconstruct endpoint bindings from shared state, receive a copy under PeekLock, evaluate the captured SNS filter policy and deliver matching messages. The broker lock coordinates one active receiver for a copy. It does not authorize the destination. Every attempt also checks the current subscription generation, receiver grant and tenant budget.
The SNS service adapter publishes to an Azure Service Bus Premium topic. A durable broker subscription exists for every SNS binding. Workers rebuild bindings from shared state, receive each copy under PeekLock, evaluate the SNS filter, and deliver it to SQS, HTTP, HTTPS, or Lambda before completing the broker message. The SNS service adapter publishes to an Azure Service Bus Premium topic. A durable broker subscription exists for every SNS binding. Workers rebuild bindings from shared state, receive each copy under PeekLock, evaluate the SNS filter, and deliver it to SQS, HTTP, HTTPS, or Lambda before completing the broker message.

Service Bus owns durable fan-out. The service adapter owns SNS routing and destination delivery.

#### Restart and binding recovery On startup, a worker replays shared subscription intent, reconciles the exact broker entities and reads their settings back before receiving. Removing and recreating a logical subscription creates a new incarnation. Retained messages from an older broker entity cannot enter the replacement subscription. Microsoft operates broker durability and availability. The selected namespace uses Premium with a configured 4096-KiB message limit, which is read back before readiness. Destination encoding can still exceed a receiver's smaller limit and is checked separately.
After a worker restart, shared subscription state is replayed, the exact Service Bus entity generation is reconciled and read back, and only then are PeekLock receives enabled. Old subscription generations are retired and cannot deliver retained backlog. After a worker restart, shared subscription state is replayed, the exact Service Bus entity generation is reconciled and read back, and only then are PeekLock receives enabled. Old subscription generations are retired and cannot deliver retained backlog.

Subscription delivery resumes from durable authority, rather than a process-local registry.

#### Limitations * **Standard SNS topics only.** Service Bus sessions are not presented as SNS FIFO, ordering or deduplication. * **SNS filter subset.** Policies are evaluated by the adapter; unsupported operators are refused. * **Four subscriber protocols.** SQS, HTTP, HTTPS and Lambda are supported; email, SMS and other protocols are refused. * **At-least-once effects.** A lock or settlement failure after destination acceptance can produce a duplicate. * **No subscription redrive.** SNS RedrivePolicy is refused; broker and service terminal states are observed separately. #### Other considerations Monitor active messages, oldest message age, dead-letter state, lock loss and worker outcomes together. An empty Service Bus subscription does not identify whether a message was filtered, delivered, expired or never published. No Service Bus SNS latency or throughput number has been published. Validate encoded message size and all destination paths in the selected Azure environment before publisher cutover. ### Via Azure Database for PostgreSQL Flexible Server | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------ | -------- | ------------ | ------------- | ---------- | -------------------------------------------------------------------------------------------------------- | | At-least-once delivery | Delivery | Supported | - | - | subscriber outcomes are independent and an uncertain acknowledgement can produce a duplicate | | Raw message delivery | Delivery | Partial | - | - | SQS only, subject to the origin's raw-delivery attribute cap; HTTP/S raw delivery is refused | | Subscriber protocols | Delivery | Partial | - | - | SQS, HTTP, HTTPS and Lambda are supported; email, SMS, Firehose, mobile push and application are refused | | Subscription redrive | Delivery | Out of scope | - | - | RedrivePolicy is refused; bounded retries end in diagnostic state | | FIFO and ordering | Ordering | Out of scope | - | - | FIFO topics, deduplication and MessageGroupId are refused | | Subscription filter policies | Routing | Partial | - | - | message-attribute and JSON-body scopes use a bounded AWS-compatible grammar subset | | HTTP/S signature compatibility | Security | Partial | - | - | detached tenant JWS requires receiver trust setup and is not AWS SigningCertURL compatibility | | Operation | Area | Support | Depth | Notes | | ------------------------- | ------------- | ------------ | ------------ | --------------------------------------------------------------------------------------- | | AddPermission | Control plane | Out of scope | Full surface | caller and destination grants remain in the environment's existing identity system | | RemovePermission | Control plane | Out of scope | Full surface | caller and destination grants remain in the environment's existing identity system | | SetTopicAttributes | Control plane | Out of scope | Full surface | arbitrary topic policy, delivery policy and encryption changes are refused | | GetTopicAttributes | Discovery | Partial | Most usage | returns represented attributes without fabricating policy or delivery-policy values | | ListSubscriptions | Discovery | Supported | Most usage | tenant-authorized snapshot pages contain no confirmation secrets or receiver handles | | ListSubscriptionsByTopic | Discovery | Supported | Most usage | tenant-authorized snapshot pages for one topic | | ListTopics | Discovery | Supported | Common | tenant-authorized snapshot pages contain at most 100 topics | | Publish | Publish | Partial | Common | commits the message and selected deliveries to Flexible Server in one transaction | | PublishBatch | Publish | Partial | Most usage | at most ten independently admitted entries within the 256-KiB aggregate budget | | ConfirmSubscription | Subscriptions | Partial | Common | activates pending HTTP/S subscriptions through a bounded, one-purpose token | | GetSubscriptionAttributes | Subscriptions | Partial | Most usage | returns supported filter, scope and SQS raw-delivery attributes | | SetSubscriptionAttributes | Subscriptions | Partial | Most usage | supports FilterPolicy, FilterPolicyScope and SQS RawMessageDelivery; redrive is refused | | Subscribe | Subscriptions | Partial | Common | same-tenant, same-region SQS, HTTP, HTTPS and unqualified Lambda destinations | | Unsubscribe | Subscriptions | Supported | Common | authenticated, generation-aware removal blocks new attempts and retires the binding | | ListTagsForResource | Tags | Supported | Most usage | returns customer tags without internal ownership metadata | | TagResource | Tags | Supported | Most usage | customer tags cannot alter internal ownership | | UntagResource | Tags | Supported | Most usage | - | | CreateTopic | Topics | Partial | Common | creates an idempotent standard topic; unsupported attributes are refused | | DeleteTopic | Topics | Partial | Common | generation-aware deletion cannot remove a replacement topic | #### How it works A publication commits the message and every selected delivery row in one PostgreSQL transaction before returning. Delivery workers claim rows, verify current authority, contact the destination and conditionally record success, retry, terminal failure or uncertainty. Microsoft operates the database service. The adapter owns SNS request semantics, filter evaluation and endpoint retry. Entra workload identity removes a static database password from this path.
In Azure, the SNS service adapter uses Microsoft Entra workload identity and TLS to commit a notification and one row per selected subscription to Azure Database for PostgreSQL Flexible Server. Delivery workers process SQS, HTTP, HTTPS, and Lambda rows independently. In Azure, the SNS service adapter uses Microsoft Entra workload identity and TLS to commit a notification and one row per selected subscription to Azure Database for PostgreSQL Flexible Server. Delivery workers process SQS, HTTP, HTTPS, and Lambda rows independently.

Flexible Server stores durable fan-out state; destination delivery remains an adapter responsibility.

#### Operations and diagnosis Flexible Server has no SNS console. Azure database metrics answer storage and connection questions; service diagnostics answer routing and delivery questions. Missing evidence is reported as unknown. An empty set of pending rows does not prove subscriber receipt. Zone-redundant availability depends on selected database configuration and regional support. Database availability does not establish notification latency or destination success.
Operational signals for Flexible Server backed SNS. Database observations show commit latency, pending rows, attempt count, next attempt time, terminal rows and connection pressure. Delivery observations show receiver acceptance or a sanitized failure. The service diagnostics join these by message and subscription identity. Operational signals for Flexible Server backed SNS. Database observations show commit latency, pending rows, attempt count, next attempt time, terminal rows and connection pressure. Delivery observations show receiver acceptance or a sanitized failure. The service diagnostics join these by message and subscription identity.

Database and delivery signals are joined by stable message and subscription identities.

#### Limitations * **Standard topics only.** FIFO, ordering, deduplication and MessageGroupId are refused. * **Filter subset.** Attribute and JSON-body scopes are available within the documented bounded grammar. * **Four subscriber protocols.** SQS, HTTP, HTTPS and Lambda are supported; email, SMS and other protocols are refused. * **No HTTP/S raw delivery.** SQS raw delivery is supported; HTTP/S receives the signed envelope. * **At-least-once duplicates.** A destination acceptance followed by lost settlement can repeat a notification. #### Other considerations Size storage, connections and workers for topic count, fan-out and delivery age. Test Entra identity, TLS, backup and recovery in the selected Azure region. Existing AWS delivery work is not transferred during cutover. No Flexible Server SNS performance result has been published; monitor this target independently. [Service Catalog](/service-adapters/catalog). # SQS (FIFO) Source: https://docs.tensor9.com/service-adapters/aws/messaging-streaming/sqs-fifo AWS SQS (FIFO). FIFO queues preserve order within a message group and drop duplicates inside a five-minute deduplication window, at lower throughput than standard queues. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure, OCI, and Private Kubernetes](#on-azure-oci-and-private-kubernetes) * [Via CloudNativePG](#via-cloudnativepg) * [On Azure](#on-azure) * [Via Azure Service Bus](#via-azure-service-bus) * [Via Azure Cosmos DB (queue)](#via-azure-cosmos-db-queue) * [Via PostgreSQL Flexible Server](#via-postgresql-flexible-server) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of SQS (FIFO) with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | SQS (FIFO) | Google Cloud | Azure, OCI, and Private Kubernetes · CloudNativePG | Azure · Azure Service Bus | Azure · Azure Cosmos DB (queue) | Azure · PostgreSQL Flexible Server | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Ordering · delivery order | Yes - per-group FIFO | Yes - head-of-group claim in SQL, same contract (per-MessageGroupId, one in flight per group) | Yes - head-of-group claim in SQL, same contract (per-MessageGroupId, one in flight per group) | Yes - broker-exclusive sessions (session = MessageGroupId), Service Bus coordinates session ownership across adapter instances | Yes - strict per-group order, one message in flight per group, cross-pod safe (server-arbitrated, never a wall-clock inversion) | Yes - head-of-group claim in SQL, same contract on the managed server (per-MessageGroupId, one in flight per group) | | Deduplication · dedup window | Yes - explicit MessageDeduplicationId + content-based, 5-min window | Yes - both modes, over a marker table in the same Postgres; 5-minute window | Yes - both modes, over a marker table in the same Postgres; 5-minute window | Partial - broker-native duplicate detection; fresh-id echo (not the original id) + no send-side SequenceNumber | Yes - both modes (explicit tag or content hash), 5-min window; echoes the original MessageId and SequenceNumber; reproduces the neither-tag-nor-dedup rejection | Yes - both modes, over a marker table in the same Postgres; 5-minute window | | Message group · group ordering | ordered per group-id | per MessageGroupId (SQL head-of-group) | per MessageGroupId (SQL head-of-group) | session = MessageGroupId (broker-exclusive) | per group-id (the ordering unit) | per MessageGroupId (SQL head-of-group) | | Delivery guarantee | ordered per group; duplicate sends suppressed for 5 minutes; consumer redelivery remains possible | at-least-once, in order per group | at-least-once, in order per group | at-least-once in order per group; duplicate sends suppressed by the broker within the window | at-least-once with in-window dedup (duplicate sends suppressed) | at-least-once, in order per group | | Dead-letter | Yes - native redrive (maxReceiveCount) | Yes - native redrive (maxReceiveCount) | Yes - native redrive (maxReceiveCount) | Yes - native delivery-count limit + auto-forward to the declared (session-enabled) DLQ entity | Partial - moved into the declared FIFO DLQ container at claim time (at-least-once); the group advances, the moved message keeps its group + order | Yes - native redrive (maxReceiveCount) | | Visibility timeout · max hold | up to 12 h | honored (30 s default) | honored (30 s default) | ≤ 5 min (session-lock cap; longer is rejected with an error, extends via renewal) | uncapped (not a broker lock; unlike the Service Bus backend's 5-min cap) | honored (30 s default) | | Receipt handle · restart durability | stateless, durable | a durable handle that survives an adapter restart | a durable handle that survives an adapter restart | - | - | a durable handle that survives an adapter restart | | Max message size · per message | 1 MiB | 256 KB | 256 KB | 256 KB | 256 KB (well under the 2 MB Cosmos document cap) | 256 KB | | Long-polling · receive wait | up to 20 s | bounded by WaitTimeSeconds (poll loop) | bounded by WaitTimeSeconds (poll loop) | native (session accept may overrun \~60 s worst case) | poll (claim on receive) | bounded by WaitTimeSeconds (poll loop) | | Message retention · message lifetime | up to 14 d | retention enforced on message age (60 s..14 d) | retention enforced on message age (60 s..14 d) | - | native per-item Cosmos TTL; a TTL-dead head unblocks its group | retention enforced on message age (60 s..14 d) | | API coverage | full | high | high | high | high | high | | Duplicate-send echo | Yes - echoes the original MessageId/SequenceNumber | - | - | Partial - returns a fresh MessageId corresponding to no delivered message | Yes - echoes the original MessageId and SequenceNumber (crash-safe) | - | | SequenceNumber on send | Yes - returned on every FIFO send | - | - | No - omitted (the broker sequence is asynchronous); receive-side sequence is real | Yes - the per-group sequence position, monotonic per group (the Service Bus backend omits it) | - | | Receipt handle · restart durability | Yes - stateless, durable | - | - | Partial - session-lock-bound; an adapter restart invalidates in-flight handles | Yes - stateless data; survives an adapter restart, with no broker lock behind it (unlike the Service Bus backend) | - | | Message retention · TTL scope | strictly per-message | - | - | expiry of the first message can expire its whole session | - | - | | Single-group throughput | up to 10 msg per receive batch | - | - | 1 msg per round-trip per group (the one-in-flight strictness); scales with concurrent groups | - | - | | Multi-pod clock skew · one-in-flight vs skew | n/a (single broker clock) | - | - | - | one active message per group is preserved; clock differences between pods can shorten the visibility window | - | | FIFO / ordering | Yes - ordered per MessageGroupId; up to 10 messages in an ordered receive batch | - | - | - | - | Yes - head-of-group claim in SQL, same contract on the managed server | | Explicit deduplication | Yes - explicit MessageDeduplicationId | - | - | - | - | Yes - served (marker table, 5-minute window) | | Content-based deduplication | Yes - SHA-256(body) dedup key | - | - | - | - | Yes - served (SHA-256 of the body, same marker table) | | Durability · cluster-rebuild survival | SQS: managed, cross-AZ | - | - | - | - | queue data is stored on the managed Flexible Server outside the appliance cluster and survives a cluster rebuild | | Shared-server tenancy · shared database | dedicated SQS | - | - | - | - | one shared Flexible Server + one shared database per appliance; every SQS queue on the appliance co-locates as a table-per-queue in that database, with no cross-appliance co-location | | Server size · SKU / storage default | AWS-managed capacity | - | - | - | - | uses a fixed default server size and storage allocation because an SQS queue declares no source instance size | | Maintenance and shared capacity · shared server | per-queue isolation | - | - | - | - | the shared server has one maintenance window and shared compute, so a heavy queue can affect neighbors on the same appliance server (the shared-server tradeoff) | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | ------------------------ | -------------- | --------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Dead-letter redrive | Dead-letter | Supported | Most usage | fires automatically once a message passes maxReceiveCount | | Content/ID deduplication | FIFO | Supported | Most usage | both modes are served: an explicit MessageDeduplicationId takes precedence over the content hash (AWS's own rule), and a content-based queue derives a stable digest of the body. The message insert and deduplication marker share one transaction. A duplicate rolls back its new row and returns the original message identifiers | | FIFO ordering | FIFO | Supported | Common | per-MessageGroupId strict order with one-in-flight per group (head-of-group claim in SQL, same contract as the portable / Flexible FIFO tiers) | | Message attributes | Messages | Supported | Common | - | | GetQueueAttributes | Queues | Supported | Most usage | reads queue attributes, including approximate message counts | | GetQueueUrl | Queues | Supported | Most usage | resolves the queue by name | | DeleteMessage | Send / receive | Supported | Common | resolves by receipt handle; a stale handle returns ReceiptHandleIsInvalid | | DeleteMessageBatch | Send / receive | Supported | Common | returns the result for each entry | | ReceiveMessage | Send / receive | Supported | Common | long-poll (WaitTimeSeconds) honored; one message in flight per MessageGroupId | | SendMessage | Send / receive | Supported | Common | MessageGroupId required on send (the AWS FIFO rule); MD5 checksums match what your SDK verifies | | SendMessageBatch | Send / receive | Supported | Common | up to 10 messages per call, each with its MessageGroupId; returns the result for each entry | | ChangeMessageVisibility | Visibility | Supported | Common | resolves by receipt handle; a stale handle is rejected rather than silently succeeding | #### PostgreSQL hosting Google operates Cloud SQL for PostgreSQL, including database backups and HA. The adapter uses short-lived Cloud SQL IAM tokens as database passwords, without an Auth Proxy or static password. Queue data stays outside the customer's Kubernetes cluster and survives its rebuild. Tensor9 operates the queue schema and adapter. Queues start empty; existing SQS messages are not migrated. Drain each message group before cutover to preserve order. #### PostgreSQL queue behavior Tensor9 adds an adapter, a proxy process, to your application pod and sets `AWS_ENDPOINT_URL_SQS` to its loopback address. Your application keeps its `SendMessage` and `ReceiveMessage` calls. The adapter implements them using PostgreSQL and returns SQS responses, error codes and MD5 checksums. Each queue has a durable PostgreSQL table with one row per message. SQL operations implement sending, receiving, deletion, delay, visibility, dead-letter delivery and counts. FIFO queues add ordering by message group, per-group sequence numbers and duplicate detection. Consumer redelivery remains possible.
Before: on AWS the application's SQS SDK calls Amazon SQS. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from a durable Postgres table per queue. Before: on AWS the application's SQS SDK calls Amazon SQS. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from a durable Postgres table per queue.

The application keeps its SQS SDK. The adapter stores each queue in a PostgreSQL table.

#### Architecture The Rust adapter stores all durable state in PostgreSQL, including messages, claims and receipt handles. Instances can restart or scale out without a shared in-memory lock table, receiver pool or renewal loop. On Cloud SQL and Azure Flexible Server, it authenticates with short-lived Cloud SQL IAM or Microsoft Entra tokens generated in-process and passed as database passwords, without a static secret. On the Postgres side, the compiler provisions **one database per appliance**, a catalog entry per queue, and **one durable table per declared queue**. Each message is a row holding the SQS body and its attributes, its message-group, a visibility deadline, a receipt handle, a receive count, and a sent-at timestamp; the table is indexed so the hot paths stay a single scan. A FIFO queue uses the message group as the ordering unit; each row's arrival position supplies its per-group sequence number. A **dead-letter** queue is another queue table, and a background **retention reaper** physically removes rows past their retention. CloudNativePG, Cloud SQL for PostgreSQL and Azure Flexible Server use the same adapter code and tables. Their operational difference is where data is stored: the managed servers are outside the appliance cluster, while CloudNativePG is inside it. See the rebuild limitation below.
The adapter stores messages, receipt handles, receive counts and visibility deadlines in PostgreSQL. Each declared queue has a table; a dead-letter queue is another table. Background cleanup enforces retention. FIFO rows also store a message group and sequence number; receives claim one message per group. The adapter stores messages, receipt handles, receive counts and visibility deadlines in PostgreSQL. Each declared queue has a table; a dead-letter queue is another table. Background cleanup enforces retention. FIFO rows also store a message group and sequence number; receives claim one message per group.

Each queue uses a PostgreSQL table, including dead-letter queues. The same adapter code runs against CloudNativePG, Cloud SQL for PostgreSQL and Azure Flexible Server.

#### Receiving messages In one SQL operation, the adapter selects messages whose visibility deadlines have passed, in arrival order and up to the requested batch size. It assigns each a fresh receipt handle, advances its visibility deadline, increments the receive count and returns the claimed rows. Rows locked by another receiver are skipped. FIFO queues claim the next eligible message in each group, keeping one in flight per group. Visibility expiry permits redelivery; it does not stop a worker that is still processing an earlier receive. `ApproximateReceiveCount` is the message's **real** receive count (the claim increments it; it is not estimated), and the SQS MD5-of-body and MD5-of-attributes are recomputed so your SDK verifies them byte-for-byte. `DelaySeconds` stamps the message's visibility deadline at send, so a delayed message remains unavailable until that deadline. A **long poll** is a poll loop bounded by `WaitTimeSeconds` (capped at 20s), returning the instant a claim succeeds. A `SendMessageBatch` is a single multi-row insert with faithful per-entry outcomes, and the message body is stored in the queue table. The adapter defaults to a 262144-byte (256 KiB) queue limit; managed SQS requests enforce the declared limit before sending. Native AWS SQS now supports messages up to 1 MiB.
A receive claims messages in one atomic step: the adapter takes the head-of-queue messages that are due (their visibility deadline has passed), in arrival order, and in the same step assigns a fresh receipt handle, pushes the visibility deadline into the future, and increments the receive count, returning the claimed messages. Each receiver atomically claims a different batch of eligible messages. Messages can be redelivered when their visibility expires. A receive claims messages in one atomic step: the adapter takes the head-of-queue messages that are due (their visibility deadline has passed), in arrival order, and in the same step assigns a fresh receipt handle, pushes the visibility deadline into the future, and increments the receive count, returning the claimed messages. Each receiver atomically claims a different batch of eligible messages. Messages can be redelivered when their visibility expires.

Concurrent receives use atomic row claims and skip rows locked by another receiver. ApproximateReceiveCount is the message's real receive count.

#### Receipt handles & visibility Receiving writes a receipt token to the message row. `DeleteMessage` and `ChangeMessageVisibility` find that row by its token, including after an adapter restart or through another adapter instance. No in-memory receiver or broker lock is required. A delete or visibility change with no matching row returns `ReceiptHandleIsInvalid`. This includes messages already deleted or claimed again under a newer token. Visibility is a timestamp on the row, defaulting to 30 seconds on receive. `ChangeMessageVisibility` can extend it without a broker lock-duration maximum; zero makes the message immediately available to claim again. Retention can delete a message while it is in flight, and a newer claim makes the old token stale.
A receipt handle is a durable token stored on the message's own row, a stable id stamped on it, with no in-memory broker lock behind it, so it resolves the same after an adapter restart: Delete and ChangeMessageVisibility find the message again by that token. The visibility timeout is a timestamp on the row with no ceiling: ChangeMessageVisibility pushes it forward with no cap, where a broker-backed backend would stop at its lock ceiling. A receipt handle is a durable token stored on the message's own row, a stable id stamped on it, with no in-memory broker lock behind it, so it resolves the same after an adapter restart: Delete and ChangeMessageVisibility find the message again by that token. The visibility timeout is a timestamp on the row with no ceiling: ChangeMessageVisibility pushes it forward with no cap, where a broker-backed backend would stop at its lock ceiling.

Receipt handles are stored with messages, so they remain usable after an adapter restart. Visibility deadlines have no broker lock-duration limit.

#### Dead-letter & retention During receive, one database transaction moves messages at or past `maxReceiveCount` from the source table to the dead-letter table and claims the remaining candidates. The move and claim commit together, so an interrupted transaction cannot leave a message deleted from the source without its DLQ copy. The DLQ uses the same queue operations and retention cleanup. Moved messages start with a reset receive count. A background cleanup task deletes messages whose send time is older than the queue's retention period, including messages in flight. Receiving does not change that timestamp. Cleanup reads the stored retention attribute, defaults to four days and clamps it to 60 seconds–14 days. A missing or invalidly short value therefore cannot delete newly sent messages immediately. One worker per database runs each cleanup pass in batches, backs off during database maintenance and isolates queue errors. A circuit breaker and per-batch time limit bound the work.
Dead-letter and retention. A single atomic operation both claims live messages and redrives poison ones: from the locked head-of-queue set, messages at or past maxReceiveCount are moved from the source into the dead-letter queue and the rest are claimed and returned, in one transaction, so the move and claims commit together. This does not prevent consumer redelivery after visibility expiry. Separately, a background retention reaper removes messages whose send time is older than the queue's retention, clamped between 60 seconds and 14 days. Dead-letter and retention. A single atomic operation both claims live messages and redrives poison ones: from the locked head-of-queue set, messages at or past maxReceiveCount are moved from the source into the dead-letter queue and the rest are claimed and returned, in one transaction, so the move and claims commit together. This does not prevent consumer redelivery after visibility expiry. Separately, a background retention reaper removes messages whose send time is older than the queue's retention, clamped between 60 seconds and 14 days.

One transaction moves failed messages to the DLQ and claims eligible messages. A background worker removes messages older than their retention period, clamped to 60 seconds-14 days.

#### FIFO ordering & dedup A FIFO queue stores the group stamped on each message, which is the ordering unit. Order within a group is a **head-of-group claim**: the receive takes the earliest message of each group that has **nothing in flight** (it confirms no message in that group is currently claimed and still invisible) and claims those heads atomically. Because each message's arrival position is a monotonic identity, the earliest-per-group is exactly the group's next in-order message, and **exactly one message is in flight per group** at a time. Groups are independent, so different receivers drain different groups concurrently, with Postgres as the only coordinator. An expired claim returns the same head to the front of its group, so a redelivery stays in sequence. Each FIFO send requires `MessageGroupId` and returns a `SequenceNumber`: the PostgreSQL-assigned arrival position formatted as a 20-digit, zero-padded number. FIFO deduplication uses a separate PostgreSQL table keyed by queue and deduplication ID. An explicit `MessageDeduplicationId` takes precedence; otherwise a queue configured for content-based deduplication uses SHA-256 of the body, excluding attributes. The message insert and deduplication marker are part of one transaction. A duplicate within 5 minutes rolls back its new message row and returns the original `MessageId` and `SequenceNumber`. Marker retention is independent of message retention, so receiving or deleting the original does not shorten the deduplication window. A claim prevents another receive in that group until deletion or visibility expiry. It does not stop a timed-out worker from continuing to process a previously received message; consumers must make repeated processing safe.
FIFO receives atomically claim the next message from each group that has no active claim. Each send returns a 20-digit, zero-padded sequence number. Explicit or content-based deduplication uses a separate PostgreSQL table with a five-minute window; repeated sends return the original message and sequence IDs. FIFO receives atomically claim the next message from each group that has no active claim. Each send returns a 20-digit, zero-padded sequence number. Explicit or content-based deduplication uses a separate PostgreSQL table with a five-minute window; repeated sends return the original message and sequence IDs.

FIFO receives claim one message per group at a time. Sends return a SequenceNumber and support a 5-minute deduplication window, using an explicit ID or a body hash when content-based deduplication is enabled.

#### Limitations △ Where SQS and this backend diverge, read before you adopt * **Throughput is bounded by one PostgreSQL server.** All of an appliance's queues share one PostgreSQL server, so peak throughput is bounded by that one server, and a heavy queue can affect its neighbors. On the Tensor9 round-trip eval the Postgres adapter holds throughput parity with native SQS (about 300 vs 286 ops/s) and a tighter end-to-end tail (p99 140 ms vs SQS's 172 ms), while SQS is faster at the median (p50 15 ms vs 24 ms), results from that moderate-rate Standard-queue workload, not a measured throughput ceiling. This benchmark does not measure FIFO throughput or every hosting option. * **CloudNativePG data lives in-cluster and does not survive a cluster rebuild.** The in-cluster CloudNativePG host keeps queue data inside the appliance cluster, so a full cluster rebuild loses it; the two managed hosts (Cloud SQL Postgres, Azure Flexible Server) keep queue data on a provider-operated server outside the cluster, so it survives a rebuild. All three hosts use the same queue implementation; database placement changes rebuild recovery. * **FIFO send deduplication lasts 5 minutes.** A duplicate send in that window returns the original IDs. Reusing an ID after the window can create a new message. Consumer redelivery remains possible even within the window. * **Stale receipt handles return an error.** `DeleteMessage` and `ChangeMessageVisibility` return `ReceiptHandleIsInvalid` when no row matches the token. * **Retention cleanup runs periodically.** An expired message can remain until the next cleanup pass. Retention is clamped to 60 seconds–14 days, with invalidly short values raised to 60 seconds to avoid deleting newly sent messages. * **Approximate counts are a point-in-time exact count.** `ApproximateNumberOfMessages` and `...NotVisible` are a real count of the visible and not-yet-visible messages in the queue's table, exact at query time, but still Approximate under concurrent traffic; the delayed-count is reported as zero. * **Control-plane calls operate on the apply-time schema.** The database, the catalog, and each per-queue table (and its dead-letter table) are created by the compiler's provisioner; runtime `CreateQueue` / `DeleteQueue` / `SetQueueAttributes` operate on that provisioned schema, and a switch between standard and FIFO is refused as a replace rather than silently mutating semantics. #### Other considerations * **Plan message cutover.** The database, catalog and queue tables start empty. Existing SQS messages are not migrated. Drain each group before switching it to preserve order. * **Provisioning and ownership.** The adapter provisions and owns its own schema atomically per queue (one database per appliance, a catalog, and one durable table per declared queue, plus its dead-letter table), so a crash never leaves a half-provisioned queue. Each queue is marked with an ownership stamp: the layer never touches a queue it doesn't own (an unstamped or foreign-owned queue detaches rather than deletes), and it refuses to drop a non-empty queue without force. * **Three hosts, one backend, keyless on the managed ones.** The same backend supports CloudNativePG, Cloud SQL Postgres and Azure Flexible Server. On the two managed hosts the connection is keyless (an Entra or Cloud SQL IAM token obtained in-process and presented as the database password, no static secret), and the provider operates the database's durability, backup, and HA while Tensor9 operates the adapter. ## On Azure, OCI, and Private Kubernetes ### Via CloudNativePG | Operation | Area | Support | Depth | Notes | | ------------------------ | -------------- | --------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Dead-letter redrive | Dead-letter | Supported | Most usage | fires automatically once a message passes maxReceiveCount | | Content/ID deduplication | FIFO | Supported | Most usage | both modes are served: an explicit MessageDeduplicationId takes precedence over the content hash (AWS's own rule), and a content-based queue derives a stable digest of the body. The message insert and deduplication marker share one transaction. A duplicate rolls back its new row and returns the original message identifiers | | FIFO ordering | FIFO | Supported | Common | per-MessageGroupId strict order with one-in-flight per group (head-of-group claim in SQL) | | Message attributes | Messages | Supported | Common | - | | GetQueueAttributes | Queues | Supported | Most usage | reads queue attributes, including approximate message counts | | GetQueueUrl | Queues | Supported | Most usage | resolves the queue by name | | DeleteMessage | Send / receive | Supported | Common | resolves by receipt handle; a stale handle returns ReceiptHandleIsInvalid | | DeleteMessageBatch | Send / receive | Supported | Common | returns the result for each entry | | ReceiveMessage | Send / receive | Supported | Common | long-poll (WaitTimeSeconds) honored; one message in flight per MessageGroupId | | SendMessage | Send / receive | Supported | Common | MessageGroupId required on send (the AWS FIFO rule); MD5 checksums match what your SDK verifies | | SendMessageBatch | Send / receive | Supported | Common | up to 10 messages per call, each with its MessageGroupId; returns the result for each entry | | ChangeMessageVisibility | Visibility | Supported | Common | resolves by receipt handle; a stale handle is rejected rather than silently succeeding | #### PostgreSQL hosting Tensor9 operates CloudNativePG inside the customer's Kubernetes cluster. Queue data is stored on that cluster's volumes and does not survive a full cluster rebuild. Plan recovery and message cutover before replacing the cluster. Tensor9 operates the queue schema and adapter. Queues start empty; existing SQS messages are not migrated. Drain each message group before cutover to preserve order. #### Queue behavior The adapter uses the same PostgreSQL tables, atomic message claims, stored receipt handles and transactional dead-letter moves described in [PostgreSQL queue behavior](/service-adapters/aws/messaging-streaming/sqs-fifo#postgresql-queue-behavior). Receipt handles survive adapter restarts; stale claims return an error. Retention still removes messages in flight. All queues share database capacity, so a busy queue can affect others. The documented Standard-queue benchmark does not establish this host's capacity or FIFO throughput. Duplicate sends are suppressed for five minutes; consumers must still handle redelivery. ## On Azure ### Via Azure Service Bus | Operation | Area | Support | Depth | Notes | | ---------------------------------------------------- | -------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Dead-letter redrive | Dead-letter | Supported | Most usage | Service Bus moves a message at the delivery-count limit to the declared session-enabled DLQ, then allows the next message in its group to proceed | | Content-based deduplication | FIFO | Supported | Most usage | broker-native duplicate detection over a content-hash message id, 5-minute window (the SQS constant), honored at apply-time when your queue declares content-based deduplication | | Dedup echo (suppressed-send response) | FIFO | Partial | Most usage | a dedup-suppressed send returns a fresh MessageId rather than echoing the original's MessageId/SequenceNumber as SQS does, so do not correlate on it | | FIFO ordering | FIFO | Supported | Common | sessions are broker-exclusive per group, so ordering holds across horizontally scaled consumers with session ownership stored in Service Bus | | MessageDeduplicationId | FIFO | Supported | Most usage | the explicit tag takes precedence over the content hash (AWS's own rule) | | SequenceNumber (send response) | FIFO | Out of scope | Most usage | Service Bus cannot return the broker sequence synchronously, so the send response omits it; the receive-side sequence number is real (queue-monotonic, which contains per-group monotonic) | | DeleteQueue / ListQueues / SetQueueAttributes / tags | Queues | Partial | Full surface | Max manages logical queues, tags and supported edits; session-enabled entities remain provisioned through Terraform. Broker-owned policy changes are rejected | | GetQueueAttributes (live message counts) | Queues | Out of scope | Full surface | live counts are unavailable through AMQP and omitted; declared configuration is read separately from the Max catalog | | PurgeQueue | Queues | Supported | Most usage | a bounded accept/drain/dispose session loop (the same documented raciness window as SQS's own purge) | | DelaySeconds (per message) | Send / receive | Out of scope | Common | rejected exactly as real SQS rejects per-message delay on a FIFO queue (a scheduled enqueue would break in-group order) | | DeleteMessage | Send / receive | Supported | Common | completes the group head within its held session | | ReceiveMessage | Send / receive | Supported | Common | session receivers: strict per-MessageGroupId order, one message in flight per group; receives are bounded by your MaxNumberOfMessages budget (extra messages are not received and discarded) | | SendMessage | Send / receive | Supported | Common | MessageGroupId required (the AWS FIFO rule) and used as the session id | | SendMessageBatch | Send / receive | Supported | Common | runs as sequential individual sends to preserve entry order and return a result for each entry | | ChangeMessageVisibility | Visibility | Partial | Common | extend = session-lock renewal, zero = in-session immediate redelivery (order preserved); the visibility timeout is capped at 5 minutes, longer is rejected with an error | #### How it works Tensor9 adds an adapter, a proxy process, to your application pod and sets `AWS_ENDPOINT_URL_SQS` to its loopback address. Your application keeps its `SendMessage` and `ReceiveMessage` calls. The adapter translates them to Azure Service Bus operations and returns SQS responses, error codes and MD5 checksums. For a `.fifo` queue, Tensor9 provisions a Service Bus queue with sessions enabled. The adapter uses `MessageGroupId` as the session ID and selects session-based receiving from the queue's `.fifo` suffix. Service Bus grants each session to one consumer at a time. The adapter delivers one message per group at a time, preserving send order across multiple consumer instances.
Before: on AWS the application's SQS SDK calls Amazon SQS FIFO. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS FIFO API from Azure Service Bus sessions. Before: on AWS the application's SQS SDK calls Amazon SQS FIFO. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS FIFO API from Azure Service Bus sessions.

The application keeps its SQS SDK. A Tensor9 adapter translates FIFO queue calls to Service Bus sessions.

#### Architecture The Rust adapter translates SQS calls to Service Bus Standard over AMQP 1.0. On send, it sets the session ID from `MessageGroupId`. On receive, it delivers one message per group, renews the session lock during the visibility window and settles messages while preserving delivery counts. Authentication uses the customer's AKS Workload Identity without a static secret. The adapter accepts Service Bus sessions independently of sending and standard-queue receiving, so a FIFO receive does not block those operations. The `.fifo` suffix selects this receive path; `MessageGroupId` supplies the session ID. The entity itself is provisioned at apply-time, session-enabled, before the first message arrives: sessions for the FIFO contract, broker-native duplicate detection over a 5-minute history window, a 5-minute session lock (giving the adapter's lock renewal a 10x margin), expiry dead-lettering, and DLQ auto-forwarding. Microsoft operates Service Bus's durability, backup, and HA; Tensor9 operates the adapter.
Architecture: the app's SQS SDK calls a Rust Tensor9 adapter over the pod loopback. The adapter serves the SQS FIFO API from an Azure Service Bus session-enabled queue whose session id is the MessageGroupId, giving strict per-group order with one message in flight per group across horizontally scaled consumers; FIFO session receives never block sends or standard receives. Architecture: the app's SQS SDK calls a Rust Tensor9 adapter over the pod loopback. The adapter serves the SQS FIFO API from an Azure Service Bus session-enabled queue whose session id is the MessageGroupId, giving strict per-group order with one message in flight per group across horizontally scaled consumers; FIFO session receives never block sends or standard receives.

A Rust adapter serves the SQS FIFO API in front of Service Bus; each session is one MessageGroupId , giving strict per-group order with one message in flight per group.

#### How sessions preserve group order A Service Bus session groups messages under one ID and grants exclusive access to one consumer. While that consumer holds the session, other consumers cannot receive its messages. Messages are delivered in enqueue order; the adapter allows only one message in flight per group. Service Bus assigns each active group to one consumer even when several application pods receive from the same queue. Consumer pods therefore need no shared lock table, leader election or partition assignment to preserve group order. * **One message in flight per group.** A group's head must be settled before its next message is delivered, the same one-in-flight contract as the memory and Postgres FIFO backends. This costs single-group throughput (one message per round-trip) in exchange for strict order. * **The session lock keeps the group held.** A held session is renewed by RenewSessionLock under your visibility window, so a slow consumer keeps its group rather than losing it mid-sequence; ChangeMessageVisibility extend maps to a lock renewal, and zero maps to an in-session immediate redelivery with order preserved. * **Redelivery counts stay accurate.** When the adapter releases a session, the in-flight message is abandoned first so its delivery count advances correctly, moving a poison message toward its redrive limit rather than being silently reset. * **Receive counts reflect delivered messages.** The adapter limits session receives to the remaining `MaxNumberOfMessages` allowance. It does not receive and discard extra messages that would increase their delivery count.
Sessions are the FIFO analog: each MessageGroupId is a broker session. The broker grants each session exclusively to one consumer, so two horizontally-scaled pods hold disjoint groups with one message in flight per group. Service Bus stores and coordinates session ownership; adapter instances do not share an in-memory lock table. Sessions are the FIFO analog: each MessageGroupId is a broker session. The broker grants each session exclusively to one consumer, so two horizontally-scaled pods hold disjoint groups with one message in flight per group. Service Bus stores and coordinates session ownership; adapter instances do not share an in-memory lock table.

Each MessageGroupId is a broker session; the broker hands each session to exactly one consumer, so ordering survives horizontal scale with no coordination between pods.

#### Duplicate detection Service Bus duplicate detection uses a 5-minute history window. Sends with the same deduplication key within that window create one queued message. A consumer can still receive that message again after visibility expires. The broker stores the deduplication state; the adapter needs no separate reconciliation loop. An explicit `MessageDeduplicationId` takes precedence. Otherwise, the adapter uses a hash of the body for every `.fifo` send. It always sets this hash because its messaging permissions do not allow it to read the queue's deduplication setting. With duplicate detection enabled, Service Bus uses the hash to suppress retries; with detection disabled, it is only an ID. The adapter separately stores the SQS `MessageId` as an application property and returns it on receive. Equal-body messages sent outside the deduplication window retain distinct SQS IDs.
Duplicate detection: on a .fifo send the adapter sets the dedup key from an explicit MessageDeduplicationId, else a content hash of the body. The queue is provisioned with broker-native duplicate detection over a 5-minute window, so a retried equal-body send is suppressed by the broker without a second enqueue; consumers can still receive a message again after visibility expires; the SQS MessageId is preserved as an application property. Duplicate detection: on a .fifo send the adapter sets the dedup key from an explicit MessageDeduplicationId, else a content hash of the body. The queue is provisioned with broker-native duplicate detection over a 5-minute window, so a retried equal-body send is suppressed by the broker without a second enqueue; consumers can still receive a message again after visibility expires; the SQS MessageId is preserved as an application property.

The adapter uses an explicit MessageDeduplicationId or a body hash as the Service Bus message ID. The broker suppresses repeated IDs within 5 minutes.

#### Dead-lettering Service Bus dead-letters a message when its delivery count reaches the limit set by `maxReceiveCount`. The next message in that group can then be delivered. The adapter cannot receive from Service Bus's internal dead-letter subqueue. Tensor9 therefore configures automatic forwarding to your declared dead-letter queue (DLQ), preserving the session ID. A FIFO source requires a FIFO DLQ, which is provisioned with sessions enabled. Messages whose time to live (TTL) expires are also dead-lettered, so they can be inspected or recovered from the DLQ. The pairing rule is enforced at compile time: a non-`.fifo` DLQ declared on a `.fifo` origin stops the build with a clear error rather than compiling a queue whose dead letters would have nowhere valid to land.
Dead-lettering: a poison message that fails delivery maxReceiveCount times is dead-lettered by the broker and its group advances; the dead letter auto-forwards to your declared, session-enabled DLQ, retaining its session id, and TTL-expired messages land there too. Dead-lettering: a poison message that fails delivery maxReceiveCount times is dead-lettered by the broker and its group advances; the dead letter auto-forwards to your declared, session-enabled DLQ, retaining its session id, and TTL-expired messages land there too.

Service Bus forwards messages exceeding maxReceiveCount to the declared DLQ, which the adapter can receive from.

#### Limitations △ Where SQS FIFO and Service Bus diverge, read before you adopt * **A dedup-suppressed send returns a fresh MessageId.** SQS echoes the original message's MessageId (and SequenceNumber) when it suppresses a duplicate; here the suppressed send returns a fresh MessageId that corresponds to no delivered message. The duplicate enqueue is still suppressed (consumer redelivery remains possible), but a producer that correlates on the duplicate's echoed id must not wait on it. * **Send responses omit `SequenceNumber`.** Service Bus cannot return its sequence number synchronously on send. Receive responses include the broker-assigned number, which increases across the queue and therefore within each group. * **The visibility timeout is capped at 5 minutes.** A visibility timeout maps to the session lock, whose ceiling is the 5-minute session lock the compiler sets (a 10x margin over the adapter's lock renewal), so a requested visibility above 5 minutes is rejected with an error rather than being silently clamped. Extension via ChangeMessageVisibility renews the lock. * **Receipt handles expire on adapter restart.** A handle requires a live session lock. After a restart, old handles are invalid and the group's in-flight message is delivered again in order when its session lock expires, within 5 minutes. * **Expiry can affect a whole session.** SQS retention applies per message. On Service Bus, an expired message at the front can expire its entire session. The expired messages are dead-lettered and can be recovered from the DLQ, but more messages may expire together than on SQS. * **Single-group throughput is one message per round-trip.** The one-in-flight strictness that guarantees per-group order costs single-group throughput: one message per round-trip per group, versus up to 10 per receive batch on SQS. Total throughput scales with the number of concurrent groups, so parallelism comes from having many groups, not from batching within one. * **An empty-queue receive can block past the requested wait.** SQS returns within WaitTimeSeconds . A Service Bus session accept is broker-controlled and is never cancelled mid-attach (a cancelled accept could strand a broker-side session lock), so a FIFO receive on an empty queue can overrun the requested wait by up to one accept window, about 60 seconds worst case (the exact figure on real Azure is reported by your own monitoring once this is running). * **Per-message DelaySeconds is rejected.** A per-message delay is rejected exactly as real SQS rejects it on a FIFO queue: a scheduled enqueue would activate after later siblings and break in-group order. Queue-level policy changes have a separate scope: Max manages logical queue settings, but the messaging backend rejects nonzero queue delay and new dead-letter routes. * **Changing FIFO or deduplication settings replaces the queue.** Sessions and duplicate detection are fixed at queue creation. Changing standard/FIFO mode or content-based deduplication destroys and recreates the queue, losing queued messages. Every FIFO compilation includes a warning about replacement. Drain the queue before changing these settings. * **A send without a deduplication ID can be accepted when SQS would reject it.** SQS requires `MessageDeduplicationId` when content-based deduplication is disabled. The adapter cannot read that queue setting with its messaging permissions, so it accepts the send. Group ordering is preserved, but the SQS validation error is not reproduced. #### Other considerations * **Data migration.** The Service Bus namespace and session-enabled queue are provisioned empty at apply-time; in-flight SQS messages are not migrated. Cut over at a drain point (or dual-write) so the new queue starts clean; for FIFO, drain per group so no group cuts over mid-sequence. * **Queue configuration is applied during provisioning.** Each queue gets a 5-minute session lock, ten times the adapter's renewal interval. The declared `maxReceiveCount` determines dead-letter forwarding to the DLQ. Runtime message operations require no queue-management credential. * **Operations and ownership.** The adapter provisions the Service Bus deployment itself: the namespace and the session-enabled queue (with duplicate detection, a 5-minute lock, expiry dead-lettering, and DLQ auto-forwarding), all derived from your SQS queues' declared shapes; the adapter is injected alongside the application. Microsoft operates Service Bus (Standard): durability, backup, and HA. What remains operational is the surrounding platform: the Azure subscription, cluster patching, and monitoring. * **FIFO requires an explicit selection.** Older deployments that used the unsplit SQS-to-Service-Bus target supported standard queues only. Tensor9 does not convert those deployments to FIFO. A FIFO queue must be declared and selected explicitly to use sessions. ### Via Azure Cosmos DB (queue) | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------- | -------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Dead-letter redrive | Dead-letter | Partial | Most usage | at claim time a head past maxReceiveCount moves to the declared (FIFO) DLQ and the group advances; the moved message keeps its group and order, at-least-once into the DLQ | | Content-based deduplication | FIFO | Supported | Most usage | a content-hash dedup key over a 5-minute window (the SQS constant); content-based deduplication is honored when your queue declares it | | Dedup echo (suppressed-send response) | FIFO | Supported | Most usage | a dedup-suppressed send echoes the original MessageId and SequenceNumber, matching the SQS response; Service Bus returns a new ID | | FIFO ordering | FIFO | Supported | Common | per-group order is server-arbitrated, so it holds across horizontally scaled consumers even under clock skew (never a wall-clock inversion); exactly one message is in flight per group, and the invariant holds under any number of concurrent consumers | | MessageDeduplicationId | FIFO | Supported | Most usage | the explicit tag takes precedence over the content hash (AWS's own rule); a FIFO send with neither a tag nor content-based-dedup is InvalidParameterValue, matching the AWS rejection; Service Bus does not reproduce this check | | SequenceNumber (send response) | FIFO | Supported | Most usage | returned on every FIFO send: the per-group sequence position, monotonic per group (Service Bus omits it) | | CreateQueue / DeleteQueue / SetQueueAttributes / tags | Queues | Partial | Full surface | Max manages logical queues on provisioned containers, tags and supported defaults. Terraform owns the account, database and containers; nonzero queue delay and new dead-letter routes are rejected | | GetQueueAttributes (approximate counts) | Queues | Partial | Full surface | approximate depth, cached about 5 s; recent sends, settlements and expiry may not yet be reflected | | DelaySeconds (per message) | Send / receive | Out of scope | Common | rejected exactly as real SQS rejects per-message delay on a FIFO queue (a scheduled enqueue would break in-group order) | | DeleteMessage | Send / receive | Supported | Common | deletes the group head by a stateless receipt handle, so a stale handle no-ops just as SQS does; deletion advances the group | | ReceiveMessage | Send / receive | Supported | Common | strict per-MessageGroupId order: exactly one message is in flight per group; durable group claims in Cosmos DB coordinate scaled consumers, while independent groups are concurrent | | SendMessage | Send / receive | Supported | Common | MessageGroupId required (the AWS FIFO rule) and is the ordering unit; each send returns a real SequenceNumber (the per-group sequence position) | | SendMessageBatch | Send / receive | Supported | Common | per-entry sends with a result for each entry | | ChangeMessageVisibility | Visibility | Supported | Common | extends the in-flight message's visibility with no cap (there is no broker lock ceiling); zero = immediate redelivery (the message returns to the front of its group in order) | #### How it works Tensor9 adds an adapter, a proxy process, to your application pod and sets `AWS_ENDPOINT_URL_SQS` to its loopback address. Your application keeps its `SendMessage` and `ReceiveMessage` calls. The adapter implements them using Cosmos DB and returns SQS responses, error codes and MD5 checksums. Each queue has a Cosmos DB container, with one document per message. The adapter uses these documents for sending, receiving, deletion, delay, visibility, dead-letter delivery and counts. FIFO queues add ordering by message group and duplicate detection, described below.
Before: on AWS the application's SQS SDK calls Amazon SQS. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from Azure Cosmos DB. Before: on AWS the application's SQS SDK calls Amazon SQS. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from Azure Cosmos DB.

The application keeps its SQS SDK. The adapter stores and reads messages in Azure Cosmos DB.

#### Architecture The Rust adapter stores durable state in Cosmos DB, so instances can restart or scale out without sharing an in-memory lock table. It authenticates through the customer's AKS Workload Identity without a static secret. Microsoft operates Cosmos DB's durability, backups and high availability; Tensor9 operates the adapter. On the Cosmos side, the compiler provisions one serverless Cosmos account and one database per appliance , and inside it **one container per declared queue** (the container id is the queue name). Every message is a document that holds the SQS body, its attributes, its receive count, and a visibility timestamp. A **Standard** queue stores its messages durably and claims eligible messages in arrival order. Concurrent claims do not establish a fairness or no-starvation guarantee. FIFO queues order by `MessageGroupId`, with one message in flight per group, as described in the FIFO section. Retention rides Cosmos's **native per-item TTL**, so an expired message is reclaimed by Cosmos itself. * **Cosmos DB coordinates message claims.** Adapter instances share message and claim records in the database, so an adapter restart does not erase them.
The adapter serves SQS requests using Azure Cosmos DB. One serverless account and database hold one container per declared queue. Cosmos stores messages and coordinates claims. FIFO queues keep strict per-group order with one message in flight per group. The adapter serves SQS requests using Azure Cosmos DB. One serverless account and database hold one container per declared queue. Cosmos stores messages and coordinates claims. FIFO queues keep strict per-group order with one message in flight per group.

A queue is one Cosmos container and each message is a document; a FIFO queue keeps strict per-group order with one message in flight per group.

#### Receiving messages Receiving atomically claims the oldest visible message, hides it from other receivers and increments its receive count. If another receiver claims the candidate first, the adapter moves to the next one. Each successful conditional write has one owner; visibility expiry still permits redelivery. FIFO queues select the next sequence position within each message group and allow one message in flight per group. Messages can be redelivered after visibility expires. `ApproximateReceiveCount` is the **real** receive count (incremented on each claim, not estimated), and the SQS MD5-of-body and MD5-of-attributes are recomputed so your SDK verifies them byte-for-byte. FIFO rejects per-message `DelaySeconds`, including zero; omit the parameter. A `SendMessageBatch` runs as per-entry writes with faithful per-entry outcomes.
A conditional write claims an eligible message. A competing receiver that loses the claim tries another candidate. Visibility expiry can cause redelivery. A conditional write claims an eligible message. A competing receiver that loses the claim tries another candidate. Visibility expiry can cause redelivery.

A receive reads the oldest visible message and claims it for exactly one consumer; if another consumer claims it first, this receive moves to the next candidate. ApproximateReceiveCount is the real receive count, incremented on each claim, never estimated.

#### Receipt handles & visibility SQS hands your consumer a **receipt handle** to delete or extend a message. Here that handle is a token for claim state stored in Cosmos DB. Because the adapter keeps no in-memory lock table, a current handle resolves the same way before and after a restart: `DeleteMessage` and `ChangeMessageVisibility` keep working. A stale handle (the message was already deleted, or a newer claim superseded this one) is a **no-op on delete**, exactly as SQS's own stale handle is; a cross-queue or unparseable handle is rejected as `ReceiptHandleIsInvalid`. `ChangeMessageVisibility` updates the stored visibility time without a broker lock-duration maximum. Zero makes the message immediately available to claim again. Retention can expire a message while it is in flight; extending visibility does not extend its lifetime. A stale handle is rejected, allowing the consumer to detect that it no longer owns the claim. Visibility expiry does not stop a worker from continuing to process an earlier receive.
A receipt handle is plain data with no broker lock behind it, so it resolves the same after the adapter restarts. The visibility timeout has no ceiling: ChangeMessageVisibility extends it forward with no cap, where a broker-backed backend would stop at its lock ceiling. A receipt handle is plain data with no broker lock behind it, so it resolves the same after the adapter restarts. The visibility timeout has no ceiling: ChangeMessageVisibility extends it forward with no cap, where a broker-backed backend would stop at its lock ceiling.

Receipt handles remain usable after an adapter restart. Visibility is stored in Cosmos DB and has no broker lock-duration limit.

#### FIFO ordering & dedup FIFO queues order messages by `MessageGroupId` and support duplicate detection. Standard queues have neither feature. Cosmos DB assigns each message a group position before enqueueing it. Concurrent senders therefore share one order without relying on their clocks. Each FIFO send returns that position as `SequenceNumber`. Sequence numbers increase but need not be consecutive; a gap does not stop the group. Receiving claims the next message from a group only if no message in that group is already in flight. The check and claim preserve this rule across concurrent receivers, including when a slow send arrives late. Different groups can be processed concurrently without coordination between consumers. When a claim times out, delivery resumes from the same message in its group. Within the 5-minute deduplication window, a repeated send is suppressed and returns the original `MessageId` and `SequenceNumber`, including after a crash during the send. The key is an explicit `MessageDeduplicationId`, or a hash of the body when content-based deduplication is enabled. A FIFO send with neither returns `InvalidParameterValue`. Standard queues reject `MessageDeduplicationId` because they do not support deduplication. * **Sending and receiving use separate tracking.** Sequence assignment does not block the group-claim mechanism used by receivers, so producers and consumers can proceed independently. * **Clock skew can shorten visibility.** The one-message-per-group claim check does not use a clock. Visibility checks do, so clock differences between consumers can cause early redelivery without allowing two active claims in the same group.
FIFO on Cosmos: each group is delivered in strict send order with exactly one message in flight per group, correct across horizontally scaled consumers; duplicate sends are suppressed and echo the original MessageId and SequenceNumber. FIFO on Cosmos: each group is delivered in strict send order with exactly one message in flight per group, correct across horizontally scaled consumers; duplicate sends are suppressed and echo the original MessageId and SequenceNumber.

Order within a group is strict send order; exactly one message is in flight per group; duplicate sends are suppressed and echo the original MessageId and SequenceNumber. This section applies to FIFO queues only.

#### Dead-letter queues A message received more than `maxReceiveCount` times is moved to the declared dead-letter queue. If the adapter crashes during the move, retry ensures it reaches the DLQ before removal from the source. A retry can produce a duplicate in the DLQ; consumers there must handle at-least-once delivery. The adapter moves a message that exceeded its receive limit to the DLQ before claiming another message in the group. The moved message retains its group and order in the FIFO DLQ, with its receive count reset to 0. If a message expires under retention instead, it is removed and the next message becomes available in the group.
A failed message is copied to its dead-letter queue and then removed from the source. If the adapter crashes between those steps, retry completes the move; the dead-letter queue can contain a duplicate. A failed message is copied to its dead-letter queue and then removed from the source. If the adapter crashes between those steps, retry completes the move; the dead-letter queue can contain a duplicate.

The adapter retries interrupted dead-letter moves. Delivery to the DLQ is at-least-once, so a crash can cause a duplicate.

#### Limitations △ Where SQS and Cosmos diverge, read before you adopt * **Approximate counts can lag by about 5 seconds.** The adapter caches counts briefly, so recent sends, settlements and expirations may not be reflected immediately. Use Azure monitoring to observe counts after deployment. * **Dead-letter delivery can produce duplicates.** Retry completes a move interrupted by an adapter crash without losing the message, but can deliver it to the DLQ more than once. * **Queue capacity is bounded by the serverless container count.** All of an appliance's queues share one serverless Cosmos account, which caps at about 500 containers, so roughly 499 queues per appliance (one container per queue). Beyond that the build stops with a clear error rather than half-provisioning. The account is **serverless by design**, a fit for idle-heavy queue fleets. * **Single region.** The account is single-region (the appliance's region); there is no geo-replica. * **Clock skew can shorten FIFO visibility.** The group claim check is independent of clocks, but visibility uses wall-clock time. Clock differences between consumers can cause early redelivery while preserving one active claim per group. * **Cosmos DB removes expired messages.** Per-item time to live (TTL) is enforced by background cleanup. Expiry can occur while a consumer is processing the message. Removing an expired first message allows the next message in its group to proceed. * **Per-message delay is rejected.** FIFO queues reject a supplied `DelaySeconds`, including zero. Omit it from individual messages. * **Separate logical queue management from container provisioning.** Max manages logical queues on provisioned containers, listings, tags and supported defaults, including retention. Terraform owns the account, database and containers; deleting a logical queue does not delete its container. Nonzero queue-level delay and new dead-letter routes are rejected. #### Other considerations * **Data migration.** The Cosmos account, database, and per-queue containers are provisioned empty at apply-time. In-flight SQS messages are not migrated; drain each group before cutover so no group switches mid-sequence. * **Operations and ownership.** The adapter provisions the Cosmos deployment itself (one serverless account, one database per appliance, and one container per declared queue), all derived from your declared queues; the adapter is injected alongside the application. Microsoft operates Cosmos's durability, backup, and HA; Tensor9 operates the adapter. What remains operational for the deployment is the surrounding platform: the Azure subscription, cluster patching, and monitoring. * **Monitor database usage.** Use Azure monitoring to track Cosmos DB request units and latency; queue counts are cached for about 5 seconds. * **Retention is native, whole-second Cosmos TTL.** A message's remaining lifetime is Cosmos's native per-item TTL set to the remaining whole-second budget (floored at one second, so Cosmos reclaims at or after the message is hidden, never before it), with no background cleanup of its own. ### Via PostgreSQL Flexible Server | Operation | Area | Support | Depth | Notes | | ------------------------ | -------------- | --------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Dead-letter redrive | Dead-letter | Supported | Most usage | fires automatically once a message passes maxReceiveCount | | Content/ID deduplication | FIFO | Supported | Most usage | both modes are served: an explicit MessageDeduplicationId takes precedence over the content hash (AWS's own rule), and a content-based queue derives a stable digest of the body. The message insert and deduplication marker share one transaction. A duplicate rolls back its new row and returns the original message identifiers | | FIFO ordering | FIFO | Supported | Common | per-MessageGroupId strict order with one-in-flight per group (head-of-group claim in SQL, same contract as the portable/CloudSQL FIFO tiers) | | Message attributes | Messages | Supported | Common | - | | GetQueueAttributes | Queues | Supported | Most usage | reads queue attributes, including approximate message counts | | GetQueueUrl | Queues | Supported | Most usage | resolves the queue by name | | DeleteMessage | Send / receive | Supported | Common | resolves by receipt handle; a stale handle returns ReceiptHandleIsInvalid | | DeleteMessageBatch | Send / receive | Supported | Common | returns the result for each entry | | ReceiveMessage | Send / receive | Supported | Common | long-poll (WaitTimeSeconds) honored; one message in flight per MessageGroupId | | SendMessage | Send / receive | Supported | Common | MessageGroupId required on send (the AWS FIFO rule); MD5 checksums match what your SDK verifies | | SendMessageBatch | Send / receive | Supported | Common | up to 10 messages per call, each with its MessageGroupId; returns the result for each entry | | ChangeMessageVisibility | Visibility | Supported | Common | resolves by receipt handle; a stale handle is rejected rather than silently succeeding | #### PostgreSQL hosting Microsoft operates PostgreSQL Flexible Server, including zone-redundant HA and point-in-time recovery backups. The adapter uses short-lived Microsoft Entra tokens as database passwords. Queue data stays outside the customer's Kubernetes cluster and survives its rebuild. One server and database hold all queues for the deployment, sharing capacity and maintenance; the default server size is fixed because SQS supplies no source instance size. Tensor9 operates the queue schema and adapter. Queues start empty; existing SQS messages are not migrated. Drain each message group before cutover to preserve order. #### Queue behavior The adapter uses the same PostgreSQL tables, atomic message claims, stored receipt handles and transactional dead-letter moves described in [PostgreSQL queue behavior](/service-adapters/aws/messaging-streaming/sqs-fifo#postgresql-queue-behavior). Receipt handles survive adapter restarts; stale claims return an error. Retention still removes messages in flight. All queues share database capacity, so a busy queue can affect others. The documented Standard-queue benchmark does not establish this host's capacity or FIFO throughput. Duplicate sends are suppressed for five minutes; consumers must still handle redelivery. [Service Catalog](/service-adapters/catalog). # SQS (Standard) Source: https://docs.tensor9.com/service-adapters/aws/messaging-streaming/sqs-standard AWS SQS (Standard). Standard queues accept messages at effectively unlimited throughput but deliver at least once and may reorder or duplicate them. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [On Google Cloud](#on-google-cloud) * [Via Pub/Sub](#via-pub/sub) * [Via Cloud SQL for PostgreSQL](#via-cloud-sql-for-postgresql) * [On Azure, OCI, and Private Kubernetes](#on-azure-oci-and-private-kubernetes) * [Via CloudNativePG](#via-cloudnativepg) * [On Azure](#on-azure) * [Via Azure Queue Storage](#via-azure-queue-storage) * [Via Azure Service Bus](#via-azure-service-bus) * [Via Azure Cosmos DB (queue)](#via-azure-cosmos-db-queue) * [Via PostgreSQL Flexible Server](#via-postgresql-flexible-server) * [On OCI](#on-oci) * [Via OCI Queue](#via-oci-queue) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of SQS (Standard) with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | SQS (Standard) | Google Cloud · Pub/Sub | Google Cloud · Cloud SQL for PostgreSQL | Azure, OCI, and Private Kubernetes · CloudNativePG | Azure · Azure Queue Storage | Azure · Azure Service Bus | Azure · Azure Cosmos DB (queue) | Azure · PostgreSQL Flexible Server | OCI · OCI Queue | | ----------------------------------------------- | -------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | Delivery guarantee | at-least-once (standard) | at-least-once (StreamingPull + ack) | at-least-once, best-effort order | at-least-once, best-effort order | at-least-once (visibility + pop\_receipt) | at-least-once (PeekLock) | at-least-once, best-effort order | at-least-once, best-effort order | at-least-once, best-effort order | | Dead-letter | Yes - native redrive (maxReceiveCount) | Yes - native dead-letter topic (max\_delivery\_attempts) | Yes - native redrive (maxReceiveCount) | Yes - native redrive (maxReceiveCount) | Partial - application-managed dead-letter delivery | Yes - native DLQ (delivery-count limit) | Yes - moved into the declared DLQ container at claim time (at-least-once) | Yes - native redrive (maxReceiveCount) | Partial - delivery-count redrive (OCI Queue's count-based dead-letter) | | Visibility timeout · max hold | up to 12 h | 10-600 s queue default; per-message 0 also accepted; other values rejected | 30 s default; no broker lock-duration cap, but retention still applies | 30 s default; no broker lock-duration cap, but retention still applies | per-message (set on each message) | ≤ 5 min (PeekLock cap; longer is rejected with an error) | uncapped (not a broker lock; extends via ChangeMessageVisibility with no ceiling) | 30 s default; no broker lock-duration cap, but retention still applies | 30 s default (OCI Queue), honored and extendable per message | | Receipt handle · restart durability | stateless, durable | ack-id (broker; stream-scoped, not restart-durable) | a durable handle that survives an adapter restart | a durable handle that survives an adapter restart | pop\_receipt: stateless and durable (survives an adapter restart) | broker lock (not restart-durable) | stateless data; survives an adapter restart, with no broker lock behind it | a durable handle that survives an adapter restart | present (resolves by receipt handle) | | Max message size · per message | 1 MiB | 256 KiB adapter default; declared limit enforced | 256 KB | 256 KB | \~48 KiB raw (64 KiB wire, base64) | 256 KB | 256 KB (well under the 2 MB Cosmos document cap) | 256 KB | 128 KB (OCI Queue's ceiling; SQS is 1 MiB) | | Long-polling · receive wait | up to 20 s | native StreamingPull (no poll floor) | bounded by WaitTimeSeconds (poll loop) | bounded by WaitTimeSeconds (poll loop) | client poll (no native long-poll) | native max-wait ≤ 20 s | poll (claim on receive) | bounded by WaitTimeSeconds (poll loop) | native long-poll on receive | | Message retention · message lifetime | up to 14 d | SQS-settable: 10 min-14 d; shorter values rejected | retention enforced on message age (60 s..14 d) | retention enforced on message age (60 s..14 d) | up to 14 d (7 d native default) | native setting provisioned through Terraform; Max returns declared retention, not a live broker read | native per-item Cosmos TTL | retention enforced on message age (60 s..14 d) | up to 7 d (OCI Queue maximum); SQS 4 d default emitted explicitly | | API coverage | full | high | high | high | high | high | high | high | high | | Durability · cluster-rebuild survival | SQS: managed, cross-AZ | - | - | - | - | - | - | queue data is stored on the managed Flexible Server outside the appliance cluster and survives a cluster rebuild | - | | Shared-server tenancy · shared database | dedicated SQS | - | - | - | - | - | - | one shared Flexible Server + one shared database per appliance; every SQS queue on the appliance co-locates as a table-per-queue in that database, with no cross-appliance co-location | - | | Server size · SKU / storage default | AWS-managed capacity | - | - | - | - | - | - | uses a fixed default server size and storage allocation because an SQS queue declares no source instance size | - | | Maintenance and shared capacity · shared server | per-queue isolation | - | - | - | - | - | - | the shared server has one maintenance window and shared compute, so a heavy queue can affect neighbors on the same appliance server (the shared-server tradeoff) | - | ## On Google Cloud ### Via Pub/Sub | Operation | Area | Support | Depth | Notes | | ------------------------------ | -------------- | --------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | Dead-letter redrive | Dead-letter | Supported | Most usage | native dead-letter topic; maxReceiveCount maps to an approximate 5-100-attempt threshold; forwarding requires the policy's IAM permissions | | Message attributes | Messages | Supported | Common | - | | SetQueueAttributes (retention) | Queues | Partial | Full surface | sets subscription retention from 10 minutes through 14 days; shorter SQS retention values are rejected | | DeleteMessage | Send / receive | Supported | Common | ack on delete; a stale receipt handle no-ops | | Long-polling | Send / receive | Supported | Most usage | native StreamingPull, without a repeated-poll interval | | ReceiveMessage | Send / receive | Supported | Common | native StreamingPull | | SendMessage | Send / receive | Supported | Common | publishes to a topic; each queue also has a pull subscription; MD5 checksums recomputed for your SDK | | ChangeMessageVisibility | Visibility | Partial | Common | maps to modifyAckDeadline; accepts 10-600 s, plus 0 for immediate redelivery; other values are rejected | #### How it works Tensor9 adds an adapter, a proxy process, to your application pod in the customer's Google Cloud environment. It sets `AWS_ENDPOINT_URL_SQS` to the adapter's loopback address. Your application keeps its `SendMessage` and `ReceiveMessage` calls; the adapter translates them to Pub/Sub operations and returns SQS responses, including error codes and recomputed MD5 checksums for the body and attributes. Each standard SQS queue maps to a Pub/Sub topic for sending and a subscription for receiving: queue `q` becomes topic `q` and subscription `q-sub`. The body is stored as Pub/Sub message data. Attributes preserve their type, string and binary values in the Pub/Sub attribute map and are reconstructed as SQS attributes on receive. Pub/Sub stores queue metadata and tracks receipt handles. This target supports standard queues; FIFO ordering and deduplication require another target.
Before: on AWS the application's SQS SDK calls Amazon SQS. After: in Google Cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from Google Pub/Sub, where an SQS queue q becomes a Pub/Sub topic q plus a subscription q-sub. Before: on AWS the application's SQS SDK calls Amazon SQS. After: in Google Cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from Google Pub/Sub, where an SQS queue q becomes a Pub/Sub topic q plus a subscription q-sub.

An SQS queue q becomes a Pub/Sub topic q and subscription q-sub . The application calls the adapter through its existing SQS SDK.

#### Architecture The Rust adapter authenticates to Pub/Sub with the customer's Workload Identity, without static credentials. Pub/Sub stores durable messages. The Max queue catalog stores declared configuration; receive buffers and acknowledgement handles belong to each adapter process and do not survive a restart. Google operates Pub/Sub's durability, delivery and high availability; Tensor9 operates the adapter. Per queue, the adapter keeps two long-lived structures. First, a **Publisher per topic**, created on the first send and held for the process lifetime: it feeds Pub/Sub's client-side batching layer, so concurrent sends to the same topic naturally coalesce into one `Publish` RPC rather than one RPC per `SendMessage`. Second, a **StreamingPull consumer per subscription**: a background task holds the subscription's message stream and forwards each received message into a **bounded in-process channel**, which `ReceiveMessage` drains, instead of issuing a blocking pull on every call. On `DeleteQueue` both are dropped; the consumer disposes its stream, which nacks any still-buffered messages so they redeliver promptly rather than waiting out the full deadline. * **Pub/Sub stores durable state.** Messages remain in Pub/Sub when an adapter restarts; local buffers and stream-scoped acknowledgement handles do not. Unacknowledged messages can be redelivered. * **Two limits control buffering.** Pub/Sub limits the number of delivered messages awaiting acknowledgement. The adapter separately limits its in-process receive buffer so idle consumers do not hold an unbounded number of messages whose acknowledgement deadlines are running.
Architecture: the app's SQS SDK calls a Rust Tensor9 adapter, which serves the SQS API from Google Pub/Sub over a keyless GCP identity. The adapter batches sends per topic and drains a long-lived consumer per subscription into a small in-process buffer, so a receive can return immediately when the buffer has an eligible message. Pub/Sub holds the topic q and the subscription q-sub, and keeps a bounded number of messages outstanding. Architecture: the app's SQS SDK calls a Rust Tensor9 adapter, which serves the SQS API from Google Pub/Sub over a keyless GCP identity. The adapter batches sends per topic and drains a long-lived consumer per subscription into a small in-process buffer, so a receive can return immediately when the buffer has an eligible message. Pub/Sub holds the topic q and the subscription q-sub, and keeps a bounded number of messages outstanding.

Pub/Sub stores durable messages. The adapter keeps a local receive buffer and stream-scoped acknowledgement handles.

#### Receiving messages A background `StreamingPull` consumer reads the subscription continuously and puts messages in a bounded buffer. `ReceiveMessage` reads that buffer. If a message arrives while the call is waiting, it can return immediately without waiting for another poll. `WaitTimeSeconds` sets how long to wait for the first message, capped at 20 seconds. Zero checks once without blocking; an empty result is valid. After the first message arrives, the adapter fills the rest of the batch from buffered messages without waiting again. `MaxNumberOfMessages` is clamped to 1–10. Pub/Sub limits unacknowledged messages, and the adapter's bounded buffer absorbs short interruptions in receiving while limiting messages held during idle periods. The adapter reconstructs attributes and recomputes MD5 checksums for SDK verification. Use Google Cloud monitoring to size the receive buffer and identify publish or delivery bottlenecks.
A receive drains a long-lived StreamingPull buffer rather than issuing a blocking pull per call. Step one: the consumer holds the message stream and forwards each message into a small in-process buffer the instant it lands. Step two: a receive honors WaitTimeSeconds as a drain budget capped at 20 seconds, where a wait of zero is a single non-blocking probe. Step three: it fills the rest of the batch, up to ten messages, from whatever is already buffered, with no extra waiting. A message arriving mid-wait is returned immediately, so there is no poll floor. A receive drains a long-lived StreamingPull buffer rather than issuing a blocking pull per call. Step one: the consumer holds the message stream and forwards each message into a small in-process buffer the instant it lands. Step two: a receive honors WaitTimeSeconds as a drain budget capped at 20 seconds, where a wait of zero is a single non-blocking probe. Step three: it fills the rest of the batch, up to ten messages, from whatever is already buffered, with no extra waiting. A message arriving mid-wait is returned immediately, so there is no poll floor.

ReceiveMessage reads the StreamingPull buffer and waits up to WaitTimeSeconds for the first message, capped at 20 seconds. A message arriving during the wait can return immediately.

#### Visibility & receipt handles `VisibilityTimeout` maps to Pub/Sub's acknowledgement deadline. Queue defaults must be 10–600 seconds. `ReceiveMessage` overrides and `ChangeMessageVisibility` accept that range plus zero, which makes the message available immediately. Other values are rejected, not clamped. A per-call `ReceiveMessage` override updates the returned messages only when it differs from the stream default. The subscriber does not automatically extend deadlines, so a message left in the buffer past its deadline can be delivered again. Consumers must handle duplicate delivery. The receipt handle is the Pub/Sub acknowledgement ID. It remains valid only while the `StreamingPull` consumer that received it is alive. `DeleteQueue`, `PurgeQueue` or a stream error disposes that consumer; the next receive starts a new one. Previously issued handles then stop resolving. Retained, unacknowledged messages can be delivered again with new handles; deleted, purged or retention-expired messages are not promised to return. Receive those messages again before deleting or extending them. `DeleteMessage` acknowledges one ID; batch delete acknowledges the IDs together.
Left: the SQS VisibilityTimeout maps to the subscription's ack deadline, accepting queue defaults from ten to six hundred seconds. Other defaults are rejected. Per-message visibility requests also accept zero for immediate redelivery. Right: the receipt handle is the opaque Pub/Sub ack-id, which is stream-scoped and not restart-durable: it is dropped on DeleteQueue, PurgeQueue, or a stream error, retained, unacknowledged messages can then be redelivered under a fresh ack-id. Left: the SQS VisibilityTimeout maps to the subscription's ack deadline, accepting queue defaults from ten to six hundred seconds. Other defaults are rejected. Per-message visibility requests also accept zero for immediate redelivery. Right: the receipt handle is the opaque Pub/Sub ack-id, which is stream-scoped and not restart-durable: it is dropped on DeleteQueue, PurgeQueue, or a stream error, retained, unacknowledged messages can then be redelivered under a fresh ack-id.

Queue visibility defaults must be 10-600 seconds. Per-message visibility also accepts 0 for immediate redelivery; other values are rejected. Receipt handles are stream-scoped Pub/Sub acknowledgement IDs and do not survive a restart.

#### Dead-letter & delivery Pub/Sub handles dead-letter delivery. The SQS `RedrivePolicy` sets the subscription's delivery-attempt limit from `maxReceiveCount`. The adapter resolves `deadLetterTargetArn`, supplied as an SQS-style ARN or bare name, to `projects//topics/`. Pub/Sub accepts a threshold of 5-100 attempts, but forwarding is best-effort: it can occur before or after that count. Delivery-attempt tracking requires the dead-letter policy and the required IAM permissions. The destination is a Pub/Sub topic, whereas an SQS dead-letter destination is another queue. Delivery is at-least-once. `ApproximateReceiveCount` uses Pub/Sub's delivery-attempt count, which is available only when the subscription has a dead-letter policy; other subscriptions return `1` on every receive. `SendMessage` uses the topic's publisher batching and returns the message ID and MD5 checksums. `SendMessageBatch` uses one multi-message Publish RPC and maps returned message IDs to entry IDs by position. A validation failure is reported for the affected entry without failing the whole batch.
Dead-lettering is native and server-side. An SQS RedrivePolicy maps to the subscription's dead-letter policy, where maxReceiveCount becomes the delivery-attempt limit and the deadLetterTargetArn is interpreted as a fully-qualified Pub/Sub dead-letter topic. Pub/Sub forwards messages to that topic on a best-effort basis, potentially before or after the configured attempt count. Pub/Sub's delivery-attempt count, surfaced as ApproximateReceiveCount, is populated only when the subscription has a dead-letter policy; without one every receive reports one. Dead-lettering is native and server-side. An SQS RedrivePolicy maps to the subscription's dead-letter policy, where maxReceiveCount becomes the delivery-attempt limit and the deadLetterTargetArn is interpreted as a fully-qualified Pub/Sub dead-letter topic. Pub/Sub forwards messages to that topic on a best-effort basis, potentially before or after the configured attempt count. Pub/Sub's delivery-attempt count, surfaced as ApproximateReceiveCount, is populated only when the subscription has a dead-letter policy; without one every receive reports one.

Pub/Sub forwards messages to a dead-letter topic on a best-effort basis. Delivery-attempt counts require a correctly configured policy and IAM permissions.

#### Limitations **Message size.** The managed adapter checks the declared `MaximumMessageSize` before publishing, including each entry of a batch. Its default is 262144 bytes (256 KiB), below [native SQS's 1 MiB limit](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html). Pub/Sub's native 10 MB limit does not increase the queue's declared allowance. **Retention.** Queue creation and `SetQueueAttributes` apply `MessageRetentionPeriod` to Pub/Sub's subscription retention. Pub/Sub requires at least ten minutes, so shorter SQS retention values are rejected. The remaining SQS range, through 14 days, is representable. Configuration reads use the queue catalog rather than substituting Pub/Sub's default lifetime. △ Pub/Sub limitations * **Approximate message counts are not reported.** Pub/Sub does not expose approximate depth cheaply, so `ApproximateNumberOfMessages`, `...NotVisible`, and `...Delayed` come back empty. Depth is for GCP monitoring once this is running. * **Ack-deadlines are not auto-extended.** The StreamingPull subscriber does not renew a delivered message's lease; the queue default must be 10-600 seconds. A message buffered past its deadline can be delivered again; consumers must tolerate duplicates. * **Receipt handles are stream-scoped, not restart-durable.** The receipt handle is the opaque Pub/Sub ack-id; it stops resolving after `DeleteQueue`, `PurgeQueue`, or a stream error. Retained, unacknowledged messages can be delivered again under a fresh ack-id. * **ApproximateReceiveCount requires a configured dead-letter policy.** It is Pub/Sub's delivery-attempt count, which Pub/Sub supplies only with a correctly configured policy and IAM permissions; the count can reset. A plain queue reports `1` on every receive. * **Nonzero delay is rejected.** Pub/Sub cannot schedule individual messages for later delivery. Queue- and message-level `DelaySeconds` must be zero.
Managed sends enforce the declared queue size before publishing. Queue retention configures the Pub/Sub subscription; values below ten minutes are rejected. Managed sends enforce the declared queue size before publishing. Queue retention configures the Pub/Sub subscription; values below ten minutes are rejected.

The queue catalog supplies the size limit and retention setting; the adapter enforces size and configures subscription retention.

#### Other considerations * **Plan message cutover.** The Pub/Sub topic `q` and subscription `q-sub` start empty. Existing SQS messages are not migrated. Drain the source queue or dual-write during the transition; application SQS calls and checksum verification remain unchanged. * **Google operates Pub/Sub.** Google manages message durability, delivery and availability. Tensor9 operates the SQS adapter. * **Published messages remain in Pub/Sub.** The adapter keeps a bounded receive buffer in memory. On orderly stream disposal it releases buffered messages for redelivery. After an abrupt failure, unacknowledged messages can be delivered again when their deadlines expire. * **Estimate Pub/Sub costs from message volume.** Billing follows the volume of data sent through Pub/Sub. The adapter does not meter additional usage. * **Operational numbers come from GCP monitoring.** Approximate message counts (`ApproximateNumberOfMessages` and its siblings) come back empty and live latency, throughput, and cost are read from GCP monitoring once the workload is running, so point dashboards and alerts at Pub/Sub's metrics, not SQS CloudWatch queue metrics. ### Via Cloud SQL for PostgreSQL | Operation | Area | Support | Depth | Notes | | ----------------------- | -------------- | --------- | ---------- | ------------------------------------------------------------------------------------------------ | | Dead-letter redrive | Dead-letter | Supported | Most usage | fires automatically once a message passes maxReceiveCount | | Message attributes | Messages | Supported | Common | - | | GetQueueAttributes | Queues | Supported | Most usage | reads queue attributes, including approximate message counts | | GetQueueUrl | Queues | Supported | Most usage | resolves the queue by name | | DelaySeconds | Send / receive | Supported | Common | a delayed message remains hidden until its delay expires | | DeleteMessage | Send / receive | Supported | Common | resolves by receipt handle; a stale handle returns ReceiptHandleIsInvalid | | DeleteMessageBatch | Send / receive | Supported | Common | returns the result for each entry | | ReceiveMessage | Send / receive | Supported | Common | long-poll (WaitTimeSeconds) honored; claims the oldest visible messages | | SendMessage | Send / receive | Supported | Common | supports both SQS protocol formats; MD5 checksums match, identical to the portable Postgres tier | | SendMessageBatch | Send / receive | Supported | Common | up to 10 messages per call; returns the result for each entry | | ChangeMessageVisibility | Visibility | Supported | Common | resolves by receipt handle; a stale handle is rejected rather than silently succeeding | #### PostgreSQL hosting Google operates Cloud SQL for PostgreSQL, including database backups and HA. The adapter uses short-lived Cloud SQL IAM tokens as database passwords, without an Auth Proxy or static password. Queue data stays outside the customer's Kubernetes cluster and survives its rebuild. Tensor9 operates the queue schema and adapter. Queues start empty; existing SQS messages are not migrated. Drain the source or coordinate dual writes before cutover. #### PostgreSQL queue behavior Tensor9 adds an adapter, a proxy process, to your application pod and sets `AWS_ENDPOINT_URL_SQS` to its loopback address. Your application keeps its `SendMessage` and `ReceiveMessage` calls. The adapter implements them using PostgreSQL and returns SQS responses, error codes and MD5 checksums. Each queue has a durable PostgreSQL table with one row per message. SQL operations implement sending, receiving, deletion, delay, visibility, dead-letter delivery and counts. Standard queues provide at-least-once delivery with best-effort ordering.
Before: on AWS the application's SQS SDK calls Amazon SQS. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from a durable Postgres table per queue. Before: on AWS the application's SQS SDK calls Amazon SQS. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from a durable Postgres table per queue.

The application keeps its SQS SDK. The adapter stores each queue in a PostgreSQL table.

#### Architecture The Rust adapter stores all durable state in PostgreSQL, including messages, claims and receipt handles. Instances can restart or scale out without a shared in-memory lock table, receiver pool or renewal loop. On Cloud SQL and Azure Flexible Server, it authenticates with short-lived Cloud SQL IAM or Microsoft Entra tokens generated in-process and passed as database passwords, without a static secret. On the Postgres side, the compiler provisions **one database per appliance**, a catalog entry per queue, and **one durable table per declared queue**. Each message is a row holding the SQS body and its attributes, its message-group, a visibility deadline, a receipt handle, a receive count, and a sent-at timestamp; the table is indexed so the hot paths stay a single scan. A Standard queue claims eligible messages in arrival order, with best-effort ordering under concurrent traffic. A **dead-letter** queue is another queue table, and a background **retention reaper** physically removes rows past their retention. CloudNativePG, Cloud SQL for PostgreSQL and Azure Flexible Server use the same adapter code and tables. Their operational difference is where data is stored: the managed servers are outside the appliance cluster, while CloudNativePG is inside it. See the rebuild limitation below.
The adapter stores messages, receipt handles, receive counts and visibility deadlines in PostgreSQL. Each declared queue has a table; a dead-letter queue is another table. Background cleanup enforces retention. Standard queues claim eligible messages with best-effort ordering. The adapter stores messages, receipt handles, receive counts and visibility deadlines in PostgreSQL. Each declared queue has a table; a dead-letter queue is another table. Background cleanup enforces retention. Standard queues claim eligible messages with best-effort ordering.

Each queue uses a PostgreSQL table, including dead-letter queues. The same adapter code runs against CloudNativePG, Cloud SQL for PostgreSQL and Azure Flexible Server.

#### Receiving messages In one SQL operation, the adapter selects messages whose visibility deadlines have passed, in arrival order and up to the requested batch size. It assigns each a fresh receipt handle, advances its visibility deadline, increments the receive count and returns the claimed rows. Rows locked by another receiver are skipped. Standard queues provide best-effort ordering. Visibility expiry permits redelivery; it does not stop a worker that is still processing an earlier receive. `ApproximateReceiveCount` is the message's **real** receive count (the claim increments it; it is not estimated), and the SQS MD5-of-body and MD5-of-attributes are recomputed so your SDK verifies them byte-for-byte. `DelaySeconds` stamps the message's visibility deadline at send, so a delayed message remains unavailable until that deadline. A **long poll** is a poll loop bounded by `WaitTimeSeconds` (capped at 20s), returning the instant a claim succeeds. A `SendMessageBatch` is a single multi-row insert with faithful per-entry outcomes, and the message body is stored in the queue table. The adapter defaults to a 262144-byte (256 KiB) queue limit; managed SQS requests enforce the declared limit before sending. Native AWS SQS now supports messages up to 1 MiB.
A receive claims messages in one atomic step: the adapter takes the head-of-queue messages that are due (their visibility deadline has passed), in arrival order, and in the same step assigns a fresh receipt handle, pushes the visibility deadline into the future, and increments the receive count, returning the claimed messages. Each receiver atomically claims a different batch of eligible messages. Messages can be redelivered when their visibility expires. A receive claims messages in one atomic step: the adapter takes the head-of-queue messages that are due (their visibility deadline has passed), in arrival order, and in the same step assigns a fresh receipt handle, pushes the visibility deadline into the future, and increments the receive count, returning the claimed messages. Each receiver atomically claims a different batch of eligible messages. Messages can be redelivered when their visibility expires.

Concurrent receives use atomic row claims and skip rows locked by another receiver. ApproximateReceiveCount is the message's real receive count.

#### Receipt handles & visibility Receiving writes a receipt token to the message row. `DeleteMessage` and `ChangeMessageVisibility` find that row by its token, including after an adapter restart or through another adapter instance. No in-memory receiver or broker lock is required. A delete or visibility change with no matching row returns `ReceiptHandleIsInvalid`. This includes messages already deleted or claimed again under a newer token. Visibility is a timestamp on the row, defaulting to 30 seconds on receive. `ChangeMessageVisibility` can extend it without a broker lock-duration maximum; zero makes the message immediately available to claim again. Retention can delete a message while it is in flight, and a newer claim makes the old token stale.
A receipt handle is a durable token stored on the message's own row, a stable id stamped on it, with no in-memory broker lock behind it, so it resolves the same after an adapter restart: Delete and ChangeMessageVisibility find the message again by that token. The visibility timeout is a timestamp on the row with no ceiling: ChangeMessageVisibility pushes it forward with no cap, where a broker-backed backend would stop at its lock ceiling. A receipt handle is a durable token stored on the message's own row, a stable id stamped on it, with no in-memory broker lock behind it, so it resolves the same after an adapter restart: Delete and ChangeMessageVisibility find the message again by that token. The visibility timeout is a timestamp on the row with no ceiling: ChangeMessageVisibility pushes it forward with no cap, where a broker-backed backend would stop at its lock ceiling.

Receipt handles are stored with messages, so they remain usable after an adapter restart. Visibility deadlines have no broker lock-duration limit.

#### Dead-letter & retention During receive, one database transaction moves messages at or past `maxReceiveCount` from the source table to the dead-letter table and claims the remaining candidates. The move and claim commit together, so an interrupted transaction cannot leave a message deleted from the source without its DLQ copy. The DLQ uses the same queue operations and retention cleanup. Moved messages start with a reset receive count. A background cleanup task deletes messages whose send time is older than the queue's retention period, including messages in flight. Receiving does not change that timestamp. Cleanup reads the stored retention attribute, defaults to four days and clamps it to 60 seconds–14 days. A missing or invalidly short value therefore cannot delete newly sent messages immediately. One worker per database runs each cleanup pass in batches, backs off during database maintenance and isolates queue errors. A circuit breaker and per-batch time limit bound the work.
Dead-letter and retention. A single atomic operation both claims live messages and redrives poison ones: from the locked head-of-queue set, messages at or past maxReceiveCount are moved from the source into the dead-letter queue and the rest are claimed and returned, in one transaction, so the move and claims commit together. This does not prevent consumer redelivery after visibility expiry. Separately, a background retention reaper removes messages whose send time is older than the queue's retention, clamped between 60 seconds and 14 days. Dead-letter and retention. A single atomic operation both claims live messages and redrives poison ones: from the locked head-of-queue set, messages at or past maxReceiveCount are moved from the source into the dead-letter queue and the rest are claimed and returned, in one transaction, so the move and claims commit together. This does not prevent consumer redelivery after visibility expiry. Separately, a background retention reaper removes messages whose send time is older than the queue's retention, clamped between 60 seconds and 14 days.

One transaction moves failed messages to the DLQ and claims eligible messages. A background worker removes messages older than their retention period, clamped to 60 seconds-14 days.

#### FIFO queues For message-group ordering and five-minute send deduplication, see [SQS FIFO](/service-adapters/aws/messaging-streaming/sqs-fifo). #### Limitations △ Where SQS and this backend diverge, read before you adopt * **Throughput is bounded by one PostgreSQL server.** All of an appliance's queues share one PostgreSQL server, so peak throughput is bounded by that one server, and a heavy queue can affect its neighbors. On the Tensor9 round-trip eval the Postgres adapter holds throughput parity with native SQS (about 300 vs 286 ops/s) and a tighter end-to-end tail (p99 140 ms vs SQS's 172 ms), while SQS is faster at the median (p50 15 ms vs 24 ms), results from that moderate-rate Standard-queue workload, not a measured throughput ceiling. This benchmark does not measure FIFO throughput or every hosting option. * **CloudNativePG data lives in-cluster and does not survive a cluster rebuild.** The in-cluster CloudNativePG host keeps queue data inside the appliance cluster, so a full cluster rebuild loses it; the two managed hosts (Cloud SQL Postgres, Azure Flexible Server) keep queue data on a provider-operated server outside the cluster, so it survives a rebuild. All three hosts use the same queue implementation; database placement changes rebuild recovery. * **Stale receipt handles return an error.** `DeleteMessage` and `ChangeMessageVisibility` return `ReceiptHandleIsInvalid` when no row matches the token. * **Retention cleanup runs periodically.** An expired message can remain until the next cleanup pass. Retention is clamped to 60 seconds–14 days, with invalidly short values raised to 60 seconds to avoid deleting newly sent messages. * **Approximate counts are a point-in-time exact count.** `ApproximateNumberOfMessages` and `...NotVisible` are a real count of the visible and not-yet-visible messages in the queue's table, exact at query time, but still Approximate under concurrent traffic; the delayed-count is reported as zero. * **Control-plane calls operate on the apply-time schema.** The database, the catalog, and each per-queue table (and its dead-letter table) are created by the compiler's provisioner; runtime `CreateQueue` / `DeleteQueue` / `SetQueueAttributes` operate on that provisioned schema, and a switch between standard and FIFO is refused as a replace rather than silently mutating semantics. #### Other considerations * **Plan message cutover.** The database, catalog and queue tables start empty. Existing SQS messages are not migrated. Drain the source or coordinate dual writes during the transition. * **Provisioning and ownership.** The adapter provisions and owns its own schema atomically per queue (one database per appliance, a catalog, and one durable table per declared queue, plus its dead-letter table), so a crash never leaves a half-provisioned queue. Each queue is marked with an ownership stamp: the layer never touches a queue it doesn't own (an unstamped or foreign-owned queue detaches rather than deletes), and it refuses to drop a non-empty queue without force. * **Three hosts, one backend, keyless on the managed ones.** The same backend supports CloudNativePG, Cloud SQL Postgres and Azure Flexible Server. On the two managed hosts the connection is keyless (an Entra or Cloud SQL IAM token obtained in-process and presented as the database password, no static secret), and the provider operates the database's durability, backup, and HA while Tensor9 operates the adapter. ## On Azure, OCI, and Private Kubernetes ### Via CloudNativePG | Operation | Area | Support | Depth | Notes | | ----------------------- | -------------- | --------- | ---------- | -------------------------------------------------------------------------------------- | | Dead-letter redrive | Dead-letter | Supported | Most usage | fires automatically once a message passes maxReceiveCount | | Message attributes | Messages | Supported | Common | - | | GetQueueAttributes | Queues | Supported | Most usage | reads queue attributes, including approximate message counts | | GetQueueUrl | Queues | Supported | Most usage | resolves the queue by name | | DelaySeconds | Send / receive | Supported | Common | a delayed message remains hidden until its delay expires | | DeleteMessage | Send / receive | Supported | Common | resolves by receipt handle; a stale handle returns ReceiptHandleIsInvalid | | DeleteMessageBatch | Send / receive | Supported | Common | returns the result for each entry | | ReceiveMessage | Send / receive | Supported | Common | long-poll (WaitTimeSeconds) honored; claims the oldest visible messages | | SendMessage | Send / receive | Supported | Common | supports both SQS protocol formats; MD5 checksums match what your SDK verifies | | SendMessageBatch | Send / receive | Supported | Common | up to 10 messages per call; returns the result for each entry | | ChangeMessageVisibility | Visibility | Supported | Common | resolves by receipt handle; a stale handle is rejected rather than silently succeeding | #### PostgreSQL hosting Tensor9 operates CloudNativePG inside the customer's Kubernetes cluster. Queue data is stored on that cluster's volumes and does not survive a full cluster rebuild. Plan recovery and message cutover before replacing the cluster. Tensor9 operates the queue schema and adapter. Queues start empty; existing SQS messages are not migrated. Drain the source or coordinate dual writes before cutover. #### Queue behavior The adapter uses the same PostgreSQL tables, atomic message claims, stored receipt handles and transactional dead-letter moves described in [PostgreSQL queue behavior](/service-adapters/aws/messaging-streaming/sqs-standard#postgresql-queue-behavior). Receipt handles survive adapter restarts; stale claims return an error. Retention still removes messages in flight. All queues share database capacity, so a busy queue can affect others. The documented Standard-queue benchmark does not establish this host's capacity or FIFO throughput. Delivery is at least once with best-effort ordering. For group ordering and deduplication, see [SQS FIFO](/service-adapters/aws/messaging-streaming/sqs-fifo). ## On Azure ### Via Azure Queue Storage | Operation | Area | Support | Depth | Notes | | ------------------------------------- | -------------- | --------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | Dead-letter redrive | Dead-letter | Partial | Most usage | the application must move failed messages to a separate queue using dequeue\_count | | CreateQueue | Queues | Supported | Most usage | queue operation | | DeleteQueue | Queues | Supported | Most usage | queue operation | | ListQueues | Queues | Supported | Most usage | queue operation | | PurgeQueue | Queues | Supported | Most usage | queue operation | | SetQueueAttributes | Queues | Partial | Full surface | Max stores visibility and retention defaults and applies them to messages; nonzero queue delay and dead-letter routes are rejected | | TagQueue / UntagQueue / ListQueueTags | Queues | Supported | Full surface | logical queue tags in the Max catalog | | Batch operations | Send / receive | Partial | Most usage | no native batching; a batch runs as N round-trips | | DelaySeconds | Send / receive | Supported | Common | maps to send-visibility | | DeleteMessage | Send / receive | Supported | Common | a stale pop\_receipt no-ops just as SQS does | | ReceiveMessage | Send / receive | Supported | Common | queue operation authenticated through workload identity | | SendMessage | Send / receive | Supported | Common | queue operation authenticated through workload identity | | ChangeMessageVisibility | Visibility | Supported | Common | resolves by pop\_receipt, stateless and durable across an adapter restart | #### How it works Tensor9 adds an adapter, a proxy process, to your application pod and sets `AWS_ENDPOINT_URL_SQS` to its loopback address. Your application keeps its `SendMessage` and `ReceiveMessage` calls. The adapter translates them to Queue Storage operations and returns SQS responses, error codes and MD5 checksums. Each declared standard SQS queue becomes a Queue Storage queue in one storage account. Receipt handles and receive counts map to Queue Storage's `pop_receipt` and `dequeue_count`. Each message body is base64-encoded, with attributes included in a JSON envelope when present. The encoded size limit leaves roughly 48 KiB for the payload, and applications must manage dead-letter delivery themselves. FIFO queues are routed to Service Bus.
Before: on AWS the application's SQS SDK calls Amazon SQS. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from Azure Queue Storage. Before: on AWS the application's SQS SDK calls Amazon SQS. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from Azure Queue Storage.

The application keeps its SQS SDK. The adapter translates its calls to Azure Queue Storage.

#### Architecture The Rust adapter uses Queue Storage's `pop_receipt` to delete messages and change visibility across calls. A handle remains usable after an adapter restart, and instances can scale out without a receiver pool, lock-renewal loop or cleanup worker. The customer's AKS Workload Identity supplies a federated token through the cluster webhook, without a static account key. Microsoft operates Queue Storage's durability, backups and high availability; Tensor9 operates the adapter. Terraform creates one storage account and one queue per declared SQS queue, preserving queue names. `message_text` contains the base64-encoded body and, when supplied, a JSON envelope with `MessageAttributes`. Queue Storage's `pop_receipt`, `dequeue_count` and `time_next_visible` provide the receipt token, receive count and visibility deadline. * **Queue Storage retains message state.** Receipt tokens remain valid across adapter restarts. Runtime operations need no receiver pool, lock-renewal loop or separate retention worker. * **One storage account, one queue per declared SQS queue, reached keylessly.** The path runs on the customer's AKS Workload Identity with no static key; Microsoft operates durability, backup, and HA, and Tensor9 operates the adapter.
Architecture: the app's SQS SDK calls a Tensor9 adapter, which serves the SQS API from Azure Queue Storage over a keyless Workload Identity path. One storage account holds one queue per declared SQS queue; each SQS message is one queue message whose message_text is the base64-wrapped body, alongside the native pop_receipt, dequeue_count and time_next_visible visibility deadline. Architecture: the app's SQS SDK calls a Tensor9 adapter, which serves the SQS API from Azure Queue Storage over a keyless Workload Identity path. One storage account holds one queue per declared SQS queue; each SQS message is one queue message whose message_text is the base64-wrapped body, alongside the native pop_receipt, dequeue_count and time_next_visible visibility deadline.

Queue Storage stores messages, receipt tokens, receive counts and visibility deadlines. The adapter translates these fields to SQS without retaining durable state.

#### Receiving messages A `ReceiveMessage` is a **bounded long-poll**. Queue Storage has no native long-poll, so the adapter turns `WaitTimeSeconds` (capped at 20, the SQS maximum) into a poll loop that runs once immediately and then sleeps **about 300 ms between empty polls** until the budget elapses; a fixed poll interval, not a latency figure. Each poll pulls up to 10 messages : SQS caps a receive at 10, and although Queue Storage itself would return up to 32 the layer honors the SQS contract. The per-receive **visibility timeout passes straight through** (Queue Storage imposes no visibility-versus-TTL coupling on receive), and SQS's 12-hour maximum sits comfortably under Queue Storage's own 7-day ceiling, so nothing is clamped. Each received message includes `message_id`, `message_text`, `pop_receipt`, `dequeue_count` and `time_next_visible`. The adapter builds the SQS receipt handle from the receipt token and sets `ApproximateReceiveCount` from the dequeue count, which starts at 1 and increases on redelivery. It decodes the body and attributes and recomputes their MD5 checksums. Delivery is at-least-once with best-effort ordering. `DelaySeconds` sets initial invisibility. Use Azure monitoring for measured receive latency and throughput.
A receive is a bounded long-poll: WaitTimeSeconds is capped at 20 and, because Queue Storage has no native long-poll, becomes a poll loop that runs once then sleeps 300 milliseconds between empty polls. Each poll pulls up to ten messages; Queue Storage returns each as message_id, message_text, pop_receipt, dequeue_count and time_next_visible, which map directly to the SQS receipt handle and ApproximateReceiveCount, with delivery at-least-once. A receive is a bounded long-poll: WaitTimeSeconds is capped at 20 and, because Queue Storage has no native long-poll, becomes a poll loop that runs once then sleeps 300 milliseconds between empty polls. Each poll pulls up to ten messages; Queue Storage returns each as message_id, message_text, pop_receipt, dequeue_count and time_next_visible, which map directly to the SQS receipt handle and ApproximateReceiveCount, with delivery at-least-once.

A receive is a bounded long-poll that returns Queue Storage's own pop\_receipt and dequeue\_count ; there is no lock to renew. ApproximateReceiveCount is the real dequeue\_count , counted from 1.

#### Receipt handles & visibility The receipt handle contains `queue | message_id | pop_receipt`, base64url-encoded with a `qs1:` prefix. Queue Storage accepts the receipt across connections and processes, so delete and visibility changes still work after an adapter restart. A stale receipt, from a deleted or re-received message, produces a successful no-op on delete or update. A malformed handle or one for another queue returns `ReceiptHandleIsInvalid`. Native Queue Storage sets visibility on individual messages and has no queue-level default. Max stores the logical SQS queue's visibility default and applies it when `ReceiveMessage` omits an override. `ChangeMessageVisibility` maps to `update_message`, which re-stamps `time_next_visible`; a zero-second change surfaces the message for **immediate re-claim**, and SQS's 12-hour maximum sits under Queue Storage's 7-day ceiling , so the full SQS range passes through with **no clamp**. Because settle is by `pop_receipt` and not a broker lock, a consumer that lost its claim to a redelivery finds its `pop_receipt` superseded; the update no-ops rather than corrupting another consumer's claim.
A receipt handle contains the queue, message_id and native pop_receipt token. Azure Queue Storage keeps the message and visibility deadline, so a current receipt still works after an adapter restart. Visibility is per-message: SQS's 12-hour maximum fits with headroom under Queue Storage's 7-day ceiling, so ChangeMessageVisibility re-stamps time_next_visible with no clamp. A receipt handle contains the queue, message_id and native pop_receipt token. Azure Queue Storage keeps the message and visibility deadline, so a current receipt still works after an adapter restart. Visibility is per-message: SQS's 12-hour maximum fits with headroom under Queue Storage's 7-day ceiling, so ChangeMessageVisibility re-stamps time_next_visible with no clamp.

Queue Storage accepts the native pop\_receipt across adapter restarts. SQS's 12-hour visibility fits within Queue Storage's 7-day ceiling.

#### Payload size and dead-letter delivery `message_text` must be safe for XML and UTF-8, so the adapter base64-encodes each body. Encoding expands it by about 4/3; Queue Storage's 64 KiB limit therefore permits roughly 48 KiB of raw payload, less when attributes add overhead. The adapter checks encoded size and returns `InvalidParameterValue` with the size and limit for an oversized send. For larger messages, use another target or store the body in Blob Storage and send its location. Queue Storage does not move messages to a dead-letter queue automatically. The adapter exposes `dequeue_count` as `ApproximateReceiveCount`; your application uses it to decide when to move a message to a separately provisioned `-poison` queue. Copying there and deleting the source are separate operations, so account for interruption between them. Cosmos DB and Service Bus perform dead-letter delivery through their adapters or broker.
Queue Storage limits message_text to 64 KiB. Base64 encoding expands the body by about one third, leaving roughly 48 KiB before attribute overhead. Oversize sends return InvalidParameterValue. Applications must move failed messages to a separately provisioned dead-letter queue using ApproximateReceiveCount; the adapter does not perform that move. Queue Storage limits message_text to 64 KiB. Base64 encoding expands the body by about one third, leaving roughly 48 KiB before attribute overhead. Oversize sends return InvalidParameterValue. Applications must move failed messages to a separately provisioned dead-letter queue using ApproximateReceiveCount; the adapter does not perform that move.

Base64 encoding reduces the usable payload to roughly 48 KiB. Queue Storage has no automatic dead-letter delivery; the application moves failed messages to a separately provisioned queue.

#### Limitations △ Where SQS and Queue Storage diverge, read before you adopt * **Classic queues only: FIFO is rejected.** Queue Storage has no session, ordering, or dedup analog, so a `.fifo` origin is routed to Service Bus at compile time, and any `.fifo` traffic that still reaches this backend (a pre-declared queue or an out-of-band URL) is rejected with an error on send, receive, create, and purge. There is no message-group ordering and no deduplication here at all. * **The usable payload is roughly 48 KiB.** Base64 encoding expands the body by about 4/3, and attribute overhead also counts toward Queue Storage's 64 KiB encoded limit. Oversized sends return `InvalidParameterValue`. See the payload-size section. * **Your application must move failed messages.** Use `ApproximateReceiveCount` to decide when to copy a message to `-poison` and delete the source. The copy and deletion are separate operations; handle retries if interrupted. * **Batch calls make sequential requests.** `SendMessageBatch`, `DeleteMessageBatch` and `ChangeMessageVisibilityBatch` make one request per entry and return each entry's success or failure. * **Some queue policies have no native target.** The Max catalog stores visibility and retention defaults, which the adapter applies to individual messages. Nonzero queue-level delay and a dead-letter route are rejected because this backend cannot apply them as queue policies. Per-message delay remains available. * **Retention uses a per-message TTL.** The Max adapter reads the queue's declared `MessageRetentionPeriod` and applies it as `message_time_to_live` on each send. SQS permits 60 seconds to 14 days. Queue Storage also permits shorter lifetimes and `-1` for no expiry, but those are native backend extensions, not SQS retention values. Queue Storage defaults to 7 days when TTL is omitted; a `DelaySeconds` that meets or exceeds the retention is the degenerate never-visible case and is rejected with an error. * **`SentTimestamp` is omitted.** The adapter does not have a reliable original enqueue time to return on redelivery. It does return `ApproximateReceiveCount` from Queue Storage's dequeue count. * **Counts come from Queue Storage.** `ApproximateNumberOfMessages` uses the service's `approximate_messages_count` from get-properties. Use Azure monitoring for request volume and latency. * **Provisioning and queue management have separate scopes.** Terraform creates the storage account and initial queues. The Max queue catalog handles logical listings, tags and supported edits; changes must pass the backend's policy and message-size checks. * **Single storage account; durability is Microsoft's.** The data-plane path reaches one storage account keylessly via AKS Workload Identity; Microsoft operates Queue Storage's durability, backup, replication, and HA, and Tensor9 operates the adapter. #### Other considerations * **Plan message cutover.** Your application, SQS SDK and queue declarations are retained, but existing messages are not migrated. New queues start empty. Drain the SQS source or dual-write during the transition. * **Your application handles dead-letter delivery.** Microsoft operates Queue Storage's durability, backup, replication and availability; Tensor9 operates the adapter. Your application moves failed messages to `-poison` using `ApproximateReceiveCount`. * **Where durability lives.** Durability is a property of the storage account, not the individual queue: its redundancy tier (LRS, ZRS, GRS, or GZRS) is a setting on the account that every queue in it inherits, so pick the tier that matches the availability your workload needs before you provision. * **Include idle polls in cost estimates.** Queue Storage bills for transactions and stored capacity. A `ReceiveMessage` long poll checks for messages about every 300 ms, so an idle long poll generates several billable requests. ### Via Azure Service Bus | Operation | Area | Support | Depth | Notes | | ---------------------------------------------------- | -------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Dead-letter redrive | Dead-letter | Supported | Most usage | native; the dead-letter threshold maps to the backend's delivery-count limit | | DeleteQueue / ListQueues / SetQueueAttributes / tags | Queues | Partial | Full surface | Max manages logical queues, tags and supported edits; native entities remain provisioned through Terraform. Retention changes, nonzero queue delay and new dead-letter routes are rejected | | GetQueueAttributes (message counts) | Queues | Out of scope | Full surface | live message counts are unavailable and omitted; declared configuration is returned separately | | DelaySeconds | Send / receive | Supported | Common | translated to a scheduled enqueue, capped at 900 s; beyond is rejected | | DeleteMessage | Send / receive | Supported | Common | completes the lock by receipt handle | | ReceiveMessage | Send / receive | Supported | Common | takes a PeekLock; ApproximateReceiveCount is a real delivery count | | SendMessage | Send / receive | Supported | Common | attributes are stored as application\_properties; MD5 checksums recomputed for your SDK | | SendMessageBatch | Send / receive | Supported | Common | up to 10 messages per call; returns the result for each entry | | ChangeMessageVisibility | Visibility | Partial | Common | renew/abandon by handle; the visibility timeout is capped at 5 minutes, longer is rejected with an error | #### How it works Tensor9 adds an adapter, a proxy process, to your application pod and sets `AWS_ENDPOINT_URL_SQS` to its loopback address. Your application keeps its `SendMessage` and `ReceiveMessage` calls. The adapter translates them to Azure Service Bus operations and returns SQS responses, error codes and MD5 checksums. A standard queue maps to a Service Bus queue using AMQP 1.0. `SendMessage` publishes through a Service Bus sender; SQS attributes become `application_properties`, and `DelaySeconds` sets a scheduled enqueue time. Receiving locks a message with PeekLock. Deleting completes it on the receiver that obtained the lock; changing visibility renews or abandons that lock. Delivery is at-least-once with best-effort ordering and no deduplication. FIFO queues use the separate session-based target.
Before: on AWS the application's SQS SDK calls Amazon SQS (a standard queue). After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from an Azure Service Bus queue over AMQP 1.0 PeekLock. Before: on AWS the application's SQS SDK calls Amazon SQS (a standard queue). After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from an Azure Service Bus queue over AMQP 1.0 PeekLock.

The application calls its existing SQS SDK; the adapter translates those calls to Azure Service Bus operations.

#### Architecture The Rust adapter translates SQS operations to Service Bus Standard over AMQP 1.0. It authenticates with the customer's AKS Workload Identity and the `Azure Service Bus Data Owner` role. This grants access to messages without a static secret; it does not grant permission to create or configure queues. SQS `ReceiveMessage` then `DeleteMessage` are separate API calls: the opaque receipt handle holds the state between them. Service Bus PeekLock works differently: the lock-token is bound to the **receiver link** that fetched it, and only that same receiver can complete, abandon, or renew it. So the adapter runs a **receiver pool**: one long-lived PeekLock receiver per queue (with `prefetch` pinned to `0`, so a message's lock clock never starts before a caller asks for it), a `receipt-handle → in-flight-message` registry, and a background renew loop that keeps each in-flight lock alive under your requested visibility window against Service Bus's 5-minute lock cap. Senders share the client; each received message is settled on the very link that fetched it. Service Bus entity CRUD (creating a queue, setting its lock duration, its DLQ, its retention) is **control-plane** (ARM), and the keyless data-plane role grants data access, not namespace entity management. The backend uses **pre-provisioned entities**: the compiler provisions the namespace and queue at apply-time (a 5-minute lock, a redelivery limit from your redrive policy, and DLQ auto-forwarding), and `CreateQueue` merely verifies the queue is reachable by opening a sender. Microsoft operates Service Bus's durability, backup, and HA; Tensor9 operates the adapter. * **Each message uses its original receiver until settlement.** The adapter records receipt handles with the Service Bus receiver that obtained each lock. Prefetch is zero, so a lock starts only when a consumer requests a message. * **Entities are provisioned at apply-time, never by the data plane.** A 5-minute lock, a redelivery limit, and DLQ forwarding are compiled from your queue's declared attributes; the adapter never needs, and never holds, a control-plane credential.
Architecture: the app's SQS SDK calls a Rust Tensor9 adapter over the pod loopback. The adapter runs a receiver pool (one long-lived PeekLock receiver per queue with prefetch pinned to zero, a receipt-handle to in-flight-message registry, and a renew loop), and serves the API from an Azure Service Bus (Standard) queue the compiler provisions at apply time with a 5-minute lock, a redelivery limit, and dead-letter forwarding. Architecture: the app's SQS SDK calls a Rust Tensor9 adapter over the pod loopback. The adapter runs a receiver pool (one long-lived PeekLock receiver per queue with prefetch pinned to zero, a receipt-handle to in-flight-message registry, and a renew loop), and serves the API from an Azure Service Bus (Standard) queue the compiler provisions at apply time with a 5-minute lock, a redelivery limit, and dead-letter forwarding.

The adapter retains each receiving connection and receipt handle until the message is deleted or its lock expires. Terraform provisions the queue's lock duration, redelivery limit and dead-letter forwarding.

#### Receiving messages `ReceiveMessage` uses PeekLock to receive and lock messages. `MaxNumberOfMessages` is clamped to 1–10 and `WaitTimeSeconds` to 20 seconds, then passed to Service Bus as its maximum wait. A standard queue needs no session-accept step, so an empty queue returns within that wait. For each message, the adapter creates a receipt handle and records which receiver holds the lock. Delete and visibility changes use that same receiver. Standard queues allow duplicates and provide best-effort ordering. `ApproximateReceiveCount` uses Service Bus's `delivery_count`, starting at 1 and increasing on each redelivery; this count also determines when a failing message is dead-lettered. The adapter recomputes MD5 checksums and reads `SentTimestamp` from the enqueue time. `DelaySeconds` schedules delivery up to 900 seconds; larger values return `InvalidParameterValue`. Batch sends process entries individually and return each entry's result, including separately scheduled delays.
A receive is a PeekLock: the receiver locks up to ten messages within the long-poll wait; an empty queue returns within the requested wait with no session accept to overrun it; each locked message is registered against its owning receiver. The Service Bus delivery_count surfaces as the real ApproximateReceiveCount, and the contract is at-least-once, best-effort order, no dedup. A receive is a PeekLock: the receiver locks up to ten messages within the long-poll wait; an empty queue returns within the requested wait with no session accept to overrun it; each locked message is registered against its owning receiver. The Service Bus delivery_count surfaces as the real ApproximateReceiveCount, and the contract is at-least-once, best-effort order, no dedup.

A receive is a PeekLock: the receiver locks up to ten messages within your long-poll wait, and the adapter assigns a receipt handle held on the same link. ApproximateReceiveCount is the real Service Bus delivery\_count .

#### Receipt handles & visibility A receipt handle has the form `sb1 | queue | lock-token`. The adapter records its Service Bus receiver because only that receiver can complete, abandon or renew the lock. This state is held between `ReceiveMessage` and a later delete or visibility change. **Receipt handles expire on adapter restart.** Restarting drops the client, receivers and registry. A later delete or visibility change with an old handle returns `ReceiptHandleIsInvalid`. The message remains in Service Bus and becomes available again when its lock expires, within 5 minutes. **A visibility request cannot exceed 5 minutes.** Larger values return `InvalidParameterValue` because Service Bus caps lock duration at 5 minutes. A positive `ChangeMessageVisibility` renews the lock; zero abandons the message for immediate redelivery. The background renewal loop maintains the lock during the requested window.
The receipt handle is a live Service Bus PeekLock bound to the receiver link; an adapter restart invalidates every outstanding handle and the message redelivers after its lock lapses, reported as an error, not silent. The visibility timeout maps to the PeekLock lock, whose ceiling is Service Bus's 5-minute lock cap; a request above 5 minutes is rejected with an error rather than being silently clamped. The receipt handle is a live Service Bus PeekLock bound to the receiver link; an adapter restart invalidates every outstanding handle and the message redelivers after its lock lapses, reported as an error, not silent. The visibility timeout maps to the PeekLock lock, whose ceiling is Service Bus's 5-minute lock cap; a request above 5 minutes is rejected with an error rather than being silently clamped.

Receipt handles depend on the live Service Bus receiver and become invalid after an adapter restart. A requested visibility timeout above 5 minutes returns an error.

#### Dead-letter & retention Service Bus dead-letters a message when its delivery count reaches the queue's limit, set from `maxReceiveCount`. Because the adapter cannot receive from Service Bus's internal dead-letter subqueue, Terraform configures automatic forwarding to your declared dead-letter queue (DLQ). Expired messages are also dead-lettered and remain available there for inspection or recovery. Terraform configures retention, lock duration and dead-letter forwarding through Azure Resource Manager (ARM) when creating the queue. The native messaging role cannot read Azure Resource Manager settings. The Max adapter answers configuration reads from its declared queue catalog; live message counts remain unavailable from this transport. Broker messages are durable, while receiver links, active locks and receipt-handle mappings belong to the adapter process.
Dead-letter is broker-native: a poison message whose delivery count reaches the redelivery limit is dead-lettered by the broker and auto-forwarded to the DLQ entity the compiler declared. Retention and the entity config are control-plane, set at apply-time via ARM; native AMQP cannot read these settings. The Max queue catalog answers declared configuration reads; live counts remain unavailable. Dead-letter is broker-native: a poison message whose delivery count reaches the redelivery limit is dead-lettered by the broker and auto-forwarded to the DLQ entity the compiler declared. Retention and the entity config are control-plane, set at apply-time via ARM; native AMQP cannot read these settings. The Max queue catalog answers declared configuration reads; live counts remain unavailable.

Service Bus forwards failed messages to your declared DLQ after maxReceiveCount deliveries. Terraform configures retention and queue settings; runtime message permissions do not allow the adapter to read them.

#### Limitations △ Where SQS (standard) and Service Bus diverge, read before you adopt * **The visibility timeout is capped at 5 minutes.** A visibility timeout maps to the PeekLock lock, whose ceiling is Service Bus's 5-minute lock-duration cap; a requested value above 5 minutes is rejected with an error ( InvalidParameterValue ) rather than being silently clamped (a clamp would risk double-processing before your window elapsed). Extension via ChangeMessageVisibility renews the lock, so long-held work is supported by re-extending, not by one long timeout. * **Receipt handles expire on adapter restart.** A later delete or visibility change returns `ReceiptHandleIsInvalid`. The message becomes available again after its Service Bus lock expires, within 5 minutes. Cosmos DB and PostgreSQL handles differ: those handles remain usable after an adapter restart. * **At-least-once, best-effort order, and no deduplication.** This is the SQS standard-queue contract: order is not guaranteed and duplicates are possible, and there is deliberately no dedup. Strict per-group order and duplicate detection are the FIFO contract: declare a `.fifo` queue, which is served on Service Bus sessions by a separate path (documented in the FIFO explainer). * **Live counts are unavailable through the messaging transport.** The Max queue catalog supplies declared retention, visibility and DLQ configuration. `ApproximateNumberOfMessages` is omitted; use Azure monitoring for live queue counts and requests. * **Separate logical queue management from broker provisioning.** The Max catalog handles queue listings, tags and supported attribute edits. Creating a logical queue checks that its provisioned Service Bus entity is reachable; deleting the logical queue does not delete that entity. Explicit retention changes, nonzero queue delay and new dead-letter routes require broker configuration and are rejected by the messaging backend. * **PurgeQueue is bounded and racy.** A purge is a bounded receive-and-complete drain loop; a concurrent producer can enqueue mid-drain and only currently visible messages are drained. This differs from native SQS PurgeQueue , which also removes in-flight messages. Use Azure monitoring to measure latency and throughput for your workload; this adaptation does not promise specific performance numbers. #### Other considerations * **Nothing in flight migrates.** The Service Bus namespace and its queues come up empty at deploy time, so messages still sitting in an SQS queue on AWS are not moved across; cut over at a drain point, or dual-write until the SQS side is drained. * **Microsoft operates Service Bus; Tensor9 operates the adapter.** Microsoft manages queue durability, replication and availability. Include the adapter in your application's operational monitoring. * **Check visibility and restart handling before cutover.** Consumers must account for the 5-minute visibility limit and receipt handles becoming invalid after an adapter restart. Uncompleted messages remain in Service Bus and can be delivered again after their locks expire; message retention and dead-letter policies still apply. * **Estimate Service Bus costs from operations and capacity.** Standard bills per messaging operation; Premium bills per provisioned messaging unit. Include receives, renewals and deletes in the estimate. ### Via Azure Cosmos DB (queue) | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------- | -------------- | --------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Dead-letter redrive | Dead-letter | Supported | Most usage | at claim time a message past maxReceiveCount moves to the declared DLQ queue via a retried copy-then-delete move; a retry can produce a duplicate in the DLQ | | CreateQueue / DeleteQueue / SetQueueAttributes / tags | Queues | Partial | Full surface | Max manages logical queues on provisioned containers, tags and supported defaults. Terraform owns the account, database and containers; nonzero queue delay and new dead-letter routes are rejected | | GetQueueAttributes (approximate counts) | Queues | Partial | Full surface | approximate depth, cached \~5 s; can briefly include documents awaiting TTL expiry | | ListQueues | Queues | Supported | Full surface | exact: the declared queue set | | DelaySeconds | Send / receive | Supported | Common | per-message delay ranges from zero to a maximum of 900 seconds; larger values return InvalidParameterValue | | DeleteMessage | Send / receive | Supported | Common | deletes by receipt handle, which is stateless, so a stale handle no-ops just as SQS does | | ReceiveMessage | Send / receive | Supported | Common | messages are claimed in arrival order, giving best-effort order and at-least-once without a fairness guarantee; ApproximateReceiveCount is the message's real receive count | | SendMessage | Send / receive | Supported | Common | each message is a Cosmos document; MD5 checksums recomputed for your SDK | | SendMessageBatch | Send / receive | Supported | Common | per-entry writes with a result for each entry | | ChangeMessageVisibility | Visibility | Supported | Common | extends the message's visibility with no cap (there is no broker lock ceiling); a stale handle is rejected rather than silently succeeding | #### How it works Tensor9 adds an adapter, a proxy process, to your application pod and sets `AWS_ENDPOINT_URL_SQS` to its loopback address. Your application keeps its `SendMessage` and `ReceiveMessage` calls. The adapter implements them using Cosmos DB and returns SQS responses, error codes and MD5 checksums. Each queue has a Cosmos DB container, with one document per message. The adapter uses these documents for sending, receiving, deletion, delay, visibility, dead-letter delivery and counts. Standard queues claim messages in arrival order, with best-effort ordering under concurrent traffic.
Before: on AWS the application's SQS SDK calls Amazon SQS. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from Azure Cosmos DB. Before: on AWS the application's SQS SDK calls Amazon SQS. After: in the target cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from Azure Cosmos DB.

The application keeps its SQS SDK. The adapter stores and reads messages in Azure Cosmos DB.

#### Architecture The Rust adapter stores durable state in Cosmos DB, so instances can restart or scale out without sharing an in-memory lock table. It authenticates through the customer's AKS Workload Identity without a static secret. Microsoft operates Cosmos DB's durability, backups and high availability; Tensor9 operates the adapter. On the Cosmos side, the compiler provisions one serverless Cosmos account and one database per appliance , and inside it **one container per declared queue** (the container id is the queue name). Every message is a document that holds the SQS body, its attributes, its receive count, and a visibility timestamp. A **Standard** queue stores its messages durably and claims eligible messages in arrival order. Concurrent claims do not establish a fairness or no-starvation guarantee. Retention rides Cosmos's **native per-item TTL**, so an expired message is reclaimed by Cosmos itself. * **Cosmos DB coordinates message claims.** Adapter instances share message and claim records in the database, so an adapter restart does not erase them.
The adapter serves SQS requests using Azure Cosmos DB. One serverless account and database hold one container per declared queue. Cosmos stores messages and coordinates claims. Standard queues claim eligible messages with at-least-once delivery and best-effort ordering. The adapter serves SQS requests using Azure Cosmos DB. One serverless account and database hold one container per declared queue. Cosmos stores messages and coordinates claims. Standard queues claim eligible messages with at-least-once delivery and best-effort ordering.

A queue is one Cosmos container and each message is a document; a Standard queue stores messages durably and claims eligible messages with best-effort ordering.

#### Receiving messages Receiving atomically claims the oldest visible message, hides it from other receivers and increments its receive count. If another receiver claims the candidate first, the adapter moves to the next one. Each successful conditional write has one owner; visibility expiry still permits redelivery. Standard queues use **at-least-once delivery with best-effort ordering**. `ApproximateReceiveCount` is the **real** receive count (incremented on each claim, not estimated), and the SQS MD5-of-body and MD5-of-attributes are recomputed so your SDK verifies them byte-for-byte. Standard queues keep a delayed message invisible for its requested `DelaySeconds`, up to a maximum of 900 seconds. An omitted value means zero; larger values return `InvalidParameterValue`. A `SendMessageBatch` runs as per-entry writes with faithful per-entry outcomes.
A conditional write claims an eligible message. A competing receiver that loses the claim tries another candidate. Visibility expiry can cause redelivery. A conditional write claims an eligible message. A competing receiver that loses the claim tries another candidate. Visibility expiry can cause redelivery.

A receive reads the oldest visible message and claims it for exactly one consumer; if another consumer claims it first, this receive moves to the next candidate. ApproximateReceiveCount is the real receive count, incremented on each claim, never estimated.

#### Receipt handles & visibility SQS hands your consumer a **receipt handle** to delete or extend a message. Here that handle is a token for claim state stored in Cosmos DB. Because the adapter keeps no in-memory lock table, a current handle resolves the same way before and after a restart: `DeleteMessage` and `ChangeMessageVisibility` keep working. A stale handle (the message was already deleted, or a newer claim superseded this one) is a **no-op on delete**, exactly as SQS's own stale handle is; a cross-queue or unparseable handle is rejected as `ReceiptHandleIsInvalid`. `ChangeMessageVisibility` updates the stored visibility time without a broker lock-duration maximum. Zero makes the message immediately available to claim again. Retention can expire a message while it is in flight; extending visibility does not extend its lifetime. A stale handle is rejected, allowing the consumer to detect that it no longer owns the claim. Visibility expiry does not stop a worker from continuing to process an earlier receive.
A receipt handle is plain data with no broker lock behind it, so it resolves the same after the adapter restarts. The visibility timeout has no ceiling: ChangeMessageVisibility extends it forward with no cap, where a broker-backed backend would stop at its lock ceiling. A receipt handle is plain data with no broker lock behind it, so it resolves the same after the adapter restarts. The visibility timeout has no ceiling: ChangeMessageVisibility extends it forward with no cap, where a broker-backed backend would stop at its lock ceiling.

Receipt handles remain usable after an adapter restart. Visibility is stored in Cosmos DB and has no broker lock-duration limit.

#### FIFO queues For message-group ordering and five-minute send deduplication, see [SQS FIFO](/service-adapters/aws/messaging-streaming/sqs-fifo). #### Dead-letter queues A message received more than `maxReceiveCount` times is moved to the declared dead-letter queue. If the adapter crashes during the move, retry ensures it reaches the DLQ before removal from the source. A retry can produce a duplicate in the DLQ; consumers there must handle at-least-once delivery.
A failed message is copied to its dead-letter queue and then removed from the source. If the adapter crashes between those steps, retry completes the move; the dead-letter queue can contain a duplicate. A failed message is copied to its dead-letter queue and then removed from the source. If the adapter crashes between those steps, retry completes the move; the dead-letter queue can contain a duplicate.

The adapter retries interrupted dead-letter moves. Delivery to the DLQ is at-least-once, so a crash can cause a duplicate.

#### Limitations △ Where SQS and Cosmos diverge, read before you adopt * **Approximate counts can lag by about 5 seconds.** The adapter caches counts briefly, so recent sends, settlements and expirations may not be reflected immediately. Use Azure monitoring to observe counts after deployment. * **Dead-letter delivery can produce duplicates.** Retry completes a move interrupted by an adapter crash without losing the message, but can deliver it to the DLQ more than once. * **Queue capacity is bounded by the serverless container count.** All of an appliance's queues share one serverless Cosmos account, which caps at about 500 containers, so roughly 499 queues per appliance (one container per queue). Beyond that the build stops with a clear error rather than half-provisioning. The account is **serverless by design**, a fit for idle-heavy queue fleets. * **Single region.** The account is single-region (the appliance's region); there is no geo-replica. * **Cosmos DB removes expired messages.** Per-item time to live (TTL) is enforced by background cleanup. Expiry can occur while a consumer is processing the message. * **Per-message delay.** Standard queues honor `DelaySeconds` up to a maximum of 900 seconds; larger values return `InvalidParameterValue`. * **Separate logical queue management from container provisioning.** Max manages logical queues on provisioned containers, listings, tags and supported defaults, including retention. Terraform owns the account, database and containers; deleting a logical queue does not delete its container. Nonzero queue-level delay and new dead-letter routes are rejected. #### Other considerations * **Data migration.** The Cosmos account, database, and per-queue containers are provisioned empty at apply-time. In-flight SQS messages are not migrated; drain the source or coordinate dual writes before cutover. * **Operations and ownership.** The adapter provisions the Cosmos deployment itself (one serverless account, one database per appliance, and one container per declared queue), all derived from your declared queues; the adapter is injected alongside the application. Microsoft operates Cosmos's durability, backup, and HA; Tensor9 operates the adapter. What remains operational for the deployment is the surrounding platform: the Azure subscription, cluster patching, and monitoring. * **Monitor database usage.** Use Azure monitoring to track Cosmos DB request units and latency; queue counts are cached for about 5 seconds. * **Retention is native, whole-second Cosmos TTL.** A message's remaining lifetime is Cosmos's native per-item TTL set to the remaining whole-second budget (floored at one second, so Cosmos reclaims at or after the message is hidden, never before it), with no background cleanup of its own. ### Via PostgreSQL Flexible Server | Operation | Area | Support | Depth | Notes | | ----------------------- | -------------- | --------- | ---------- | ----------------------------------------------------------------------------------------------------------------------- | | Dead-letter redrive | Dead-letter | Supported | Most usage | fires automatically once a message passes maxReceiveCount | | Message attributes | Messages | Supported | Common | - | | GetQueueAttributes | Queues | Supported | Most usage | reads queue attributes, including approximate message counts | | GetQueueUrl | Queues | Supported | Most usage | resolves the queue by name | | DelaySeconds | Send / receive | Supported | Common | a delayed message remains hidden until its delay expires | | DeleteMessage | Send / receive | Supported | Common | resolves by receipt handle; a stale handle returns ReceiptHandleIsInvalid | | DeleteMessageBatch | Send / receive | Supported | Common | returns the result for each entry | | ReceiveMessage | Send / receive | Supported | Common | long-poll (WaitTimeSeconds) honored; claims the oldest visible messages | | SendMessage | Send / receive | Supported | Common | supports both SQS protocol formats; MD5 checksums match what your SDK verifies, identical to the portable Postgres tier | | SendMessageBatch | Send / receive | Supported | Common | up to 10 messages per call; returns the result for each entry | | ChangeMessageVisibility | Visibility | Supported | Common | resolves by receipt handle; a stale handle is rejected rather than silently succeeding | #### PostgreSQL hosting Microsoft operates PostgreSQL Flexible Server, including zone-redundant HA and point-in-time recovery backups. The adapter uses short-lived Microsoft Entra tokens as database passwords. Queue data stays outside the customer's Kubernetes cluster and survives its rebuild. One server and database hold all queues for the deployment, sharing capacity and maintenance; the default server size is fixed because SQS supplies no source instance size. Tensor9 operates the queue schema and adapter. Queues start empty; existing SQS messages are not migrated. Drain the source or coordinate dual writes before cutover. #### Queue behavior The adapter uses the same PostgreSQL tables, atomic message claims, stored receipt handles and transactional dead-letter moves described in [PostgreSQL queue behavior](/service-adapters/aws/messaging-streaming/sqs-standard#postgresql-queue-behavior). Receipt handles survive adapter restarts; stale claims return an error. Retention still removes messages in flight. All queues share database capacity, so a busy queue can affect others. The documented Standard-queue benchmark does not establish this host's capacity or FIFO throughput. Delivery is at least once with best-effort ordering. For group ordering and deduplication, see [SQS FIFO](/service-adapters/aws/messaging-streaming/sqs-fifo). ## On OCI ### Via OCI Queue | Operation | Area | Support | Depth | Notes | | ----------------------- | -------------- | --------- | ------------ | ------------------------------------------------------------------------------------------- | | Dead-letter | Dead-letter | Partial | Most usage | redrive maps to OCI Queue's delivery-count dead-letter (count-based) | | Max message size | Queues | Supported | Full surface | 128 KB, OCI Queue's message ceiling | | Message retention | Queues | Supported | Full surface | the SQS 4-day default is emitted explicitly; retention runs up to OCI Queue's 7-day maximum | | Visibility timeout | Queues | Supported | Full surface | OCI Queue's 30 s default, passed through and honored | | DeleteMessage | Send / receive | Supported | Common | served onto OCI Queue's DeleteMessage | | Long-polling | Send / receive | Supported | Most usage | native long-poll on GetMessages | | ReceiveMessage | Send / receive | Supported | Common | served onto OCI Queue's GetMessages | | SendMessage | Send / receive | Supported | Common | served onto OCI Queue's PutMessages; MD5 checksums recomputed for your SDK | | ChangeMessageVisibility | Visibility | Supported | Common | resolves by receipt handle; the 30 s visibility default is honored and extended per message | #### How it works Tensor9 adds an adapter, a proxy process, to your application pod in OCI and sets `AWS_ENDPOINT_URL_SQS` to its loopback address. Your application keeps its `SendMessage` and `ReceiveMessage` calls. The adapter translates them to OCI Queue operations and returns SQS responses, error codes and MD5 checksums. Each declared standard SQS queue becomes an OCI queue in the customer's compartment. `SendMessage` puts a message; `ReceiveMessage` gets one with a receipt; `DeleteMessage` deletes it using that receipt. OCI Queue stores messages, receipts, delivery counts and visibility deadlines, so the adapter holds no durable state. FIFO queues require a different target because OCI Queue does not support group ordering or deduplication.
Before: on AWS the application's SQS SDK calls Amazon SQS. After: in OCI cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from OCI Queue. Before: on AWS the application's SQS SDK calls Amazon SQS. After: in OCI cloud the same application and SDK call a Tensor9 adapter, which serves the SQS API from OCI Queue.

The application keeps its SQS SDK. The adapter translates calls to OCI Queue.

#### Architecture The Rust adapter uses OCI Queue receipts across calls and processes, so receipt handles remain usable after an adapter restart. Adapter instances can scale out without coordinating durable state. Authentication uses the customer's OCI workload identity without a static credential. Oracle operates queue durability, backups, replication and high availability; Tensor9 operates the adapter. Terraform creates one OCI queue per declared SQS queue in the customer's compartment, preserving its name. Queue visibility uses the declared SQS default; both services default to 30 seconds. Retention maps to OCI Queue's retention, capped at 7 days. `maxReceiveCount` sets the delivery-count threshold. OCI Queue manages these settings and tracks queued and in-flight messages.
Architecture: the app's SQS SDK calls a Tensor9 adapter, which serves the SQS API from OCI Queue using workload identity. Each declared SQS queue is provisioned as one OCI queue in the customer's compartment, with its name, default visibility, retention and redrive threshold. OCI Queue stores receipt tokens, receive counts and visibility deadlines. Architecture: the app's SQS SDK calls a Tensor9 adapter, which serves the SQS API from OCI Queue using workload identity. Each declared SQS queue is provisioned as one OCI queue in the customer's compartment, with its name, default visibility, retention and redrive threshold. OCI Queue stores receipt tokens, receive counts and visibility deadlines.

Each SQS queue maps to an OCI queue in the customer's compartment. OCI Queue stores receipt tokens, receive counts and visibility deadlines.

#### Receiving messages A `ReceiveMessage` is a **bounded long-poll**. `WaitTimeSeconds` (capped at 20, the SQS maximum) maps to OCI Queue's **get long-poll timeout**, which itself allows up to 30 seconds, so the full SQS wait range passes through with **no clamp**. Each get pulls up to 10 messages : SQS caps a receive at 10, and although OCI Queue itself would return up to 20 the adapter honors the SQS contract. The per-receive **visibility timeout passes straight through** to OCI Queue's per-receive visibility, and OCI Queue applies it when it hands the message out. The OCI receipt becomes the SQS receipt handle. `ApproximateReceiveCount` uses OCI Queue's delivery count, starting at 1 and increasing on redelivery. The adapter reconstructs the body and attributes and recomputes MD5 checksums for SDK verification. Delivery is at-least-once with best-effort ordering. Single sends use OCI put; batch sends use a multi-message put with a result for each entry. Deletion uses the receipt. Use OCI monitoring for measured latency and throughput.
A receive is a bounded long-poll: WaitTimeSeconds is capped at 20 and maps to OCI Queue's get long-poll timeout, which itself allows up to 30 seconds, so the full SQS range passes with no clamp. Each get pulls up to ten messages; OCI Queue returns each with a native receipt and a delivery count, which map directly to the SQS receipt handle and ApproximateReceiveCount, with delivery at-least-once. A receive is a bounded long-poll: WaitTimeSeconds is capped at 20 and maps to OCI Queue's get long-poll timeout, which itself allows up to 30 seconds, so the full SQS range passes with no clamp. Each get pulls up to ten messages; OCI Queue returns each with a native receipt and a delivery count, which map directly to the SQS receipt handle and ApproximateReceiveCount, with delivery at-least-once.

A receive is a bounded long-poll that returns OCI Queue's own receipt and delivery count; there is no lock to renew. ApproximateReceiveCount is the real delivery count, counted from 1.

#### Receipt handles & visibility The receipt handle contains OCI Queue's receipt token. Delete and visibility updates use this token across calls, connections and adapter restarts. A receipt superseded by another receive, or belonging to an already deleted message, produces a successful no-op. Malformed handles and handles for another queue return `ReceiptHandleIsInvalid`. The **visibility timeout maps to OCI Queue's per-message visibility**. The queue's default is provisioned on the OCI queue, and OCI Queue's 30-second default equals the SQS default , so an unset queue matches SQS exactly. The per-receive `VisibilityTimeout` rides the get, and `ChangeMessageVisibility` maps to **update**, which re-stamps the message's visibility; a zero-second change surfaces the message for **immediate re-claim**, and SQS's 12-hour maximum sits at OCI Queue's own **12-hour ceiling**, so the full SQS range passes through with **no clamp**. Because deletion and visibility changes use the native `receipt`, a consumer that lost its claim to a redelivery finds its `receipt` superseded, and the update no-ops rather than corrupting another consumer's claim.
A receipt handle is OCI Queue's own receipt, with no broker lock or in-memory registry behind it, so it resolves the same after the adapter restarts. Visibility is per-message: OCI Queue's 30-second default equals the SQS default and SQS's 12-hour maximum sits at OCI Queue's 12-hour ceiling, so ChangeMessageVisibility updates visibility with no clamp. A receipt handle is OCI Queue's own receipt, with no broker lock or in-memory registry behind it, so it resolves the same after the adapter restarts. Visibility is per-message: OCI Queue's 30-second default equals the SQS default and SQS's 12-hour maximum sits at OCI Queue's 12-hour ceiling, so ChangeMessageVisibility updates visibility with no clamp.

OCI Queue stores delivery state and accepts a current receipt across adapter restarts. The 30-second visibility default and 12-hour maximum match SQS.

#### Dead-letter & retention `maxReceiveCount` sets the queue's delivery-count threshold. When the count exceeds it, OCI Queue moves the message to its built-in dead-letter queue. The adapter does not copy or delete messages to perform that move. The threshold is preserved, but the destination is always the built-in dead-letter queue; an arbitrary SQS queue name or ARN cannot select a different destination. During Terraform provisioning, Tensor9 explicitly sets the four-day retention default (`345600` seconds) when no value is declared. A declared value above OCI's seven-day maximum (`604800` seconds) is capped at seven days with a warning. A queue declared with SQS's 14-day retention therefore expires messages a week earlier. This is a provisioning conversion; it does not describe a runtime `SetQueueAttributes` request.
Dead-letter and retention. Left: the SQS redrive maxReceiveCount maps to the queue's delivery-count threshold, and once a message's delivery count crosses it OCI Queue moves the message to its own built-in dead-letter queue natively, the adapter moves nothing by hand. Right: SQS message retention maps to OCI Queue's retention, the SQS four-day default 345600 is emitted explicitly, and OCI Queue's seven-day maximum 604800 caps a longer setting, so a message set to SQS's 14-day retention ages out up to a week earlier. Dead-letter and retention. Left: the SQS redrive maxReceiveCount maps to the queue's delivery-count threshold, and once a message's delivery count crosses it OCI Queue moves the message to its own built-in dead-letter queue natively, the adapter moves nothing by hand. Right: SQS message retention maps to OCI Queue's retention, the SQS four-day default 345600 is emitted explicitly, and OCI Queue's seven-day maximum 604800 caps a longer setting, so a message set to SQS's 14-day retention ages out up to a week earlier.

OCI Queue moves messages to its built-in dead-letter queue when their delivery count exceeds maxReceiveCount. Terraform provisioning caps retention at 7 days with a warning.

#### Limitations △ Where SQS and OCI Queue diverge, read before you adopt * **Classic queues only; FIFO is rejected with an error.** OCI Queue has no ordering, session, or deduplication analog, so a `.fifo` origin is routed to an ordering-capable backend at compile time, and any `.fifo` traffic that still reaches this backend (a pre-declared queue or an out-of-band URL) is rejected with an error on send, receive, create, and purge. There is no message-group ordering and no deduplication here at all. * **128 KB maximum message, smaller than SQS's 1 MiB.** OCI Queue caps a message at `131072` bytes, against SQS's 1 MiB, so an oversize send is rejected with `InvalidParameterValue` (naming the size and the limit), never silently truncated. If your messages routinely exceed 128 KB, shrink the payload, or use the claim-check pattern (store the body in OCI Object Storage and send a pointer). * **Retention tops out at 7 days, shorter than SQS's 14.** OCI Queue's retention is set from the SQS retention with the 4-day default (`345600`) written out explicitly, and OCI Queue's 7-day maximum (`604800`) caps a longer setting; a message held to SQS's 14-day retention ages out up to a week earlier, while within 7 days the mapping is exact. See the dead-letter and retention section. * **Dead-letter is delivery-count based.** The SQS redrive `maxReceiveCount` maps directly to OCI Queue's delivery-count threshold and OCI Queue moves the message to its own built-in dead-letter queue natively; the count travels exactly, but the target is OCI Queue's built-in per-queue dead-letter rather than an arbitrary named queue or ARN. * **Queue-level delay is not preserved.** During Terraform provisioning, a positive `delay_seconds` is dropped with a warning because OCI Queue has no corresponding setting. Review delayed-delivery requirements before choosing this target. * **Batch operations use OCI Queue batches.** `SendMessageBatch` and `DeleteMessageBatch` use multi-message put and delete operations with results for each entry. `ApproximateNumberOfMessages` and `ApproximateNumberOfMessagesNotVisible` use OCI Queue's visible and in-flight statistics. * **Control-plane and provisioning.** Each declared SQS queue is provisioned as one OCI queue in the customer's compartment at apply-time (the queue create is itself keyless); `ListQueues` answers with the declared set, and control operations that would reconfigure a queue out from under the declared model return a real error rather than doing the wrong thing. * **Single managed queue service; durability is Oracle's.** The data-plane path reaches OCI Queue keylessly via OCI workload identity; Oracle operates OCI Queue's durability, backup, replication, and HA, and Tensor9 operates the adapter. #### Other considerations * **In-flight messages don't migrate.** The OCI queue is provisioned empty at apply-time, so anything still sitting in SQS stays on AWS. Drain the SQS queue at the cutover point, or dual-write to both until the SQS side is empty. * **Check the retention and size limits before cutover.** Queue names, visibility and redrive thresholds map to OCI settings. Provisioning caps retention above 7 days with a warning, and messages above 128 KB are rejected; review queues using SQS's 14-day retention or messages above 128 KB. * **Estimate cost from OCI requests and message volume.** Include the workload's send, receive and delete rates when estimating target costs. [Service Catalog](/service-adapters/catalog). # API Gateway (REST) Source: https://docs.tensor9.com/service-adapters/aws/networking-traffic/api-gateway-rest Publishes REST APIs in front of Lambda, HTTP or AWS service backends, with stages, request and response mapping templates, API keys and usage plans. Preview ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | - | | Azure | - | | OCI | - | | Private Kubernetes | - | ## Mapping and limitations API Gateway (REST) → the same API Gateway API, served by a Tensor9 adapter on the target cloud rather than re-modelled onto a native gateway. A deploy skips the REST API rather than faking it; the rest of the stack deploys normally. [Service Catalog](/service-adapters/catalog). # API Gateway v2 (HTTP/WebSocket) Source: https://docs.tensor9.com/service-adapters/aws/networking-traffic/api-gateway-v2-http-websocket AWS API Gateway v2 (HTTP/WebSocket). Serves lower cost HTTP APIs with a smaller feature set than REST, plus WebSocket APIs that hold connections open for two way messages. Preview ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | - | | Azure | - | | OCI | - | | Private Kubernetes | - | ## Mapping and limitations API Gateway v2 (HTTP and WebSocket) → the same API Gateway API, served by a Tensor9 adapter on the target cloud rather than re-modelled onto a native gateway. A deploy skips the HTTP or WebSocket API rather than faking it; the rest of the stack deploys normally. [Service Catalog](/service-adapters/catalog). # Application Load Balancer Source: https://docs.tensor9.com/service-adapters/aws/networking-traffic/application-load-balancer AWS Application Load Balancer. Distributes HTTP and HTTPS requests across target groups at layer 7, choosing targets by host, path, header or query string. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | - | ## How the targets compare Each row compares a capability of Application Load Balancer with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | Application Load Balancer | Google Cloud | Azure | OCI | | ----------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------- | --------------------------- | ---------------------------------------------- | | Traffic path | AWS load balancer | Google Cloud Load Balancing | Azure Application Gateway | OCI Flexible Load Balancer | | Request routing | ALB listener conditions | Path, host, header, query, method; weights | Host and path | Path, host, header, query | | TLS termination | ALB certificate | Google-managed certificate | Azure Key Vault certificate | OCI certificate and listener TLS configuration | | Health checks | Per target group | Per backend service | Per backend setting | Per backend set | | Session stickiness | Configured cookie lifetime | Cookie duration up to one day | Browser-session affinity | Cookie duration preserved | | Client endpoint | AWS hostname | Target cloud address | Target cloud address | Target cloud address | | Login at the load balancer · OIDC / OAuth termination | Yes - authenticate-oidc / authenticate-cognito | Partial - Identity-Aware Proxy on the backend service | - | - | | API coverage | full | partial | partial | partial | ### Infrastructure-only adaptation | Capability | Application Load Balancer | OCI | | ------------------ | -------------------------- | ---------------------------------------------- | | Traffic path | AWS load balancer | OCI Flexible Load Balancer | | Request routing | ALB listener conditions | Path, host, header, query | | TLS termination | ALB certificate | OCI certificate and listener TLS configuration | | Health checks | Per target group | Per backend set | | Session stickiness | Configured cookie lifetime | Cookie duration preserved | | Client endpoint | AWS hostname | Target cloud address | | API coverage | full | partial | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | -------------------------------------------- | -------------- | --------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Authentication (OIDC / OAuth) | Access control | Partial | Most usage | Listener authentication maps to Identity-Aware Proxy on the backend service. External identity providers and Cognito need the Identity Platform arrangement. | | Health checks | Backends | Supported | Common | Checks belong to each backend service. Google HTTP(S) checks require status 200; AWS matchers accepting other codes need a compatible health endpoint. | | Session stickiness | Backends | Supported | Most usage | Generated-cookie affinity is supported, with a maximum duration of one day. | | Load balancer address | Consumers | Supported | Common | Clients use the Google address. Supported references in an infrastructure translation are updated during the build. | | Traffic forwarding | Data plane | Supported | Common | Google Cloud Load Balancing receives client connections and forwards requests to the backends. | | HTTPS / TLS termination | Listeners | Supported | Common | Google-managed certificates provide HTTPS, including multiple domains. | | Listeners & target groups | Listeners | Supported | Common | Listeners map to forwarding rules and proxies; target groups map to backend services selected by URL maps. | | Routing rules (path / host / header / query) | Routing | Partial | Most usage | URL maps handle path, host, header, query, and method conditions. Source-IP conditions and fixed responses are outside this mapping. | | Weighted target groups | Routing | Supported | Most usage | Weighted target groups become weighted backend services. | #### Managing the load balancer Your application and Terraform use the AWS Elastic Load Balancing API through the adapter. The adapter keeps AWS-shaped load balancer, listener, and target-group identities, records configuration changes, and applies them to Google Cloud Load Balancing. A target group is the set of backends that a listener forwards traffic to. Management changes and traffic take different paths. The adapter handles calls such as `CreateLoadBalancer`, `CreateListener`, and `RegisterTargets`. The cloud load balancer receives client connections and forwards them to your application. A successful management request can precede completion of the cloud change. Wait for the load balancer to become available and check the native resources before sending production traffic. The adapter retains the requested configuration while background work applies it.
AWS management calls go through the adapter to cloud configuration. Client traffic goes through the cloud load balancer to application backends. AWS management calls go through the adapter to cloud configuration. Client traffic goes through the cloud load balancer to application backends.
#### Google resources A forwarding rule receives traffic at the frontend address. A target proxy handles HTTP or HTTPS, a URL map chooses the backend, and a backend service represents each target group. Health checks belong to the backend services. HTTPS uses a Google-managed certificate for your DNS names, including multi-domain certificates and the target's mutual-TLS arrangement; an ACM ARN does not contain certificate material that can be copied. #### Routing and session affinity Path, host, header, query, and method conditions map to URL-map rules. Weighted target groups become weighted backend services. Rule priority determines match order. Source-IP conditions and fixed-response actions are outside this mapping. Lambda target groups are outside this mapping. A function deployed behind a separate HTTP or serverless backend does not become an AWS Lambda target registration. Cookie stickiness becomes generated-cookie affinity, with a maximum duration of one day. Review longer AWS cookie lifetimes before moving. #### Frontend security The Max adapter can translate attached security-group IPv4 client ranges into Cloud Armor policies. This requires your explicit choice to use the paid Cloud Armor service. Each listener has its own policy; a request outside the allowed ranges receives HTTP 403, whereas an AWS security group drops the connection. This enforcement path supports IPv4 HTTP listeners and at most 10 client CIDR ranges per listener. Redirect actions are refused because they can bypass backend policy evaluation. The policy is attached and observed before the frontend is made available. You can explicitly choose to omit frontend security-group enforcement, but must then supply and verify the intended access control separately. #### Health checks and authentication Health checks use the target group's path, port, interval, timeout, and thresholds. Google HTTP(S) health checks require status 200. An AWS matcher that accepts other codes or a range of codes needs a health endpoint that returns 200 when healthy; response-body matching does not replace that status requirement. Listener authentication maps to Identity-Aware Proxy on the backend service. Google identity is the direct case; an external identity provider or Cognito user pool needs the Identity Platform arrangement described by that authentication mapping. Check the login flow as well as the load balancer's health. #### Target registration and readiness `RegisterTargets` and `DeregisterTargets` update target-group membership. Instance registrations resolve through the adapter's EC2 inventory; IP registrations identify the backend address and port. The target group, load balancer, and backend must belong to a compatible network. The adapter applies a listener after its target group and network dependencies are ready. Native health checks then determine which backends receive traffic. Verify the provider's health status and test an application request: a registered target alone does not establish that its application is ready. `DescribeTargetHealth` reports registration and zone eligibility from the adapter's saved state. Its `healthy` result does not confirm that the native load balancer's probe succeeded. Check native backend health and a complete application request before sending production traffic. Drain connections before deregistering backends or deleting a load balancer. Removing configuration does not transfer active connections to a replacement. #### Deployment and cutover Deploy the adapter with permission to manage load balancers and their network dependencies in the customer's cloud account. AWS-facing credentials authorize management calls; the adapter uses the target cloud's credentials to apply changes. Configure public or private exposure, frontend access rules, backend access, and health-check access together. Prepare DNS and firewall allowlists for the new address. In an infrastructure-only deployment, references inside the translated stack are updated during the build; external DNS and clients still need a cutover. At Max, the adapter also maintains the AWS resource identities used by management calls. Check certificates, routing, native health checks, and a complete client request before changing DNS. Keep the old load balancer available while existing connections drain. Application sessions and active connections are not copied by this adapter. ## On Azure | Operation | Area | Support | Depth | Notes | | ----------------------------- | -------------- | ------------ | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | Authentication (OIDC / OAuth) | Access control | Out of scope | Most usage | Listener-based OIDC or OAuth login is outside this option. Handle authentication in the application or an identity-aware proxy. | | Health checks | Backends | Supported | Common | Health probes belong to backend settings. Azure also offers response-body matching as a target capability. | | Session stickiness | Backends | Supported | Most usage | Cookie affinity lasts for the browser session; Azure has no configured cookie-duration setting. | | gRPC backends | Backends | Out of scope | Most usage | This option does not provide gRPC to backends. | | Load balancer address | Consumers | Supported | Common | Clients use the Azure address. Supported references in an infrastructure translation are updated during the build. | | Traffic forwarding | Data plane | Supported | Common | Azure Application Gateway v2 receives HTTP or HTTPS traffic and forwards it to the backends. | | HTTPS / TLS termination | Listeners | Supported | Common | The gateway uses Azure Key Vault certificates, including multi-site setups. | | Listeners & target groups | Listeners | Supported | Common | Application Gateway listeners and backend pools represent AWS listeners and target groups. | | Routing rules (path / host) | Routing | Partial | Most usage | Host and path conditions map to multi-site listeners and path rules. Header, method, query, and source-IP conditions are outside this mapping. | | Weighted target groups | Routing | Out of scope | Most usage | Weighted target-group forwarding is outside the Application Gateway mapping. | #### Managing the load balancer Your application and Terraform use the AWS Elastic Load Balancing API through the adapter. The adapter keeps AWS-shaped load balancer, listener, and target-group identities, records configuration changes, and applies them to Azure Application Gateway. A target group is the set of backends that a listener forwards traffic to. Management changes and traffic take different paths. The adapter handles calls such as `CreateLoadBalancer`, `CreateListener`, and `RegisterTargets`. The cloud load balancer receives client connections and forwards them to your application. A successful management request can precede completion of the cloud change. Wait for the load balancer to become available and check the native resources before sending production traffic. The adapter retains the requested configuration while background work applies it.
AWS management calls go through the adapter to cloud configuration. Client traffic goes through the cloud load balancer to application backends. AWS management calls go through the adapter to cloud configuration. Client traffic goes through the cloud load balancer to application backends.
#### Application Gateway resources An ALB maps to Application Gateway v2: listeners accept HTTP or HTTPS, backend pools represent target groups, and routing rules select a pool. The gateway needs a dedicated subnet. An ALB does not require a separate Azure Standard Load Balancer. HTTPS uses a certificate from Azure Key Vault, including multi-site setups, Server Name Indication (SNI), and the target's mutual-TLS arrangement. An ACM ARN is a reference, not the certificate and private key; supply the certificate through the target arrangement. #### Routing and cookies Host conditions map to multi-site listeners, and path conditions map to path rules. Header, method, query, source-IP conditions, weighted target groups, and fixed responses are outside this mapping. gRPC to backends is also outside this option. Cookie affinity keeps a session on a backend, but Application Gateway has no configured cookie lifetime: affinity lasts for the browser session. Listener-based OIDC or OAuth login is not mapped; handle login in your application or an identity-aware proxy. #### Frontend security The Max adapter enforces supported frontend security-group rules through a network security group (NSG) on the gateway's dedicated subnet. Rules allow the selected IPv4 client ranges on the listener ports. The subnet also needs Azure's GatewayManager control ports 65200-65535 and AzureLoadBalancer health traffic. Gateway requirements include unrestricted outbound access from that subnet. A dedicated subnet prevents those gateway rules from changing another workload's access. Unsupported selectors are rejected; the frontend waits for its network security policy before becoming available. #### Backend health Each backend setting has a health probe. The mapping preserves the path, expected status codes, interval, timeout, unhealthy threshold, and explicit port. Application Gateway also supports response-body matching, which is a target capability rather than an AWS setting that must be recreated. #### Target registration and readiness `RegisterTargets` and `DeregisterTargets` update target-group membership. Instance registrations resolve through the adapter's EC2 inventory; IP registrations identify the backend address and port. The target group, load balancer, and backend must belong to a compatible network. The adapter applies a listener after its target group and network dependencies are ready. Native health checks then determine which backends receive traffic. Verify the provider's health status and test an application request: a registered target alone does not establish that its application is ready. `DescribeTargetHealth` reports registration and zone eligibility from the adapter's saved state. Its `healthy` result does not confirm that the native load balancer's probe succeeded. Check native backend health and a complete application request before sending production traffic. Drain connections before deregistering backends or deleting a load balancer. Removing configuration does not transfer active connections to a replacement. #### Deployment and cutover Deploy the adapter with permission to manage load balancers and their network dependencies in the customer's cloud account. AWS-facing credentials authorize management calls; the adapter uses the target cloud's credentials to apply changes. Configure public or private exposure, frontend access rules, backend access, and health-check access together. Prepare DNS and firewall allowlists for the new address. In an infrastructure-only deployment, references inside the translated stack are updated during the build; external DNS and clients still need a cutover. At Max, the adapter also maintains the AWS resource identities used by management calls. Check certificates, routing, native health checks, and a complete client request before changing DNS. Keep the old load balancer available while existing connections drain. Application sessions and active connections are not copied by this adapter. ## On OCI | Operation | Area | Support | Depth | Notes | | ------------------------------------------------ | -------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Authentication (OIDC / OAuth) | Access control | Out of scope | Most usage | Listener authentication is outside this option. Handle login in the application or an identity-aware proxy. | | WAF attachment | Access control | Out of scope | Most usage | WAF attachment is outside this option. | | Health checks | Backends | Supported | Common | The backend-set check uses the path, expected status, timeout, interval, port, and consecutive-failure count. | | Session stickiness | Backends | Supported | Most usage | Cookie persistence on the backend set includes the configured duration. | | Target registration | Backends | Partial | Most usage | Register backends through the adapter; existing AWS target registrations are not copied. | | Access logs / idle timeout / deletion protection | Configuration | Out of scope | Full surface | These AWS settings are outside the mapping and are reported for review. | | Bandwidth | Configuration | Partial | Full surface | The flexible load balancer starts with a 10-100 Mbps bandwidth range, adjustable for the deployment. | | Network placement | Configuration | Partial | Full surface | The target network replaces the AWS multi-subnet layout. Verify public or private exposure and backend connectivity. | | Load balancer address | Consumers | Partial | Common | Clients use the target OCI load-balancer address. The original AWS-managed hostname is not retained; infrastructure references to AWS-specific identifiers without a target equivalent are rejected or removed from outputs with a reported issue. | | Traffic forwarding | Data plane | Supported | Common | Oracle operates the native load balancer that receives client requests. | | HTTPS / TLS termination | Listeners | Partial | Common | HTTPS requires an OCI listener certificate and its TLS configuration. Supply or provision that certificate; an ACM reference alone does not provide it. The mapping does not reproduce the AWS SNI certificate list. | | Listeners & target groups | Listeners | Supported | Common | Each listener selects an OCI backend set representing its target group. An unresolved default target group is rejected. | | Routing rules (path / host / header / query) | Routing | Partial | Most usage | Path, host, header, and query conditions map to OCI routing policies. Method and source-IP conditions, weighted target groups, and fixed responses are outside this mapping. | #### Managing the load balancer Your application and Terraform use the AWS Elastic Load Balancing API through the adapter. The adapter keeps AWS-shaped load balancer, listener, and target-group identities, records configuration changes, and applies them to OCI Flexible Load Balancer. A target group is the set of backends that a listener forwards traffic to. Management changes and traffic take different paths. The adapter handles calls such as `CreateLoadBalancer`, `CreateListener`, and `RegisterTargets`. The cloud load balancer receives client connections and forwards them to your application. A successful management request can precede completion of the cloud change. Wait for the load balancer to become available and check the native resources before sending production traffic. The adapter retains the requested configuration while background work applies it.
AWS management calls go through the adapter to cloud configuration. Client traffic goes through the cloud load balancer to application backends. AWS management calls go through the adapter to cloud configuration. Client traffic goes through the cloud load balancer to application backends.
#### Listeners and backend sets One OCI listener represents each ALB listener, and one backend set represents each target group. The listener's default action selects its backend set. The native load balancer uses a bandwidth range, initially 10-100 Mbps; adjust it for the deployment. The AWS multi-subnet layout becomes the target deployment's network arrangement. Confirm whether the load balancer is public or private and whether its subnet and security rules admit the intended clients and backends. #### Request routing Path, host, header, and query conditions map to a routing policy per listener, in rule-priority order. Method conditions, source-IP conditions, weighted target groups, and fixed-response actions are outside this mapping. OCI backend-set server weights do not reproduce a listener rule that splits traffic between target groups. #### HTTPS and certificates The HTTPS mapping uses TLS on the OCI listener, with a certificate supplied through OCI Certificates or the load balancer's certificate configuration. An ACM ARN identifies an AWS certificate; it does not supply the certificate and private key needed by OCI. Provision or import the target certificate and attach it before enabling the HTTPS frontend. The mapped arrangement uses one certificate per listener and does not reproduce an AWS listener's SNI certificate list. Backend TLS is a separate setting; enabling it does not secure a frontend that is still configured for HTTP. Verify the client-facing handshake and the backend connection before cutover. OIDC and OAuth authenticate actions are outside this option; login belongs in the application or an identity-aware proxy. #### Health checks, cookies, and operations Backend sets specify the health-check path, response codes, interval, timeout, port, and consecutive-failure count. The default path is /. The existing mapping turns HTTP and HTTPS probes into HTTP probes; other checks use TCP. Cookie persistence includes the configured duration. AWS access-log settings, idle timeout, deletion protection, and WAF attachment are outside this profile. Prepare the target logging and operational settings explicitly. Client DNS must resolve to the OCI address; an AWS hosted-zone ID is not an OCI resource identifier. #### Target registration and readiness `RegisterTargets` and `DeregisterTargets` update target-group membership. Instance registrations resolve through the adapter's EC2 inventory; IP registrations identify the backend address and port. The target group, load balancer, and backend must belong to a compatible network. The adapter applies a listener after its target group and network dependencies are ready. Native health checks then determine which backends receive traffic. Verify the provider's health status and test an application request: a registered target alone does not establish that its application is ready. `DescribeTargetHealth` reports registration and zone eligibility from the adapter's saved state. Its `healthy` result does not confirm that the native load balancer's probe succeeded. Check native backend health and a complete application request before sending production traffic. Drain connections before deregistering backends or deleting a load balancer. Removing configuration does not transfer active connections to a replacement. #### Deployment and cutover Deploy the adapter with permission to manage load balancers and their network dependencies in the customer's cloud account. AWS-facing credentials authorize management calls; the adapter uses the target cloud's credentials to apply changes. Configure public or private exposure, frontend access rules, backend access, and health-check access together. Prepare DNS and firewall allowlists for the new address. In an infrastructure-only deployment, references inside the translated stack are updated during the build; external DNS and clients still need a cutover. At Max, the adapter also maintains the AWS resource identities used by management calls. Check certificates, routing, native health checks, and a complete client request before changing DNS. Keep the old load balancer available while existing connections drain. Application sessions and active connections are not copied by this adapter. [Service Catalog](/service-adapters/catalog). # CloudFront Source: https://docs.tensor9.com/service-adapters/aws/networking-traffic/cloudfront AWS CloudFront. Caches origin responses at edge locations worldwide, terminating TLS and routing URL paths to different origins through cache behaviors. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [Via Azure Front Door Premium](#via-azure-front-door-premium) * [Via Azure Front Door Standard](#via-azure-front-door-standard) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | - | | Private Kubernetes | - | ## How the targets compare Each row compares a capability of CloudFront with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | CloudFront | Google Cloud | Azure · Azure Front Door Premium | Azure · Azure Front Door Standard | | ------------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | Edge network · who serves the traffic | CloudFront's global PoPs | Google Cloud CDN on the external load balancer | Azure Front Door's global edge | Azure Front Door Standard's global edge | | Origins · origin domains → backend | Yes - multiple origins + origin groups with failover | Partial - first HTTP backend service or bucket backend; additional origins and groups are reported | Partial - origin-group members selected by health probes | Partial - origin-group members selected by health probes; public endpoints required | | Cache behaviors · paths, TTLs, cache key | Yes - path patterns, per-behavior TTLs, cache key, methods | Partial - backend cache policy with default/max/client TTLs, negative caching and cache keys; header/method differences remain | Partial - path rules with one cache duration and query-string behavior; separate min/max TTL and header/method differences remain | Partial - path rules with one cache duration and query-string behavior; separate min/max TTL and header/method differences remain | | Compression · automatic gzip/brotli | Yes - automatic edge compression | Yes - the backend's compression mode | Yes - the route's compression setting | Yes - the route's compression setting | | HTTPS redirect · viewer protocol policy | Yes - redirect-to-https / https-only | Yes - a url\_map HTTPS redirect rule | Yes - the route's HTTPS-redirect setting | Yes - the route's HTTPS-redirect setting | | Custom domains + TLS · aliases + certificate | Yes - alternate domain names + ACM viewer certificate | Partial - target-managed certificate after domain validation; ACM material and TLS settings require review | Partial - target-managed certificate after domain validation; ACM material and TLS settings require review | Partial - target-managed certificate after domain validation; ACM material and TLS settings require review | | WAF / geo restrictions · web ACL + geo | Yes - AWS WAF web ACL + geo restrictions | No - Cloud Armor configured separately; no automatic AWS web ACL or geo translation | Partial - custom match, rate-limit and country rules; Microsoft-managed rule selection differs from AWS groups | Partial - custom match, rate-limit and country rules; managed rules and bot protection require Premium | | Origin access & signed URLs · origin and viewer authorization | Yes - OAI / OAC + trusted-signer signed URLs | Partial - Cloud CDN signed URLs require a new HMAC signer; private bucket access or HTTP-origin controls are configured separately | Partial - approved Private Link access to supported origins; viewer signed-URL verification requires a separate application design | Partial - public origin with separate access controls; no Private Link or CloudFront viewer-signature verification | | Edge functions · Lambda\@Edge / Functions | Yes - Lambda\@Edge + CloudFront Functions | No - no function runtime; unsupported associations reported and standalone functions rejected | No - rule conditions and actions; no arbitrary function runtime | Partial - header, URL and redirect rules; no arbitrary function runtime | | API coverage | full | high | high | high | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | -------------------------------------------------------------------------------------------------------- | ---------------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Origin access (OAI / OAC) | Access control | Partial | Most usage | configure private bucket IAM or the HTTP origin's access controls separately. Attaching a backend to the load balancer does not prevent direct access. An origin access identity without a supported translation causes a build error | | Signed URLs | Access control | Partial | Most usage | Cloud CDN uses HMAC signed URLs and cookies. Replace the CloudFront RSA/ECDSA signer and issue new URLs; configure origin access separately. | | WAF & geo restrictions | Access control | Out of scope | Most usage | Automatic WAF/geo translation is outside this mapping. Configure Cloud Armor separately; backend buckets support a narrower edge security policy than backend services. | | Cache behaviors | Cache | Partial | Most usage | Backend cdn\_policy retains cache mode, default/max/client TTLs, negative caching and cache-key settings. URL-map matches select paths; per-behavior header forwarding and method differences are reported. | | Content delivery | Content delivery | Supported | Common | served on Google Cloud CDN: enable\_cdn = true plus a cdn\_policy on the external load balancer's backend inside your Google Cloud project; the load balancer fronts it with a URL map, HTTPS proxy, and forwarding rule | | DNS cutover | Content delivery | Supported | Common | Supported Route 53 aliases point to the target frontend. CloudFront domain\_name, arn and hosted\_zone\_id are not target identifiers; unsupported output references are removed and active configuration references cause a build error. | | Custom domains + TLS | Domains & TLS | Partial | Most usage | Custom domains use target-managed certificates. Complete domain validation before cutover; an ACM ARN supplies no certificate material. Minimum TLS-version and SSL support-method differences are reported. | | Edge logic (Lambda\@Edge / CloudFront Functions) | Edge logic | Out of scope | Most usage | Cloud CDN does not run Lambda\@Edge or CloudFront Functions code. Unsupported associations are omitted and reported; standalone functions cause a build error. | | Policy & monitoring facets (cache / origin-request / response-headers policies, monitoring subscription) | Facets | Out of scope | Full surface | these resources are not supported by this mapping; the build reports an error | | Origins | Origins | Partial | Common | The first HTTP origin becomes a backend service; a bucket origin becomes a backend bucket. Confirm unresolved origin addresses. Additional origins and origin groups are omitted and reported. | #### How it works Tensor9 translates the CloudFront distribution into a Google external Application Load Balancer with Cloud CDN enabled on its backend. A forwarding rule supplies the public address, an HTTPS proxy terminates TLS, and a URL map selects the backend. Google operates the cache and serves requests; Tensor9 provisions the resources in the customer's project. A custom DNS name can remain in use after its record points to the new frontend and its certificate is ready. The CloudFront-generated hostname and existing cached objects do not move to Google. Plan for cache misses during cutover.
Requests pass through the Google load balancer and cache to the selected origin. Requests pass through the Google load balancer and cache to the selected origin.
#### Origins and cache configuration A bucket origin uses a CDN-enabled backend bucket; a custom HTTP origin uses a backend service. Confirm any origin address that compilation cannot resolve. This mapping selects the first origin; additional origins and CloudFront origin-group failover require separate configuration and are reported during compilation. Supported path behaviors become URL-map matchers. Each backend has a `cdn_policy` for cache mode, default, maximum and client TTLs, negative caching, and the cache key's host, path and query settings. Behaviors sharing a backend also share its cache policy. Check whether the source needs different policies for those paths. The origin-headers cache mode respects the origin's `Cache-Control` instructions. For example, a policy might set default and client TTLs to 3600 seconds, a maximum of 86400 seconds, and cache a 404 response for 120 seconds; these are illustrative settings, not promised deployment defaults. Per-behavior forwarded-header lists and allowed-method lists are not fully preserved. Review the reported differences before caching authenticated or user-specific responses. #### Origin connectivity and access The load balancer's backend configuration identifies where requests go; being in the same project does not make an origin reachable or private. Configure the network path, firewall rules, DNS, and backend access for the selected origin type. A custom origin can be outside the project and may need additional connectivity. CloudFront origin access control authenticates requests to supported origins; it is distinct from a private network connection. On Google, private bucket access uses the appropriate bucket IAM grant. An HTTP origin needs its own controls to reject direct or unauthorized requests. Attaching a backend to a load balancer does not, by itself, make that load balancer the only way to reach it. Provisioning uses the deployment's Google identity. Limit that identity's resource permissions separately from the access rules governing viewer and origin requests. #### Signed URLs and cookies CloudFront signed URLs and cookies use a public/private key pair: RSA or ECDSA. Cloud CDN uses a shared HMAC signing key. Update the application that issues signed URLs to use the Cloud CDN format and key; previously issued CloudFront signatures will not validate there. Viewer authorization and origin protection are separate. Configure private bucket access where applicable. For an HTTP origin, follow Google's requirements to validate signed requests and decide whether to reject unsigned requests. Adding a signing key alone does not make every path private.
The application signs a Cloud CDN request; the edge validates it, while origin access requires separate controls. The application signs a Cloud CDN request; the edge validates it, while origin access requires separate controls.
#### Web application firewall This distribution mapping does not automatically translate the AWS web ACL or geographic restrictions. Configure Google Cloud Armor separately if those controls are required. A backend security policy supports managed WAF rules, custom rules and rate controls on a backend service. A backend bucket supports the narrower edge security policy, which does not provide the same managed-rule and rate-limiting features. Choose the policy type for the selected backend and test both allowed and blocked requests. #### Edge functions and encryption Cloud CDN does not execute Lambda\@Edge or CloudFront Functions code. Move application code to a suitable runtime and reconnect the request flow when that behavior is required. The translation reports omitted function associations; a standalone CloudFront function without a supported translation causes a build error. CloudFront field-level encryption encrypts selected request fields at the edge. It is a separate feature, not a customer function runtime, and this mapping does not reproduce it. #### Custom domains, TLS and cutover Each supported alias receives a Google-managed certificate on the HTTPS proxy. Validate domain ownership and wait for certificate readiness before changing DNS. An ACM reference does not supply certificate material; the source minimum TLS version and SSL support method are not copied field for field. Verify the target TLS policy. Supported Route 53 aliases are updated to the Google frontend address. CloudFront `domain_name`, `arn`, and `hosted_zone_id` are not native Google identifiers. References without a target equivalent are removed from outputs or rejected in active configuration; review consumers outside the translated stack. #### Operations and remaining limits Google's edge locations replace CloudFront's price-class selection. Configure Cloud Logging and Cloud Monitoring for the target; AWS access-log and additional-metrics settings are not copied as distribution attributes. Monitor cache hits, origin requests and errors after cutover. Standalone cache, origin-request, response-headers policy, monitoring-subscription, or origin-access-identity resources that have no supported attachment cannot be translated. Review those resources and the per-behavior differences with the distribution before deployment. Provider references: [content access control](https://docs.cloud.google.com/cdn/docs/authenticate-content) and [signed URLs](https://docs.cloud.google.com/cdn/docs/using-signed-urls). ## On Azure ### Via Azure Front Door Premium | Operation | Area | Support | Depth | Notes | | -------------------------------------------------------------------------------------------------------- | ---------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Origin access (OAI / OAC) | Access control | Partial | Most usage | Premium can connect to a supported origin through an approved Private Link endpoint. Configure the compatible origin and close unwanted alternate access paths. A standalone origin access identity without a supported translation causes a build error | | Signed URLs & trusted keys | Access control | Out of scope | Most usage | CloudFront signed URLs and signed cookies use RSA or ECDSA signatures that Front Door cannot verify; it is a different request authorization scheme, out of this option's scope and flagged during the build | | WAF & geo restrictions | Access control | Partial | Most usage | Custom match, rate-limit and country rules become target WAF rules attached through a security policy. Premium supports Microsoft-managed rules and bot protection; AWS managed groups require target-specific rule selection. | | Cache behaviors | Cache | Partial | Most usage | Path-matched rules retain cache duration and query-string behavior. CloudFront default TTL supplies a single duration; separate minimum/maximum constraints and per-behavior header/method differences are reported. | | Content delivery | Content delivery | Supported | Common | served on Azure Front Door: Tensor9 provisions a Front Door profile and endpoint that front your origin, with an origin group and origins pointing at your backend; Microsoft operates the edge | | DNS cutover | Content delivery | Supported | Common | Supported Route 53 aliases point to the target frontend. CloudFront domain\_name, arn and hosted\_zone\_id are not target identifiers; unsupported output references are removed and active configuration references cause a build error. | | Custom domains + TLS | Domains & TLS | Partial | Most usage | Custom domains use target-managed certificates. Complete domain validation before cutover; an ACM ARN supplies no certificate material. Minimum TLS-version and SSL support-method differences are reported. | | Edge logic (Lambda\@Edge / CloudFront Functions) | Edge logic | Out of scope | Most usage | Rules engine conditions can express headers, URL changes and redirects, but cannot run arbitrary function code. Unsupported associations are omitted and reported; standalone functions cause a build error. | | Policy & monitoring facets (cache / origin-request / response-headers policies, monitoring subscription) | Facets | Out of scope | Full surface | these resources are not supported by this mapping; the build reports an error | | Origins | Origins | Partial | Common | Origin hosts become members of an origin group. Confirm unresolved addresses and review differences between source failover conditions and Front Door health-probe selection. | #### How it works Tensor9 translates the CloudFront distribution into an Azure Front Door Premium profile, endpoint, origin group, origins, route and rule set. Microsoft operates the edge cache and serves requests. The customer controls the origin and the target configuration. A custom DNS name can remain after its record and certificate are ready for Front Door. The CloudFront-generated hostname and cached objects do not transfer. Expect origin requests as the new cache fills.
Front Door routes a request through cache rules to an origin in the configured group. Front Door routes a request through cache rules to an origin in the configured group.
#### Origins and routing The endpoint supplies the public host. A route associates that endpoint with an origin group and the rules applied to requests. Each declared origin host becomes an origin; the group uses health probes to select available origins. This differs from CloudFront origin-group failover, so review failover conditions and origin selection. Confirm any origin address that compilation cannot resolve. An S3 origin follows the selected storage mapping to the target endpoint. Validate the origin host header, TLS and network access against that endpoint rather than retaining an AWS-specific address. #### Origin access Front Door Premium can reach supported origins through Private Link. For a Kubernetes service, provide the compatible Azure Private Link service and load balancer arrangement, then approve Front Door's managed private endpoint. An arbitrary private cluster is not reachable merely because the profile uses Premium. Traffic reaches the origin through a private connection over Microsoft's network. The origin still needs to accept that private traffic, and the platform team must close any unwanted public or alternate access paths. Private Link does not require a connector process inside the application cluster. CloudFront OAI and OAC govern authenticated access to supported origins; OAC is not a general private-network connection. The target requires the access controls appropriate to its origin type. Neither Standard nor Premium verifies existing CloudFront viewer signed-URL or signed-cookie signatures; that viewer-authorization scheme needs a separate application design.
Premium reaches a supported origin through an approved Private Link connection. Premium reaches a supported origin through an approved Private Link connection.
#### Web application firewall A Front Door firewall policy contains the target WAF rules; a security policy attaches it to the endpoint. Supported custom AWS match and rate-limit rules are expressed using Front Door conditions and actions, and geographic restrictions use country-match rules. An AWS `web_acl_id` is a reference, not an Azure policy. Premium supports Microsoft-managed WAF rule sets and bot protection as well as custom rules. AWS managed groups are not the same rule sets: select the corresponding target protections and test their effect. Complex AWS conditions without a matching Front Door expression remain a reported limitation. #### Cache behavior Default and ordered cache behaviors become rules matched by path. A route-configuration override sets the cache duration and query-string behavior; compression is configured on the route. Check the query parameters included in the cache key, especially for personalized responses. CloudFront's minimum, default and maximum TTLs do not map to three independent Front Door settings. The default TTL supplies the single cache duration; separate minimum and maximum constraints are reported during compilation. Forwarded-header lists and allowed-method lists also have differences. Verify the effective cache key and request handling rather than assuming that two source requests always remain distinct. #### Rules and application code The Rules engine can express header changes, URL or path rewrites and redirects. Those behaviors can be implemented with target rule conditions and actions on both Front Door tiers; arbitrary Lambda\@Edge or CloudFront Functions code does not run there. Function associations that cannot be translated are omitted and reported. A standalone CloudFront function without a supported translation causes a build error. Move required application logic to a suitable runtime before cutover. CloudFront field-level encryption also has no equivalent in this mapping. #### Domains and certificates Each alias becomes a Front Door custom domain with a managed certificate. Complete domain validation and wait for certificate readiness before updating DNS. Front Door issues and renews that certificate; it does not copy an ACM certificate from its ARN. Review minimum TLS version and SSL support-method differences. Supported Route 53 aliases are updated to the Front Door endpoint. CloudFront `domain_name`, `arn` and `hosted_zone_id` do not become Azure identifiers. References without a target equivalent are removed from outputs or rejected in active configuration. Update external DNS consumers and allowlists separately. #### Operations and remaining limits Azure's edge locations replace CloudFront price classes. Use Azure Monitor and diagnostic settings for access logs and metrics; an AWS monitoring subscription is not a Front Door setting. Review cache hit rates, origin health, latency and errors after cutover. Cache, origin-request and response-headers policy resources need a supported distribution attachment and translation. Standalone policies, monitoring subscriptions and origin-access identities without one cause a build error. Resolve those resources together with the behavior they configure. Provider reference: [securing Front Door origins](https://learn.microsoft.com/en-us/azure/frontdoor/origin-security). ### Via Azure Front Door Standard | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------------------------------------------- | ---------------- | ------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Origin access & signed URLs | Access control | Partial | Most usage | Standard reaches public origins. Restrict origin network access and validate configured origin headers; a secret header is not private networking. Private Link requires Premium. Existing CloudFront viewer signatures are not verified. | | Signed URLs | Access control | Out of scope | Most usage | CloudFront trusted-signer and trusted-key-group signed URLs are a different request authorization scheme (Front Door cannot verify CloudFront's RSA or ECDSA signatures), so signed URLs have no counterpart and are flagged during the build | | WAF / geo restrictions | Access control | Partial | Most usage | Custom match, rate-limit and country rules become target WAF rules attached through a security policy. Microsoft-managed rules and bot protection require Premium. Unsupported source conditions are reported. | | Cache behaviors | Cache | Partial | Most usage | Path-matched rules retain cache duration and query-string behavior. CloudFront default TTL supplies a single duration; separate minimum/maximum constraints and per-behavior header/method differences are reported. | | Content delivery | Content delivery | Supported | Common | served on Azure Front Door Standard: Tensor9 provisions a Front Door Standard profile and endpoint that front your origin, with an origin group and origins pointing at your backend; Microsoft operates the edge | | DNS cutover | Content delivery | Supported | Common | Supported Route 53 aliases point to the target frontend. CloudFront domain\_name, arn and hosted\_zone\_id are not target identifiers; unsupported output references are removed and active configuration references cause a build error. | | Custom domains + TLS | Domains & TLS | Partial | Most usage | Custom domains use target-managed certificates. Complete domain validation before cutover; an ACM ARN supplies no certificate material. Minimum TLS-version and SSL support-method differences are reported. | | Declarative edge logic (Rules engine) | Edge logic | Partial | Most usage | Header, URL and redirect behavior can use Rules engine conditions and actions. Arbitrary function code, external calls and request-body inspection are outside this translation; unsupported associations are reported and standalone functions rejected. | | Policy & monitoring facets (standalone cache / origin-request / response-headers policies, monitoring subscription) | Facets | Out of scope | Full surface | a cache, origin-request, or response-headers policy referenced by a behavior is re-expressed in that behavior's route and rules; a standalone policy claimed by no distribution, and the monitoring subscription, have no compiler and cause a build error | | Origins | Origins | Partial | Common | Origin hosts become members of an origin group. Confirm unresolved addresses and review differences between source failover conditions and Front Door health-probe selection. Standard requires a public origin endpoint. | #### How it works Tensor9 translates the CloudFront distribution into an Azure Front Door Standard profile, endpoint, origin group, origins, route and rule set. Microsoft operates the edge cache and serves requests. The customer controls the origin and the target configuration. A custom DNS name can remain after its record and certificate are ready for Front Door. The CloudFront-generated hostname and cached objects do not transfer. Expect origin requests as the new cache fills.
Front Door routes a request through cache rules to an origin in the configured group. Front Door routes a request through cache rules to an origin in the configured group.
#### Origins and routing The endpoint supplies the public host. A route associates that endpoint with an origin group and the rules applied to requests. Each declared origin host becomes an origin; the group uses health probes to select available origins. This differs from CloudFront origin-group failover, so review failover conditions and origin selection. Confirm any origin address that compilation cannot resolve. An S3 origin follows the selected storage mapping to the target endpoint. Validate the origin host header, TLS and network access against that endpoint rather than retaining an AWS-specific address. #### Origin access Front Door Standard needs an origin reachable through a public endpoint; it does not support Private Link origins. Use Premium with a compatible Private Link arrangement when the origin must have no public endpoint. Restrict direct access to a public origin. The backend can validate a shared secret inserted in an origin request header, but that secret must be protected and rotated. Azure's origin-security guidance also combines the `AzureFrontDoor.Backend` network selector with validation of the expected `X-Azure-FDID` value. A header alone is not network isolation. CloudFront OAI and OAC govern authenticated access to supported origins; OAC is not a general private-network connection. The target requires the access controls appropriate to its origin type. Neither Standard nor Premium verifies existing CloudFront viewer signed-URL or signed-cookie signatures; that viewer-authorization scheme needs a separate application design.
Standard reaches a public endpoint whose network and application controls restrict access. Standard reaches a public endpoint whose network and application controls restrict access.
#### Web application firewall A Front Door firewall policy contains the target WAF rules; a security policy attaches it to the endpoint. Supported custom AWS match and rate-limit rules are expressed using Front Door conditions and actions, and geographic restrictions use country-match rules. An AWS `web_acl_id` is a reference, not an Azure policy. Standard supports custom match rules, rate limits and country-based rules. Microsoft-managed rule sets and bot protection require Premium. A source web ACL that depends on managed groups therefore needs a different protection plan; custom rules alone do not reproduce those groups. #### Cache behavior Default and ordered cache behaviors become rules matched by path. A route-configuration override sets the cache duration and query-string behavior; compression is configured on the route. Check the query parameters included in the cache key, especially for personalized responses. CloudFront's minimum, default and maximum TTLs do not map to three independent Front Door settings. The default TTL supplies the single cache duration; separate minimum and maximum constraints are reported during compilation. Forwarded-header lists and allowed-method lists also have differences. Verify the effective cache key and request handling rather than assuming that two source requests always remain distinct. #### Rules and application code The Rules engine can express header changes, URL or path rewrites and redirects. Those behaviors can be implemented with target rule conditions and actions on both Front Door tiers; arbitrary Lambda\@Edge or CloudFront Functions code does not run there. Function associations that cannot be translated are omitted and reported. A standalone CloudFront function without a supported translation causes a build error. Move required application logic to a suitable runtime before cutover. CloudFront field-level encryption also has no equivalent in this mapping. #### Domains and certificates Each alias becomes a Front Door custom domain with a managed certificate. Complete domain validation and wait for certificate readiness before updating DNS. Front Door issues and renews that certificate; it does not copy an ACM certificate from its ARN. Review minimum TLS version and SSL support-method differences. Supported Route 53 aliases are updated to the Front Door endpoint. CloudFront `domain_name`, `arn` and `hosted_zone_id` do not become Azure identifiers. References without a target equivalent are removed from outputs or rejected in active configuration. Update external DNS consumers and allowlists separately. #### Operations and remaining limits Azure's edge locations replace CloudFront price classes. Use Azure Monitor and diagnostic settings for access logs and metrics; an AWS monitoring subscription is not a Front Door setting. Review cache hit rates, origin health, latency and errors after cutover. Cache, origin-request and response-headers policy resources need a supported distribution attachment and translation. Standalone policies, monitoring subscriptions and origin-access identities without one cause a build error. Resolve those resources together with the behavior they configure. Provider reference: [securing Front Door origins](https://learn.microsoft.com/en-us/azure/frontdoor/origin-security). [Service Catalog](/service-adapters/catalog). # Network Firewall Source: https://docs.tensor9.com/service-adapters/aws/networking-traffic/network-firewall AWS Network Firewall. Stateful traffic filtering for VPC subnets, with Suricata-compatible rule groups, domain allow lists and intrusion detection applied at firewall endpoints. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | - | ## How the targets compare Each row compares a capability of Network Firewall with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | Network Firewall | Google Cloud | Azure | OCI | | ---------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------ | | 5-tuple stateful rules · protocol / CIDR / port allow-deny | Yes | Yes - network firewall policy rules with L4 match | Yes - network\_rule\_collection rules | Yes - security rules over emitted address lists + services | | Domain allowlists · TLS SNI / HTTP Host | Yes | Yes - match dest\_fqdns (Cloud NGFW FQDN objects) | Yes - application\_rule\_collection destination\_fqdns | Yes - url\_list + a security rule condition | | Suricata rule strings · IDS/IPS signatures | Yes | Partial - re-home onto Cloud NGFW security profiles / intrusion prevention | Partial - re-home onto Azure Firewall Premium IDPS, not a rule collection | Partial - re-home onto the OCI Network Firewall inspection surface | | TLS inspection · decrypt + inspect | Yes | Partial - Cloud NGFW TLS inspection policy, a separate surface | Partial - Azure Firewall Premium TLS inspection, a separate surface | Partial - OCI decryption profiles / rules, a separate surface | | Flow / alert logging | Yes | Partial - rule enable\_logging + Cloud Logging, not a translated rule | Partial - Azure Firewall diagnostic logs, not a translated rule | Partial - OCI Network Firewall logging, not a translated rule | | VPC inspection routing · policy association + endpoint | Yes | Partial - policy-to-network association wired by the operator | - | - | | API coverage | full | partial | partial | partial | | VPC inspection routing · endpoint + route tables | Yes | - | Partial - AzureFirewallSubnet + route tables wired by the operator | - | | VPC inspection routing · subnet + route tables | Yes | - | - | Partial - firewall subnet + route tables wired by the operator | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | ----------------------------- | ---------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Suricata rule strings | Advanced rules | Partial | Most usage | raw Suricata signatures re-home onto Cloud NGFW security profiles / intrusion prevention (an org-scoped surface), not a plain L4 rule; surfaced, never silently dropped | | TLS inspection | Advanced rules | Out of scope | Full surface | aws\_networkfirewall\_tls\_inspection\_configuration re-homes onto Cloud NGFW TLS inspection policies (a distinct surface), surfaced not recreated | | Flow / alert logging | Observability | Out of scope | Full surface | aws\_networkfirewall\_logging\_configuration is replaced by the rule's own enable\_logging + Cloud Logging, not a translated rule | | 5-tuple stateful rules | Rule translation | Supported | Common | each stateful\_rule maps to a google\_compute\_network\_firewall\_policy\_rule: an L4 match of src\_ip\_ranges / dest\_ip\_ranges + layer4\_configs (ip\_protocol + ports) | | Domain allowlists / denylists | Rule translation | Supported | Most usage | a rules\_source\_list maps to a rule whose match names dest\_fqdns (Cloud NGFW FQDN objects); ALLOWLIST → allow, DENYLIST → deny | | Stateless 5-tuple rules | Rule translation | Partial | Most usage | the 5-tuple match projects; AWS-specific custom actions have no Cloud NGFW analog and are surfaced, not recreated | | VPC inspection routing | Traffic steering | Partial | Common | the policy + rules emit; the policy-to-network association + firewall-endpoint attachment is deployment-specific and wired by the operator (surfaced, never assumed) | #### How it works An AWS Network Firewall is a managed firewall attached to a firewall policy, and the policy references a tree of rule groups that decide what traffic to allow, drop, or alert on. To run that same protection on the customer's GCP, Tensor9 reads the firewall, its policy, and the whole rule-group tree, and emits the GCP-native equivalent: a **Cloud NGFW network firewall policy** and a set of policy rules that state the translated allow and deny decisions. Cloud NGFW works differently from a routed appliance. There is no firewall instance and no dedicated firewall subnet; the policy is a distributed enforcement layer that Google runs inside the VPC fabric, and it applies to the networks it is associated with. Once the build is done the fabric filters packets directly and Tensor9 is not in the path. The allow/deny rules become native policy rules. The Suricata signature engine and TLS inspection are deep-packet features that re-home onto Cloud NGFW's security profiles and TLS inspection policy, each named in the sections below.
On AWS a workload subnet routes egress through a managed Network Firewall endpoint that inspects it before the internet. On GCP a Cloud NGFW policy the build emits is enforced in the VPC fabric on the workload subnet itself, so there is no separate inspection hop; the fabric filters packets directly and nothing of Tensor9 sits in the path. On AWS a workload subnet routes egress through a managed Network Firewall endpoint that inspects it before the internet. On GCP a Cloud NGFW policy the build emits is enforced in the VPC fabric on the workload subnet itself, so there is no separate inspection hop; the fabric filters packets directly and nothing of Tensor9 sits in the path.

The workload is untouched; the Cloud NGFW policy is enforced on the subnet in the VPC fabric, so there is no separate appliance the traffic passes through.

#### Architecture Network Firewall spreads its configuration across a firewall, its policy, and the rule groups the policy references. Cloud NGFW collapses that into one network firewall policy holding a flat list of policy rules. The build emits the policy and one **policy rule per translated rule**; the rule groups do not survive as a grouping layer, so a stateful rule group of ten rules becomes ten policy rules. Each policy rule has an integer **priority**, an **action** of allow or deny, a **direction** of ingress or egress, and a match. The priority is the evaluation order, and the build assigns a deterministic priority per rule so the sequence is stable across rebuilds. The policy is **associated with the VPC networks** it governs rather than placed in a route; that association, and the choice of which networks to govern, is a property of the customer's project.
The Cloud NGFW the build emits is one network firewall policy holding a flat, priority-ordered list of policy rules, associated with the VPC networks it governs. Each AWS rule becomes exactly one policy rule; there is no appliance and no firewall subnet. The Cloud NGFW the build emits is one network firewall policy holding a flat, priority-ordered list of policy rules, associated with the VPC networks it governs. Each AWS rule becomes exactly one policy rule; there is no appliance and no firewall subnet.

One policy, a flat list of priority-ordered rules, associated with the VPC networks it governs: each AWS rule becomes exactly one policy rule, and the priority is the evaluation order.

#### How the rule groups map The policy's rule groups translate by kind, each rule becoming one policy rule. A stateful rule group of 5-tuple rules becomes a set of **ingress** policy rules: each rule keeps its protocol, its source and destination IP ranges, and its ports in an L4 match, and its AWS action sets the policy-rule action (pass or alert becomes allow, drop or reject becomes deny). A rule group built from a domain list becomes **egress** policy rules that match on destination FQDNs, with allow or deny taken from whether the list is an allowlist or a denylist. A rule group written as raw Suricata signatures has no policy-rule form. Cloud NGFW runs signature-based intrusion prevention through security profiles, which is a distinct capability rather than an allow/deny rule, so the build surfaces those signatures for the operator to enable through a security profile instead of discarding them. >The allow/deny rules that make up most policies translate directly. \>
Each AWS rule group maps to Cloud NGFW policy rules: a stateful rule group of 5-tuple rules becomes ingress allow rules with an L4 match, a rule-source-list of domains becomes egress allow rules matching destination FQDNs, and a Suricata rule-string group has no policy-rule form and re-homes onto security profiles. Each AWS rule group maps to Cloud NGFW policy rules: a stateful rule group of 5-tuple rules becomes ingress allow rules with an L4 match, a rule-source-list of domains becomes egress allow rules matching destination FQDNs, and a Suricata rule-string group has no policy-rule form and re-homes onto security profiles.

The allow/deny rule groups translate onto policy rules one for one; a Suricata signature group has no policy-rule form and points at Cloud NGFW's security profiles instead.

#### Stateful 5-tuple rules A stateful rule in Network Firewall matches on the 5-tuple: protocol, source address, destination address, source port, and destination port, with an action of pass, drop, or alert. A Cloud NGFW policy rule matches on source and destination IP ranges plus an L4 configuration of protocol and ports, so each rule translates onto one policy rule. TCP, UDP, and ICMP map to themselves; an AWS `ANY` address becomes `0.0.0.0/0`, and an `ANY` destination port drops the port constraint so the rule matches any port. Two properties are explicit on Cloud NGFW that Network Firewall leaves implicit. The first is **direction**: a 5-tuple rule becomes an ingress policy rule, and the build sets the direction rather than inferring it at match time. The second is the **action**: Cloud NGFW rules are allow or deny individually, so an AWS drop or reject becomes a deny policy rule rather than an omission, and an alert rule becomes an allow rule with rule logging turned on so the traffic is recorded without being blocked. * **One AWS rule, one policy rule:** a `tcp` rule from `10.0.0.0/16` to any host on port `443` becomes a single ingress policy rule with an L4 match on tcp and port 443. * \>**Each rule keeps its action.**> pass and alert become allow, drop and reject become deny, and an alert rule turns on rule logging so it records without blocking. * **Wildcards normalize:** an AWS `ANY` address becomes `0.0.0.0/0`, and an `ANY` destination port removes the port constraint. #### Domain allowlists Network Firewall filters egress by domain through a rule group built from a domain list: a set of target domains, tagged for TLS SNI or HTTP Host matching, generated as an allowlist or a denylist. Cloud NGFW expresses the same control through a policy rule whose match specifies a set of destination FQDNs. A domain list translates into egress policy rules: the domains become the FQDN objects the rule matches, and the list's allowlist or denylist nature sets the rule action. The protocol side follows the domain tag. A TLS-SNI target becomes an FQDN match on TCP port 443, and an HTTP-Host target becomes a match on TCP port 80. Wildcards reconcile: a Network Firewall entry beginning with a leading dot (`.example.com`, the domain and its subdomains) becomes the Cloud NGFW FQDN wildcard form `*.example.com`, >and an exact domain stays as itself. \> * **Domains become FQDN matches:** each domain-list target becomes a destination FQDN an egress policy rule matches on. * **SNI vs Host sets the port:** a TLS-SNI target matches on TCP port 443; an HTTP-Host target matches on TCP port 80. * **Leading-dot wildcards translate.** `.example.com` becomes `*.example.com`, matching the domain and its subdomains. #### Suricata signatures and TLS inspection Two Network Firewall capabilities are deep-packet features rather than allow/deny rules, and Cloud NGFW serves them through surfaces separate from the policy rules. The first is the Suricata signature engine: a rule group can hold raw Suricata IDS/IPS rule strings that match on packet content. Cloud NGFW runs signature-based intrusion prevention through **security profiles**, which are organization-scoped and available at the enterprise tier. The protection has a native form, but the AWS signature text is not a policy rule and does not translate into one, so the build surfaces the signatures for the operator to configure on a security profile. The second is TLS inspection. Network Firewall can decrypt outbound TLS with a certificate and inspect the plaintext. Cloud NGFW performs TLS inspection through a **TLS inspection policy**, a distinct object from the firewall policy rules. The build reports the TLS-inspection configuration rather than recreating it in a rule. Flow and alert logging is likewise not a rule: Cloud NGFW records rule hits through the rule's own logging and Cloud Logging, and the AWS logging configuration is replaced by that rather than emitted. * **Suricata → security profiles:** intrusion prevention is an organization-scoped, enterprise-tier security profile; the AWS signature text is surfaced for the operator to configure, not converted into a policy rule. * **TLS inspection → TLS inspection policy:** outbound decrypt-and-inspect is a separate policy object, reported as an advanced feature rather than recreated in a rule. * **Logging → rule logging and Cloud Logging:** rule hits are recorded through the rule's own logging; the AWS logging configuration is not a rule and does not become one. #### Traffic steering A firewall only filters traffic it governs, and on Cloud NGFW the scope of governance is an association rather than a route. Network Firewall attaches an inspection endpoint into a subnet, and the VPC route tables send traffic through that endpoint; the exact wiring depends on the VPC layout and is not part of the firewall's rules. Cloud NGFW has no appliance to route through. The policy is enforced in the VPC fabric, and it takes effect on a network once the policy is **associated** with that network. The build emits the policy and its rules; the choice of which VPC networks the policy governs is a property of the customer's project, so that association is the operator's to set. At the enterprise tier, running intrusion prevention also involves attaching a firewall endpoint, which is a separate operator step. The rules are in place; the association determines where they apply. * **The policy and rules are emitted:** the network firewall policy and one policy rule per translated rule are provisioned at the build. * **Association sets the scope:** the policy governs a VPC network once it is associated with it, and which networks to govern is a property of the customer's project. * **Intrusion prevention adds an endpoint:** at the enterprise tier, running signature-based prevention involves attaching a firewall endpoint, a separate operator step from the allow/deny rules. #### Limitations The list below is the whole go/no-go picture for Cloud NGFW. The allow/deny rule tree translates into policy rules; the deep-packet features and the network association are where an AWS Network Firewall and Cloud NGFW genuinely diverge. △ Where AWS Network Firewall and GCP Cloud NGFW diverge, read before you adopt * **Suricata signatures do not become rules:** a rule group of raw Suricata IDS/IPS strings has no policy-rule form. Cloud NGFW's security profiles are the native form for signature-based prevention, but they are organization-scoped and enterprise-tier, and the signature text is surfaced for the operator to configure rather than translated. * **TLS inspection is a separate policy object:** outbound TLS decrypt-and-inspect is a Cloud NGFW TLS inspection policy, not a firewall-policy rule. The build reports the TLS-inspection configuration rather than recreating it, so it is enabled by the operator after the build. * **Rule groups flatten into policy rules.** Cloud NGFW has no rule-collection grouping, so each AWS rule becomes one policy rule and the rule-group organization is not preserved. The build assigns a deterministic priority per rule so evaluation order is stable, but the original grouping is not a Cloud NGFW concept. * **Stateless custom actions are dropped:** the 5-tuple of a stateless rule translates, but AWS-specific custom actions (publish-metric actions and their dimensions) have no Cloud NGFW analog and are surfaced, not recreated. * **Multiple policies merge onto one:** if the stack defines more than one firewall policy, the matched rule groups aggregate into a single Cloud NGFW policy. The per-policy separation is reported rather than silently collapsed. * **Network association is wired by the operator:** the policy and rules are emitted, but which VPC networks the policy governs (and, for intrusion prevention, the firewall-endpoint attach) depend on the customer's project layout and are the operator's to complete. #### Other considerations Beyond the rule translation, a few operational realities are worth planning for when a Network Firewall stack moves to Cloud NGFW. * **Nothing to migrate:** a firewall holds no data. The policy and its rules are provisioned at the build, and there is no traffic history or state to copy. * **Google operates the enforcement layer.** Cloud NGFW is a managed, distributed layer in the VPC fabric, so Google runs its availability and scaling, and Tensor9 is not in the packet path. * **The enterprise tier follows the features you use:** allow/deny filtering runs at the standard tier; the security profiles and TLS inspection the advanced Network Firewall features re-home onto require the enterprise tier and, for security profiles, organization-level configuration. * \>**References to the firewall's identity have no equivalent.**> a firewall is a traffic-inspection layer, not an app-consumed endpoint, so an external read of the firewall's ARN or endpoint identity has no portable Cloud NGFW equivalent and is reported rather than repointed. ## On Azure | Operation | Area | Support | Depth | Notes | | ----------------------------- | ---------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Suricata rule strings | Advanced rules | Partial | Most usage | raw Suricata IDS/IPS signatures (rules\_source.rules\_string) re-home onto Azure Firewall Premium IDPS, not a plain rule collection; surfaced for the operator to re-home rather than silently dropped | | TLS inspection | Advanced rules | Out of scope | Full surface | aws\_networkfirewall\_tls\_inspection\_configuration is a distinct managed surface (Azure Firewall Premium TLS inspection), surfaced not recreated | | Flow / alert logging | Observability | Out of scope | Full surface | aws\_networkfirewall\_logging\_configuration is replaced by Azure Firewall's own diagnostic-log surface, not a translated rule | | 5-tuple stateful rules | Rule translation | Supported | Common | each stateful\_rule (protocol / source+destination CIDR / port) maps to a network\_rule\_collection rule: protocols, source\_addresses, destination\_addresses, destination\_ports | | Domain allowlists / denylists | Rule translation | Supported | Most usage | a rules\_source\_list (TLS\_SNI / HTTP\_HOST targets) maps onto an application\_rule\_collection rule with destination\_fqdns; a leading-dot AWS wildcard becomes an Azure \*.domain wildcard | | Stateless 5-tuple rules | Rule translation | Partial | Most usage | the 5-tuple match projects; AWS-specific custom actions (publishMetricAction / dimensions) have no Azure analog and are surfaced, not recreated | | VPC inspection routing | Traffic steering | Partial | Common | the firewall + policy + rules emit; the inspection subnet (AzureFirewallSubnet) + route-table steering is deployment-specific and wired by the operator (surfaced, never assumed) | #### How it works An AWS Network Firewall is a managed firewall attached to a firewall policy, and the policy references a tree of rule groups that decide what traffic to allow, drop, or alert on. To run that same protection on the customer's Azure, Tensor9 reads the firewall, its policy, and the whole rule-group tree, and emits the Azure-native equivalent: an **Azure Firewall**, an **Azure Firewall Policy**, and a rule-collection group that holds the translated rules. Azure Firewall is a managed, stateful firewall, the same category of product as Network Firewall. Microsoft runs its data plane, so once the build is done the Azure Firewall inspects packets directly and Tensor9 is not in the path. What translates cleanly is the allow/deny rule tree. What has no plain-rule form (the Suricata signature engine, TLS inspection, and flow logging) moves onto Azure Firewall Premium's own surfaces, and each of those is named in the sections below rather than assumed.
On AWS a workload subnet routes egress through a managed Network Firewall endpoint that inspects it before the internet. On Azure the same workload subnet routes through an Azure Firewall the build emits, which inspects it the same way; the firewall inspects packets directly and nothing of Tensor9 sits in the path. On AWS a workload subnet routes egress through a managed Network Firewall endpoint that inspects it before the internet. On Azure the same workload subnet routes through an Azure Firewall the build emits, which inspects it the same way; the firewall inspects packets directly and nothing of Tensor9 sits in the path.

The workload and its routing are untouched; only the firewall that inspects the traffic changes, and it runs in Azure.

#### Architecture Network Firewall spreads its configuration across three resource types: the firewall, its policy, and the rule groups the policy references. Azure Firewall keeps the same separation. The build emits an Azure Firewall running in a dedicated `AzureFirewallSubnet` with a public IP, attached to an Azure Firewall Policy, and the policy holds a single rule-collection group. Inside that group the translated rules land in two collections. A **network rule collection** holds the stateful 5-tuple rules (protocol, source and destination addresses, destination ports). An **application rule collection** holds the domain allowlists (the FQDNs egress traffic may reach). The firewall runs at the Standard tier for allow/deny filtering; the Premium tier adds the intrusion-detection and TLS-inspection surfaces the advanced Network Firewall features move onto, covered further down.
The Azure firewall the build emits is one composition: an Azure Firewall in a dedicated AzureFirewallSubnet with a public IP, attached to an Azure Firewall Policy, which holds one rule-collection group containing a network rule collection and an application rule collection. The Azure firewall the build emits is one composition: an Azure Firewall in a dedicated AzureFirewallSubnet with a public IP, attached to an Azure Firewall Policy, which holds one rule-collection group containing a network rule collection and an application rule collection.

One firewall, one policy, one rule-collection group: the firewall inspects traffic, the policy states the rules, and the group holds the two rule collections translated from the AWS configuration.

#### How the rule groups map The policy's rule groups translate by kind. A stateful rule group of 5-tuple rules becomes a network rule collection: each rule keeps its protocol, its source and destination addresses, and its destination ports, and an AWS `ANY` or `0.0.0.0/0` becomes the Azure any-address token. A rule group built from a domain list becomes an application rule collection, where each entry is a destination FQDN the traffic may reach. Every rule group in the policy folds into the one emitted rule-collection group. A rule group written as raw Suricata signatures is the one kind with no rule-collection form. Azure runs signature-based intrusion detection at the Premium tier, which is a distinct engine rather than an allow/deny rule, so the build surfaces those signatures for the operator to enable on Premium instead of silently discarding them. The allow/deny rules that make up the bulk of most policies translate directly.
Each AWS rule group maps to one Azure rule collection: a stateful rule group of 5-tuple rules becomes a network rule collection, a rule-source-list of domains becomes an application rule collection, and a Suricata rule-string group has no rule-collection form and moves onto Premium intrusion detection. Each AWS rule group maps to one Azure rule collection: a stateful rule group of 5-tuple rules becomes a network rule collection, a rule-source-list of domains becomes an application rule collection, and a Suricata rule-string group has no rule-collection form and moves onto Premium intrusion detection.

The allow/deny rule groups translate onto rule collections one for one; a Suricata signature group has no rule-collection form and points at Premium intrusion detection instead.

#### Stateful 5-tuple rules A stateful rule in Network Firewall matches on the classic 5-tuple: protocol, source address, destination address, source port, and destination port, with an action of pass, drop, or alert. Azure's network rule collection matches on the same fields, so each rule translates field for field. TCP, UDP, and ICMP map as themselves; an AWS `IP` or `ANY` protocol becomes Azure's `Any`. A CIDR is kept verbatim, and a wildcard address becomes the Azure any-address token. Two differences are worth stating flatly. Azure evaluates a rule collection under a single action, so the translated network rule collection is emitted with an `Allow` action and holds the rules that permit traffic; a rule whose intent is to drop is expressed by absence from the allowlist plus the collection's default deny, not by a per-rule drop verb. And Azure orders rule collections by an explicit numeric priority rather than by the rule-group reference order, so the build assigns a deterministic priority to the collection. * **Protocol, addresses, and ports come across field for field:** a `tcp` rule from `10.0.0.0/16` to any host on port `443` becomes the same match in the network rule collection. * **Wildcards normalize:** an AWS `ANY` or `0.0.0.0/0` address becomes Azure's any-address token, and an `IP`/`ANY` protocol becomes `Any`. * **Allow-plus-default-deny, not per-rule drop.** Azure evaluates a collection under one action, so permitted traffic is listed and everything else is denied by default rather than by a drop rule. #### Domain allowlists Network Firewall filters egress by domain through a rule group built from a domain list: a set of target domains, tagged for TLS SNI or HTTP Host matching, generated as an allowlist or a denylist. Azure expresses the same control as an application rule collection, where each rule names a set of destination FQDNs and the protocol (HTTP or HTTPS) the match applies to. A domain list translates into that collection: the domains become the destination FQDNs, and a TLS-SNI target maps to an HTTPS rule while an HTTP-Host target maps to an HTTP rule. AWS and Azure spell wildcards differently, and the build reconciles it. A Network Firewall entry that begins with a leading dot (`.example.com`, meaning the domain and its subdomains) becomes Azure's `*.example.com` form. An exact domain stays as it is. The result is that egress a Network Firewall policy allowed to `*.example.com` is allowed to the same set of hosts on Azure. * **Domains become destination FQDNs:** each domain-list target lands in the application rule collection as an FQDN the workload may reach. * **SNI vs Host sets the protocol:** a TLS-SNI target becomes an HTTPS rule; an HTTP-Host target becomes an HTTP rule. * **Leading-dot wildcards translate.** `.example.com` becomes Azure's `*.example.com`, matching the domain and its subdomains. #### Suricata signatures and TLS inspection Two Network Firewall capabilities are deep-packet features rather than allow/deny rules, and Azure serves them through its Premium tier rather than through a rule collection. The first is the Suricata signature engine: a rule group can hold raw Suricata IDS/IPS rule strings that match on packet content, not only the 5-tuple. Azure Firewall Premium runs its own signature-based intrusion detection and prevention, so the protection has a native equivalent, but the AWS signature text is not a rule collection and does not translate into one. The build surfaces the signatures so the operator can enable equivalent detection on Premium. The second is TLS inspection. Network Firewall can decrypt outbound TLS with a certificate and inspect the plaintext against its rules. Azure Firewall Premium terminates and inspects TLS as a distinct policy capability, separate from the rule collections. The build does not recreate the decryption configuration inside a rule; it reports the TLS-inspection configuration as an advanced feature to enable on Premium. Flow and alert logging is the same story: Network Firewall's logging configuration is replaced by Azure Firewall's own diagnostic logs rather than emitted as a rule. * **Suricata → Premium intrusion detection:** the signature protection runs on Azure Firewall Premium's intrusion detection, but the AWS signature text is surfaced for the operator to enable, not converted into a rule. * **TLS inspection → Premium TLS inspection:** outbound decrypt-and-inspect is a Premium policy capability, reported as an advanced feature rather than recreated in a rule. * **Logging → diagnostic logs.** Azure Firewall emits flow and rule-hit logs through Azure Monitor diagnostic settings; the AWS logging configuration is not a rule and is not emitted as one. #### Traffic steering A firewall only inspects traffic that reaches it, and on both clouds the routing that steers traffic through the firewall is a separate concern from the rules. Network Firewall attaches an inspection endpoint into a subnet, and the VPC route tables send traffic through that endpoint. The exact subnet and route-table wiring depends on the VPC layout, and it is not encoded in the firewall's own rules. Azure works the same way. The emitted Azure Firewall lives in the `AzureFirewallSubnet`, and the VNet route tables have to point at the firewall's private address for it to inspect a given subnet's traffic. The build emits the firewall, the policy, and the rules, and it declares the inspection subnet as a value the operator supplies, because the right subnet and the routes that steer traffic through the firewall are properties of the customer's network, not of the AWS rule tree. The operator wires the route so traffic is inspected; the firewall and its rules are already in place. * **The firewall and rules are emitted:** the Azure Firewall, its policy, and the translated rule collections are provisioned at the build. * **The inspection subnet is supplied by the operator:** the firewall attaches to the `AzureFirewallSubnet`, which is a property of the customer's VNet rather than of the AWS rules. * **The route is the operator's to wire:** the VNet route tables must point the workload subnets at the firewall for it to inspect their traffic, exactly as the VPC route tables did on AWS. #### Limitations The list below is the whole go/no-go picture for Azure Firewall. The allow/deny rule tree translates; the deep-packet features and the routing are where an AWS Network Firewall and Azure Firewall genuinely diverge. △ Where AWS Network Firewall and Azure Firewall diverge, read before you adopt * **Suricata signatures do not become rules:** a rule group of raw Suricata IDS/IPS strings has no rule-collection form. Azure Firewall Premium's intrusion detection is the native equivalent, but the signature text is surfaced for the operator to enable, not translated. If a policy leaned on custom Suricata detections, plan to re-express them on Premium. * **TLS inspection is a Premium capability, not a rule:** outbound TLS decrypt-and-inspect is configured on the Premium policy, separate from the rule collections. The build reports the TLS-inspection configuration rather than recreating it, so it is enabled by the operator after the build. * **Rule collections enforce one action.** Azure evaluates a network or application rule collection under a single action, so permitted traffic is listed in an allow collection and everything else is denied by default. A policy that mixed pass and drop rules within one rule group is expressed as an allowlist plus the default deny, not as per-rule drops. * **Stateless custom actions are dropped:** the 5-tuple of a stateless rule translates, but AWS-specific custom actions (publish-metric actions and their dimensions) have no Azure analog and are surfaced, not recreated. * **Multiple policies merge onto one:** if the stack defines more than one firewall policy, the matched rule groups aggregate into a single Azure policy and rule-collection group. The per-policy separation is reported rather than silently collapsed. * **Inspection routing is wired by the operator:** the firewall and its rules are emitted, but the AzureFirewallSubnet attachment and the VNet routes that steer traffic through the firewall depend on the customer's network layout and are the operator's to complete. #### Other considerations Beyond the rule translation, a few operational realities are worth planning for when a Network Firewall stack moves to Azure Firewall. * **Nothing to migrate:** a firewall holds no data. The Azure Firewall, policy, and rules are provisioned at the build, and there is no traffic history or state to copy. * **Microsoft operates the data plane.** Azure Firewall is a managed service, so once it is provisioned Microsoft runs its availability, scaling, and patching, and Tensor9 is not in the packet path. * **The tier follows the features you use:** allow/deny filtering runs at the Standard tier; the intrusion-detection and TLS-inspection surfaces the advanced Network Firewall features move onto require the Premium tier, which is an operator choice with its own cost. * **References to the firewall's identity have no equivalent:** a firewall is a traffic-inspection appliance, not an app-consumed endpoint, so an external read of the firewall's ARN or endpoint identity has no portable Azure equivalent and is reported rather than repointed. ## On OCI | Operation | Area | Support | Depth | Notes | | ----------------------------- | ---------------- | ------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Suricata rule strings | Advanced rules | Partial | Most usage | raw Suricata signatures re-home onto the OCI Network Firewall's own inspection surface, not a plain security rule; surfaced, never silently dropped | | TLS inspection | Advanced rules | Out of scope | Full surface | aws\_networkfirewall\_tls\_inspection\_configuration re-homes onto OCI decryption profiles / rules (a distinct surface), surfaced not recreated | | Flow / alert logging | Observability | Out of scope | Full surface | aws\_networkfirewall\_logging\_configuration is replaced by the OCI Network Firewall's own logging, not a translated rule | | 5-tuple stateful rules | Rule translation | Supported | Common | each stateful\_rule maps onto a security rule whose condition references emitted address lists (source / destination CIDR) + a service (the destination port) | | Domain allowlists / denylists | Rule translation | Supported | Most usage | a rules\_source\_list maps onto a url\_list + a security rule whose condition references it; ALLOWLIST → ALLOW, DENYLIST → DROP | | Stateless 5-tuple rules | Rule translation | Partial | Most usage | the 5-tuple match projects; AWS-specific custom actions have no OCI analog and are surfaced, not recreated | | VPC inspection routing | Traffic steering | Partial | Common | the firewall + policy + rules emit; the firewall's subnet attachment + route-table steering is deployment-specific and wired by the operator (surfaced, never assumed) | #### How it works An AWS Network Firewall is a managed firewall attached to a firewall policy, and the policy references a tree of rule groups that decide what traffic to allow, drop, or alert on. To run that same protection on the customer's OCI, Tensor9 reads the firewall, its policy, and the whole rule-group tree, and emits the Oracle-native equivalent: an **OCI Network Firewall** attached to an **OCI Network Firewall Policy** whose security rules state the translated decisions. The OCI Network Firewall is a managed, stateful firewall, the same category of product as Network Firewall. Oracle runs its data plane, so once the firewall is attached to a subnet and the route points at it, it inspects packets directly and Tensor9 is not in the path. The allow/deny rules become security rules, though OCI expresses them through named objects rather than inline values, which the next section walks through. The Suricata signature engine and TLS inspection move onto the firewall's own inspection surface and decryption profiles, named below rather than assumed.
On AWS a workload subnet routes egress through a managed Network Firewall endpoint that inspects it before the internet. On OCI the same workload subnet routes through an OCI Network Firewall the build emits, which inspects it the same way; the firewall inspects packets directly and nothing of Tensor9 sits in the path. On AWS a workload subnet routes egress through a managed Network Firewall endpoint that inspects it before the internet. On OCI the same workload subnet routes through an OCI Network Firewall the build emits, which inspects it the same way; the firewall inspects packets directly and nothing of Tensor9 sits in the path.

The workload and its routing are untouched; only the firewall that inspects the traffic changes, and it runs in OCI.

#### Architecture Network Firewall spreads its configuration across the firewall, its policy, and the rule groups the policy references. The OCI Network Firewall keeps the firewall and policy, but its rules are shaped differently. A **security rule** in the policy does not hold a CIDR, a port, or a domain inline. Its condition references **named objects**: an address list for source and destination addresses, a service for a port, and a url list for domains. So the build emits more than a rule. For each translated rule it emits the address lists, services, and url lists the rule needs, then a security rule whose condition points at them by name. That indirection has a real payoff: a CIDR that appears in several rules becomes one address list they all reference, rather than being repeated inline. The firewall attaches to a subnet in a compartment, and the security rules take their action (allow, drop, or reject) per rule.
The OCI firewall the build emits is a Network Firewall attached to a firewall policy. A security rule in the policy does not inline its match; its condition references named objects the build also emits: an address list for CIDRs, a service for ports, and a url list for domains. The OCI firewall the build emits is a Network Firewall attached to a firewall policy. A security rule in the policy does not inline its match; its condition references named objects the build also emits: an address list for CIDRs, a service for ports, and a url list for domains.

A security rule holds no addresses or ports of its own; its condition references named address lists, services, and url lists the build emits alongside it.

#### How the rule groups map The policy's rule groups translate by kind. A stateful rule group of 5-tuple rules becomes a set of security rules: for each rule the build emits an address list for the source and destination CIDRs and a service for the destination port, then a security rule whose condition references them and whose action is taken from the AWS action (pass becomes allow, drop becomes drop, reject becomes reject). A rule group built from a domain list becomes a url list holding the domains and a security rule that references it, allow for an allowlist and drop for a denylist. A rule group written as raw Suricata signatures is the one kind with no security-rule form. The OCI Network Firewall runs its own signature-based inspection as a capability of the same firewall rather than as a security rule, so the build surfaces those signatures for the operator to enable on the firewall's inspection surface rather than discarding them. The allow/deny rules that make up the bulk of most policies translate directly.
Each AWS rule group maps to an OCI security rule: a stateful 5-tuple group becomes a security rule referencing an address list and a service, a domain allowlist becomes a url list and a security rule that references it, and a Suricata rule-string group has no security-rule form and moves onto the firewall's inspection surface. Each AWS rule group maps to an OCI security rule: a stateful 5-tuple group becomes a security rule referencing an address list and a service, a domain allowlist becomes a url list and a security rule that references it, and a Suricata rule-string group has no security-rule form and moves onto the firewall's inspection surface.

The allow/deny rule groups translate onto security rules over emitted address lists, services, and url lists; a Suricata signature group has no security-rule form and points at the firewall's inspection surface instead.

#### Stateful 5-tuple rules A stateful rule in Network Firewall matches on the 5-tuple: protocol, source address, destination address, source port, and destination port, with an action of pass, drop, or alert. An OCI security rule matches on the same shape, but through the named objects rather than inline values. For each rule the build emits an address list for the source CIDR and one for the destination CIDR, a service for the destination port, and a security rule whose condition names those objects. Where the AWS rule uses `ANY` for an address or port, that part of the condition is omitted, so the rule matches any value there. The action maps per rule. A pass becomes `ALLOW`, a drop becomes `DROP`, and a reject becomes `REJECT`, which is the OCI verb for an actively refused connection. Because the addresses and ports live in named objects, a CIDR or port that several rules share is emitted once and referenced by each rule, so the emitted policy stays compact even when the AWS rule set repeats the same networks. * **Match lives in named objects:** a `tcp` rule from `10.0.0.0/16` to any host on port `443` becomes an address list, a service, and a security rule whose condition references them. * **The action is kept per rule:** pass becomes `ALLOW`, drop becomes `DROP`, and reject becomes `REJECT`. * **Shared networks are emitted once:** a CIDR or port used by several rules becomes one address list or service the rules reference, and an `ANY` value is omitted from the condition so it matches anything. #### Domain allowlists Network Firewall filters egress by domain through a rule group built from a domain list: a set of target domains, tagged for TLS SNI or HTTP Host matching, generated as an allowlist or a denylist. The OCI Network Firewall expresses the same control through a **url list** and a security rule that references it. A domain list becomes a url list holding the domains as URL patterns, and a security rule whose condition names that url list, with an action of allow for an allowlist or drop for a denylist. The domains become URL patterns. A Network Firewall entry that begins with a leading dot (`.example.com`, the domain and its subdomains) becomes the OCI wildcard URL pattern, and an exact domain stays as it is. Egress a Network Firewall policy allowed to `*.example.com` is allowed to the same set of hosts through the url list the security rule references. * **Domains become a url list:** each domain-list target lands in a url list as a URL pattern, and a security rule references the list. * **Allowlist vs denylist sets the action:** an allowlist becomes an `ALLOW` security rule and a denylist becomes a `DROP` one. * **Leading-dot wildcards translate.** `.example.com` becomes the OCI wildcard URL pattern, matching the domain and its subdomains. #### Suricata signatures and TLS inspection Two Network Firewall capabilities are deep-packet features rather than allow/deny rules, and the OCI Network Firewall serves them as capabilities of the same firewall rather than as security rules. The first is the Suricata signature engine: a rule group can hold raw Suricata IDS/IPS rule strings that match on packet content. The OCI Network Firewall runs its own signature-based intrusion detection and prevention, so the protection has a native form, but the AWS signature text is not a security rule and does not translate into one. The build surfaces the signatures for the operator to enable on the firewall's inspection surface. The second is TLS inspection. Network Firewall can decrypt outbound TLS with a certificate and inspect the plaintext. The OCI Network Firewall decrypts and inspects TLS through decryption profiles and decryption rules , a distinct part of the policy from the security rules. The build reports the TLS-inspection configuration for the operator to attach rather than recreating it in a security rule. Logging follows the same pattern: the OCI Network Firewall records traffic through its own logging, so Network Firewall's separate logging configuration is replaced by the firewall's logging rather than emitted as a rule. * **Suricata → the firewall's inspection surface:** signature-based inspection is a capability of the same firewall; the AWS signature text is surfaced for the operator to enable, not converted into a security rule. * **TLS inspection → decryption profiles and rules:** outbound decrypt-and-inspect is a distinct part of the policy, reported as an advanced feature rather than recreated in a security rule. * **Logging is the firewall's own:** the OCI Network Firewall records traffic through its logging; the AWS logging configuration is not a rule and is not emitted as one. #### Traffic steering A firewall only inspects traffic that reaches it, and on both clouds the routing that steers traffic through the firewall is separate from the rules. Network Firewall attaches an inspection endpoint into a subnet, and the VPC route tables send traffic through that endpoint. The exact subnet and route-table wiring depends on the VPC layout and is not part of the firewall's rules. The OCI Network Firewall attaches to a subnet in a compartment, and the VCN route tables have to point at the firewall's private address for it to inspect a given subnet's traffic. The build emits the firewall, the policy, the security rules, and the named objects the rules reference, and it declares the compartment and the firewall subnet as values the operator supplies, because those are properties of the customer's tenancy and network rather than of the AWS rule tree. The operator wires the route so traffic is inspected; the firewall and its rules are already in place. * **The firewall, rules, and named objects are emitted:** the OCI Network Firewall, its policy, the security rules, and the address lists, services, and url lists they reference are provisioned at the build. * **The compartment and subnet are supplied by the operator:** the firewall attaches to a subnet in a compartment, which are properties of the customer's tenancy rather than of the AWS rules. * **The route is the operator's to wire:** the VCN route tables must point the workload subnets at the firewall for it to inspect their traffic, exactly as the VPC route tables did on AWS. #### Limitations The list below is the whole go/no-go picture for the OCI Network Firewall. The allow/deny rule tree translates into security rules over named objects; the deep-packet features and the routing are where an AWS Network Firewall and the OCI Network Firewall genuinely diverge. △ Where AWS Network Firewall and the OCI Network Firewall diverge, read before you adopt * **Suricata signatures do not become rules:** a rule group of raw Suricata IDS/IPS strings has no security-rule form. The OCI Network Firewall's own signature inspection is the native form, but the signature text is surfaced for the operator to enable, not translated. If a policy leaned on custom Suricata detections, plan to re-express them on the firewall's inspection surface. * **TLS inspection is a separate part of the policy:** outbound TLS decrypt-and-inspect is a decryption profile and decryption rules, not a security rule. The build reports the TLS-inspection configuration rather than recreating it, so it is attached by the operator after the build. * **Rules match through named objects:** a security rule references address lists, services, and url lists rather than inlining a CIDR or port. The translation is faithful, but the emitted policy is a set of named objects plus rules rather than a flat rule list, which is worth knowing when reading or auditing it. * **Stateless custom actions are dropped:** the 5-tuple of a stateless rule translates, but AWS-specific custom actions (publish-metric actions and their dimensions) have no OCI analog and are surfaced, not recreated. * **Multiple policies merge onto one:** if the stack defines more than one firewall policy, the matched rule groups aggregate into a single OCI policy. The per-policy separation is reported rather than silently collapsed. * **Inspection routing is wired by the operator:** the firewall and its rules are emitted, but the compartment, the firewall subnet, and the VCN routes that steer traffic through the firewall depend on the customer's tenancy and network and are the operator's to complete. #### Other considerations Beyond the rule translation, a few operational realities are worth planning for when a Network Firewall stack moves to the OCI Network Firewall. * **Nothing to migrate:** a firewall holds no data. The firewall, policy, rules, and named objects are provisioned at the build, and there is no traffic history or state to copy. * **Oracle operates the data plane:** the OCI Network Firewall is a managed service, so once it is attached Oracle runs its availability, scaling, and patching, and Tensor9 is not in the packet path. * **The policy reads as named objects plus rules:** auditing the emitted firewall means reading the security rules together with the address lists, services, and url lists they reference, which is the OCI shape rather than a single inline rule list. * **References to the firewall's identity have no equivalent:** a firewall is a traffic-inspection appliance, not an app-consumed endpoint, so an external read of the firewall's ARN or endpoint identity has no portable OCI equivalent and is reported rather than repointed. [Service Catalog](/service-adapters/catalog). # Network Load Balancer Source: https://docs.tensor9.com/service-adapters/aws/networking-traffic/network-load-balancer AWS Network Load Balancer. Layer 4 load balancing that distributes TCP, UDP and TLS connections across target groups, with one static IP address per Availability Zone. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | - | ## How the targets compare Each row compares a capability of Network Load Balancer with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | Network Load Balancer | Google Cloud | Azure | OCI | | ----------------- | ------------------------- | ---------------------------------------- | ---------------------------- | ------------------------- | | Traffic path | AWS network load balancer | Google passthrough Network Load Balancer | Azure Standard Load Balancer | OCI Network Load Balancer | | TLS termination | Optional TLS listener | Backend termination | Backend termination | Backend termination | | Static addressing | Elastic IP | Regional reserved address | Static Standard public IP | OCI reserved addresses | | Client address | Client-IP preservation | Client IP preserved | Client IP preserved | Client IP preserved | | Health checks | Per target group | Per backend service | TCP or HTTP(S) probes | Per backend set | | Client endpoint | AWS hostname | Target cloud address | Target cloud address | Target cloud address | | API coverage | full | partial | partial | partial | ### Infrastructure-only adaptation | Capability | Network Load Balancer | Google Cloud | OCI | | ----------------- | ------------------------- | ---------------------------------------- | ------------------------- | | Traffic path | AWS network load balancer | Google passthrough Network Load Balancer | OCI Network Load Balancer | | TLS termination | Optional TLS listener | Backend termination | Backend termination | | Static addressing | Elastic IP | Regional reserved address | OCI reserved addresses | | Client address | Client-IP preservation | Client IP preserved | Client IP preserved | | Health checks | Per target group | Per backend service | Per backend set | | Client endpoint | AWS hostname | Target cloud address | Target cloud address | | API coverage | full | partial | partial | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | ---------------------- | ---------- | ------------ | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Client IP preservation | Addressing | Supported | Most usage | Passthrough forwarding preserves the client source address at the backend. | | Static addresses | Addressing | Supported | Most usage | Google regional reserved addresses provide a stable frontend. | | Health checks | Backends | Supported | Common | Native health checks belong to each backend service. | | Load balancer address | Consumers | Supported | Common | Clients use the Google address. Supported references in an infrastructure translation are updated during the build. | | Traffic forwarding | Data plane | Supported | Common | Google's passthrough load balancer forwards client traffic directly to the backends. | | TCP / UDP listeners | Listeners | Supported | Common | TCP and UDP listeners map to forwarding rules. L3\_DEFAULT supports combined traffic but forwards all supported protocols on all ports; firewall rules must enforce the intended access. | | TLS listeners | Listeners | Out of scope | Most usage | This passthrough target does not terminate TLS; the backend must terminate the TCP-carried TLS connection. | #### Managing the load balancer Your application and Terraform use the AWS Elastic Load Balancing API through the adapter. The adapter keeps AWS-shaped load balancer, listener, and target-group identities, records configuration changes, and applies them to Google passthrough Network Load Balancer. A target group is the set of backends that a listener forwards traffic to. Management changes and traffic take different paths. The adapter handles calls such as `CreateLoadBalancer`, `CreateListener`, and `RegisterTargets`. The cloud load balancer receives client connections and forwards them to your application. A successful management request can precede completion of the cloud change. Wait for the load balancer to become available and check the native resources before sending production traffic. The adapter retains the requested configuration while background work applies it.
AWS management calls go through the adapter to cloud configuration. Client traffic goes through the cloud load balancer to application backends. AWS management calls go through the adapter to cloud configuration. Client traffic goes through the cloud load balancer to application backends.
#### Regional forwarding resources The load balancer uses a reserved address, forwarding rules, a regional backend service, and regional health checks. Each TCP or UDP listener becomes a forwarding rule with its protocol and port. A combined TCP+UDP listener can use L3\_DEFAULT, which forwards all supported protocols on all ports. That forwarding rule is broader than the source listener; firewall rules must restrict traffic to the intended protocols and ports. A public frontend uses a regional external address; an internal frontend uses an address on the target subnet. The backend service can contain backends across the region's zones. Confirm their placement and connectivity rather than assuming AWS availability-zone identifiers transfer. #### Client addresses and TLS Passthrough forwarding preserves the client's source IP at the backend. Application allowlists and logs can read that address without X-Forwarded-For or a proxy-protocol header. A connection is assigned using its source and destination addresses, source and destination ports, and protocol. The target does not terminate TLS. An AWS TLS listener uses TCP passthrough, with TLS terminated by your backend. Configure the certificate there before switching traffic. #### Health checks and observability The target supports TCP, HTTP, HTTPS, SSL, and gRPC probes. Health checks belong to the backend service and use the configured path, port, interval, timeout, and thresholds. Permit both application traffic and health-check traffic in the target firewall. Flow and backend-service logs describe network connections. They do not reproduce AWS TLS access logs or application request logs; collect request-level information in the backend when required. #### Target registration and readiness `RegisterTargets` and `DeregisterTargets` update target-group membership. Instance registrations resolve through the adapter's EC2 inventory; IP registrations identify the backend address and port. The target group, load balancer, and backend must belong to a compatible network. The adapter applies a listener after its target group and network dependencies are ready. Native health checks then determine which backends receive traffic. Verify the provider's health status and test an application request: a registered target alone does not establish that its application is ready. `DescribeTargetHealth` reports registration and zone eligibility from the adapter's saved state. Its `healthy` result does not confirm that the native load balancer's probe succeeded. Check native backend health and a complete application request before sending production traffic. Drain connections before deregistering backends or deleting a load balancer. Removing configuration does not transfer active connections to a replacement. #### Deployment and cutover Deploy the adapter with permission to manage load balancers and their network dependencies in the customer's cloud account. AWS-facing credentials authorize management calls; the adapter uses the target cloud's credentials to apply changes. Configure public or private exposure, frontend access rules, backend access, and health-check access together. Prepare DNS and firewall allowlists for the new address. In an infrastructure-only deployment, references inside the translated stack are updated during the build; external DNS and clients still need a cutover. At Max, the adapter also maintains the AWS resource identities used by management calls. Check certificates, routing, native health checks, and a complete client request before changing DNS. Keep the old load balancer available while existing connections drain. Application sessions and active connections are not copied by this adapter. ## On Azure | Operation | Area | Support | Depth | Notes | | ---------------------- | ------------- | ------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Client IP preservation | Addressing | Supported | Most usage | The backend receives the original client source address. | | Static addresses | Addressing | Supported | Most usage | Static Standard public addresses provide a stable frontend. | | Health checks | Backends | Supported | Common | TCP or HTTP(S) probes are associated with load-balancing rules. | | Access logs | Configuration | Out of scope | Full surface | Azure Monitor provides metrics and health-event logs; this target does not provide AWS-style per-request access logs. | | Load balancer address | Consumers | Supported | Common | Clients use the Azure address. Supported references in an infrastructure translation are updated during the build. | | Traffic forwarding | Data plane | Supported | Common | Azure Standard Load Balancer forwards traffic directly to the backends. | | TCP / UDP listeners | Listeners | Supported | Common | TCP and UDP use native rules. Preserving a combined listener on one port requires rules for both protocols; Azure All is an internal HA-ports mode, not a port-scoped public rule. | | TLS listeners | Listeners | Out of scope | Most usage | Standard Load Balancer does not terminate TLS; pass the connection over TCP to a backend that terminates it. | #### Managing the load balancer Your application and Terraform use the AWS Elastic Load Balancing API through the adapter. The adapter keeps AWS-shaped load balancer, listener, and target-group identities, records configuration changes, and applies them to Azure Standard Load Balancer. A target group is the set of backends that a listener forwards traffic to. Management changes and traffic take different paths. The adapter handles calls such as `CreateLoadBalancer`, `CreateListener`, and `RegisterTargets`. The cloud load balancer receives client connections and forwards them to your application. A successful management request can precede completion of the cloud change. Wait for the load balancer to become available and check the native resources before sending production traffic. The adapter retains the requested configuration while background work applies it.
AWS management calls go through the adapter to cloud configuration. Client traffic goes through the cloud load balancer to application backends. AWS management calls go through the adapter to cloud configuration. Client traffic goes through the cloud load balancer to application backends.
#### Frontend, rules, and backend pool The Standard Load Balancer has a frontend address, a rule per listener, a backend address pool, and health probes. TCP and UDP listeners retain their protocol and port. Preserving a combined TCP+UDP listener on one port requires separate rules for the two protocols. Azure All-protocol rules are HA-port rules for an internal load balancer and cover all ports; they are not a port-scoped public listener. A public frontend uses a static Standard public IP. An internal frontend uses a private subnet address. A zone-redundant deployment can serve backends across the region's zones; verify the selected frontend and backend placement for the customer's network. #### Client addresses and TLS Azure forwards traffic while preserving the client's source IP. Connection distribution uses the source and destination addresses, source and destination ports, and protocol. The backend can use the original address for access control and logging. Standard Load Balancer does not terminate TLS. An AWS TLS listener becomes TCP passthrough and your backend must terminate TLS. Prepare that endpoint and its certificate before cutover. #### Health probes and logs TCP, HTTP, or HTTPS probes are associated with load-balancing rules. The mapping preserves the applicable interval, port, path, and failure threshold. Allow AzureLoadBalancer health traffic as well as the intended client traffic in the backend network security rules. Azure Monitor provides metrics and health-event logs. Standard Load Balancer does not provide AWS-style per-request access logs; collect application request details at the backend. #### Target registration and readiness `RegisterTargets` and `DeregisterTargets` update target-group membership. Instance registrations resolve through the adapter's EC2 inventory; IP registrations identify the backend address and port. The target group, load balancer, and backend must belong to a compatible network. The adapter applies a listener after its target group and network dependencies are ready. Native health checks then determine which backends receive traffic. Verify the provider's health status and test an application request: a registered target alone does not establish that its application is ready. `DescribeTargetHealth` reports registration and zone eligibility from the adapter's saved state. Its `healthy` result does not confirm that the native load balancer's probe succeeded. Check native backend health and a complete application request before sending production traffic. Drain connections before deregistering backends or deleting a load balancer. Removing configuration does not transfer active connections to a replacement. #### Deployment and cutover Deploy the adapter with permission to manage load balancers and their network dependencies in the customer's cloud account. AWS-facing credentials authorize management calls; the adapter uses the target cloud's credentials to apply changes. Configure public or private exposure, frontend access rules, backend access, and health-check access together. Prepare DNS and firewall allowlists for the new address. In an infrastructure-only deployment, references inside the translated stack are updated during the build; external DNS and clients still need a cutover. At Max, the adapter also maintains the AWS resource identities used by management calls. Check certificates, routing, native health checks, and a complete client request before changing DNS. Keep the old load balancer available while existing connections drain. Application sessions and active connections are not copied by this adapter. ## On OCI | Operation | Area | Support | Depth | Notes | | ------------------------- | ------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Client IP preservation | Addressing | Supported | Most usage | The backend receives the original client source address. | | Static addresses | Addressing | Supported | Most usage | OCI reserved addresses provide a stable frontend. | | Health checks | Backends | Supported | Common | Checks use the path, expected status, timeout, interval, and failure threshold. | | Target registration | Backends | Partial | Most usage | Register the native backends through the adapter; AWS registrations are not copied. | | Cross-zone load balancing | Configuration | Supported | Full surface | The target is regional; backend placement spans the selected regional network. | | Network placement | Configuration | Partial | Full surface | The target subnet arrangement replaces AWS multi-subnet placement. Verify public or private exposure. | | Load balancer address | Consumers | Partial | Common | Clients use the target OCI load-balancer address. The original AWS-managed hostname is not retained; infrastructure references to AWS-specific identifiers without a target equivalent are rejected or removed from outputs with a reported issue. | | Traffic forwarding | Data plane | Supported | Common | OCI Network Load Balancer forwards traffic to the application backends. | | TCP / UDP listeners | Listeners | Supported | Common | TCP, UDP, and mixed TCP+UDP listeners map to native OCI listeners. | | TLS listeners | Listeners | Out of scope | Most usage | The network load balancer does not terminate TLS. Use TCP passthrough with TLS terminated by the backend. | #### Managing the load balancer Your application and Terraform use the AWS Elastic Load Balancing API through the adapter. The adapter keeps AWS-shaped load balancer, listener, and target-group identities, records configuration changes, and applies them to OCI Network Load Balancer. A target group is the set of backends that a listener forwards traffic to. Management changes and traffic take different paths. The adapter handles calls such as `CreateLoadBalancer`, `CreateListener`, and `RegisterTargets`. The cloud load balancer receives client connections and forwards them to your application. A successful management request can precede completion of the cloud change. Wait for the load balancer to become available and check the native resources before sending production traffic. The adapter retains the requested configuration while background work applies it.
AWS management calls go through the adapter to cloud configuration. Client traffic goes through the cloud load balancer to application backends. AWS management calls go through the adapter to cloud configuration. Client traffic goes through the cloud load balancer to application backends.
#### Listeners, addresses, and placement TCP, UDP, and mixed TCP+UDP listeners map to native OCI listeners and backend sets. Reserved IP addresses provide a stable frontend, and the target preserves the client's source address at the backend. The network load balancer is regional. Its public or private subnet arrangement replaces the AWS multi-subnet layout; verify the selected network, frontend exposure, backend rules, and health-check access. #### TLS and health checks OCI Network Load Balancer does not terminate TLS. The TLS listener maps to TCP passthrough, and the backend must terminate the encrypted connection. This is different from provisioning an HTTP listener on an application load balancer. Health checks use the path, expected status, timeout, interval, and failure threshold. Test those checks and a client connection before relying on a newly registered backend. #### Endpoint changes Clients use the OCI address rather than the AWS load balancer hostname. Update external DNS and allowlists. In the infrastructure translation, an AWS-specific reference without a target equivalent causes a build error. The Max adapter retains AWS-shaped management identities separately from native resource identifiers. #### Target registration and readiness `RegisterTargets` and `DeregisterTargets` update target-group membership. Instance registrations resolve through the adapter's EC2 inventory; IP registrations identify the backend address and port. The target group, load balancer, and backend must belong to a compatible network. The adapter applies a listener after its target group and network dependencies are ready. Native health checks then determine which backends receive traffic. Verify the provider's health status and test an application request: a registered target alone does not establish that its application is ready. `DescribeTargetHealth` reports registration and zone eligibility from the adapter's saved state. Its `healthy` result does not confirm that the native load balancer's probe succeeded. Check native backend health and a complete application request before sending production traffic. Drain connections before deregistering backends or deleting a load balancer. Removing configuration does not transfer active connections to a replacement. #### Deployment and cutover Deploy the adapter with permission to manage load balancers and their network dependencies in the customer's cloud account. AWS-facing credentials authorize management calls; the adapter uses the target cloud's credentials to apply changes. Configure public or private exposure, frontend access rules, backend access, and health-check access together. Prepare DNS and firewall allowlists for the new address. In an infrastructure-only deployment, references inside the translated stack are updated during the build; external DNS and clients still need a cutover. At Max, the adapter also maintains the AWS resource identities used by management calls. Check certificates, routing, native health checks, and a complete client request before changing DNS. Keep the old load balancer available while existing connections drain. Application sessions and active connections are not copied by this adapter. [Service Catalog](/service-adapters/catalog). # Route 53 Source: https://docs.tensor9.com/service-adapters/aws/networking-traffic/route-53 AWS Route 53. Authoritative DNS hosting with public and private hosted zones, health-checked failover, latency and weighted routing policies, plus domain registration. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) * [Via Route 53 (any cloud)](#via-route-53-any-cloud) * [Via Cloudflare DNS](#via-cloudflare-dns) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of Route 53 with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | Route 53 | Google Cloud | Azure | OCI | Private Kubernetes · Route 53 (any cloud) | Private Kubernetes · Cloudflare DNS | | ---------------------------------- | ------------------------------ | ------------------------------------------------------------- | -------------------------------------------- | --------------------------------------------- | ----------------------------------------- | ----------------------------------- | | Query serving · the data plane | Route 53 authoritative servers | Cloud DNS servers | Azure DNS servers | OCI DNS servers | the same Route 53 servers | Cloudflare authoritative servers | | Record changes · the control plane | Route 53 API | Max: adapter; infrastructure: native resources | adapter → Azure DNS API | adapter → OCI DNS API | adapter → Route 53 API | adapter → Cloudflare API | | Hosted zones · create vs look up | created by your stack | Max: public-zone lifecycle; infrastructure: create or look up | public-zone lifecycle + existing-zone lookup | looked up, must already exist | looked up, must already exist | looked up, must already exist | | Record types · supported surface | full record surface | Max: 17 types including SPF; infrastructure: SPF becomes TXT | 10 supported runtime types | 14 ordinary types; provider-managed authority | all standard types | all standard types (except SPF) | | API coverage | full | minimal | minimal | minimal | partial | partial | ### Infrastructure-only adaptation | Capability | Route 53 | Google Cloud | | ---------------------------------- | ------------------------------ | ------------------------------------------------------------- | | Query serving · the data plane | Route 53 authoritative servers | Cloud DNS servers | | Record changes · the control plane | Route 53 API | Max: adapter; infrastructure: native resources | | Hosted zones · create vs look up | created by your stack | Max: public-zone lifecycle; infrastructure: create or look up | | Record types · supported surface | full record surface | Max: 17 types including SPF; infrastructure: SPF becomes TXT | | API coverage | full | minimal | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------------------- | ----------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DNS query serving | Data plane | Supported | Common | Cloud DNS authoritative servers answer DNS queries directly. Public delegation must identify the selected provider. | | Health checks | Health & resolver | Out of scope | Most usage | The infrastructure mapping omits standalone Route 53 health checks and reports the omission; it does not configure Cloud DNS health-based routing. | | Resolver (endpoints, rules, DNSSEC, firewall, query logs) | Health & resolver | Out of scope | Full surface | Outside this zones-and-records mapping. See the separate Resolver mapping for hybrid DNS; unsupported standalone resources stop the build. | | Alias records | Records | Partial | Most usage | Infrastructure references resolve to the replacement resource address, with omitted target-health evaluation reported. Runtime AliasTarget requests are unsupported; Cloud DNS native ALIAS records do not establish Route 53 alias support. | | Multi-value record sets | Records | Supported | Most usage | All values of an ordinary record set are retained. This does not reproduce weighted or health-checked routing. | | Record management | Records | Supported | Common | The adapter applies supported record creates, updates and deletes through the Cloud DNS API. Deployment configuration defaults an unset TTL to 300 seconds. | | Record types | Records | Supported | Common | The Max runtime supports 17 record types, including SPF. Infrastructure-only SPF declarations become TXT. | | Runtime record changes | Records | Supported | Most usage | The application uses the Route 53 API through the adapter, which holds the provider credential. PENDING becomes INSYNC after target application; this does not certify global DNS propagation or cache expiry. | | Routing policies (weighted / latency / failover / geolocation / multivalue) | Routing | Out of scope | Most usage | Outside this mapping. Infrastructure translation can retain ordinary record data while reporting omitted routing behavior; a native target routing service is not configured by that omission. | | Delegation sets | Zones | Out of scope | Full surface | Reusable AWS delegation sets are outside this mapping; unsupported resource declarations stop the build. | | Hosted zones | Zones | Supported | Common | Infrastructure supports native zone creation and managed-zone-name lookup. The runtime adapter creates and deletes managed public zones and returns assigned authority. | | Private hosted zones | Zones | Supported | Most usage | The infrastructure mapping creates a private managed zone attached to the target VPC network. The runtime public-zone lifecycle does not manage private zones. | #### How it works Tensor9 translates Route 53 zones and records into Cloud DNS managed zones and record sets in the customer's Google Cloud project. Google's authoritative servers answer DNS queries. Applications use the Route 53 API through the adapter for supported runtime zone and record changes.
An application changes records through Tensor9; the adapter applies the change using its provider credential. An application changes records through Tensor9; the adapter applies the change using its provider credential.

Record changes use the adapter; DNS queries go to the authoritative provider.

#### Zones and records With Infrastructure-only adaptation, a declared zone becomes a managed zone; an explicit lookup uses an existing Cloud DNS managed-zone name, which can differ from the DNS domain. The infrastructure mapping supports private zones attached to the target VPC network. The runtime adapter manages its own public zones and reports Google-assigned authority and nameservers. Looking up an existing zone does not transfer its ownership to the adapter. Do not have another controller write the same zone; a missing or replaced previously observed zone requires explicit repair. The Max runtime supports A, AAAA, CAA, CNAME, DS, HTTPS, MX, NAPTR, NS, PTR, SOA, SPF, SRV, SSHFP, SVCB, TLSA and TXT, retaining all values in an ordinary record set. Publishing DNSSEC-related records does not configure signing or key management. Infrastructure-only adaptation converts deprecated `SPF` declarations to `TXT` and defaults an unset TTL to 300 seconds. #### Aliases and routing An infrastructure alias follows a referenced resource to its target-cloud address. Cloud DNS also offers an apex ALIAS type for public zones, but that native capability is separate from resolving a resource reference during deployment. Route 53 target-health evaluation is not retained. Runtime `AliasTarget` requests are unsupported. Weighted, latency, failover, geolocation and related Route 53 routing policies are outside this mapping. Deployment can retain a record's name, type and values while reporting the omitted policy. Cloud DNS has native routing policies, but the presence of a native feature does not configure the requested AWS behavior. Ordinary multiple-value records do not reproduce health-checked or weighted routing. #### Runtime changes Your application sends Route 53 API requests to the configured adapter. The adapter records the requested change and applies it with the provider credential it holds. `ChangeResourceRecordSets` returns a change receipt; `GetChange` reports `PENDING` until target application completes, then `INSYNC`. Here, `INSYNC` reports target application. It does not guarantee that every authoritative server has observed the change or that recursive caches have expired. Keep polling for completion and allow for DNS caching during cutover. Google applies a native Changes batch atomically. The adapter still observes target completion before finishing a receipt. Public-zone lifecycle, authority readback and record changes use the same managed zone; private-zone infrastructure support does not imply the same runtime lifecycle for private zones. #### Remaining limits Standalone Route 53 health checks are omitted and reported by the Cloud DNS infrastructure mapping. Delegation sets and Resolver resources are outside the zones-and-records mapping. The separate [Resolver page](/service-adapters/aws/networking-traffic/route-53-resolver) describes hybrid DNS. Confirm network DNS access for a private zone and update external consumers of AWS zone IDs or nameservers. A new public zone requires delegation to its Google nameservers. Runtime reconciliation requires the native record inventory and change history to fit in one response each; it refuses paged native results. This limit is separate from pagination of the Route 53 record-list API. Keep Google's assigned apex nameservers. Supported apex TTL and SOA updates do not permit reassignment of those nameservers or standalone deletion of apex authority records. DS records at the zone apex and SOA records below it are refused. Remove ordinary records before deleting the zone. #### Cutover and operations Apply the required records, verify authoritative answers, and then update public nameserver delegation where the provider changes. Keep the previous zone available while cached records and delegation expire. This process applies declared records; it does not copy an entire existing zone or its query history. Record changes require the appliance and its provider credentials. Once applied, records remain available from the authoritative provider while the appliance is unavailable. Certificate-validation records are coordinated with certificate management; review external DNS controllers and any credentials they previously used. ## On Azure | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------------------- | ----------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DNS query serving | Data plane | Supported | Common | Azure DNS authoritative servers answer DNS queries directly. Public delegation must identify the selected provider. | | Health checks | Health & resolver | Out of scope | Most usage | Standalone Route 53 health-check resources are outside this mapping and stop the build. | | Resolver (endpoints, rules, DNSSEC, firewall, query logs) | Health & resolver | Out of scope | Full surface | Outside this zones-and-records mapping. See the separate Resolver mapping for hybrid DNS; unsupported standalone resources stop the build. | | Alias records | Records | Out of scope | Most usage | Route 53 aliases are outside this mapping. Azure native aliases target supported Azure resources, not arbitrary apex hostnames; a resolved ordinary address record is a different behavior. | | Multi-value record sets | Records | Supported | Most usage | All values of an ordinary record set are retained. This does not reproduce weighted or health-checked routing. | | Record management | Records | Supported | Common | The adapter applies supported record creates, updates and deletes through the Azure DNS API. Deployment configuration defaults an unset TTL to 300 seconds. | | Record types | Records | Supported | Common | The runtime target supports A, AAAA, CAA, CNAME, MX, NS, PTR, SOA, SRV and TXT. It rejects unsupported types; infrastructure SPF-to-TXT conversion is a separate behavior. | | Runtime record changes | Records | Supported | Most usage | The application uses the Route 53 API through the adapter, which holds the provider credential. PENDING becomes INSYNC after target application; this does not certify global DNS propagation or cache expiry. | | Routing policies (weighted / latency / failover / geolocation / multivalue) | Routing | Out of scope | Most usage | Outside this mapping. Infrastructure translation can retain ordinary record data while reporting omitted routing behavior; a native target routing service is not configured by that omission. | | Delegation sets | Zones | Out of scope | Full surface | Reusable AWS delegation sets are outside this mapping; unsupported resource declarations stop the build. | | Hosted zones | Zones | Partial | Common | The runtime adapter creates and deletes managed public zones. Deployment-time lookups require an existing zone and do not transfer it to runtime ownership. The subscription and resource group must already exist. | | Private hosted zones | Zones | Out of scope | Most usage | Private-zone configuration is outside this selected public-zone mapping. A VPC-associated declaration stops the build. | #### How it works Tensor9 applies Route 53 records to Azure DNS in the customer's subscription and resource group. Azure serves authoritative queries. The adapter also provides the Route 53 public-zone lifecycle and record-change API, with Azure-assigned authority returned to the application.
An application changes records through Tensor9; the adapter applies the change using its provider credential. An application changes records through Tensor9; the adapter applies the change using its provider credential.

Record changes use the adapter; DNS queries go to the authoritative provider.

#### Zones and record ownership A deployment-time zone lookup requires an existing Azure DNS zone; it does not transfer that zone to runtime ownership. The runtime adapter can create and delete the public zone it manages; its subscription and resource group must already exist. Do not share write ownership of that zone with another controller. An unrelated zone with the same name is not automatically adopted. Two logical zones with the same DNS name need separate Azure placement. The runtime target supports `A`, `AAAA`, `CAA`, `CNAME`, `MX`, `NS`, `PTR`, `SOA`, `SRV` and `TXT`. It retains Azure-assigned apex nameservers and the SOA primary. Other supported SOA fields and authority TTLs can change. Standalone deletion of apex authority records is refused; remove ordinary records before deleting the hosted zone. Record types outside the selected API, private zones and native aliases remain outside this runtime contract. #### Aliases and routing Azure native aliases target supported Azure resources; they do not provide an arbitrary-hostname alias at a zone apex. This mapping does not reproduce Route 53 aliases or their target-health evaluation. A resolved infrastructure address can instead be an ordinary address record; that does not preserve ongoing alias behavior. Route 53 routing policies are outside this mapping. Deployment may retain ordinary record values while reporting the omitted policy. Traffic Manager provides separate Azure traffic-routing features and must be configured explicitly when that behavior is required. Multiple values alone do not reproduce a weighted or health-checked answer. #### Runtime changes Your application sends Route 53 API requests to the configured adapter. The adapter records the requested change and applies it with the provider credential it holds. `ChangeResourceRecordSets` returns a change receipt; `GetChange` reports `PENDING` until target application completes, then `INSYNC`. Here, `INSYNC` reports target application. It does not guarantee that every authoritative server has observed the change or that recursive caches have expired. Keep polling for completion and allow for DNS caching during cutover. The requested change batch is recorded together, but Azure applies individual record sets separately. Intermediate answers can therefore contain part of a multi-record change. Conditional writes detect changes made by another writer. Plan related record changes so that this transition is acceptable. #### Remaining limits Health-check resources, delegation sets and Resolver resources are outside this zone-and-record mapping. Azure Private DNS is a separate target service; this public-zone lifecycle does not create a private zone. The runtime API rejects unsupported DNS types rather than changing their meaning. An infrastructure `SPF`-to-`TXT` conversion does not imply runtime `SPF` support. Native record inventory must fit in one response. The adapter refuses incomplete inventory before destructive reconciliation. Runtime record names use bounded ASCII DNS labels; escaped or binary labels and non-UTF-8 TXT/CAA data are unsupported. If a previously observed zone disappears, repair its association explicitly; the adapter does not automatically allocate new authority. Ownership markers help detect unrelated zones and records, but another writer can copy those markers. Keep write ownership exclusive to the adapter. #### Cutover and operations Apply the required records, verify authoritative answers, and then update public nameserver delegation where the provider changes. Keep the previous zone available while cached records and delegation expire. This process applies declared records; it does not copy an entire existing zone or its query history. Record changes require the appliance and its provider credentials. Once applied, records remain available from the authoritative provider while the appliance is unavailable. Certificate-validation records are coordinated with certificate management; review external DNS controllers and any credentials they previously used. ## On OCI | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------------------- | ----------------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DNS query serving | Data plane | Supported | Common | OCI DNS authoritative servers answer DNS queries directly. Public delegation must identify the selected provider. | | Health checks | Health & resolver | Out of scope | Most usage | Standalone Route 53 health-check resources are outside this mapping and stop the build. | | Resolver (endpoints, rules, DNSSEC, firewall, query logs) | Health & resolver | Out of scope | Full surface | Outside this zones-and-records mapping. See the separate Resolver mapping for hybrid DNS; unsupported standalone resources stop the build. | | Alias records | Records | Partial | Most usage | Infrastructure references resolve to the replacement resource address. Route 53 target-health evaluation is omitted and reported; native apex-alias behavior depends on the selected provider. | | Multi-value record sets | Records | Supported | Most usage | All values of an ordinary record set are retained. This does not reproduce weighted or health-checked routing. | | Record management | Records | Supported | Common | The adapter applies supported record creates, updates and deletes through the OCI DNS API. Deployment configuration defaults an unset TTL to 300 seconds. | | Record types | Records | Supported | Common | A, AAAA, CAA, CNAME, DS, MX, NAPTR, NS, PTR, SPF, SRV, SSHFP, TLSA and TXT. OCI maintains the zone’s SOA and assigned apex nameservers. HTTPS and SVCB are not included in this documented OCI mapping. An unrecognized type stops the deploy with a clear error. | | Runtime record changes | Records | Supported | Most usage | The application uses the Route 53 API through the adapter, which holds the provider credential. PENDING becomes INSYNC after target application; this does not certify global DNS propagation or cache expiry. | | Routing policies (weighted / latency / failover / geolocation / multivalue) | Routing | Out of scope | Most usage | Outside this mapping. Infrastructure translation can retain ordinary record data while reporting omitted routing behavior; a native target routing service is not configured by that omission. | | Delegation sets | Zones | Out of scope | Full surface | Reusable AWS delegation sets are outside this mapping; unsupported resource declarations stop the build. | | Hosted zones | Zones | Partial | Common | The zone must already exist at OCI DNS and is looked up by domain name; this mapping manages records within it. | | Private hosted zones | Zones | Out of scope | Most usage | Private-zone configuration is outside this selected public-zone mapping. A VPC-associated declaration stops the build. | #### How it works Tensor9 applies declared Route 53 records to an existing OCI DNS zone in the customer's compartment. The appliance holds the OCI credential. OCI authoritative servers answer public DNS queries directly.
An application changes records through Tensor9; the adapter applies the change using its provider credential. An application changes records through Tensor9; the adapter applies the change using its provider credential.

Record changes use the adapter; DNS queries go to the authoritative provider.

#### Zones, records and aliases The zone is looked up by domain name and must already exist. The documented record types are A, AAAA, CAA, CNAME, DS, MX, NAPTR, NS, PTR, SPF, SRV, SSHFP, TLSA and TXT. All values in an ordinary record set are retained; an unset deployment TTL defaults to 300 seconds. OCI retains the deprecated `SPF` type and maintains the zone’s SOA and assigned apex nameservers. `HTTPS` and `SVCB` are not included in this documented OCI mapping. An infrastructure alias resolves a resource reference to its target-cloud address. OCI also offers a native public-zone ALIAS pseudo-type at the apex; that capability is distinct from resolving a deployment reference. Route 53 alias target-health evaluation is omitted. Multiple address values do not by themselves provide health-checked routing. #### Runtime changes Your application sends Route 53 API requests to the configured adapter. The adapter records the requested change and applies it with the provider credential it holds. `ChangeResourceRecordSets` returns a change receipt; `GetChange` reports `PENDING` until target application completes, then `INSYNC`. Here, `INSYNC` reports target application. It does not guarantee that every authoritative server has observed the change or that recursive caches have expired. Keep polling for completion and allow for DNS caching during cutover. #### Routing and unsupported resources The record mapping does not configure OCI traffic-steering policies. Route 53 routing policies are omitted and reported while ordinary record data can still be applied. OCI has separate steering and health-check services, but their existence does not activate a matching AWS policy. A standalone Route 53 health-check resource stops the build for this target. Private zones, delegation sets and Resolver resources are also outside this zones-and-records mapping. OCI private DNS and the separate Resolver mapping have their own configuration; these limits do not mean OCI lacks private DNS. #### Cutover and operations Apply the required records, verify authoritative answers, and then update public nameserver delegation where the provider changes. Keep the previous zone available while cached records and delegation expire. This process applies declared records; it does not copy an entire existing zone or its query history. Record changes require the appliance and its provider credentials. Once applied, records remain available from the authoritative provider while the appliance is unavailable. Certificate-validation records are coordinated with certificate management; review external DNS controllers and any credentials they previously used. ## On Private Kubernetes ### Via Route 53 (any cloud) | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------------------- | ----------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DNS query serving | Data plane | Supported | Common | Route 53 authoritative servers answer DNS queries directly. Public delegation must identify the selected provider. | | Health checks | Health & resolver | Out of scope | Most usage | Standalone Route 53 health-check resources are outside this mapping and stop the build. | | Resolver (endpoints, rules, DNSSEC, firewall, query logs) | Health & resolver | Out of scope | Full surface | Outside this zones-and-records mapping. See the separate Resolver mapping for hybrid DNS; unsupported standalone resources stop the build. | | Alias records | Records | Partial | Most usage | Infrastructure references resolve to the replacement resource address. Route 53 target-health evaluation is omitted and reported; native apex-alias behavior depends on the selected provider. | | Multi-value record sets | Records | Supported | Most usage | All values of an ordinary record set are retained. This does not reproduce weighted or health-checked routing. | | Record management | Records | Supported | Common | The adapter applies supported record creates, updates and deletes through the Route 53 API. Deployment configuration defaults an unset TTL to 300 seconds. | | Record types | Records | Supported | Common | all standard record types; Route 53 keeps the SPF type rather than folding it into TXT; an unrecognized type stops the deploy with a clear error | | Runtime record changes | Records | Supported | Most usage | The application uses the Route 53 API through the adapter, which holds the provider credential. PENDING becomes INSYNC after target application; this does not certify global DNS propagation or cache expiry. | | Routing policies (weighted / latency / failover / geolocation / multivalue) | Routing | Out of scope | Most usage | Outside this mapping. Infrastructure translation can retain ordinary record data while reporting omitted routing behavior; a native target routing service is not configured by that omission. | | Delegation sets | Zones | Out of scope | Full surface | Reusable AWS delegation sets are outside this mapping; unsupported resource declarations stop the build. | | Hosted zones | Zones | Partial | Common | The zone must already exist at Route 53 and is looked up by domain name; this mapping manages records within it. | | Private hosted zones | Zones | Out of scope | Most usage | Private-zone configuration is outside this selected public-zone mapping. A VPC-associated declaration stops the build. | #### How it works Your workload can run in the customer's Kubernetes environment while its public DNS zone remains in Route 53. Tensor9 applies declared records through the Route 53 API using an AWS credential supplied to the appliance. AWS continues serving the zone; nameserver delegation can remain unchanged.
An application changes records through Tensor9; the adapter applies the change using its provider credential. An application changes records through Tensor9; the adapter applies the change using its provider credential.

Record changes use the adapter; DNS queries go to the authoritative provider.

#### Zone and record configuration The hosted zone must already exist. The mapping resolves it by domain name, using an explicitly specified zone before the zone reference or deployment default domain. It writes the declared records into that zone; it does not create or copy the entire zone. Standard record types, including native `SPF`, and all values in an ordinary record set are retained. An unset deployment TTL defaults to 300 seconds. An infrastructure alias follows its referenced resource to the target-cloud address, but target-health evaluation is omitted. #### Runtime changes Your application sends Route 53 API requests to the configured adapter. The adapter records the requested change and applies it with the provider credential it holds. `ChangeResourceRecordSets` returns a change receipt; `GetChange` reports `PENDING` until target application completes, then `INSYNC`. Here, `INSYNC` reports target application. It does not guarantee that every authoritative server has observed the change or that recursive caches have expired. Keep polling for completion and allow for DNS caching during cutover. #### Limits of the record mapping Keeping Route 53 as the provider does not preserve every Route 53 configuration feature. This mapping applies ordinary records and omits source traffic-routing policies with a reported warning. Multiple address values do not reproduce weighted, latency, failover or health-checked routing. Health-check resources, delegation sets and Resolver resources stop the build in this mapping. Private-zone lookup is also outside its contract: a domain-only lookup cannot safely distinguish public and private zones with the same name. These are mapping limits, not restrictions of Route 53 itself. #### Operations Scope the appliance's AWS credential to the intended zone and protect its renewal process. Application calls use the configured adapter endpoint; the application does not need the provider credential held by the appliance. Check records against the new workload addresses before cutover. AWS can continue answering applied records while the appliance is unavailable, but further changes require the appliance and working credentials. Preserve external controllers and certificate-validation behavior only where they remain compatible with the selected record ownership. ### Via Cloudflare DNS | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------------------- | ----------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DNS query serving | Data plane | Supported | Common | Cloudflare authoritative servers answer DNS queries directly. Public delegation must identify the selected provider. | | Health checks | Health & resolver | Out of scope | Most usage | Standalone Route 53 health-check resources are outside this mapping and stop the build. | | Resolver (endpoints, rules, DNSSEC, firewall, query logs) | Health & resolver | Out of scope | Full surface | Outside this zones-and-records mapping. See the separate Resolver mapping for hybrid DNS; unsupported standalone resources stop the build. | | Alias records | Records | Partial | Most usage | Infrastructure references resolve to the replacement resource address. Route 53 target-health evaluation is omitted and reported; native apex-alias behavior depends on the selected provider. | | Multi-value record sets | Records | Supported | Most usage | All values of an ordinary record set are retained. This does not reproduce weighted or health-checked routing. | | Record management | Records | Supported | Common | The adapter applies supported record creates, updates and deletes through the Cloudflare API. Deployment configuration defaults an unset TTL to 300 seconds. | | Record types | Records | Supported | Common | all standard record types, except the deprecated SPF type (which folds into TXT); an unrecognized type stops the deploy with a clear error | | Runtime record changes | Records | Supported | Most usage | The application uses the Route 53 API through the adapter, which holds the provider credential. PENDING becomes INSYNC after target application; this does not certify global DNS propagation or cache expiry. | | Routing policies (weighted / latency / failover / geolocation / multivalue) | Routing | Out of scope | Most usage | Outside this mapping. Infrastructure translation can retain ordinary record data while reporting omitted routing behavior; a native target routing service is not configured by that omission. | | Delegation sets | Zones | Out of scope | Full surface | Reusable AWS delegation sets are outside this mapping; unsupported resource declarations stop the build. | | Hosted zones | Zones | Partial | Common | The zone must already exist at Cloudflare and is looked up by domain name; this mapping manages records within it. | | Private hosted zones | Zones | Out of scope | Most usage | Private-zone configuration is outside this selected public-zone mapping. A VPC-associated declaration stops the build. | #### How it works Tensor9 applies declared Route 53 records to an existing Cloudflare zone. The appliance holds the Cloudflare credential and handles supported application record-change requests. Cloudflare's authoritative DNS servers answer public queries.
An application changes records through Tensor9; the adapter applies the change using its provider credential. An application changes records through Tensor9; the adapter applies the change using its provider credential.

Record changes use the adapter; DNS queries go to the authoritative provider.

#### Zones and records Onboard the domain to Cloudflare and create the zone before deployment. The mapping looks it up by domain name. Standard record types and all values in an ordinary record set are retained; an unset deployment TTL defaults to 300 seconds. Deprecated `SPF` declarations become `TXT`. An infrastructure alias follows the referenced resource to its target-cloud address. Cloudflare CNAME flattening provides native apex-hostname resolution. Route 53 alias target-health evaluation is omitted, and ordinary multiple-value answers do not reproduce health-checked routing. #### DNS-only and proxied records Records are DNS-only by default. A Cloudflare proxied record sends supported application traffic through its edge, where the configured CDN, WAF and DDoS features apply. Enabling that proxy and choosing its protections are separate Cloudflare settings; creating an ordinary DNS record does not enable them. #### Runtime changes Your application sends Route 53 API requests to the configured adapter. The adapter records the requested change and applies it with the provider credential it holds. `ChangeResourceRecordSets` returns a change receipt; `GetChange` reports `PENDING` until target application completes, then `INSYNC`. Here, `INSYNC` reports target application. It does not guarantee that every authoritative server has observed the change or that recursive caches have expired. Keep polling for completion and allow for DNS caching during cutover. A successful Cloudflare API write can complete without an asynchronous native change operation. That does not bypass DNS cache lifetimes or prove worldwide propagation. #### Routing and remaining limits Route 53 routing policies are omitted and reported while ordinary record data can still be applied. Cloudflare Load Balancing provides separate traffic-routing features; this DNS record mapping does not configure them. Health checks, delegation sets, private zones and Resolver resources are outside this mapping. It uses Cloudflare public authoritative zones; Cloudflare private-DNS products are not selected by this option. Subdomain zone setup has its own Cloudflare plan requirements and is distinct from private DNS. #### Cutover and operations Apply the required records, verify authoritative answers, and then update public nameserver delegation where the provider changes. Keep the previous zone available while cached records and delegation expire. This process applies declared records; it does not copy an entire existing zone or its query history. Record changes require the appliance and its provider credentials. Once applied, records remain available from the authoritative provider while the appliance is unavailable. Certificate-validation records are coordinated with certificate management; review external DNS controllers and any credentials they previously used. [Service Catalog](/service-adapters/catalog). # Route 53 Resolver Source: https://docs.tensor9.com/service-adapters/aws/networking-traffic/route-53-resolver AWS Route 53 Resolver. Answers DNS queries inside a VPC and forwards them between VPCs and on-premises networks through inbound and outbound endpoints. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of Route 53 Resolver with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | Route 53 Resolver | Google Cloud | Azure | OCI | Private Kubernetes | | ------------------------------------------------ | ----------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | Inbound endpoint · on-premises → private names | Yes | Partial - DNS policy enable\_inbound\_forwarding; Google-allocated IPs | Yes - inbound endpoint, a private IP in a delegated subnet | Yes - listening resolver endpoint, a VNIC in a subnet | Partial - served by the cluster's CoreDNS, not Cloudflare | | Outbound endpoint · private → on-premises names | Yes | Partial - forwarding managed zone; no addressable endpoint | Yes - outbound endpoint + DNS forwarding ruleset | Yes - forwarding resolver endpoint | Partial - served by the cluster's CoreDNS, not Cloudflare | | Conditional forwarding · domain → target servers | Yes | Yes - forwarding zone forwarding\_config.target\_name\_servers | Yes - forwarding rule: domain\_name + target\_dns\_servers | Yes - resolver rule: qname\_cover\_conditions + destination\_addresses | Partial - CoreDNS forward plugin / stub domains | | Per-target forwarding port · non-53 targets | Yes | No - Cloud DNS forwards on 53; no port field | Yes - target\_dns\_servers takes an ip and a port | No - OCI forwards on 53; no port field | Partial - CoreDNS forward keeps the target port | | DNSSEC validation | Yes | No - zone signing is separate from recursive validation | No - zone signing is separate from recursive validation | No - zone signing is separate from recursive validation | No - zone signing is separate from recursive validation | | DNS Firewall · domain block/allow | Yes | Partial - Cloud DNS response policy; managed lists require replacement lists | Partial - Azure DNS security policy; AWS-managed lists require replacement lists | No - no OCI-native resolver DNS firewall; surfaced | Partial - Cloudflare Gateway DNS filtering; managed lists require replacement lists | | Query logging | Yes | Yes - DNS policy enable\_logging → Cloud Logging | Yes - Azure Monitor diagnostic settings | Yes - OCI Logging on the resolver | Yes - Cloudflare public-zone analytics / Logpush plus separate CoreDNS query logs | | API coverage | full | partial | high | partial | partial | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | ---------------------------- | ------------- | ------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DNS Firewall (domain lists) | DNS controls | Partial | Most usage | Custom lists map to response-policy rules: allow uses bypass, blocking uses a configured local answer. Match precedence and response behavior differ; AWS-managed lists require replacements. | | DNSSEC validation | DNS controls | Out of scope | Most usage | Zone signing and recursive validation are different operations. This mapping does not translate the Route 53 Resolver validation setting. | | Conditional forwarding rules | Forwarding | Supported | Common | a FORWARD rule becomes a forwarding managed zone: dns\_name holds the domain, forwarding\_config.target\_name\_servers hold the target IPs on the private path, and the rule association becomes the attached network | | Inbound endpoint | Hybrid DNS | Partial | Common | the inbound endpoint becomes a google\_dns\_policy with enable\_inbound\_forwarding on the network; Google allocates the inbound forwarder IPs (no addressable endpoint to pin) | | Outbound endpoint | Hybrid DNS | Partial | Common | the outbound endpoint has no addressable analog; Cloud DNS performs the forwarding egress itself from a forwarding managed zone | | Query logging | Observability | Supported | Most usage | the query-log config + association become enable\_logging on the network's DNS policy, streaming to Cloud Logging | #### How it works Tensor9 translates Route 53 Resolver configuration into Cloud DNS policies and forwarding zones on the customer's Google Cloud network. An inbound-forwarding policy lets on-premises resolvers query private names. A forwarding zone sends queries for a specified domain to the configured on-premises DNS servers. Google serves these queries directly. #### Inbound access and network placement The inbound endpoint becomes a `google_dns_policy` with `enable_inbound_forwarding`. Google allocates the forwarding addresses; update on-premises conditional forwarders to use the addresses reported after deployment. The target network and hybrid connection must provide the required DNS reachability. The AWS endpoint's chosen IPs, subnet placement, security group and per-availability-zone network interfaces do not become corresponding Cloud DNS endpoint resources. Configure the target network's access controls and redundancy for its actual forwarding path.
An on-premises resolver queries a Google-allocated inbound address to resolve a private name. An on-premises resolver queries a Google-allocated inbound address to resolve a private name.

The arrows show the direction of the DNS query.

#### Conditional forwarding An outbound endpoint and its `FORWARD` rule become a forwarding managed zone. The rule's domain becomes `dns_name`; target IPs become `forwarding_config.target_name_servers`, with the private forwarding path. The rule association becomes the zone's network attachment. Cloud DNS manages outbound forwarding without an endpoint IP that you can select. It forwards to port 53. A non-53 target port cannot be preserved and is reported for correction; changing only the destination IP would not make that rule equivalent. #### DNS filtering and validation Custom DNS Firewall domain lists map to Cloud DNS response-policy rules on the network. An allow rule uses bypass behavior. Blocking uses a configured local response, which differs from AWS DNS Firewall's response options; do not assume an identical NXDOMAIN response. Response policies match DNS names and do not reproduce the full ordered AWS rule-group model. Query logging provides the record needed for alert-only behavior. Review which queries are logged and how alerts are selected. AWS-managed domain lists need replacement lists; a native Google list is not assumed to contain the same domains. Route 53 Resolver checks DNSSEC signatures on answers it receives. Signing a hosted zone adds signatures to that zone's records; it does not enable recursive validation for the client network. This mapping does not translate the Resolver DNSSEC-validation setting. #### Logging and cutover The network DNS policy enables query logging to Cloud Logging. Update collection, retention and queries for the Google log format. Existing AWS query history remains in its original destination unless separately exported. Before switching clients, test inbound private names and outbound forwarded names, including network failures and blocked domains. Replace external references to AWS endpoint IDs or rule ARNs that have no target equivalent. Forwarding configuration is recreated; cached answers and query history are not copied. Provider reference: [Cloud DNS response policies](https://docs.cloud.google.com/dns/docs/zones/manage-response-policies). ## On Azure | Operation | Area | Support | Depth | Notes | | ---------------------------- | ------------- | ------------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DNS Firewall (domain lists) | DNS controls | Partial | Most usage | a DNS Firewall rule-group becomes an Azure DNS security policy (domain list + traffic rule Allow/Block/Alert); AWS-managed domain lists require replacement lists | | DNSSEC validation | DNS controls | Out of scope | Most usage | Zone signing and recursive validation are different operations. This mapping does not translate the Route 53 Resolver validation setting. | | Conditional forwarding rules | Forwarding | Supported | Common | a FORWARD rule becomes a forwarding rule (domain\_name + target\_dns\_servers\[ip:port]); the per-target port is preserved, and the rule association becomes a virtual-network link | | Inbound endpoint | Hybrid DNS | Supported | Common | the inbound Resolver endpoint becomes an Azure Private DNS Resolver inbound endpoint, a private IP in a delegated subnet; Azure allocates the address (a static request is honored) | | Outbound endpoint | Hybrid DNS | Supported | Common | the outbound Resolver endpoint becomes an Azure outbound endpoint with a DNS forwarding ruleset attached | | Query logging | Observability | Supported | Most usage | DNS query logs come from a linked DNS resolver policy and its Azure Monitor diagnostic settings; resource metrics are a separate stream. | #### How it works Tensor9 translates Route 53 Resolver endpoints and rules into Azure Private DNS Resolver resources on the customer's virtual network. An inbound endpoint accepts private-name queries from connected networks. An outbound endpoint and forwarding ruleset send matching queries to configured DNS servers. Azure operates the resolver. #### Endpoint placement Each endpoint requires its own subnet delegated to `Microsoft.Network/dnsResolvers`. The inbound endpoint has a private IP allocated from its subnet; a supported static-address request can select that address. Update on-premises conditional forwarders to use the new inbound IP. The outbound endpoint is associated with a subnet but is not provisioned with an IP address like the inbound endpoint. A forwarding ruleset attaches to it. Azure manages service resilience; AWS per-zone interface placement does not translate into a matching set of Azure interfaces. Review target subnet access controls separately from the AWS endpoint security group.
An on-premises resolver queries the Azure inbound endpoint; the outbound endpoint is not a client-facing IP. An on-premises resolver queries the Azure inbound endpoint; the outbound endpoint is not a client-facing IP.

The arrows show the direction of the DNS query.

#### Conditional forwarding A `FORWARD` rule becomes a ruleset entry with the source `domain_name` and `target_dns_servers`. Each target keeps its IP and port, including a non-53 port. The ruleset's virtual-network link activates forwarding for that VNet. Multiple domains can have separate rules. Enabling or disabling a rule changes whether it forwards; removing a VNet link changes where the entire ruleset applies. Check that the selected network can reach every target DNS server. #### DNS filtering and validation DNS Firewall domain rules map to an Azure DNS resolver policy with domain lists and traffic rules. Allow, Block and Alert actions apply to queries from linked virtual networks. Custom lists transfer; AWS-managed lists require an explicitly chosen replacement. Microsoft threat intelligence is a different list, even when it serves the same security purpose. Route 53 Resolver checks DNSSEC signatures on answers it receives. Signing a hosted zone adds signatures to that zone's records; it does not enable recursive validation for the client network. This mapping does not translate the Resolver DNSSEC-validation setting. #### Logging and cutover Enable query logging on the DNS resolver policy and send its diagnostic logs to the selected Azure Monitor destination. Those DNS query logs are distinct from resource metrics. Update log queries and retention for the Azure format and confirm the policy is linked to each network whose queries must be recorded. Recreate forwarding and filtering configuration, then test private names, forwarded domains and policy actions before changing client DNS settings. Existing query history and resolver caches are not transferred. External AWS endpoint IDs and rule ARNs need target-specific replacements. Provider references: [endpoints and rulesets](https://learn.microsoft.com/en-us/azure/dns/private-resolver-endpoints-rulesets) and [DNS query logs](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/tables/dnsquerylogs). ## On OCI | Operation | Area | Support | Depth | Notes | | ---------------------------- | ------------- | ------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DNS Firewall (domain lists) | DNS controls | Out of scope | Most usage | Resolver domain filtering is outside this mapping. Network Firewall URL inspection or a timed-out forwarded query is not equivalent to AWS DNS Firewall actions. | | DNSSEC validation | DNS controls | Out of scope | Most usage | Zone signing and recursive validation are different operations. This mapping does not translate the Route 53 Resolver validation setting. | | Conditional forwarding rules | Forwarding | Supported | Common | a FORWARD rule becomes a resolver rule (qname\_cover\_conditions + destination\_addresses via the forwarding endpoint); defined on the VCN resolver, so the association is represented by that resolver | | Inbound endpoint | Hybrid DNS | Supported | Common | the inbound endpoint becomes a listening resolver endpoint on the VCN's resolver, a VNIC in a subnet | | Outbound endpoint | Hybrid DNS | Supported | Common | the outbound endpoint becomes a forwarding resolver endpoint the resolver rules egress through | | Query logging | Observability | Supported | Most usage | the query-log config + association become OCI Logging on the VCN's resolver | #### How it works Tensor9 translates the Route 53 Resolver configuration into listening endpoints, forwarding endpoints and rules on the customer's OCI VCN resolver. Listening endpoints receive queries for private names; forwarding endpoints send domain-matched queries to the configured DNS servers. Oracle operates the resolver. #### Endpoints and private names An endpoint uses a virtual network interface in a supplied subnet. The listening address is the destination for on-premises conditional forwarders. Private views attached to the VCN resolver determine the private zones it can answer. OCI governs endpoint placement. AWS per-availability-zone IP assignments do not produce an identical set of target interfaces. Configure subnet access and any OCI network security group for the listening and forwarding traffic; the presence of an AWS endpoint security group does not by itself enforce the same policy on OCI.
An OCI listening endpoint receives private-name queries for the VCN resolver. An OCI listening endpoint receives private-name queries for the VCN resolver.

The arrows show the direction of the DNS query.

#### Conditional forwarding A `FORWARD` rule becomes an OCI resolver rule: `qname_cover_conditions` identifies the domain, `destination_addresses` lists target servers, and `source_endpoint_name` selects the forwarding endpoint. Adding the rule to the VCN resolver makes it apply to that resolver; there is no separate AWS-style rule-association resource. Target IPs and the domain are retained. Forwarding uses port 53; OCI has no per-destination port setting in this mapping. A rule requiring another port needs a different arrangement and is reported for correction. #### DNS controls This mapping does not provide an OCI resolver DNS Firewall equivalent. Review domain filtering separately. Network Firewall URL rules inspect different traffic, and forwarding an unwanted domain to an unresponsive server produces a timeout; neither should be described as the same allow, block and alert behavior as AWS DNS Firewall. AWS-managed domain lists also require a replacement policy. Route 53 Resolver checks DNSSEC signatures on answers it receives. Signing a hosted zone adds signatures to that zone's records; it does not enable recursive validation for the client network. This mapping does not translate the Resolver DNSSEC-validation setting. #### Logging and cutover Resolver query logging uses OCI Logging. Configure the log destination, access and retention, then update searches and alerts for the OCI fields. Existing AWS log history stays in its original destination unless separately exported. Update on-premises conditional forwarders to the listening address. Test private views, forwarded domains, network failures and log collection before cutover. Resolver configuration is recreated; caches are not copied. External consumers of AWS endpoint IDs or rule ARNs need corresponding target configuration. ## On Private Kubernetes | Operation | Area | Support | Depth | Notes | | ---------------------------- | ------------- | ------------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DNS Firewall (domain lists) | DNS controls | Partial | Most usage | Custom block/allow lists map to Gateway DNS policies for queries routed through Gateway. Local CoreDNS answers and direct on-premises forwarding bypass that filtering path. | | DNSSEC validation | DNS controls | Out of scope | Most usage | Zone signing and recursive validation are different operations. This mapping does not translate the Route 53 Resolver validation setting. | | Conditional forwarding rules | Forwarding | Partial | Common | Domain-specific rules map to CoreDNS forward configuration, including target server ports. The customer operates that resolver configuration; this does not provide the Route 53 Resolver rule-management API or native Cloudflare private-name resolution. | | Inbound endpoint | Hybrid DNS | Partial | Common | CoreDNS answers private-name queries through a listener and network access arranged by the customer. This does not create an AWS-managed Resolver endpoint or a native Cloudflare private resolver. | | Outbound endpoint | Hybrid DNS | Partial | Common | CoreDNS forwards queries over the customer's network connection to the configured DNS servers. The customer supplies routing and firewall access; AWS endpoint addresses and endpoint-management APIs are not retained. | | Query logging | Observability | Supported | Most usage | public-zone queries use Cloudflare DNS analytics / Logpush; private queries handled by CoreDNS use cluster logs. These streams have different fields and query coverage | #### How it works The Private Kubernetes mapping uses CoreDNS in the appliance cluster for private and hybrid queries. Cloudflare authoritative DNS serves public zones, while Cloudflare Gateway filters DNS traffic that is routed through it. These services handle different query paths. #### Private names and conditional forwarding CoreDNS answers the cluster's local names and sends configured private domains to their DNS servers through the `forward` plugin. A source `FORWARD` rule supplies the domain, target IPs and ports. Configuration applies to workloads using that CoreDNS service. On-premises clients need a reachable, access-controlled cluster DNS listening address. The customer must provide the network path in both directions. A standard internal Kubernetes DNS service is not automatically an inbound endpoint for another network. Cloudflare public authoritative DNS does not answer private queries in this arrangement.
A reachable cluster DNS listener lets on-premises resolvers query CoreDNS; customer networking controls that access. A reachable cluster DNS listener lets on-premises resolvers query CoreDNS; customer networking controls that access.

The arrows show the direction of the DNS query.

#### DNS filtering Custom DNS Firewall lists map to Gateway DNS policies with block or allow actions. Configure the query path through Gateway for those policies to apply. Queries answered locally or sent directly to an on-premises server do not pass through Gateway merely because the policy exists. Use query logging and alert rules for the source alert-only requirement. AWS-managed lists need an explicitly selected replacement. Gateway DNS filtering is separate from Cloudflare's authoritative DNS Firewall product. #### DNSSEC and logs Route 53 Resolver checks DNSSEC signatures on answers it receives. Signing a hosted zone adds signatures to that zone's records; it does not enable recursive validation for the client network. This mapping does not translate the Resolver DNSSEC-validation setting. Public-zone analytics and Logpush report queries received by Cloudflare authoritative DNS. CoreDNS logs report private queries answered or forwarded by the cluster. Gateway activity logs cover traffic inspected by Gateway. Configure collection for the paths you use; public-zone analytics cannot substitute for private-query logs. #### Cutover and operations The customer operates CoreDNS availability, capacity and network access within the cluster. Cloudflare operates its public DNS and Gateway services. Retain DNS access during a cluster change and test forwarded domains before moving on-premises clients to the new listening address. Recreate configuration and update external AWS endpoint IDs and rule ARNs. Resolver caches and existing query history are not transferred. Confirm private, public and filtered queries independently, including which log stream records each one. Provider references: [Gateway DNS filtering](https://developers.cloudflare.com/cloudflare-one/traffic-policies/get-started/dns/) and [Gateway activity logs](https://developers.cloudflare.com/cloudflare-one/insights/logs/dashboard-logs/gateway-logs/). [Service Catalog](/service-adapters/catalog). # VPC Source: https://docs.tensor9.com/service-adapters/aws/networking-traffic/vpc A private, logically isolated network in AWS with your own CIDR ranges, subnets per Availability Zone, route tables, security groups and network ACLs. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of VPC with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | VPC | Google Cloud | Azure | OCI | | ------------------------ | ----------------------------------- | ----------------------------------------- | ---------------------------------------------- | -------------------------------------------------- | | Network management | AWS EC2 VPC APIs | AWS-compatible adapter + Google Cloud VPC | AWS-compatible adapter + Azure Virtual Network | AWS-compatible adapter + OCI Virtual Cloud Network | | Traffic rules | security groups and subnet ACLs | target rules and membership | target rules and membership | target rules and membership | | Routing and connectivity | route tables, gateways, and peering | target routes and gateways | target routes and gateways | target routes and gateways | | Public addresses | AWS Elastic IP | new target-cloud public IP | new target-cloud public IP | new target-cloud public IP | | Network interfaces | AWS ENI identity and configuration | interface created with the VM | interface created with the VM | interface created with the VM | | API coverage | full | partial | partial | partial | ### Infrastructure-only adaptation | Capability | VPC | Google Cloud | Azure | OCI | Private Kubernetes | | ------------------------ | ----------------------------------- | ----------------------------------------- | ---------------------------------------------- | -------------------------------------------------- | --------------------------------------------------- | | Network management | AWS EC2 VPC APIs | AWS-compatible adapter + Google Cloud VPC | AWS-compatible adapter + Azure Virtual Network | AWS-compatible adapter + OCI Virtual Cloud Network | customer's existing cluster network | | Traffic rules | security groups and subnet ACLs | target rules and membership | target rules and membership | target rules and membership | configured separately on the cluster | | Routing and connectivity | route tables, gateways, and peering | target routes and gateways | target routes and gateways | target routes and gateways | existing cluster and customer network configuration | | Public addresses | AWS Elastic IP | new target-cloud public IP | new target-cloud public IP | new target-cloud public IP | - | | Network interfaces | AWS ENI identity and configuration | interface created with the VM | interface created with the VM | interface created with the VM | - | | API coverage | full | partial | partial | partial | minimal | ## On Google Cloud | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------- | ------------- | -------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DHCP options | Addressing | Out of scope | - | - | custom AWS DHCP option sets are outside this mapping; configure target DNS servers, search domains, and other required settings separately | | Elastic IP | Addressing | Partial | - | AllocateAddress, AssociateAddress, DescribeAddresses, DescribeAddressesAttribute, DisassociateAddress, ReleaseAddress | AllocateAddress provisions a target public IP and returns its assigned value. AWS-format allocation IDs remain available to callers; the public IP itself changes when moving clouds. Customer-owned address pools are outside this mapping | | IP Address Manager (IPAM) | Addressing | Out of scope | - | GetIpamAddressHistory | AWS IPAM pool allocation and automatic CIDR allocation are outside this mapping; use explicit address ranges | | IPv6 / secondary CIDR | Addressing | Partial | - | AssociateSubnetCidrBlock, AssociateVpcCidrBlock, DisassociateSubnetCidrBlock, DisassociateVpcCidrBlock | IPv4 address ranges are preserved in the AWS model and target subnetworks. AWS IPAM and Amazon-provided IPv6 aggregates are outside this mapping; Google assigns public IPv6 ranges at a different level | | Client VPN | Connectivity | Out of scope | - | ApplySecurityGroupsToClientVpnTargetNetwork | AWS Client VPN endpoints and their authorization rules are outside this VPC mapping | | EC2-Classic / ClassicLink | Connectivity | Out of scope | - | AttachClassicLinkVpc, DescribeMovingAddresses, DetachClassicLinkVpc, MoveAddressToVpc, RestoreAddressToClassic | the retired EC2-Classic and ClassicLink operations are outside this VPC mapping | | Site-to-Site VPN | Connectivity | Out of scope | - | CreateVpnConnectionRoute, DeleteVpnConnectionRoute | VPN tunnels and their gateway configuration are outside this VPC mapping; configure the customer connection on the target separately | | VPC endpoints (PrivateLink) | Connectivity | Out of scope | - | - | private service endpoints require a service-specific target configuration; this VPC mapping does not create an equivalent private endpoint automatically | | VPC peering | Connectivity | Partial | - | AcceptVpcPeeringConnection, CreateVpcPeeringConnection, DeleteVpcPeeringConnection, DescribeVpcPeeringConnections, ModifyVpcPeeringConnectionOptions, RejectVpcPeeringConnection | Create/Accept/Describe/Delete manage both directions of Google VPC Network Peering. The adapter is bound to one account and region; cross-account, cross-region, and AWS peering DNS options are outside this mapping | | Network interface (ENI) | Network | Partial | - | AssignIpv6Addresses, AssignPrivateIpAddresses, AttachNetworkInterface, CreateNetworkInterface, DeleteNetworkInterface, DescribeNetworkInterfaceAttribute, DescribeNetworkInterfaces, DetachNetworkInterface, ModifyNetworkInterfaceAttribute, ResetNetworkInterfaceAttribute, UnassignIpv6Addresses, UnassignPrivateIpAddresses | CreateNetworkInterface records the subnet, private address, and groups. A VM launch naming that interface creates the attached target interface. Standalone attach/detach and multiple-interface launches are outside this mapping | | Subnets | Network | Supported | - | CreateDefaultSubnet, CreateSubnet, DeleteSubnet, DescribeSubnets, ModifySubnetAttribute | subnet APIs preserve IPv4 ranges and AWS-facing identifiers; the adapter creates the corresponding target subnets and tracks their state | | Virtual network (VPC) | Network | Supported | - | CreateDefaultVpc, CreateVpc, DeleteVpc, DescribeVpcAttribute, DescribeVpcs, ModifyVpcAttribute, ModifyVpcTenancy | VPC APIs manage a custom-mode Google Cloud network; the AWS VPC CIDR stays in the API model, while native address ranges belong to subnetworks | | Flow logs | Observability | Adapter-served | - | CreateFlowLogs, DeleteFlowLogs, DescribeFlowLogs, GetFlowLogsIntegrationTemplate | native subnet flow logging uses Google fields and destinations. The 60s and 600s aggregation windows are preserved. AWS destinations require acknowledgement of the destination difference; single-interface capture, AWS filter expressions, and ACCEPT-only or REJECT-only capture are not represented | | Resource tagging | Operations | Partial | - | CreateTags, DeleteTags, DescribeTags | creation tags and CreateTags/DeleteTags/DescribeTags operate on the AWS-facing resource record; review tag readback and filtering behavior for the operations your application uses | | Availability-zone placement | Placement | Partial | - | DescribeAvailabilityZones, ModifyAvailabilityZoneGroup | target subnets are regional; choose availability zones or domains on workloads that need separation, rather than relying on an AWS subnet zone | | Internet gateway | Routing | Supported | - | AttachInternetGateway, CreateInternetGateway, DeleteInternetGateway, DescribeInternetGateways, DetachInternetGateway | the adapter manages the AWS gateway and attachment lifecycle using Google internet routing; public reachability also requires the appropriate addresses and firewall rules | | NAT gateway | Routing | Supported | - | CreateNatGateway, DeleteNatGateway, DescribeNatGateways | public NAT requests configure Cloud Router and Cloud NAT for the selected subnetworks. Private NAT and multiple-address NAT requests are outside this mapping | | Route tables | Routing | Partial | - | AssociateRouteTable, CreateRoute, CreateRouteTable, DeleteRoute, DeleteRouteTable, DescribeRouteTables, DisableVgwRoutePropagation, DisassociateRouteTable, EnableVgwRoutePropagation, ReplaceRoute | route APIs preserve subnet associations and internet-gateway, NAT, and peering next hops. Google supplies peered-network routes; unsupported next hops, including transit gateways and network interfaces, are rejected | | Network ACLs | Security | Adapter-served | - | CreateNetworkAcl, CreateNetworkAclEntry, DeleteNetworkAcl, DeleteNetworkAclEntry, DescribeNetworkAcls, ReplaceNetworkAclAssociation, ReplaceNetworkAclEntry | ACL operations maintain ordered subnet rules. Google connection tracking differs from AWS stateless filtering; applying the reduced behavior requires an explicit acknowledgement | | Security groups | Security | Supported | - | AssociateSecurityGroupVpc, AuthorizeSecurityGroupEgress, AuthorizeSecurityGroupIngress, CreateSecurityGroup, DeleteSecurityGroup, DescribeSecurityGroupRules, DescribeSecurityGroups, DisassociateSecurityGroupVpc, GetSecurityGroupsForVpc, ModifySecurityGroupRules, RevokeSecurityGroupEgress, RevokeSecurityGroupIngress | CIDR rules become Google firewall rules. Same-VPC group references use instance network tags, with membership updated as groups change. Cross-VPC group references cannot use those tags and are rejected during target configuration | | VPC-wide encryption enforcement | Security | Out of scope | - | GetVpcResourcesBlockingEncryptionEnforcement | the AWS enforcement setting is not applied to the target network; requests are rejected unless that difference is explicitly acknowledged. Use target controls to meet an encryption requirement | #### How it works Your application can keep using the AWS EC2 APIs to create networks, subnets, security groups, and other supported VPC resources in the customer's environment. The Tensor9 service adapter accepts those requests and manages the corresponding Google Cloud resources. Terraform's AWS provider uses the same API path. The adapter checks the request and permissions, assigns an AWS-format resource ID, and saves the requested configuration. A background worker applies that configuration to the target cloud and tracks the result. For example, `CreateVpc` records the VPC together with its default security group, main route table, and default network ACL. `DescribeVpcs` reports `pending` until the cloud configuration has been applied, then `available`. Creating an API record does not mean the network is ready. The customer's cloud carries application traffic. The adapter handles network-management API calls; it does not forward the application's packets.
AWS SDK or Terraform sends network-management requests to the Tensor9 adapter. The adapter saves the requested configuration and applies it through the target cloud API. Application packets travel through the customer's cloud network. AWS SDK or Terraform sends network-management requests to the Tensor9 adapter. The adapter saves the requested configuration and applies it through the target cloud API. Application packets travel through the customer's cloud network.
#### Network layout The VPC becomes a custom-mode Google Cloud network. Google places address ranges on subnetworks, so the AWS VPC's overall CIDR remains part of the API model while each subnet's IPv4 range becomes a native subnetwork range. Subnet references continue to identify the corresponding target subnet. A Google subnetwork is regional. An AWS subnet's Availability Zone does not fix the target instance's zone; review instance placement when the application depends on zone separation. Terraform subnet declarations using `count` or `for_each` retain their instance keys and address expressions.
Google Cloud VPC contains regional subnetworks. Workloads attach to those subnets, with access controlled by firewall rules. Google Cloud VPC contains regional subnetworks. Workloads attach to those subnets, with access controlled by firewall rules.
#### Security groups and subnet filters Security-group rules become Google Cloud firewall rules with the requested direction, protocol, ports, and address ranges. Group membership uses network tags: a same-VPC rule that names another group selects instances with that group's tag. Membership changes must therefore update the native instance tags as well as the AWS-facing group record. Google source tags match within their own network and against primary internal addresses. A reference to a group in a peered VPC cannot be represented by the same tag rule and is rejected during application of the configuration. Secondary and alias IP behavior needs separate review. Network access control list (ACL) operations maintain ordered subnet rules, but Google's firewall tracks connections and cannot preserve AWS's stateless filtering exactly. Applying that reduced behavior requires an explicit acknowledgement; an accepted ACL API request alone does not establish equivalent packet filtering. Prefix-list security rules are omitted with a recorded limitation, leaving less traffic permitted. #### Routes and connectivity Route-table APIs retain subnet associations and supported next hops. Internet-gateway routing uses Google's internet gateway; private-subnet egress uses a Cloud Router and Cloud NAT for the selected subnetworks. A default internet route alone does not make an instance publicly reachable: addresses and firewall rules also matter. Private NAT gateways and multiple-address NAT requests are outside this mapping. Same-account, same-region VPC peering creates both directions of Google VPC Network Peering after acceptance. Routes naming that peering remain visible through the AWS API, while Google installs the peered subnet routes. If the peering is deleted, the AWS route can remain as a blackhole. Cross-account or cross-region peering and AWS peering DNS options are outside the adapter's current binding. Transit-gateway, instance, interface, and other unsupported route next hops are rejected. Private Google Access provides access to Google APIs from eligible private workloads. Customer-published private services need a separate Private Service Connect configuration; an AWS PrivateLink endpoint is not created by this VPC mapping. #### Addresses and interfaces The adapter keeps AWS-format VPC, subnet, security-group, and address identifiers for API callers and tracks the corresponding cloud resources separately. An AWS ID is not a native cloud resource name. `AllocateAddress` obtains a target-cloud public IP and waits for that allocation before returning the address. It does not preserve an existing AWS Elastic IP. A standalone `CreateNetworkInterface` records the subnet, private address, and security groups; the VM launch that names that interface creates the attached target interface. Standalone attach/detach operations and launches with multiple interfaces are outside this mapping. #### Other compatibility differences Use explicit IPv4 subnet ranges. AWS IPAM allocation, Amazon-provided IPv6 address blocks, custom DHCP option sets, and VPC-wide encryption enforcement are not reproduced. The public IPv6 aggregate AWS assigns to a VPC is different from Google's independently allocated public subnet ranges. Flow-log requests enable native subnet logging in Google Cloud. Records use Google's schema and logging destination; the 60-second and 600-second aggregation windows are preserved. An S3 or CloudWatch destination requires acknowledgement that it will not receive those records. Single-interface capture, AWS log-filter expressions, and ACCEPT-only or REJECT-only capture are not represented. #### Deployment and ongoing changes The application signs its EC2 requests with credentials accepted by the appliance's IAM/STS service. The adapter checks the requested action and resource before changing anything. Target-cloud credentials stay with the adapter; the application does not need a second set of cloud API calls. Keep using the normal status checks after a create or update. The adapter keeps requested configuration and observed cloud state separately, retries changes that can be retried, and reports errors when a requested setting cannot be applied. A `Describe` response uses the saved AWS identity and configuration, with current cloud-assigned values where needed. Deletion checks dependencies: for example, a subnet remains in use while its NAT gateway is still being removed. Before moving traffic, check subnet ranges and placement, test allowed and denied connections, and confirm that private-subnet egress uses the intended NAT gateway. Review peering, private service access, DNS, and log destinations separately. Public IP addresses change, so update DNS records and external allowlists. Existing connections do not survive the move to the new network. ## On Azure | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------- | ------------- | -------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DHCP options | Addressing | Out of scope | - | - | custom AWS DHCP option sets are outside this mapping; configure target DNS servers, search domains, and other required settings separately | | IPv6 / secondary CIDR | Addressing | Partial | - | AssociateSubnetCidrBlock, AssociateVpcCidrBlock, DisassociateSubnetCidrBlock, DisassociateVpcCidrBlock | IPv4 VNet and subnet ranges are preserved. The runtime API can manage additional IPv4 VPC ranges; AWS IPAM allocation and Amazon-provided IPv6 blocks are outside this mapping | | VPC endpoints (PrivateLink) | Connectivity | Out of scope | - | - | private service endpoints require a service-specific target configuration; this VPC mapping does not create an equivalent private endpoint automatically | | VPC peering | Connectivity | Supported | - | AcceptVpcPeeringConnection, CreateVpcPeeringConnection, DeleteVpcPeeringConnection, DescribeVpcPeeringConnections, ModifyVpcPeeringConnectionOptions, RejectVpcPeeringConnection | Create/Accept/Describe/Delete manage two directional VNet peerings. Cross-account and cross-region requests are outside the adapter binding; AWS peering DNS options are not translated | | Subnets | Network | Supported | - | CreateDefaultSubnet, CreateSubnet, DeleteSubnet, DescribeSubnets, ModifySubnetAttribute | subnet APIs preserve IPv4 ranges and AWS-facing identifiers; the adapter creates the corresponding target subnets and tracks their state | | Virtual network (VPC) | Network | Supported | - | CreateDefaultVpc, CreateVpc, DeleteVpc, DescribeVpcAttribute, DescribeVpcs, ModifyVpcAttribute, ModifyVpcTenancy | VPC APIs manage an Azure VNet and its address spaces; the adapter preserves AWS-facing identities while tracking Azure resource IDs | | Flow logs | Observability | Adapter-served | - | CreateFlowLogs, DeleteFlowLogs, DescribeFlowLogs, GetFlowLogsIntegrationTemplate | flow telemetry uses Azure logging fields and destinations; AWS-format records and AWS destinations are not preserved | | Availability-zone placement | Placement | Partial | - | DescribeAvailabilityZones, ModifyAvailabilityZoneGroup | target subnets are regional; choose availability zones or domains on workloads that need separation, rather than relying on an AWS subnet zone | | Internet gateway | Routing | Supported | - | AttachInternetGateway, CreateInternetGateway, DeleteInternetGateway, DescribeInternetGateways, DetachInternetGateway | AWS gateway and attachment records map to Azure internet connectivity. Public access needs an explicit outbound method or public frontend plus suitable security rules; a default route alone is insufficient | | NAT gateway | Routing | Supported | - | CreateNatGateway, DeleteNatGateway, DescribeNatGateways | public NAT requests configure Azure NAT Gateway with a public IP and the required subnet associations. Private NAT and multiple-address NAT requests are outside this mapping | | Route tables | Routing | Partial | - | AssociateRouteTable, CreateRoute, CreateRouteTable, DeleteRoute, DeleteRouteTable, DescribeRouteTables, DisableVgwRoutePropagation, DisassociateRouteTable, EnableVgwRoutePropagation, ReplaceRoute | route APIs manage Azure route tables and subnet associations for supported next hops. Peering uses Azure-installed routes; transit-gateway, interface, instance, and other unsupported next hops are rejected | | Network ACLs | Security | Adapter-served | - | CreateNetworkAcl, CreateNetworkAclEntry, DeleteNetworkAcl, DeleteNetworkAclEntry, DescribeNetworkAcls, ReplaceNetworkAclAssociation, ReplaceNetworkAclEntry | ordered subnet ACL rules are composed with NSG rules. Azure connection tracking differs from AWS stateless filtering; reduced behavior requires acknowledgement, and unrepresentable selector or ordering combinations are rejected | | Security groups | Security | Supported | - | AssociateSecurityGroupVpc, AuthorizeSecurityGroupEgress, AuthorizeSecurityGroupIngress, CreateSecurityGroup, DeleteSecurityGroup, DescribeSecurityGroupRules, DescribeSecurityGroups, DisassociateSecurityGroupVpc, GetSecurityGroupsForVpc, ModifySecurityGroupRules, RevokeSecurityGroupEgress, RevokeSecurityGroupIngress | rules become Azure NSG rules with direction, ports, protocols, and address ranges. Same-VPC group references use Application Security Groups and interface membership; unsupported selector combinations are rejected | | VPC-wide encryption enforcement | Security | Out of scope | - | GetVpcResourcesBlockingEncryptionEnforcement | the AWS enforcement setting is not applied to the target network; requests are rejected unless that difference is explicitly acknowledged. Use target controls to meet an encryption requirement | #### How it works Your application can keep using the AWS EC2 APIs to create networks, subnets, security groups, and other supported VPC resources in the customer's environment. The Tensor9 service adapter accepts those requests and manages the corresponding Azure resources. Terraform's AWS provider uses the same API path. The adapter checks the request and permissions, assigns an AWS-format resource ID, and saves the requested configuration. A background worker applies that configuration to the target cloud and tracks the result. For example, `CreateVpc` records the VPC together with its default security group, main route table, and default network ACL. `DescribeVpcs` reports `pending` until the cloud configuration has been applied, then `available`. Creating an API record does not mean the network is ready. The customer's cloud carries application traffic. The adapter handles network-management API calls; it does not forward the application's packets.
AWS SDK or Terraform sends network-management requests to the Tensor9 adapter. The adapter saves the requested configuration and applies it through the target cloud API. Application packets travel through the customer's cloud network. AWS SDK or Terraform sends network-management requests to the Tensor9 adapter. The adapter saves the requested configuration and applies it through the target cloud API. Application packets travel through the customer's cloud network.
#### Network layout The VPC becomes an Azure Virtual Network (VNet), with subnets using the requested IPv4 ranges. The adapter keeps AWS subnet IDs so applications can use them in later requests. Azure VNet address spaces hold the network's CIDR ranges; subnet ranges must fit within them. Azure subnets are regional. Choose availability zones on the workloads that need them rather than relying on an AWS subnet's zone. Subnet `count` and `for_each` declarations keep their instance keys and address expressions. Review the resulting VNet and workload placement together.
Azure Virtual Network contains regional subnets. Workloads attach to those subnets, with access controlled by NSGs and application groups. Azure Virtual Network contains regional subnets. Workloads attach to those subnets, with access controlled by NSGs and application groups.
#### Security groups and subnet filters Azure network security groups (NSGs) enforce the translated allow and deny rules. Rules preserve protocols, ports, direction, and address ranges while using Azure's ordered priorities. Same-VPC references to another security group use **Application Security Groups**, which select the member interfaces. The adapter updates that membership when an instance's security groups change. Review rules that combine group identity with subnet network ACLs (ordered, stateless traffic filters). Azure limits which selectors one rule can combine. AKS-managed node interfaces also cannot join Application Security Groups: where a subnet mixes managed nodes with other interfaces, a subnet-wide approximation requires explicit acknowledgement because it changes which traffic the rule selects. For network ACLs, the adapter composes ordered subnet filters with the security-group rules. Azure NSGs are stateful, so the translation cannot promise AWS's stateless return-traffic behavior. Configurations whose rule ordering or selectors cannot be represented are rejected. A reduced stateful mapping requires explicit acknowledgement. #### Routes and connectivity Supported route-table operations configure Azure routes and subnet associations. Internet access needs an explicit outbound method or an appropriate public frontend; do not rely on default outbound connectivity. Private-subnet egress uses an Azure NAT Gateway with a public IP, attached to the subnets that need it. Private NAT gateways and multiple-address NAT requests are outside this mapping. Same-account, same-region VPC peering becomes two directional VNet peerings. The AWS API retains the peering lifecycle and route references, while Azure installs routes to the peered address spaces. Cross-account or cross-region requests are outside the adapter's binding, even though Azure has broader peering capabilities. AWS peering DNS options require a separate private-DNS design. Transit-gateway, instance, interface, and other unsupported route next hops are rejected. Azure Private Endpoint, Private Link, VPN, and custom DNS services require their own configuration; a VPC endpoint declaration does not create them automatically. #### Addresses and interfaces The adapter keeps AWS-format VPC, subnet, security-group, and address identifiers for API callers and tracks the corresponding cloud resources separately. An AWS ID is not a native cloud resource name. `AllocateAddress` obtains a target-cloud public IP and waits for that allocation before returning the address. It does not preserve an existing AWS Elastic IP. A standalone `CreateNetworkInterface` records the subnet, private address, and security groups; the VM launch that names that interface creates the attached target interface. Standalone attach/detach operations and launches with multiple interfaces are outside this mapping. #### Other compatibility differences Use explicit IPv4 subnet ranges. AWS IPAM allocation, Amazon-provided IPv6 address blocks, and custom DHCP option sets are outside this mapping. Azure's support for multiple VNet address ranges does not reproduce AWS address-allocation behavior. A VPC-wide encryption requirement must be expressed through supported target controls. Flow telemetry uses Azure's logging facilities, with Azure fields and destinations. Update collectors and queries that expect AWS flow-log records or an AWS destination. Check log coverage for the subnets and interfaces the application depends on. #### Deployment and ongoing changes The application signs its EC2 requests with credentials accepted by the appliance's IAM/STS service. The adapter checks the requested action and resource before changing anything. Target-cloud credentials stay with the adapter; the application does not need a second set of cloud API calls. Keep using the normal status checks after a create or update. The adapter keeps requested configuration and observed cloud state separately, retries changes that can be retried, and reports errors when a requested setting cannot be applied. A `Describe` response uses the saved AWS identity and configuration, with current cloud-assigned values where needed. Deletion checks dependencies: for example, a subnet remains in use while its NAT gateway is still being removed. Before moving traffic, check subnet ranges and placement, test allowed and denied connections, and confirm that private-subnet egress uses the intended NAT gateway. Review peering, private service access, DNS, and log destinations separately. Public IP addresses change, so update DNS records and external allowlists. Existing connections do not survive the move to the new network. ## On OCI | Capability | Area | Support | Required tier | Operations | Notes | | --------------------------- | ------------- | -------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | DHCP options | Addressing | Out of scope | - | - | custom AWS DHCP option sets are outside this mapping; configure target DNS servers, search domains, and other required settings separately | | IPv6 / secondary CIDR | Addressing | Out of scope | - | AssociateSubnetCidrBlock, AssociateVpcCidrBlock, DisassociateSubnetCidrBlock, DisassociateVpcCidrBlock | the primary IPv4 VCN and subnet ranges are preserved; IPv6 associations and secondary CIDR blocks are outside this profile | | VPC endpoints (PrivateLink) | Connectivity | Out of scope | - | - | private service endpoints require a service-specific target configuration; this VPC mapping does not create an equivalent private endpoint automatically | | VPC peering | Connectivity | Out of scope | - | AcceptVpcPeeringConnection, CreateVpcPeeringConnection, DeleteVpcPeeringConnection, DescribeVpcPeeringConnections, ModifyVpcPeeringConnectionOptions, RejectVpcPeeringConnection | OCI Local and Remote Peering require separate configuration and are outside this profile's automatic mapping | | Subnets | Network | Supported | - | CreateDefaultSubnet, CreateSubnet, DeleteSubnet, DescribeSubnets, ModifySubnetAttribute | subnet APIs preserve IPv4 ranges and AWS-facing identifiers; the adapter creates the corresponding target subnets and tracks their state | | Virtual network (VPC) | Network | Supported | - | CreateDefaultVpc, CreateVpc, DeleteVpc, DescribeVpcAttribute, DescribeVpcs, ModifyVpcAttribute, ModifyVpcTenancy | the adapter maps the AWS VPC model to an OCI VCN with its IPv4 address range; AWS-facing identities remain separate from OCI resource IDs | | Flow logs | Observability | Adapter-served | - | CreateFlowLogs, DeleteFlowLogs, DescribeFlowLogs, GetFlowLogsIntegrationTemplate | flow telemetry uses OCI Logging and OCI fields; AWS flow-log format and destinations are not preserved | | Availability-zone placement | Placement | Partial | - | DescribeAvailabilityZones, ModifyAvailabilityZoneGroup | target subnets are regional; choose availability zones or domains on workloads that need separation, rather than relying on an AWS subnet zone | | Internet gateway | Routing | Partial | - | AttachInternetGateway, CreateInternetGateway, DeleteInternetGateway, DescribeInternetGateways, DetachInternetGateway | public connectivity uses an OCI internet gateway and route, a public address, and security rules permitting the traffic; the adapter preserves the requested gateway and routing configuration | | NAT gateway | Routing | Supported | - | CreateNatGateway, DeleteNatGateway, DescribeNatGateways | outbound-only subnet access maps to an OCI NAT Gateway and a private route table whose default route names the gateway | | Route tables | Routing | Partial | - | AssociateRouteTable, CreateRoute, CreateRouteTable, DeleteRoute, DeleteRouteTable, DescribeRouteTables, DisableVgwRoutePropagation, DisassociateRouteTable, EnableVgwRoutePropagation, ReplaceRoute | the VCN uses OCI route tables; internet and NAT routing map to their OCI gateways. Other custom next hops require target-specific configuration | | Network ACLs | Security | Out of scope | - | CreateNetworkAcl, CreateNetworkAclEntry, DeleteNetworkAcl, DeleteNetworkAclEntry, DescribeNetworkAcls, ReplaceNetworkAclAssociation, ReplaceNetworkAclEntry | AWS subnet network ACLs are outside this profile; stateful OCI NSG rules do not reproduce their stateless behavior | | Security groups | Security | Supported | - | AssociateSecurityGroupVpc, AuthorizeSecurityGroupEgress, AuthorizeSecurityGroupIngress, CreateSecurityGroup, DeleteSecurityGroup, DescribeSecurityGroupRules, DescribeSecurityGroups, DisassociateSecurityGroupVpc, GetSecurityGroupsForVpc, ModifySecurityGroupRules, RevokeSecurityGroupEgress, RevokeSecurityGroupIngress | rules use OCI NSGs attached to VNICs, preserving direction, protocol, ports, and CIDRs. Peer groups use native NSG references. OCI security lists also permit traffic, so review their combined effect | #### How it works Your application can keep using the AWS EC2 APIs to create networks, subnets, security groups, and other supported VPC resources in the customer's environment. The Tensor9 service adapter accepts those requests and manages the corresponding OCI resources. Terraform's AWS provider uses the same API path. The adapter checks the request and permissions, assigns an AWS-format resource ID, and saves the requested configuration. A background worker applies that configuration to the target cloud and tracks the result. For example, `CreateVpc` records the VPC together with its default security group, main route table, and default network ACL. `DescribeVpcs` reports `pending` until the cloud configuration has been applied, then `available`. Creating an API record does not mean the network is ready. The customer's cloud carries application traffic. The adapter handles network-management API calls; it does not forward the application's packets.
AWS SDK or Terraform sends network-management requests to the Tensor9 adapter. The adapter saves the requested configuration and applies it through the target cloud API. Application packets travel through the customer's cloud network. AWS SDK or Terraform sends network-management requests to the Tensor9 adapter. The adapter saves the requested configuration and applies it through the target cloud API. Application packets travel through the customer's cloud network.
#### Network layout The VPC maps to an OCI Virtual Cloud Network (VCN), with the VPC's IPv4 range in the VCN and each subnet range in an OCI subnet. AWS-facing identifiers remain available to later API requests; the adapter tracks the corresponding OCI identities separately. The mapped OCI subnets are regional. Availability-domain and fault-domain placement belongs to the workloads using those subnets, so review it when the application relies on AWS Availability Zone separation. Subnet `count` and `for_each` declarations keep their keys and address expressions.
OCI Virtual Cloud Network contains regional subnets. Workloads attach to those subnets, with access controlled by network security groups. OCI Virtual Cloud Network contains regional subnets. Workloads attach to those subnets, with access controlled by network security groups.
#### Security groups and subnet filters Security groups map to OCI network security groups (NSGs) attached to the relevant virtual network interfaces. Rules specify direction, protocol, ports, and CIDR selectors. A rule naming a peer group uses OCI's native NSG reference; a reference to a group outside the mapped environment needs separate configuration. OCI combines permissions from subnet security lists and interface NSGs. The adapter must account for both when applying the requested ingress policy. Adding a restrictive NSG does not cancel an existing security-list allow rule; review rules already present in the customer's VCN as well as the groups the application creates. Internet reachability also requires the relevant route, gateway, and public address. AWS subnet network ACLs are outside this mapping. Do not treat stateful NSG rules as equivalent stateless filters. Check both permitted and denied connections after deploying the VCN. #### Routes and connectivity The VCN uses OCI route tables and an internet gateway for public routing. Subnets that require outbound-only access use an OCI NAT Gateway and a private route table whose default route points to it. A custom next hop requires an appropriate OCI route; the AWS route-table declaration does not cover every OCI routing option. Private service access and connections to other VCNs need OCI-specific configuration. Service Gateway, Private Endpoint, Local Peering, and Remote Peering have different attachment and routing rules from AWS PrivateLink and VPC peering. They are outside this profile's claimed automatic mapping. #### Addresses and interfaces The adapter keeps AWS-format VPC, subnet, security-group, and address identifiers for API callers and tracks the corresponding cloud resources separately. An AWS ID is not a native cloud resource name. `AllocateAddress` obtains a target-cloud public IP and waits for that allocation before returning the address. It does not preserve an existing AWS Elastic IP. A standalone `CreateNetworkInterface` records the subnet, private address, and security groups; the VM launch that names that interface creates the attached target interface. Standalone attach/detach operations and launches with multiple interfaces are outside this mapping. #### Other compatibility differences The primary IPv4 VCN and subnet ranges are preserved. AWS IPAM allocation, IPv6 associations, secondary CIDR blocks, custom DHCP option sets, and subnet network ACLs are outside this profile. OCI offers related native features, but their presence alone does not preserve the AWS configuration. Flow telemetry uses OCI Logging and OCI's record format. Update destinations and queries that depend on AWS flow logs. Public addresses are new OCI reserved addresses, so plan DNS and allowlist changes. #### Deployment and ongoing changes The application signs its EC2 requests with credentials accepted by the appliance's IAM/STS service. The adapter checks the requested action and resource before changing anything. Target-cloud credentials stay with the adapter; the application does not need a second set of cloud API calls. Keep using the normal status checks after a create or update. The adapter keeps requested configuration and observed cloud state separately, retries changes that can be retried, and reports errors when a requested setting cannot be applied. A `Describe` response uses the saved AWS identity and configuration, with current cloud-assigned values where needed. Deletion checks dependencies: for example, a subnet remains in use while its NAT gateway is still being removed. Before moving traffic, check subnet ranges and placement, test allowed and denied connections, and confirm that private-subnet egress uses the intended NAT gateway. Review peering, private service access, DNS, and log destinations separately. Public IP addresses change, so update DNS records and external allowlists. Existing connections do not survive the move to the new network. ## On Private Kubernetes | Capability | Area | Support | Required tier | Operations | Notes | | ------------------------------ | ----------- | ------------ | ------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | Infrastructure references | Compilation | Supported | - | - | source VPC-family outputs remain resolvable for the translated stack; generated identifiers are not independently provisioned cloud networks | | Runtime VPC management | Network | Out of scope | - | - | this target does not provide the AWS VPC create, update, or discovery API for the existing cluster network | | AWS traffic-policy enforcement | Security | Out of scope | - | - | source security groups and subnet ACLs are not translated into enforced Kubernetes network policy by this target | #### How it works The customer's Kubernetes cluster already supplies the network used by the application. Tensor9 translates the workload resources to use that cluster. It keeps the source VPC-family references needed by the generated infrastructure, but creates no separate VPC, subnet, NAT gateway, or AWS security-group boundary through this target. #### What source network outputs mean A source network identifier can feed another resource or module output even though the target cluster has no corresponding AWS network object. Compilation preserves a resolvable value for that reference and records which source resource it came from. The value is not a routable address or proof that a new network was provisioned. Use the deployed workload and Service endpoints to determine actual connectivity. #### Traffic policy and external access The customer platform team must configure the cluster's network plugin, network policies, ingress or load balancer integration, DNS, and outbound routes. This VPC target does not convert source security groups or stateless subnet ACLs into enforced Kubernetes rules. Verify both allowed and denied connections, including traffic between application components and other workloads sharing the cluster. #### Runtime API and cutover Applications that create or change AWS VPC resources at runtime cannot use this existing-network target for those operations. The AWS-compatible VPC management described for Google Cloud, Azure, and OCI is a different mapping. Before cutover, prepare cluster connectivity, deploy the translated workloads, and update DNS and external allowlists for their target endpoints. [Service Catalog](/service-adapters/catalog). # VPC Flow Logs Source: https://docs.tensor9.com/service-adapters/aws/networking-traffic/vpc-flow-logs AWS VPC Flow Logs. Records metadata about accepted and rejected IP traffic on a VPC, subnet or network interface, delivered to CloudWatch Logs, S3 or Firehose. Preview ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | - | | Azure | - | | OCI | - | | Private Kubernetes | - | ## Mapping and limitations VPC Flow Logs capture accepted and rejected network traffic through an AWS delivery role and log destination. Off AWS, the target environment's network and logging integration supplies observability; the AWS flow-log pipeline, delivery role and log group are not recreated. Configure traffic-log collection and retention on the target. General application logs alone do not establish equivalent network-flow coverage. [Service Catalog](/service-adapters/catalog). # AppConfig Source: https://docs.tensor9.com/service-adapters/aws/other-services/appconfig AWS AppConfig. Distributes application configuration and feature flags to running applications, rolling changes out gradually and rolling back on a CloudWatch alarm. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud, OCI, and Private Kubernetes](#on-google-cloud-oci-and-private-kubernetes) * [On Azure](#on-azure) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of AppConfig with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | AppConfig | Google Cloud, OCI, and Private Kubernetes | Azure | | ----------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Feature-flag evaluation (enabled state + typed value) | Yes | Yes - Flagsmith evaluates a flag's enabled state and value through its SDK / REST API | Yes - App Configuration evaluates native feature flags through its SDK | | Dynamic / free-form configuration | Yes | Partial - served as Flagsmith remote-config values; the AWS.AppConfig.FeatureFlags typed-attribute schema is not enforced | Yes - served as App Configuration key-values in the same store | | Per-environment targeting | Yes | Yes - native Flagsmith environments under a Flagsmith project | Partial - App Configuration labels hold the environment dimension; the AppConfig application and configuration-profile hierarchy is not recreated | | Progressive rollout (bake / growth / final-bake) | Yes | No - this mapping publishes an environment's flag values together; implement a staged schedule in the release pipeline | No - App Configuration serves the current value with no managed ramp; a staged rollout becomes a CI / pipeline responsibility | | Monitored, rollback-capable deployment | Yes | No - no managed deployment object; a rollback is republishing the prior value (a CI / pipeline responsibility) | No - no managed deployment object; a rollback is republishing the prior value (a CI / pipeline responsibility), aided by the store's key-value revision history | | Automatic rollback on a CloudWatch alarm | Yes | No - no alarm-driven auto-revert; monitoring and revert move to CI / pipeline | No - no alarm-driven auto-revert; monitoring and revert move to CI / pipeline | | Config validators (JSON-Schema / Lambda) | Yes | No - run the required AppConfig JSON-Schema or Lambda validation in the release pipeline | No - run the required AppConfig JSON-Schema or Lambda validation in the release pipeline | | Runs outside AWS | No - AppConfig is AWS-only | Yes - Flagsmith runs on the appliance's Kubernetes cluster | - | | API coverage | full | partial | partial | | Runs off AWS (native Azure managed service) | No - AppConfig is AWS-only | - | Yes - Azure App Configuration is a native Azure managed service | ## On Google Cloud, OCI, and Private Kubernetes | Operation | Area | Support | Depth | Notes | | -------------------------------------- | ------------------- | ------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | GetLatestConfiguration | Config reads | Supported | Common | polls the current flag set and values; served as a Flagsmith SDK flag fetch against the in-cluster server | | StartConfigurationSession | Config reads | Supported | Common | the application opens a flag / config read session; served by the Flagsmith SDK client instead of the AppConfig agent | | CreateDeploymentStrategy | Deployment controls | Out of scope | Full surface | the staged bake / growth / final-bake ramp has no Flagsmith analog; the rollout schedule is a CI / pipeline responsibility | | Deployment validators + alarm rollback | Deployment controls | Out of scope | Full surface | JSON-Schema / Lambda validators and CloudWatch-alarm automatic rollback have no counterpart; validation and revert move to CI / pipeline | | StartDeployment / StopDeployment | Deployment controls | Out of scope | Full surface | no monitored deployment object; a rollback is republishing the prior flag value | | Feature flag enabled state | Feature flags | Supported | Common | a flag's on/off state reads back through the Flagsmith SDK | | Feature flag typed value | Feature flags | Partial | Most usage | the value lands as a Flagsmith remote-config value; the AWS.AppConfig.FeatureFlags attribute schema is not enforced | | CreateApplication / CreateEnvironment | Provisioning | Supported | Most usage | the application and environment become a Flagsmith project and environment at deploy time | | CreateConfigurationProfile | Provisioning | Partial | Most usage | a feature-flag profile becomes Flagsmith flags; a free-form profile becomes remote-config values | | CreateHostedConfigurationVersion | Provisioning | Supported | Most usage | the configuration document is the source the flags are provisioned from | #### How it works This target replaces the AppConfig agent with Flagsmith, an open-source feature-flag and configuration server on the appliance's Kubernetes cluster. Update application reads to use the Flagsmith SDK or REST API for each flag's enabled state and value. At deployment, Tensor9 creates flags and remote-configuration values from the `AWS.AppConfig.FeatureFlags` JSON document. AppConfig's staged rollout, deployment monitoring and automatic rollback are unsupported; implement those release controls in your deployment process.
Before: on AWS the application reads flags and configuration from the AppConfig agent, which fetches them from AWS AppConfig. After: in the target cloud the same application reads the same flags through the Flagsmith SDK from a self-hosted Flagsmith server running on the appliance's cluster. Before: on AWS the application reads flags and configuration from the AppConfig agent, which fetches them from AWS AppConfig. After: in the target cloud the same application reads the same flags through the Flagsmith SDK from a self-hosted Flagsmith server running on the appliance's cluster.

The application reads flag state and values through the Flagsmith SDK or REST API. Flagsmith runs on the appliance cluster.

#### Architecture Terraform creates a Flagsmith Kubernetes Deployment on port 8000 and a PostgreSQL StatefulSet with a persistent volume. A ClusterIP Service exposes Flagsmith to the application; a headless Service exposes PostgreSQL to Flagsmith. PostgreSQL stores flag state and values, which Flagsmith evaluates when the SDK refreshes its data. A provisioning step reads the AppConfig document and creates flags through the Flagsmith administration API before the application starts reading them. After provisioning, the application reads the running Flagsmith service. Your platform team operates Flagsmith and PostgreSQL, including availability, capacity, backups and upgrades.
Architecture: on the appliance cluster the application reads flags from a Flagsmith server Deployment, which stores its state in an in-cluster Postgres StatefulSet. At deploy time a provisioning step reads the AppConfig configuration document and creates the flags in Flagsmith through its admin API. Architecture: on the appliance cluster the application reads flags from a Flagsmith server Deployment, which stores its state in an in-cluster Postgres StatefulSet. At deploy time a provisioning step reads the AppConfig configuration document and creates the flags in Flagsmith through its admin API.

The Flagsmith server and its Postgres run on the appliance cluster; a one-time provisioning step at deploy creates the flags from the AppConfig configuration document, and the application reads from the server after that.

#### Creating flags from the configuration document An `AWS.AppConfig.FeatureFlags` profile has a top-level `values` map, one entry per flag, each with an `enabled` boolean and optional typed attributes. The provisioning step walks that map and creates one Flagsmith flag per entry: the flag's on/off state maps to Flagsmith's enabled state, and a flag's attributes become its remote-config value. A configuration profile that holds free-form JSON rather than feature flags becomes remote-config values on the same Flagsmith environment. AppConfig validates feature-flag attributes against declared types and constraints. Flagsmith stores the translated values but does not apply that schema. Validate content before provisioning or editing flags so future values meet your application's expectations.
An AWS.AppConfig.FeatureFlags document has a values map, one entry per flag with an enabled boolean and typed attributes. Each entry becomes a Flagsmith flag that keeps its enabled state and value; the typed-attribute schema is not enforced by Flagsmith. An AWS.AppConfig.FeatureFlags document has a values map, one entry per flag with an enabled boolean and typed attributes. Each entry becomes a Flagsmith flag that keeps its enabled state and value; the typed-attribute schema is not enforced by Flagsmith.

Each AWS.AppConfig.FeatureFlags entry becomes a Flagsmith flag; its enabled state and typed value come with it, and free-form configuration becomes remote-config values.

#### Reading flag state and values Flagsmith returns a flag's enabled state and its associated configuration value, such as a rollout percentage, variant name or tuning constant. Update application integration to the Flagsmith SDK while preserving how the application uses those values. #### Per-environment targeting An AppConfig application maps to a Flagsmith project, and each AppConfig environment maps to a Flagsmith environment. Environments such as staging and production keep independent states and values for the project's flags. AppConfig configuration profiles, profile versions and deployment history are not recreated. The target contains the provisioned flag state. Keep the source document and change history in the system that provisions Flagsmith. #### Reading at runtime Replace reads from the AppConfig agent's loopback address with the Flagsmith SDK or REST API for the in-cluster server. The SDK caches the environment's flags and refreshes periodically; reads against that cache do not require a request per flag. Configure the Flagsmith SDK with the server URL and the appropriate environment key. The native SDK uses that key; cluster workload identity does not replace Flagsmith authentication. Size Flagsmith and PostgreSQL for refresh traffic. Choose cache settings based on how long the application can tolerate stale flags during a server interruption. #### Replacing rollout and rollback controls This target provisions current flag values but does not recreate AppConfig deployment strategies, deployment monitoring or validators. Add the following controls to your release process where your application requires them. * **Staged rollout.** An AppConfig deployment strategy ramps a new value to a growing fraction of the fleet over a bake window (a growth factor, a growth type, a final bake time). Flagsmith flips a flag for the whole environment at once. To stage a Flagsmith rollout, drive it from the percentage-split value the application reads, changed on a schedule by the provisioning process. * **Monitored, rollback-capable deployment.** An AppConfig deployment is an object with a lifecycle that can be halted and rolled back. Flagsmith has no deployment object; a rollback is republishing the prior flag value. Keep the prior value recoverable in the provisioning pipeline so a revert is one step. * **Automatic rollback on an alarm.** AppConfig reverts a deployment automatically when a bound CloudWatch alarm fires during the bake. Flagsmith does not watch a metric. Reproduce this as a pipeline that watches the signal and republishes the prior value on a bad reading. * **Pre-deploy validators.** AppConfig can block a new version on a JSON-Schema or Lambda validator before it ships. Flagsmith enforces no validator on a flag change. Run that validation in CI, on the flag content, before it is provisioned. #### Other considerations * **Provision the final AppConfig state.** Flagsmith is initialized from the configuration document at deployment. If AppConfig changes during cutover, update that document before provisioning so the target receives the intended final values. * **Operate Flagsmith and its database.** The appliance hosts the server, PostgreSQL and their Kubernetes Services. Your team manages cluster capacity and patching, Flagsmith upgrades and database backups. ## On Azure | Operation | Area | Support | Depth | Notes | | -------------------------------------- | ------------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | GetLatestConfiguration | Config reads | Supported | Common | polls the current flags and values; served as an App Configuration SDK read of the store's keys and feature flags for the workload's label | | StartConfigurationSession | Config reads | Supported | Common | the application opens a flag / config read session; served by the Azure App Configuration SDK client instead of the AppConfig agent | | CreateDeploymentStrategy | Deployment controls | Out of scope | Full surface | the staged bake / growth / final-bake ramp has no App Configuration analog; the rollout schedule is a CI / pipeline responsibility | | Deployment validators + alarm rollback | Deployment controls | Out of scope | Full surface | JSON-Schema / Lambda validators and CloudWatch-alarm automatic rollback have no counterpart; validation and revert move to CI / pipeline | | StartDeployment / StopDeployment | Deployment controls | Out of scope | Full surface | no managed deployment object; the store's key-value revision history keeps the prior value recoverable, but the revert is a manual or pipeline step | | Feature flag enabled state | Feature flags | Supported | Common | a flag's on/off state reads back through the native App Configuration feature-flag SDK | | Feature flag typed value | Feature flags | Supported | Most usage | the value is kept on the native feature flag; App Configuration does not enforce the AWS.AppConfig.FeatureFlags attribute schema | | CreateApplication / CreateEnvironment | Provisioning | Partial | Most usage | the environment dimension maps to App Configuration labels; the AppConfig application and configuration-profile hierarchy is not recreated | | CreateConfigurationProfile | Provisioning | Supported | Most usage | a feature-flag profile becomes native feature flags; a free-form profile becomes key-values | | CreateHostedConfigurationVersion | Provisioning | Supported | Most usage | the configuration document is the source Terraform provisions the store's entries from | #### How it works Azure App Configuration replaces the AppConfig agent as the source of feature flags and dynamic configuration. Update application reads to use the Azure App Configuration SDK against the managed store in the customer's subscription. Terraform provisions feature flags and key-values from the `AWS.AppConfig.FeatureFlags` document. AppConfig's staged rollout, deployment monitoring and automatic rollback are not recreated; implement those controls in the process that publishes configuration changes.
Before: on AWS the application reads flags and configuration from the AppConfig agent, which fetches them from AWS AppConfig. After: in Azure subscription the same application reads the same flags through the Azure App Configuration SDK from a native, Microsoft-managed App Configuration store. Before: on AWS the application reads flags and configuration from the AppConfig agent, which fetches them from AWS AppConfig. After: in Azure subscription the same application reads the same flags through the Azure App Configuration SDK from a native, Microsoft-managed App Configuration store.

The application reads flag state and values through the Azure App Configuration SDK. Microsoft operates the configuration store.

#### Architecture The compiler emits an `azurerm_app_configuration` store in the target subscription, one `azurerm_app_configuration_feature` for each AppConfig feature flag, and one `azurerm_app_configuration_key` for each free-form configuration key. Terraform provisions all of them from the AppConfig configuration document at apply time. The store is a managed Azure service: Microsoft runs its storage, replication, and availability, and the flags and values live in the customer's own store. The application connects directly to Azure App Configuration using its SDK and the workload's Azure identity. Terraform provisions configuration, so there is no Tensor9 adapter in the read path.
Architecture: in Azure subscription the application reads from a Microsoft-managed App Configuration store through the App Configuration SDK. Terraform provisions the store, one native feature flag per AppConfig flag, and one key-value per free-form key, from the AppConfig configuration document. No runtime component sits in the read path. Architecture: in Azure subscription the application reads from a Microsoft-managed App Configuration store through the App Configuration SDK. Terraform provisions the store, one native feature flag per AppConfig flag, and one key-value per free-form key, from the AppConfig configuration document. No runtime component sits in the read path.

Terraform provisions the App Configuration store, one native feature flag per AppConfig flag, and one key-value per free-form key; the application reads through the SDK, and no runtime component sits in the path.

#### Creating flags and configuration keys An `AWS.AppConfig.FeatureFlags` profile has a top-level `values` map, one entry per flag, each with an `enabled` boolean and optional typed attributes. Terraform walks that map and provisions one native App Configuration feature flag per entry: the flag's on/off state maps to the feature flag's enabled state, and its attributes supply the flag's value. A configuration profile that holds free-form JSON rather than feature flags is provisioned as key-values in the same store, one per top-level key. Azure App Configuration stores translated values but does not apply AppConfig's declared attribute types and constraints. Validate new content before provisioning it so future edits remain compatible with the application.
An AWS.AppConfig.FeatureFlags document has a values map, one entry per flag with an enabled boolean and typed attributes. Each entry becomes a native App Configuration feature flag whose enabled state and value both survive; free-form JSON keys become key-values. An AWS.AppConfig.FeatureFlags document has a values map, one entry per flag with an enabled boolean and typed attributes. Each entry becomes a native App Configuration feature flag whose enabled state and value both survive; free-form JSON keys become key-values.

Each AWS.AppConfig.FeatureFlags entry becomes a native App Configuration feature flag; it retains its enabled state and typed value, and free-form configuration becomes key-values.

#### Reading feature flags and configuration Feature flags use App Configuration's feature-flag key convention and content type. The Azure SDK and Feature Management libraries read their enabled state and any associated value. Free-form configuration uses regular key-values. * **Enabled state.** The SDK reads the flag's boolean state, corresponding to the AppConfig enabled field. * **Typed values are preserved.** AppConfig's typed attributes become the feature flag's value. The application reads the value; App Configuration does not enforce the attribute type or its constraints, so that discipline lives wherever the flag content is authored. * **Free-form config shares the store.** A non-flag configuration profile is provisioned as key-values in the same store, so a workload that mixes feature flags with dynamic configuration reads both from one place through one SDK. * **SSM configuration can use the same store.** SSM Parameter Store maps to Azure App Configuration key-values. AppConfig feature flags use feature-flag entries, allowing the SDK to distinguish them from ordinary configuration values. #### Per-environment targeting AppConfig scopes configuration by application and environment: the same flag can be on in one environment and off in another. App Configuration expresses the environment dimension with labels. A key or feature flag can hold a different value per label, and the SDK selects a label at read time, so one store serves several environments and the application reads the value for its own. The AppConfig environment maps onto the label the workload reads. AppConfig profile versions and deployment history are not imported. The target stores current values by label and maintains its own key-value revision history. Keep the AppConfig source history in the system that provisions the store. #### Reading at runtime Replace reads from the AppConfig agent's loopback address with Azure App Configuration SDK reads for the workload's environment label. The SDK caches keys and flags and refreshes on an interval or change signal; reads against that cache do not require a network request per value. The SDK authenticates with the workload's Azure identity, without a separate credential for this path. Microsoft operates store availability, replication and patching. Your team selects the store tier, access policy and client cache settings. #### Replacing rollout and rollback controls This target provisions current configuration but does not recreate AppConfig deployment strategies, monitored deployment objects or validators. Add the following controls to your release process where required. * **Staged rollout.** An AppConfig deployment strategy ramps a new value to a growing fraction of the fleet over a bake window (a growth factor, a growth type, a final bake time). App Configuration serves the current value of a key or flag with no managed ramp. To stage a rollout, drive it from a percentage value the application reads, changed on a schedule by whatever provisions the store. * **Monitored, rollback-capable deployment.** An AppConfig deployment is an object with a lifecycle that can be halted and rolled back. App Configuration has no deployment object; a rollback is republishing the prior value (its key-value revision history makes the prior value recoverable, but the revert is a manual or pipeline step, not an automatic one). * **Automatic rollback on an alarm.** AppConfig reverts a deployment automatically when a bound CloudWatch alarm fires during the bake. App Configuration does not watch a metric. Reproduce this as a pipeline that watches the signal and republishes the prior value on a bad reading. * **Pre-deploy validators.** AppConfig can block a new version on a JSON-Schema or Lambda validator before it ships. App Configuration enforces no validator on a value change. Run that validation in CI, on the flag content, before it is provisioned. Run validation, staged updates and monitoring-driven reverts in the process that provisions the store. Include these controls in the migration plan alongside the SDK change. #### Other considerations * **Provision the final AppConfig state.** Terraform creates flags and key-values from the configuration document. If AppConfig changes during cutover, update the document before applying Terraform. * **Operations and ownership.** App Configuration is a managed Azure service, so its storage, replication, and availability are Microsoft's to run; there is no self-hosted server and no Tensor9 runtime component to operate. What remains operational is the surrounding platform: the Azure subscription, the store's SKU and access policy, and the identity the workload reads with. [Service Catalog](/service-adapters/catalog). # EMR Source: https://docs.tensor9.com/service-adapters/aws/other-services/emr AWS EMR. Provisions clusters of EC2 instances running Spark, Hive and other Hadoop ecosystem frameworks, usually against data held in S3. ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | - | | OCI | - | | Private Kubernetes | - | ## Mapping and limitations On Google Cloud, an EMR cluster becomes a Dataproc cluster with master and worker counts, boot-disk sizes, an autoscaling policy and idle deletion. Select the Dataproc image and machine types for your workload. Bootstrap scripts, EMR steps and application configuration need separate migration; the translated cluster initially uses the default network and service account unless you configure them. These target services provide the mapping described above, within its stated limits. | Cloud | Target service | | ------------ | -------------- | | Google Cloud | Dataproc | [Service Catalog](/service-adapters/catalog). # GuardDuty Source: https://docs.tensor9.com/service-adapters/aws/other-services/guardduty AWS GuardDuty. Continuously analyzes CloudTrail, VPC flow and DNS logs for signs of compromise, and raises findings with a severity score. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | - | ## How the targets compare Each row compares a capability of GuardDuty with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | GuardDuty | Google Cloud | Azure | OCI | | ----------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | Continuous managed detection | Yes | Yes - Google-operated Security Command Center detectors | Yes - Microsoft-operated Defender plans | Yes - Oracle-operated Cloud Guard detector recipes | | Detection sources · telemetry analyzed | CloudTrail, VPC Flow Logs, DNS, EKS audit, S3 data events | Supported Google Cloud logs and signals; required services and modules must be enabled | Azure signals supported by the configured Defender plans | OCI activity, configuration and threat signals supported by the selected recipes | | Finding types · portability | GuardDuty finding types | Security Command Center categories; different detector rules | Defender alert types; different rules and categories | Cloud Guard problem types and detector rules | | Severity-scored findings | Yes | Yes | Yes | Yes | | Filters, IP sets, threat-intel sets | Yes | Partial - Review and recreate supported mute rules and other target-specific settings | Partial - Review and recreate supported target-specific rules | Partial - Review and recreate supported detector-rule conditions and managed lists | | SIEM integrations and response | Yes | Yes - Pub/Sub exports and Google SecOps integrations | Yes - Microsoft Sentinel integrations, analytics rules and playbooks | Yes - OCI Events and optional Cloud Guard responder recipes | | Enablement · how protection is configured | Detector and feature settings | Security Command Center service tier, detectors and log sources | Resource-specific Defender plans and settings | Detector and responder recipes attached to a compartment target | | API coverage | full | partial | partial | partial | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------- | ------------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Account activity threat detection (CloudTrail analysis) | Detection sources | Supported | Common | Event Threat Detection analyzes supported Google Cloud audit logs. Its events and finding categories differ from GuardDuty's CloudTrail findings. | | DNS threat detection (DNS query-log analysis) | Detection sources | Supported | Most usage | Event Threat Detection can analyze supported DNS logs. Its detector rules differ from GuardDuty's DNS finding catalog. | | Kubernetes audit-log threat detection (EKS) | Detection sources | Supported | Most usage | Event Threat Detection analyzes supported GKE audit logs; Container Threat Detection covers runtime signals. Coverage differs from GuardDuty's EKS rules. | | Malware Protection (EBS volume scan) | Detection sources | Partial | Full surface | Virtual Machine Threat Detection uses a different scanning mechanism from GuardDuty's EBS volume scanning; compare the malware coverage your workload needs. | | Network threat detection (VPC Flow Log analysis) | Detection sources | Supported | Most usage | Google Cloud network-threat detection uses its supported network telemetry. Configure the required logs and review which GuardDuty detections have an equivalent. | | Detector enablement (SCC Event Threat Detection) | Detector | Supported | Common | Maps to Security Command Center's managed threat detectors, including Event Threat Detection. The required services and logging must be enabled; a custom finding source alone does not enable detection. | | Detector-set tier gating | Detector | Partial | Full surface | Detector availability depends on the Security Command Center service tier and enabled modules. Choose the services and log sources required for the intended coverage. | | GuardDuty finding-type catalog | Findings | Out of scope | Full surface | GuardDuty finding-type strings do not map one-for-one to Security Command Center categories. Update automations that match specific AWS finding names. | | SIEM / event fan-out | Findings | Supported | Common | Findings can be exported to Pub/Sub and integrated with Google SecOps. Adapt response rules to the target's finding schema. | | Severity-scored findings | Findings | Supported | Common | Security Command Center produces severity-scored findings with Google Cloud categories. They are not GuardDuty finding types. | | Finding filters (aws\_guardduty\_filter) | Suppression + lists | Out of scope | Full surface | GuardDuty filters do not translate directly into Security Command Center mute rules. Review and recreate required suppression. | | Threat-intel sets (aws\_guardduty\_threatintelset) | Suppression + lists | Out of scope | Full surface | GuardDuty threat-intelligence feeds require target-specific configuration; they are not copied directly. | | Trusted IP sets (aws\_guardduty\_ipset) | Suppression + lists | Out of scope | Full surface | GuardDuty trusted-IP lists require target-specific configuration; they are not copied directly. | #### How it works On Google Cloud, GuardDuty maps to Security Command Center (SCC) threat-detection services. Google operates detection over the customer's Google Cloud resources and produces findings with severity levels. This is a best-effort mapping between different security products: each required threat scenario needs a coverage review. Event Threat Detection analyzes supported logs; other SCC services cover container and VM threats. Configure the required services and log inputs in the customer's organization or projects, and connect findings to Pub/Sub or Google SecOps for downstream processing. GuardDuty findings, filters and response rules need translation to the target services.
GuardDuty maps to Google Security Command Center threat detection over Google Cloud resources. Configure the needed detectors, log sources and finding exports; coverage differs by detector. GuardDuty maps to Google Security Command Center threat detection over Google Cloud resources. Configure the needed detectors, log sources and finding exports; coverage differs by detector.

SCC provides the intended Google Cloud detection services. Review detector coverage and configure finding export.

#### SCC service configuration Configure SCC's built-in threat-detection services for the customer's organization or projects. The selected detectors determine the required log inputs and service tier. GuardDuty feature toggles map to this service configuration rather than to one identical Google detector. Enable the required logs and finding exports, then test the response rules that consume SCC findings. [Google's service list](https://docs.cloud.google.com/security-command-center/docs/concepts-security-sources) describes the detectors available for each workload.
The Google target configures Security Command Center services for the customer's organization or projects, supplies the required logs and exports findings for response rules. The Google target configures Security Command Center services for the customer's organization or projects, supplies the required logs and exports findings for response rules.
#### How findings map GuardDuty identifiers such as `UnauthorizedAccess:EC2/SSHBruteForce`, `CryptoCurrency:EC2/BitcoinTool.B` and `Recon:EC2/Portscan` do not become SCC finding identifiers. SCC detectors produce their own categories and severity levels, so update rules that match the AWS identifiers. Review EventBridge filters, suppression rules and downstream parsers that use GuardDuty finding names, including patterns such as `CryptoCurrency:*`. Determine whether a Google detector covers each required case before rewriting the rule. The diagram describes areas to review, not verified one-to-one finding mappings.
GuardDuty finding names differ from SCC categories. Review SSH, cryptomining and network detection separately; no one-to-one catalog mapping is implied. GuardDuty finding names differ from SCC categories. Review SSH, cryptomining and network detection separately; no one-to-one catalog mapping is implied.

SCC uses its own finding categories. Review each rule that matches a GuardDuty finding type and rewrite it for the relevant SCC category.

#### Detection sources Cloud Audit Logs, network logs, DNS logs and GKE signals are candidate inputs for the Google services used in this mapping. Coverage depends on the specific detector and its supported log sources. Review the threats your application must detect instead of assuming that similar log names imply equivalent analysis. GuardDuty Malware Protection scans EBS volume snapshots. SCC Virtual Machine Threat Detection examines running VM memory. These inspect different data, so assess the required malware scenarios separately; memory inspection does not replace every snapshot-scan use case.
Review Google audit, network, DNS and container detection against the required GuardDuty scenarios. VM memory inspection differs from EBS snapshot scanning. Review Google audit, network, DNS and container detection against the required GuardDuty scenarios. VM memory inspection differs from EBS snapshot scanning.

Review each Google detector and its log inputs against the required threat scenarios.

#### SCC activation and detectors Event Threat Detection analyzes supported log sources. Container and Virtual Machine Threat Detection examine different workload signals. Select the services for the customer's actual workloads and confirm their availability in the chosen SCC subscription. Scope access to findings and exports as well as access to the protected resources. Security operators need the target's categories, severity and resource identifiers in their response procedures. #### Limitations △ Detection differences * **GuardDuty finding identifiers and rules are not translated.** Rules matching `UnauthorizedAccess:EC2/SSHBruteForce` or `CryptoCurrency:*` need a detector-by-detector coverage review and new target rules. * **Filters, trusted IP sets and threat feeds are not generated.** GuardDuty filters, trusted IP sets and threat-intelligence sets do not have a one-to-one mapping. Configure the corresponding target controls for each requirement and review their differences. * **Malware scanning differs.** GuardDuty EBS snapshot scans and SCC VM memory inspection examine different data. Select protection based on the workload and threat being detected. #### Other considerations * **Configure the customer scope.** Use the customer organization or project scope selected for SCC. Historical GuardDuty findings are not copied into the target. * **Configure finding export and response rules.** Configure Pub/Sub export, Google SecOps rules and response automation for the selected detectors. GuardDuty EventBridge rules need review against SCC categories and severity. * **Check the current service tier and detector settings.** SCC service availability and pricing depend on the subscription. Confirm each required detector and log input individually. ## On Azure | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------- | ------------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Account activity threat detection (CloudTrail analysis) | Detection sources | Supported | Common | Azure activity detection uses Azure's activity and resource-management signals. The events and alert types differ from CloudTrail-based findings. | | DNS threat detection (DNS query-log analysis) | Detection sources | Supported | Most usage | DNS-related threats are detected through the signals available to the configured Defender plans. Review coverage for the DNS threats your application needs. | | Kubernetes audit-log threat detection (EKS) | Detection sources | Supported | Most usage | Defender for Containers analyzes AKS audit and runtime signals. Its detection rules differ from GuardDuty's EKS rules. | | Malware Protection (EBS volume scan) | Detection sources | Partial | Full surface | Defender for Servers provides malware detection, including agentless scanning in Plan 2. Select the required plan and scanning settings; detection differs from GuardDuty's EBS scan. | | Network threat detection (VPC Flow Log analysis) | Detection sources | Partial | Most usage | Network alerts depend on Defender's available telemetry and plan configuration; they do not reproduce GuardDuty's VPC Flow Log finding catalog. | | Detector enablement (Defender for Cloud plans) | Detector | Supported | Common | Maps to Microsoft Defender for Cloud plans, including Servers and Storage. Protection is configured by resource type rather than through one GuardDuty detector. | | GuardDuty finding-type catalog | Findings | Out of scope | Full surface | GuardDuty finding-type strings are AWS-specific. Update automations that match them to use the relevant Defender alert types. | | Publishing destination (S3 finding export) | Findings | Out of scope | Full surface | GuardDuty's S3 finding-export configuration does not translate directly. Select an Azure export destination and configure the required integration. | | SIEM / event fan-out | Findings | Supported | Common | Microsoft Sentinel receives Defender alerts through its integrations. Response rules need to use Defender alert types rather than AWS event names. | | Severity-scored findings | Findings | Supported | Common | Defender produces severity-scored Azure security alerts. Alert types and scoring differ from GuardDuty's. | | Finding filters (aws\_guardduty\_filter) | Suppression + lists | Out of scope | Full surface | GuardDuty suppression rules do not translate directly. Review and recreate required exclusions using Defender's supported configuration. | | Threat-intel sets (aws\_guardduty\_threatintelset) | Suppression + lists | Out of scope | Full surface | Custom GuardDuty threat feeds require a target-specific integration; enabling Defender does not copy them. | | Trusted IP sets (aws\_guardduty\_ipset) | Suppression + lists | Out of scope | Full surface | GuardDuty trusted-IP lists do not translate directly. Review the exclusions supported by the relevant Azure detector. | #### How it works On Azure, GuardDuty maps to Microsoft Defender for Cloud protection plans. Microsoft operates detection over Azure resources and produces security alerts with severity levels. This is a best-effort mapping: Defender uses different signals and rules, so review each required threat scenario. Select the Defender plans and settings for the customer's servers, storage, containers and subscription activity. Configure Microsoft Sentinel or another supported integration for downstream analysis and response. The relevant plan, telemetry and alert names differ from GuardDuty's detector and finding types.
GuardDuty maps to Azure Defender for Cloud plans selected for the customer resources. Configure required telemetry and Sentinel integration; detection fidelity is imperfect. GuardDuty maps to Azure Defender for Cloud plans selected for the customer resources. Configure required telemetry and Sentinel integration; detection fidelity is imperfect.

Defender plans provide the intended Azure detection services. Configure required plan settings and Sentinel integration.

#### Defender plan configuration Defender protection is configured by resource type and plan, commonly through `azurerm_security_center_subscription_pricing`. Servers uses `VirtualMachines` and Storage uses `StorageAccounts` at the Standard tier. Containers and subscription-activity protection require their corresponding settings. Check the effective plan, subplan and extensions for the customer's resources. A subscription-level setting does not establish that every workload or scanning feature is protected. Choose required telemetry collection and Sentinel integration alongside the plans.
Defender uses resource-specific plans and settings. Servers and Storage have their own subscription pricing resources; other protection and scanning settings must be selected for the workload. Defender uses resource-specific plans and settings. Servers and Storage have their own subscription pricing resources; other protection and scanning settings must be selected for the workload.

The detector compiles onto the Defender for Cloud plans: one azurerm\_security\_center\_subscription\_pricing per resource type, at the Standard tier, enabled for the whole subscription.

#### How findings map GuardDuty finding identifiers such as `UnauthorizedAccess:EC2/SSHBruteForce`, `CryptoCurrency:EC2/BitcoinTool.B` and `Recon:EC2/Portscan` are AWS-specific. Defender uses its own alert names and severity assignments for threats such as brute-force attempts, cryptomining and port scans. Review EventBridge filters, suppression rules and downstream parsers that use GuardDuty names, including patterns such as `CryptoCurrency:*`. Rewrite them for Defender alert families and the signals each enabled plan analyzes. Enabling plans does not translate those rules or establish one-to-one detection coverage.
GuardDuty finding names differ from Defender alert names. Review detection behavior and rewrite rules for the required threats. GuardDuty finding names differ from Defender alert names. Review detection behavior and rewrite rules for the required threats.

Defender uses its own alert families. Review each rule that matches a GuardDuty finding type and rewrite it for the relevant Defender alert.

#### Detection sources Map server, storage, container and subscription-activity threats to the relevant Defender protection. DNS protection is part of Defender for Servers; the older standalone Defender for DNS plan is not the basis for a new deployment. Review the actual protected resources and signals for each GuardDuty use case. Defender for Servers includes endpoint protection, and Plan 2 also offers agentless scanning, including malware scanning. Compare the selected subplan and scanning settings with the application's EBS malware requirements. Different scanning mechanisms do not imply identical detection. See [Microsoft's plan comparison](https://learn.microsoft.com/en-us/azure/defender-for-cloud/plan-defender-for-servers-select-plan).
Compare AWS detection requirements with Azure protection plans and active settings. Server malware protection includes endpoint and agentless options with different coverage. Compare AWS detection requirements with Azure protection plans and active settings. Server malware protection includes endpoint and agentless options with different coverage.

Review each AWS detection requirement against the selected Azure protection and scanning settings.

#### Severity, response, and fan-out On AWS, GuardDuty findings can feed Security Hub and EventBridge. On Azure, Defender alerts can feed Microsoft Sentinel, a security information and event management (SIEM) service. Configure the connection, analytics rules and playbooks for the required responses. Rewrite EventBridge filters against the relevant Defender alert names and test severity handling and response actions. Similar alert topics do not imply that the rule matches the same event or has the same false-positive behavior.
Defender alerts can feed Microsoft Sentinel. Configure analytics rules and playbooks for the required responses and verify their behavior. Defender alerts can feed Microsoft Sentinel. Configure analytics rules and playbooks for the required responses and verify their behavior.

A configured Sentinel connection receives Defender alerts for analytics rules and response playbooks.

#### Limitations △ Configuration gaps * **Protection depends on plan and workload settings.** Select Containers and Resource Manager protection, agents and scanning extensions as required. Review effective settings and detection coverage in the customer subscription. * **Finding names and automation differ.** Rules matching GuardDuty identifiers such as `UnauthorizedAccess:EC2/SSHBruteForce` or `CryptoCurrency:*` need review against Defender alert behavior and names. * **GuardDuty filters, IP sets and threat feeds are not translated.** The build does not recreate `aws_guardduty_filter`, `aws_guardduty_ipset` or `aws_guardduty_threatintelset`. Assess target controls for each requirement. * **Network and malware coverage need separate verification.** Similar plan names do not prove that every GuardDuty network finding or EBS malware scenario is detected. Check active subplans, extensions and workload settings. #### Other considerations * **The plans apply at subscription scope.** Review plan scope and cost across the customer subscription. Historical GuardDuty findings are not migrated. * **Select additional protection for the workload.** Configure required protection beyond Servers and Storage explicitly. Product plan names and availability change; use current Microsoft documentation when choosing those settings. * **Configure Sentinel integration.** Configure the alert connection, analytics rules and playbooks separately, then test the responses the application requires. ## On OCI | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------- | ------------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Account activity threat detection (CloudTrail analysis) | Detection sources | Supported | Common | Cloud Guard activity detectors analyze OCI Audit events. Their rules differ from GuardDuty's CloudTrail finding types. | | Configuration / posture detection | Detection sources | Supported | Most usage | Cloud Guard configuration detectors identify resource configuration issues in addition to activity-based threats. | | Kubernetes audit-log threat detection (EKS) | Detection sources | Partial | Most usage | OKE coverage depends on Cloud Guard's available detector recipes. It does not reproduce GuardDuty's EKS audit-log finding catalog. | | Network threat detection (VPC Flow Log analysis) | Detection sources | Partial | Full surface | Cloud Guard's activity and configuration detection differs from GuardDuty's network-flow analysis. Review the specific network threats your workload requires. | | Threat-intel detection | Detection sources | Supported | Most usage | Cloud Guard threat detectors use OCI's threat signals and rules; findings do not match GuardDuty's catalog one-for-one. | | Detector enablement (Cloud Guard recipes + target) | Detector | Supported | Common | Maps to Cloud Guard with a compartment target and the required detector recipes. Recipes determine which resources and activity are monitored. | | GuardDuty finding-type catalog | Findings | Out of scope | Full surface | GuardDuty finding-type strings do not map one-for-one to Cloud Guard problem types. Update automations that match AWS finding names. | | Response / auto-remediation | Findings | Supported | Common | Cloud Guard responder recipes can act on detected problems. Responses are opt-in and can change live resources; review them before enabling. | | Severity-scored findings | Findings | Supported | Common | Cloud Guard produces severity-scored problems using its own problem types and scoring. | | Finding filters (aws\_guardduty\_filter) | Suppression + lists | Out of scope | Full surface | GuardDuty filters do not translate directly. Review and recreate required suppression in Cloud Guard detector-rule conditions. | | Threat-intel sets (aws\_guardduty\_threatintelset) | Suppression + lists | Out of scope | Full surface | GuardDuty threat-intelligence feeds require target-specific configuration; they are not copied directly. | | Trusted IP sets (aws\_guardduty\_ipset) | Suppression + lists | Out of scope | Full surface | GuardDuty trusted-IP lists require Cloud Guard managed-list or detector-rule configuration; they are not copied directly. | #### How it works On OCI, GuardDuty maps to Cloud Guard detector recipes, sets of rules that analyze OCI resources and produce problems with severity levels. Oracle operates the service. This is a best-effort mapping: the OCI rules and signals do not reproduce the GuardDuty finding catalog. Enable Cloud Guard, define the target compartment and attach detector recipes for the required threats. Configure OCI Events or responder recipes for downstream actions. Review required network and container detections separately; extra configuration checks do not fill gaps in those scenarios.
GuardDuty maps to OCI Cloud Guard targets and detector recipes. Select and verify the required rules; responder recipes and OCI Events provide response options. GuardDuty maps to OCI Cloud Guard targets and detector recipes. Select and verify the required rules; responder recipes and OCI Events provide response options.

Cloud Guard targets and detector recipes provide the intended OCI detection architecture.

#### Cloud Guard target configuration Cloud Guard uses a tenancy configuration and targets that identify monitored compartments. Terraform represents these with `oci_cloud_guard_cloud_guard_configuration` and `oci_cloud_guard_target`. Detector recipes attached to each target determine which rules run. Select and verify Activity, Configuration and Threat recipes against the customer's requirements. Responder recipes are separate controls that can change resources. Select the reporting region for the customer's deployment.
Cloud Guard uses tenancy enablement, a target compartment and attached detector recipes. The deployment selects the reporting region. Cloud Guard uses tenancy enablement, a target compartment and attached detector recipes. The deployment selects the reporting region.

Enable Cloud Guard, define a compartment target and select the detector recipes needed for the workload.

#### How findings map GuardDuty identifiers such as `UnauthorizedAccess:EC2/SSHBruteForce`, `CryptoCurrency:EC2/BitcoinTool.B` and `Recon:EC2/Portscan` are AWS-specific. Cloud Guard reports problems for suspicious activity, public exposure and risky configuration using its own names and severity assignments. Review EventBridge filters, suppression rules and downstream parsers that use GuardDuty finding names, including patterns such as `CryptoCurrency:*`. Rewrite them for the relevant Cloud Guard problem types and detector rules. Enabling Cloud Guard does not translate this automation or establish one-to-one detection coverage.
GuardDuty finding names differ from Cloud Guard problem types. Review detection behavior and severity for each required case. GuardDuty finding names differ from Cloud Guard problem types. Review detection behavior and severity for each required case.

Cloud Guard uses its own problem types. Review each GuardDuty rule and rewrite it for the relevant Cloud Guard detector rule or problem.

#### Detection sources Cloud Guard Activity, Configuration and Threat recipes analyze OCI Audit events, resource settings and threat signals. The specific rules and available data determine coverage. Configuration checks are a separate capability from GuardDuty threat detection. Review GuardDuty VPC Flow Log network analysis and EKS audit-log scenarios individually. Cloud Guard's configuration checks do not replace those detections. Document missing scenarios and decide whether other OCI controls can meet the application's requirements.
Cloud Guard offers audit, configuration and threat rules. Additional configuration checks do not replace missing network or container detection. Cloud Guard offers audit, configuration and threat rules. Additional configuration checks do not replace missing network or container detection.

Cloud Guard offers configuration checks; they do not fill network or container detection gaps.

#### Detector and responder recipes Attach the required detector recipes to the target and tune their rules. Verify the findings produced for the customer's resources; selecting a recipe name alone does not establish equivalent coverage. Responder recipes can act on problems by changing public bucket access or security-list rules. Review the actions and resources they can affect, then configure and test the intended responses. See [Oracle's target guide](https://docs.oracle.com/en-us/iaas/Content/cloud-guard/using/targets-create.htm) for recipe selection.
Detector recipes attached to a target produce problems. Explicitly configured responder recipes can act on affected resources. Detector recipes attached to a target produce problems. Explicitly configured responder recipes can act on affected resources.

Detector recipes produce problems. Separately configured responder recipes can act on affected resources.

#### Limitations △ Configuration gaps * **Detection depends on the attached recipes.** Select detector recipes and verify the required rules on the target before relying on detection. Configure responder actions separately. * **Finding names and GuardDuty rules are not translated.** Rules matching `UnauthorizedAccess:EC2/SSHBruteForce` or `CryptoCurrency:*` need review against Cloud Guard problem types and detection behavior. * **Filters, IP sets and threat feeds require review.** The build does not recreate `aws_guardduty_filter`, `aws_guardduty_ipset` or `aws_guardduty_threatintelset`. Assess which Cloud Guard rules or lists meet each requirement. * **Network and container detection gaps remain.** Cloud Guard configuration checks do not replace GuardDuty VPC Flow Log analysis or EKS audit-log coverage. Assess each required detection separately. #### Other considerations * **Cloud Guard enablement and target scope differ.** The build enables Cloud Guard and creates a compartment target. Historical GuardDuty findings are not copied, and target creation alone does not verify active detection. * **Confirm reporting region and recipe attachments.** Confirm the configured reporting region and detector recipe attachments. Enable responder recipes only for the actions the customer wants automated. * **Configure response automation.** Configure required OCI Events rules or responder recipes separately. Review the resources a responder can change and test each response before using it. [Service Catalog](/service-adapters/catalog). # GuardDuty Detector Source: https://docs.tensor9.com/service-adapters/aws/other-services/guardduty-detector AWS GuardDuty Detector. The per region resource that switches GuardDuty on for an account, holding which data sources are analyzed and how often findings publish. ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | - | ## Mapping and limitations A GuardDuty detector enables AWS-account threat detection. The corresponding native services are Security Command Center on Google Cloud, Microsoft Defender for Cloud on Azure and Cloud Guard on OCI. These services inspect their own cloud's resources and issue their own findings. AWS detector IDs, finding types and data-source settings are not interchangeable with those target services. No private Kubernetes detector mapping is offered here. These target services provide the mapping described above, within its stated limits. | Cloud | Target service | | ------------ | ---------------------------- | | Google Cloud | Security Command Center | | Azure | Microsoft Defender for Cloud | | OCI | OCI Cloud Guard | [Service Catalog](/service-adapters/catalog). # Managed Service for Apache Flink Source: https://docs.tensor9.com/service-adapters/aws/other-services/managed-service-for-apache-flink Runs Apache Flink stream processing applications, with AWS handling the job cluster, checkpoints to S3 and parallelism scaling. Preview ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | - | | Azure | - | | OCI | - | | Private Kubernetes | - | ## Mapping and limitations On Google Cloud, the application resource maps to a Dataflow Flex Template job and its parallelism sets the maximum worker count. Dataflow runs Apache Beam: an AWS Flink JAR is not a Flex Template. You must port and publish the pipeline, replace the template path and configure its service account. Flink checkpoint intervals, snapshots and per-KPU settings do not transfer. This is a partial infrastructure mapping, not unchanged execution of a Flink application. [Service Catalog](/service-adapters/catalog). # MemoryDB Source: https://docs.tensor9.com/service-adapters/aws/other-services/memorydb AWS MemoryDB. A Redis and Valkey compatible in-memory database that commits writes to a multi-AZ transaction log, making it durable enough to be the primary store. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of MemoryDB with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | MemoryDB | Google Cloud | Azure | OCI | Private Kubernetes | | ---------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------- | | Redis / Valkey wire protocol | Valkey / Redis (RESP) | direct connection; configure target auth and topology | direct connection; configure target auth and topology | direct connection; configure target auth and topology | direct connection; configure target auth and topology | | Engine + version | MemoryDB (Valkey 7.2 / Redis OSS) | Valkey 7.2-9.0 (Memorystore for Valkey) | Redis Enterprise (Azure Managed Redis) | Valkey 8.1 / 7.2 · Redis 7.0 (OCI Cache) | Valkey (self-hosted, Bitnami chart) | | Durability on failover · the commit guarantee | synchronous multi-AZ transaction log, so no acknowledged write is lost | async replication + AOF / RDB, with a failover data-loss window | zone-redundant + optional AOF; async geo-replication, with a failover loss window | managed backup + Object Storage only; loss window not published | RDB + AOF you configure (PVC): a single-node fsync, not a multi-AZ log | | Durability / HA owner · who operates the store | AWS-managed | Google (Memorystore) | Microsoft (Azure Managed Redis) | Oracle (OCI Cache) | you (self-managed on Kubernetes) | | Cluster mode / sharding | always sharded (num\_shards, up to 500 shards) | sharded (Memorystore for Valkey / Redis Cluster) | clustered by default (OSSCluster); shards auto-managed | sharded (3-99 shards) or non-sharded (1-5 nodes) | chart default primary-replica; Cluster sharding is your setup | | Replication / HA · replicas + failover | Multi-AZ replicas, 99.99% SLA | cross-zone replicas + auto-failover, up to 99.99% SLA | replication + auto-failover, zone-redundant by default | primary + up to 4 replicas; SLA % not published | 1 primary + replicas + Sentinel that you configure; no SLA | | Persistence | the multi-AZ transaction log (durable by design) | AOF (per-second) + RDB snapshots | optional RDB + AOF (HA, not a backup) | managed backup + Object Storage (AOF / RDB params not exposed) | RDB + AOF you configure (PVC-backed) | | Encryption in transit | Yes | Yes - client TLS terminates on the target's TLS listener | Yes - client TLS terminates on the target's TLS listener | Yes - client TLS terminates on the target's TLS listener | Partial - configure server and client TLS; off by default in the chart | | Encryption at rest | at-rest encryption + KMS CMK | CMEK (new instances only) | platform key + CMK (all tiers) | platform-managed (CMK not documented) | the PersistentVolume / node-disk layer's | | AUTH / ACL / RBAC | ACL user groups + IAM | Redis / Valkey AUTH + IAM | access keys + Entra ID (default) | cache-user ACL policies + renewable IAM tokens | password AUTH (default) + ACL via config | | Search / JSON / modules | core types only (no modules) | vector / JSON / Bloom (Memorystore for Valkey) | RediSearch + JSON + Bloom + TimeSeries built in | core types + Lua; no modules documented | core Valkey types; no modules bundled | | API coverage | full | high | high | high | high | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------------------------------- | ---------------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Data-plane commands (strings, hashes, sorted sets, streams, pub/sub, Lua) | Data plane | Supported | Common | the full RESP data plane runs on a native Memorystore for Valkey engine; the Redis / Valkey client is unchanged and no proxy sits in the path | | Backup / snapshot / restore | Durability | Supported | Most usage | RDB export / import to Cloud Storage plus scheduled managed backups; restore seeds a new instance | | Persistence (AOF / RDB) | Durability | Supported | Most usage | AOF always syncs locally before acknowledgment; everysec and no use periodic or OS-managed flushing. RDB snapshots provide separate recovery points; replicas remain asynchronous | | Strong consistency + durable commit | Durability | Partial | Most usage | Memorystore replicates asynchronously; Google states that on an unexpected failover acknowledged writes may be lost, so MemoryDB's commit-to-log guarantee has no counterpart (AOF / RDB narrow the window, they don't close it) | | Search / JSON / modules (FT.\* / JSON.\*) | Extensions | Supported | Full surface | Memorystore for Valkey provides vector search, JSON and Bloom capabilities | | Replication / HA / failover | Operations | Supported | Most usage | cross-zone read replicas + automatic failover; up to 99.99% SLA on Memorystore for Valkey / Redis Cluster | | Admin / config / replication commands (CONFIG / DEBUG / SAVE / SLAVEOF / CLUSTER admin) | Restricted admin | Out of scope | Full surface | the admin / replication / cluster surface (CONFIG / DEBUG / SAVE / SLAVEOF / CLUSTER admin) moves to the Google control plane; Memorystore additionally blocks CLIENT / COMMAND / OBJECT / ACL / MODULE, and CONFIG moves to gcloud flags | | AUTH / ACL / RBAC | Security | Supported | Most usage | Redis / Valkey AUTH + IAM authentication | | Encryption at rest | Security | Partial | Most usage | CMEK (customer\_managed\_key) on new instances only; platform-managed otherwise | | Encryption in transit (TLS) | Security | Supported | Common | in-transit encryption always applied; optional application-layer TLS 1.2+ | | Cluster mode (slot topology) | Topology | Supported | Most usage | MemoryDB is always sharded; the right target is the sharded Memorystore for Valkey / Redis Cluster (up to 250 shards), not the single-shard classic instance | #### How it works The adapter changes the application endpoint from MemoryDB to Memorystore for Valkey. Your Redis or Valkey client connects directly using the Redis serialization protocol (RESP). Tensor9 does not proxy these requests. The target supports Redis data types and operations such as `GET`, `SET`, `HSET`, `ZADD`, `XADD`, transactions and Lua. A sharded MemoryDB deployment needs Memorystore for Valkey with cluster mode enabled or Memorystore for Redis Cluster. Check restricted commands, authentication and topology when configuring the client.
Before: on AWS your app's Redis client speaks RESP to a MemoryDB cluster. After: on Google Cloud the same app runs the same client speaking the same RESP wire to a native Memorystore for Valkey instance; the endpoint is rewired at the build, with no proxy in the data path; configure the target's authentication, TLS and topology. Before: on AWS your app's Redis client speaks RESP to a MemoryDB cluster. After: on Google Cloud the same app runs the same client speaking the same RESP wire to a native Memorystore for Valkey instance; the endpoint is rewired at the build, with no proxy in the data path; configure the target's authentication, TLS and topology.

The application connects directly to Memorystore for Valkey using RESP.

#### Persistence and failover Memorystore replicates asynchronously, so an unexpected failover can lose acknowledged writes that have not reached a replica. Persistence is configured separately: append-only files (AOF) support `appendfsync` values `always`, `everysec` (the default) and `no`; RDB snapshots run every 1, 6, 12 or 24 hours. `always` completes a local fsync before acknowledgment. With `everysec`, about a second of recent disk writes can be lost; `no` delegates flushing to the operating system. A local sync does not ensure that the replica promoted during failover has the write. Provision at least one replica per shard for automatic failover. Google describes recovery as taking tens of seconds. A promoted replica may be missing writes that the primary acknowledged. `WAIT` waits for acknowledgments from a chosen number of replicas, but does not guarantee that failover preserves every acknowledged write.
MemoryDB commits each write to a distributed multi-AZ transactional log across three Availability Zones before acknowledging the client, so the write is durable at ack. Memorystore local persistence depends on the selected policy: with AOF appendfsync always, local fsync completes before acknowledgment; everysec syncs about once per second, and RDB snapshots capture periodic recovery points. Replication is asynchronous, so failover can lose writes missing from the promoted replica even after a local sync. MemoryDB commits each write to a distributed multi-AZ transactional log across three Availability Zones before acknowledging the client, so the write is durable at ack. Memorystore local persistence depends on the selected policy: with AOF appendfsync always, local fsync completes before acknowledgment; everysec syncs about once per second, and RDB snapshots capture periodic recovery points. Replication is asynchronous, so failover can lose writes missing from the promoted replica even after a local sync.

MemoryDB commits across zones before acknowledgment. Memorystore replication is asynchronous; persistence settings do not remove the failover loss window.

#### Sharding and the keyspace MemoryDB partitions keys across up to 500 shards, each with a primary and up to five replicas. Memorystore for Valkey supports up to 250 shards with zero to five replicas each. Both use Redis Cluster hash slots, including the cross-slot restrictions on multi-key operations. Size large datasets within Memorystore's 250-shard limit, using a larger node type if necessary. A cluster-aware client discovers the target topology and follows `MOVED` and `ASK` redirects. The target does not retain the source cluster's shard placement.
A MemoryDB cluster partitions the keyspace across shards, each a primary plus up to five replicas. Memorystore for Valkey with Cluster Mode Enabled maps directly: it scales to up to 250 shards, one primary per shard with zero to five replicas, using the same Redis Cluster hash-slot keyspace with target shard placement determined by its own topology. A MemoryDB cluster partitions the keyspace across shards, each a primary plus up to five replicas. Memorystore for Valkey with Cluster Mode Enabled maps directly: it scales to up to 250 shards, one primary per shard with zero to five replicas, using the same Redis Cluster hash-slot keyspace with target shard placement determined by its own topology.

Memorystore uses Redis Cluster hash slots, with up to 250 shards instead of MemoryDB's 500.

#### Snapshots, auth, and encryption **Backups.** MemoryDB snapshots to S3 have up to 35-day retention and are separate from its transaction log. Memorystore RDB backups can be exported to Cloud Storage and scheduled daily, with retention up to 365 days (35 by default). A restore creates a new instance. Each RDB backup captures one point in time; it does not preserve later writes. **Authentication.** MemoryDB clients use Redis access-control list (ACL) users. Memorystore offers IAM authentication with short-lived tokens and basic `AUTH`. Configure the target credentials and token refresh in the application; source ACL users are not the same as target IAM identities. **Encryption.** MemoryDB uses a default or customer KMS key at rest and requires TLS. Memorystore supports customer-managed encryption keys (CMEK) through Cloud KMS for persisted data. TLS is available and automatically enabled with IAM authentication. The customer key protects persisted AOF/RDB files and backups; Memorystore does not encrypt the live in-memory dataset.
Three operational surfaces map across with named differences. Backups: MemoryDB snapshots to Amazon S3 map to Memorystore RDB backups exported to a Cloud Storage bucket, on a daily schedule. Auth: MemoryDB Redis ACL users map to Memorystore IAM authentication plus basic AUTH. Encryption: MemoryDB always-on at-rest with a KMS customer key maps to Memorystore CMEK via Cloud KMS on persisted data, with TLS in transit. Three operational surfaces map across with named differences. Backups: MemoryDB snapshots to Amazon S3 map to Memorystore RDB backups exported to a Cloud Storage bucket, on a daily schedule. Auth: MemoryDB Redis ACL users map to Memorystore IAM authentication plus basic AUTH. Encryption: MemoryDB always-on at-rest with a KMS customer key maps to Memorystore CMEK via Cloud KMS on persisted data, with TLS in transit.

Configure Cloud Storage backups, target authentication and Cloud KMS encryption separately.

#### Limitations Review persistence, failover, shard limits and target security settings before migrating the dataset. △ Where MemoryDB and Memorystore for Valkey diverge * **250-shard maximum.** MemoryDB supports up to 500 shards. Size the target node types within the lower limit. * **Customer keys protect persisted data.** Cloud KMS covers persisted AOF/RDB files and backups; the live in-memory dataset is not encrypted. * **Authentication changes.** Configure IAM tokens or an AUTH credential; MemoryDB ACL users are not automatically retained. #### Other considerations Seed the new instance from an RDB restore or repopulate rebuildable data before cutover. Changing the endpoint does not transfer the live keyspace. Google operates the engine, replication, snapshots and failover. You select instance and shard sizing, replicas and the AOF/RDB policy. Configure backups and test recovery against the application's data-loss requirements. ## On Azure | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------------------------------- | ---------------- | ------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Data-plane commands (strings, hashes, sorted sets, streams, pub/sub, Lua) | Data plane | Supported | Common | the full RESP data plane runs on a native Azure Managed Redis (Redis Enterprise) engine; the client is unchanged and no proxy sits in the path | | Backup / snapshot / restore | Durability | Supported | Most usage | Import / Export on all tiers (to a storage account); restore seeds a new instance | | Persistence (AOF / RDB) | Durability | Partial | Most usage | optional RDB + AOF to a managed disk (for HA, not a point-in-time backup) | | Strong consistency + durable commit | Durability | Partial | Most usage | Azure Managed Redis is zone-redundant with optional AOF (per-second fsync) for intra-region HA, and active geo-replication is asynchronous multi-primary; a failover still has a data-loss window where MemoryDB's synchronous multi-AZ log has none | | Search / JSON / modules (FT.\* / JSON.\*) | Extensions | Supported | Full surface | RediSearch (vector and full-text), JSON, Bloom and TimeSeries are built into the target engine | | Replication / HA / failover | Operations | Supported | Most usage | replication + automatic failover on all tiers; zone-redundant by default | | Admin / config / replication commands (CONFIG / DEBUG / SAVE / SLAVEOF / CLUSTER admin) | Restricted admin | Out of scope | Full surface | the admin / replication / cluster surface moves to the Azure control plane; Azure additionally disables the ACL command, SELECT is unusable on the clustered tier, and FLUSHALL / FLUSHDB are blocked under active geo-replication | | AUTH / ACL / RBAC | Security | Supported | Most usage | access keys + Microsoft Entra ID (the default); data-access ACL (preview) | | Encryption at rest | Security | Supported | Most usage | platform-managed key by default; customer-managed key (CMK via Key Vault) on all tiers | | Encryption in transit (TLS) | Security | Supported | Common | TLS on all tiers (minimum TLS 1.2) | | Cluster mode (slot topology) | Topology | Supported | Most usage | clustered by default (the OSSCluster policy reproduces the Redis Cluster API); shards are auto-managed, so MemoryDB's always-sharded topology maps directly | #### How it works The adapter changes the application endpoint from MemoryDB to Azure Managed Redis. Your Redis client connects directly using the Redis serialization protocol (RESP). Azure Managed Redis runs the Redis Enterprise engine with Redis 7.4.x support. Review command restrictions and target authentication when configuring the client. Azure Managed Redis includes RediSearch, RedisJSON, RedisBloom and RedisTimeSeries. Module availability does not establish MemoryDB-compatible durability: the two services acknowledge and persist writes differently.
Before: on AWS your app's Redis client speaks RESP to a MemoryDB cluster. After: on Azure the same client speaks the same RESP wire to a native Azure Managed Redis instance running the Redis Enterprise engine; the endpoint is rewired at the build, with no proxy in the data path; configure the target's authentication, TLS and topology. Before: on AWS your app's Redis client speaks RESP to a MemoryDB cluster. After: on Azure the same client speaks the same RESP wire to a native Azure Managed Redis instance running the Redis Enterprise engine; the endpoint is rewired at the build, with no proxy in the data path; configure the target's authentication, TLS and topology.

The application connects directly to Azure Managed Redis using RESP.

#### Persistence and zone failover Azure Managed Redis acknowledges writes on the primary and replicates asynchronously. Configure persistence separately: RDB snapshots run every 60 minutes, 6 hours or 12 hours; AOF is saved once per second. The Enterprise engine no longer offers the per-write `always` option. On an HA instance, AOF runs only on replica shards; primary shards have `appendonly` disabled. Acknowledgment therefore does not wait for disk persistence. Persistence is not a backup or point-in-time recovery feature, and the service SLA does not guarantee protection from data loss. Zone redundancy is the default in regions with availability zones and requires the HA configuration. Microsoft describes typical zone-failover downtime as 10-15 seconds. Retry dropped in-flight requests. A promoted replica may lack recent acknowledged writes, usually a period measured in seconds that depends on replication lag.
MemoryDB commits each write to a distributed multi-AZ transactional log before acknowledging, so it is durable at ack. Azure Managed Redis acknowledges from the primary shard in memory, replicates asynchronously to replicas in another zone, and persists AOF once per second on the replica shards only, since the primary has appendonly disabled. So a write is acknowledged before it is durable. MemoryDB commits each write to a distributed multi-AZ transactional log before acknowledging, so it is durable at ack. Azure Managed Redis acknowledges from the primary shard in memory, replicates asynchronously to replicas in another zone, and persists AOF once per second on the replica shards only, since the primary has appendonly disabled. So a write is acknowledged before it is durable.

The primary acknowledges writes before replica-side AOF persistence. MemoryDB commits to its distributed log first.

#### Sharding, the keyspace, and active geo-replication MemoryDB shards its keyspace. Azure Managed Redis is clustered by default; the `OSSCluster` policy supports Redis Cluster hash slots and `MOVED`/`ASK` redirects. Azure chooses shard count from the service size (SKU), so you size the instance instead of specifying the source shard count. Active geo-replication supports up to five writable instances. Each commits locally and propagates writes asynchronously; conflict-free replicated data types (CRDTs) merge concurrent updates. A region failure can lose recent writes that have not propagated. Active geo-replication cannot be combined with RDB/AOF persistence. Assess both the conflict behavior and the loss window when choosing it.
A MemoryDB cluster's shard-and-hash-slot keyspace maps to Azure Managed Redis clustered by default with the OSSCluster policy, which reproduces the Redis Cluster API. Active geo-replication links up to five instances in an active-active configuration using conflict-free replicated data types; cross-region replication is asynchronous and eventual, committing locally and propagating in the background, so recent writes to a failed region can be lost. A MemoryDB cluster's shard-and-hash-slot keyspace maps to Azure Managed Redis clustered by default with the OSSCluster policy, which reproduces the Redis Cluster API. Active geo-replication links up to five instances in an active-active configuration using conflict-free replicated data types; cross-region replication is asynchronous and eventual, committing locally and propagating in the background, so recent writes to a failed region can be lost.

OSSCluster supports Redis Cluster hash slots. Active geo-replication uses asynchronous CRDT conflict merging.

#### Backups, auth, and encryption **Backups.** Use Azure Managed Redis Import and Export with a storage account to seed an instance or capture a backup. These are separate from RDB/AOF persistence. MemoryDB snapshots to S3 do not transfer automatically. **Authentication.** Azure Managed Redis defaults to Microsoft Entra ID on new instances, with managed identity enabled. Add users or service principals to its Redis users list and configure the application identity. Access keys remain available and can be disabled after Entra authentication is configured. MemoryDB ACL users are not automatically converted to Entra identities. **Encryption.** Azure Managed Redis encrypts disk data with platform-managed keys and supports a customer key through Azure Key Vault in all tiers. It requires TLS 1.2 or 1.3 by default. The customer key covers persistence disks, OS disks and export files; the service does not encrypt the live in-memory dataset.
Three operational surfaces map over with named differences. Backups: MemoryDB snapshots to Amazon S3 map to Azure Managed Redis Import/Export against a storage account. Auth: MemoryDB Redis ACL users map to Microsoft Entra ID, the default on new caches, with a Redis users list and access keys. Encryption: MemoryDB always-on at-rest with a KMS customer key maps to platform-managed keys in all tiers plus a customer-managed key via Key Vault, with TLS 1.2 or 1.3 required; in-memory data is not encrypted. Three operational surfaces map over with named differences. Backups: MemoryDB snapshots to Amazon S3 map to Azure Managed Redis Import/Export against a storage account. Auth: MemoryDB Redis ACL users map to Microsoft Entra ID, the default on new caches, with a Redis users list and access keys. Encryption: MemoryDB always-on at-rest with a KMS customer key maps to platform-managed keys in all tiers plus a customer-managed key via Key Vault, with TLS 1.2 or 1.3 required; in-memory data is not encrypted.

Import/Export handles backups; Entra handles identities; Key Vault customer keys protect disk data.

#### Limitations Review the persistence, replication, sharding and encryption differences before migrating. △ Where MemoryDB and Azure Managed Redis diverge * **Geo-replication excludes RDB/AOF.** Up to five writable instances propagate changes asynchronously and merge with CRDTs. Recent writes to a failed region can be lost. * **Azure chooses shard count.** Select the instance size (SKU); the source shard layout is not retained. * **The live dataset is not encrypted.** The customer key protects persistence disks, OS disks and export files, not data in RAM. #### Other considerations Seed the new instance with Import from a storage account or repopulate rebuildable data. Changing the endpoint does not transfer the live keyspace. Microsoft operates the engine, replication and failover. You select capacity, HA and persistence settings and whether to use active geo-replication. Review its incompatibility with AOF/RDB and test the selected recovery path. ## On OCI | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------------------------------- | ---------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Data-plane commands (strings, hashes, sorted sets, streams, pub/sub, Lua) | Data plane | Supported | Common | the full RESP data plane runs on a native OCI Cache (Valkey / Redis) engine; the client is unchanged and no proxy sits in the path | | Backup / snapshot / restore | Durability | Supported | Most usage | on-demand backups (no built-in scheduler, so use cron), export / import to Object Storage, restore to a new cluster | | Persistence (AOF / RDB) | Durability | Out of scope | Most usage | the engine's AOF / RDB parameters are not exposed; durability is managed backup + Object Storage import, not a tunable persistence mode | | Strong consistency + durable commit | Durability | Partial | Most usage | OCI Cache does not expose AOF / RDB knobs; durability rests on managed backup + Object Storage snapshots, and the failover data-loss window and SLA percentage are not published, so MemoryDB's synchronous multi-AZ commit log has no counterpart | | Search / JSON / modules (FT.\* / JSON.\*) | Extensions | Out of scope | Full surface | core types + server-side Lua; no search / JSON / Bloom modules are documented | | Replication / HA / failover | Operations | Partial | Most usage | a primary + up to 4 replicas across fault / availability domains; the exact failover mechanics and SLA percentage are not published | | Admin / config / replication commands (CONFIG / DEBUG / SAVE / SLAVEOF / CLUSTER admin) | Restricted admin | Out of scope | Full surface | the admin / replication / cluster surface moves to the OCI control plane; OCI additionally blocks CONFIG SET / REWRITE (CONFIG GET is kept) and FAILOVER | | AUTH / ACL / RBAC | Security | Supported | Most usage | OCI Cache users with command/key/channel ACL policies; IAM connection tokens expire after one hour | | Encryption at rest | Security | Out of scope | Most usage | at-rest encryption and customer-managed keys are not documented as customer-set arguments; they are platform-managed and not published | | Encryption in transit (TLS) | Security | Supported | Common | TLS is mandatory by default on 6379 | | Cluster mode (slot topology) | Topology | Supported | Most usage | sharded (3-99 shards, up to 100 nodes) or non-sharded (1-5 nodes), so MemoryDB's always-sharded topology maps directly | #### How it works The adapter changes the application endpoint from MemoryDB to OCI Cache. OCI Cache supports Valkey 8.1 or 7.2 and Redis 7.0 using the Redis serialization protocol (RESP), with TLS required by default. The application connects directly. Review restricted commands and configure the client for the target topology and security settings. OCI Cache offers a sharded topology for partitioned datasets. Its documented persistence and failover behavior does not establish MemoryDB's guarantee that each acknowledged write is committed across availability zones.
Before: on AWS your app's Redis client speaks RESP to a MemoryDB cluster. After: on Oracle Cloud the same client speaks the same RESP wire to a native OCI Cache cluster running Valkey or Redis; the endpoint is rewired at the build, with no proxy in the data path; configure the target's authentication, TLS and topology. Before: on AWS your app's Redis client speaks RESP to a MemoryDB cluster. After: on Oracle Cloud the same client speaks the same RESP wire to a native OCI Cache cluster running Valkey or Redis; the endpoint is rewired at the build, with no proxy in the data path; configure the target's authentication, TLS and topology.

The application connects directly to OCI Cache using RESP over TLS.

#### Persistence and recovery guarantees OCI Cache uses primary and replica nodes, but the reviewed documentation does not state whether replication is synchronous or asynchronous or define a failover data-loss window. It provides on-demand RDB snapshots and Object Storage export, with no AOF setting or built-in backup scheduler. Schedule backups through the CLI or API if needed. These documented controls do not establish that every acknowledged write survives failure. OCI Cache places nodes across fault and availability domains where possible. Each shard, or a non-sharded cluster, can have a primary and up to four replicas. The failover loss window, exact mechanics and an OCI Cache-specific SLA percentage are unpublished in the reviewed documentation. Establish the required recovery-point objective (RPO), the amount of data the application can afford to lose, with the provider before storing irreplaceable records. A cache rebuilt from a separately durable store avoids relying on an unspecified recovery guarantee.
MemoryDB commits each write to a documented distributed multi-AZ transactional log before acknowledging, so its durability is a published guarantee. OCI Cache acknowledges from the primary node and keeps replicas, but Oracle does not publish whether replication is synchronous or asynchronous; durability rests on on-demand RDB snapshots and Object Storage export, with no AOF and no built-in scheduler. So its recovery guarantee is not documented. MemoryDB commits each write to a documented distributed multi-AZ transactional log before acknowledging, so its durability is a published guarantee. OCI Cache acknowledges from the primary node and keeps replicas, but Oracle does not publish whether replication is synchronous or asynchronous; durability rests on on-demand RDB snapshots and Object Storage export, with no AOF and no built-in scheduler. So its recovery guarantee is not documented.

MemoryDB documents distributed commit durability. The reviewed OCI Cache documentation does not define replication mode or failover data loss.

#### Sharding and the keyspace OCI Cache sharded clusters use an odd shard count from 3 to 99, up to 100 nodes total and up to 500 GB per node. Each shard has a primary and up to four replicas. Redis Cluster hash slots and `MOVED`/`ASK` redirects support cluster-aware clients; the node and replica limits still differ from MemoryDB. MemoryDB allows five replicas per shard; OCI Cache allows four. Size read-heavy shards accordingly and verify that the client supports cluster mode with hostnames. OCI also offers non-sharded clusters of one to five nodes for datasets that fit one primary.
A MemoryDB cluster partitions the keyspace across shards, each a primary plus up to five replicas. An OCI Cache sharded cluster maps directly: an odd shard count from three to ninety-nine, each shard a primary plus up to four replicas, up to one hundred nodes and five hundred gigabytes per node, using the Redis Cluster hash-slot keyspace. A non-sharded OCI Cache cluster is one to five nodes. A MemoryDB cluster partitions the keyspace across shards, each a primary plus up to five replicas. An OCI Cache sharded cluster maps directly: an odd shard count from three to ninety-nine, each shard a primary plus up to four replicas, up to one hundred nodes and five hundred gigabytes per node, using the Redis Cluster hash-slot keyspace. A non-sharded OCI Cache cluster is one to five nodes.

OCI Cache supports 3-99 shards, using odd shard counts, with up to four replicas per shard and 100 nodes total.

#### Backups, auth, and encryption **Backups.** OCI Cache creates on-demand RDB snapshots with retention from 1 to 35 days (7 by default), and can export them to Object Storage for longer retention or cross-region transfer. No built-in scheduler is provided; automate the CLI or API, for example with cron, for regular backups. **Authentication.** Create [OCI Cache users](https://docs.oracle.com/en-us/iaas/Content/ocicache/cache-users.htm) with ACL policies that restrict commands, keys and Pub/Sub channels. Associate them with the target cluster; source MemoryDB users are not copied automatically. With [IAM authentication](https://docs.oracle.com/en-us/iaas/Content/ocicache/iam-authentication.htm), the application connects using the cache username and a generated token that expires after one hour, so configure token renewal. IAM permissions also govern cluster administration and token creation. Private subnets and network security groups restrict network access separately. **Encryption.** OCI Cache requires TLS connections by default. The reviewed cluster-creation documentation does not expose an at-rest customer key or OCI Vault key argument. Do not assume that a MemoryDB customer-key requirement is supported.
Three operational surfaces map across with named differences. Backups: MemoryDB snapshots to Amazon S3 map to OCI Cache on-demand RDB snapshots exported to an Object Storage bucket, driven by your own scheduler. Auth: OCI Cache users have ACL policies for commands, keys and channels; IAM authentication supplies expiring connection tokens. OCI IAM also controls cluster administration. Encryption: MemoryDB always-on at-rest with a KMS customer key has no documented customer-set OCI Cache analog, though TLS in transit is mandatory by default. Three operational surfaces map across with named differences. Backups: MemoryDB snapshots to Amazon S3 map to OCI Cache on-demand RDB snapshots exported to an Object Storage bucket, driven by your own scheduler. Auth: OCI Cache users have ACL policies for commands, keys and channels; IAM authentication supplies expiring connection tokens. OCI IAM also controls cluster administration. Encryption: MemoryDB always-on at-rest with a KMS customer key has no documented customer-set OCI Cache analog, though TLS in transit is mandatory by default.

Schedule Object Storage backups, configure cache users and renew connection tokens.

#### Limitations Review undocumented recovery guarantees, backup scheduling and access controls before migration. △ Where MemoryDB and OCI Cache diverge * **Recovery guarantees need verification.** The reviewed documentation does not state replication mode or the amount of data a failover can lose. Do not assume MemoryDB-compatible distributed durability. * **Backups require scheduling.** RDB snapshots have 1-35-day retention and Object Storage export. No AOF setting or built-in scheduler is documented. * **Configure target users and token renewal.** Recreate the required command, key and channel permissions in OCI Cache ACL policies. IAM connection tokens expire after one hour. * **A customer-managed at-rest key was not established.** TLS is mandatory by default, but the reviewed cluster resource does not expose a customer or Vault key. * **Four replicas per shard.** MemoryDB permits five. Size read-heavy shards within OCI's replica and total-node limits. #### Other considerations Seed the new cluster from an RDB snapshot in Object Storage or repopulate rebuildable data. Changing the endpoint does not copy the live keyspace. Oracle operates replication and failover. You schedule snapshots and export, for example through cron. Obtain the recovery and access guarantees needed for the workload; retry-safe operations do not recover records that were never persisted. ## On Private Kubernetes | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------------------------------- | ---------------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Data-plane commands (strings, hashes, sorted sets, streams, pub/sub, Lua) | Data plane | Supported | Common | the full RESP data plane runs on a Valkey deployment you run in the target cluster (the Bitnami chart); the client is unchanged and no proxy sits in the path | | Backup / snapshot / restore | Durability | Partial | Most usage | manual SAVE + copy dump.rdb off the PVC, which you manage | | Persistence (AOF / RDB) | Durability | Supported | Most usage | configure RDB and AOF on a PersistentVolume; appendfsync always syncs locally before acknowledgment, but asynchronous failover can still lose acknowledged writes | | Strong consistency + durable commit | Durability | Partial | Most usage | durability is yours: RDB + AOF (appendfsync you set) on a PersistentVolume; a single-node fsync narrows but never equals MemoryDB's synchronous multi-AZ commit log, and there is no vendor SLA | | Search / JSON / modules (FT.\* / JSON.\*) | Extensions | Out of scope | Full surface | core Valkey types; no modules are bundled with the chart | | Replication / HA / failover | Operations | Partial | Most usage | 1 primary + replicas with Sentinel HA that you configure; there is no vendor SLA | | Admin / config / replication commands (CONFIG / DEBUG / SAVE / SLAVEOF / CLUSTER admin) | Restricted admin | Partial | Full surface | you operate the server and can configure Valkey administrative and replication commands; access still depends on your permissions and configuration | | AUTH / ACL / RBAC | Security | Partial | Most usage | password AUTH is on by default; ACL is available via engine config, your setup | | Encryption at rest | Security | Partial | Most usage | delegated to the Kubernetes PersistentVolume / node-disk layer, so encryption is your storage class's | | Encryption in transit (TLS) | Security | Partial | Common | TLS you configure (off by default in the chart) | | Cluster mode (slot topology) | Topology | Partial | Most usage | the chart default is primary-replica, not Valkey Cluster sharding (the engine supports Cluster; enabling and operating sharding is your setup), where MemoryDB is always sharded | #### How it works The adapter changes the application endpoint from MemoryDB to a Valkey deployment installed with the Bitnami Helm chart in the customer's Kubernetes cluster. Your client connects directly using the Redis serialization protocol (RESP). You operate the Valkey server and configure its authentication and topology. Self-hosting gives you control over Valkey administration, including commands such as `CONFIG` and `SLAVEOF`. It also makes persistence, replication, TLS, backups and failover your deployment responsibilities. The chart does not provide MemoryDB's managed durability guarantee.
Before: on AWS your app's Redis client speaks RESP to a managed MemoryDB cluster. After: on Kubernetes cluster the same client speaks the same RESP wire to a Valkey deployment you run via the Bitnami Helm chart; the endpoint is rewired at the build, with no proxy in the data path; configure the target's authentication, TLS and topology, and you operate the server. Before: on AWS your app's Redis client speaks RESP to a managed MemoryDB cluster. After: on Kubernetes cluster the same client speaks the same RESP wire to a Valkey deployment you run via the Bitnami Helm chart; the endpoint is rewired at the build, with no proxy in the data path; configure the target's authentication, TLS and topology, and you operate the server.

The application connects directly to the Valkey service in the customer's Kubernetes cluster.

#### Persistence and failover you operate Valkey replicates asynchronously. Configure an append-only file (AOF) on a PersistentVolume and choose `appendfsync`: `always` syncs each write batch locally before acknowledgment, `everysec` can lose about one second of disk writes, and `no` delegates flushing to the operating system without a fixed loss bound. RDB snapshots are another recovery option. `WAIT` and `WAITAOF` can wait for replica or AOF acknowledgments, but do not guarantee that failover preserves acknowledged writes. A local disk sync is not a distributed commit across zones. For automatic failover, configure Sentinel to monitor the primary and promote a replica. Choose the replica count and failure-domain placement for the deployment. A promoted replica can be missing acknowledged writes; failover timing and PersistentVolume recovery depend on your configuration and have no managed SLA.
MemoryDB commits each write to a managed distributed multi-AZ transactional log before acknowledging, so it is durable at ack. Self-hosted Valkey local persistence depends on your policy. AOF appendfsync always completes a local fsync before acknowledgment; everysec syncs about once per second, and no delegates flushing to the operating system without a fixed loss bound. RDB snapshots provide another recovery point. Replication is asynchronous, so failover can lose acknowledged writes missing from the promoted replica despite local persistence. MemoryDB commits each write to a managed distributed multi-AZ transactional log before acknowledging, so it is durable at ack. Self-hosted Valkey local persistence depends on your policy. AOF appendfsync always completes a local fsync before acknowledgment; everysec syncs about once per second, and no delegates flushing to the operating system without a fixed loss bound. RDB snapshots provide another recovery point. Replication is asynchronous, so failover can lose acknowledged writes missing from the promoted replica despite local persistence.

Local AOF/RDB persistence and asynchronous replication do not reproduce MemoryDB's distributed commit log.

#### Topology and the keyspace MemoryDB partitions its keyspace across shards. The Bitnami chart's default primary-replica topology uses a single primary instead of Valkey Cluster sharding. The dataset and write load must fit that primary unless you configure a different deployment. Valkey supports Cluster mode, but shard placement and slot migration require separate setup and operation. Decide whether to size one primary or operate a sharded cluster. Configure the client for the chosen topology; a client that requires cluster discovery may need changes for the default non-cluster deployment.
A MemoryDB cluster always partitions the keyspace across shards. The Bitnami chart's default is a single primary with replicas, not Valkey Cluster sharding, so a sharded MemoryDB cluster maps to a vertically sized primary. The Valkey engine supports Cluster mode for the hash-slot keyspace, but enabling and operating sharding is your setup. A MemoryDB cluster always partitions the keyspace across shards. The Bitnami chart's default is a single primary with replicas, not Valkey Cluster sharding, so a sharded MemoryDB cluster maps to a vertically sized primary. The Valkey engine supports Cluster mode for the hash-slot keyspace, but enabling and operating sharding is your setup.

The default topology uses one primary; sharded Valkey Cluster operation needs separate configuration.

#### Persistence storage, auth, and encryption **Backups.** Run `SAVE` or `BGSAVE` and copy `dump.rdb` from the PersistentVolume to durable storage outside the cluster. You set the schedule and retention and test restoration. MemoryDB's managed S3 snapshot service is not recreated. **Authentication.** The chart enables password `AUTH` by default. Configure Valkey ACL users and access strings to implement the required permissions. These users are part of the deployment you manage. **Encryption.** At-rest encryption depends on the PersistentVolume storage class and underlying disks. If the customer requires their own key, select storage that supports that key and configure it explicitly. The chart's default TLS setting is off; enable TLS and configure certificates to preserve encrypted client connections.
Three operational surfaces are yours to run. Backups: MemoryDB snapshots to Amazon S3 become a manual SAVE plus copying dump.rdb off the PersistentVolume, which you manage. Auth: MemoryDB Redis ACL users map to password AUTH on by default plus ACL via engine config, your setup. Encryption: MemoryDB always-on at-rest with a KMS customer key becomes the encryption of Kubernetes PersistentVolume storage class, and TLS in transit is off by default in the chart until you configure it. Three operational surfaces are yours to run. Backups: MemoryDB snapshots to Amazon S3 become a manual SAVE plus copying dump.rdb off the PersistentVolume, which you manage. Auth: MemoryDB Redis ACL users map to password AUTH on by default plus ACL via engine config, your setup. Encryption: MemoryDB always-on at-rest with a KMS customer key becomes the encryption of Kubernetes PersistentVolume storage class, and TLS in transit is off by default in the chart until you configure it.

You configure backup copies, ACL users, encrypted storage and TLS.

#### Limitations Review persistence, replica placement, failover, backup operation and encrypted connections before migration. △ Where MemoryDB and self-hosted Valkey diverge * **TLS is off by default.** Enable it in chart values and configure certificates for encrypted client connections. * **You operate backups.** Use `SAVE`/`BGSAVE`, copy `dump.rdb` outside the cluster and test restoration. * **Storage controls at-rest encryption.** A customer-key requirement needs a storage class and disk configuration that explicitly support that key; generic encrypted storage is insufficient. * **Sharding requires separate setup.** The default topology has a single primary with replicas. Size that primary or configure and operate Valkey Cluster. #### Other considerations Seed the deployment from a compatible RDB/AOF import or repopulate rebuildable data. Active sessions do not survive the endpoint change. You operate the server, underlying compute and storage, upgrades, replicas, persistence, TLS and backups. Include those resources and operating work in the deployment cost; the chart does not provide a managed availability or durability SLA. [Service Catalog](/service-adapters/catalog). # MWAA (Managed Airflow) Source: https://docs.tensor9.com/service-adapters/aws/other-services/mwaa-managed-airflow AWS MWAA (Managed Airflow). Hosts Apache Airflow, picking up DAG files from an S3 bucket and managing the scheduler, workers and web interface. ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | - | | OCI | - | | Private Kubernetes | - | ## Mapping and limitations On Google Cloud, MWAA becomes a Cloud Composer environment. Airflow version, scheduler count, worker minimum and maximum, and Airflow configuration overrides translate. Upload DAGs, plugins and requirements to Composer's own bucket. Configure the target network, service account, web-access allowlist and maintenance window separately; the AWS settings do not transfer automatically. These target services provide the mapping described above, within its stated limits. | Cloud | Target service | | ------------ | ------------------------ | | Google Cloud | Cloud Composer (Airflow) | [Service Catalog](/service-adapters/catalog). # SageMaker (Inference) Source: https://docs.tensor9.com/service-adapters/aws/other-services/sagemaker-inference AWS SageMaker (Inference). Real-time model inference through SageMaker Runtime endpoints. This entry covers inference only, not notebooks, training jobs or the wider SageMaker platform. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [On Google Cloud and Azure](#on-google-cloud-and-azure) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | - | | Private Kubernetes | - | ## How the targets compare Each row compares a capability of SageMaker (Inference) with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | SageMaker (Inference) | Google Cloud and Azure | | ------------ | --------------------- | ---------------------- | | API coverage | full | partial | ## On Google Cloud and Azure | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------------- | ------------------ | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | InvokeEndpoint | Inference | Supported | Common | Forwards a request body of up to 6 MiB to one configured model container and returns its response body and content type. | | InvokeEndpointAsync | Inference | Out of scope | Full surface | Asynchronous inference is unsupported; requests return an error. | | InvokeEndpointWithResponseStream / InvokeEndpointWithBidirectionalStream | Inference | Out of scope | Full surface | Streaming inference is outside this adapter's subset. | | ContentType, Accept, CustomAttributes and InferenceId | Inference headers | Supported | Common | These values reach the model container. CustomAttributes in its response are returned to the caller. | | Explanation, inference-component, session and prefix-cache controls | Inference options | Out of scope | Full surface | These request controls are not implemented or forwarded to the model. Do not rely on them taking effect. | | Batch transform, training jobs, tuning and notebooks | ML platform | Out of scope | Full surface | These are separate workloads, not synchronous endpoint inference; they remain on AWS. | | TargetVariant / TargetModel / TargetContainerHostname | Model routing | Out of scope | Full surface | Explicit variant, multi-model and inference-pipeline container selection are refused. Each configured endpoint serves one model container. | | Model, endpoint configuration and endpoint Terraform resources | Provisioning | Partial | Most usage | Deploys the model image as a Kubernetes Deployment and Service. Model artifacts, AWS instance sizing and weighted production variants need separate handling. | | CreateModel / CreateEndpointConfig / CreateEndpoint / UpdateEndpoint / DeleteEndpoint | Runtime management | Out of scope | Full surface | Provision serving resources through the origin stack. Live SageMaker management API calls are not covered by this inference adapter. | #### How inference runs Your application sends a synchronous `InvokeEndpoint` request through its SageMaker Runtime client. The service adapter forwards the payload to your inference container in the customer's Kubernetes cluster on Azure or Google Cloud, then returns the container's response. The model and its input format stay yours; this mapping does not substitute Vertex AI, Azure Machine Learning or a different model. Each configured endpoint addresses one model container. Requests naming a different endpoint, a production variant or a multi-model target do not silently run against the default model. Use one model and one endpoint per compiled module for this subset. Configure the client's endpoint name to match the deployed endpoint; custom AWS endpoint aliases are not preserved automatically. #### Prepare the model and serving capacity Tensor9 maps the origin stack's model, endpoint configuration and endpoint resources to a Kubernetes Deployment and Service using the model's container image. The endpoint is the served resource; its model and endpoint configuration accompany that provisioning. They do not provide separate model-management or endpoint-management APIs. A model or configuration without an endpoint does not create an inference server. Keep the three resources in one module with one declared model image. Make that image pullable from the customer's environment and provide the model files it needs. The container must start its inference server itself. This path does not append SageMaker's `serve` argument or copy the model's environment-variable map automatically; package or configure the required startup behavior and environment explicitly. Unlike SageMaker hosting, this path does not unpack `ModelDataUrl` into `/opt/ml/model` before startup. The artifact URL is passed to the container; the container must fetch it with suitable credentials, or you must supply the files another way. An image expecting preinstalled model files needs adjustment. AWS instance types, initial instance counts and production-variant weights do not configure equivalent capacity here. Size CPU, memory and any accelerators for the customer's cluster, and validate startup, readiness and inference latency with the actual model. AWS execution roles and endpoint KMS keys are not applied unchanged; configure target identity and storage encryption. #### Inference-only boundaries This adapter covers synchronous requests to a provisioned model. It does not cover asynchronous or streaming inference, batch transform, weighted variants, multi-model routing or runtime endpoint management. Changes to serving resources go through the origin stack rather than SageMaker management calls. Notebooks, training and hyperparameter tuning remain on AWS. The [SageMaker AI (Training and Notebooks)](/service-adapters/aws/available-on-aws-only/sagemaker-ai-training-and-notebooks) entry describes that separate scope. Inference is part of SageMaker AI too; the two index entries distinguish the capabilities Tensor9 adapts, not two unrelated AWS products. #### AWS reference AWS documents the [inference container contract](https://docs.aws.amazon.com/sagemaker/latest/dg/your-algorithms-inference-code.html) and the [InvokeEndpoint API](https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_runtime_InvokeEndpoint.html). Those describe AWS behavior; the operation table above defines this adapter's narrower scope. [Service Catalog](/service-adapters/catalog). # Step Functions Source: https://docs.tensor9.com/service-adapters/aws/other-services/step-functions AWS Step Functions. Runs workflows as JSON-defined state machines, with retries, error handling, parallel and map states. Preview ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | - | | Azure | - | | OCI | - | | Private Kubernetes | - | ## Mapping and limitations Step Functions → Temporal, self-hosted on the target cloud, with state machines interpreted from their Amazon States Language definition. On Google Cloud the compile also emits a Workflows resource without its definition and reports the omission, because the definition has no mechanical translation to the Workflows DSL. The rest of the stack deploys normally. [Service Catalog](/service-adapters/catalog). # Transfer Family (SFTP) Source: https://docs.tensor9.com/service-adapters/aws/other-services/transfer-family-sftp The SFTP capability of AWS Transfer Family: managed file transfers into and out of S3 and EFS. This entry covers SFTP only, not FTPS, FTP or AS2. This bounded SFTP mapping also serves the [Transfer Family deployment](/service-adapters/aws/other-services/transfer-family-sftp-family) and [Transfer server](/service-adapters/aws/other-services/transfer-server-sftp) entries. Infrastructure only: SFTP with SERVICE\_MANAGED users and startup SSH keys. Cluster deployments use a single-writer server; user changes are not a live Transfer management API. Approved no-cluster designs use mounted object storage on Google Cloud and OCI; Azure Container Apps uses Azure Files rather than Blob storage. Capacity, private client routing and host-key trust require configuration; custom identity providers, multiple home-directory mappings and AWS IAM path-policy enforcement are not reproduced. AWS Transfer management APIs, managed workflows, FTPS, FTP and AS2 are not provided by this mapping. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud, Azure, OCI, and Private Kubernetes](#on-google-cloud-azure-oci-and-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of Transfer Family (SFTP) with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | Transfer Family (SFTP) | Google Cloud, Azure, OCI, and Private Kubernetes | | ------------------------------------------- | --------------------------------- | -------------------------------------------------------- | | SFTP (SSH file transfer) | Yes - managed endpoint | Yes - atmoz/sftp on :22 | | User identity (name / posix uid-gid / home) | Yes - SERVICE\_MANAGED directory | Yes - startup user configuration | | SSH public-key login | Yes - aws\_transfer\_ssh\_key | Yes - authorized-key configuration | | Object-store backing | Yes - S3 bucket | Yes - mapped object store; Azure Files on Container Apps | | Endpoint reach · public / internal | public / VPC (managed) | host-specific public/private endpoint | | Live user-management API | Yes - add/remove a user via API | No - startup config; update + restart | | FTPS / FTP | Yes - SFTP + FTPS + FTP | No - SFTP only; FTPS/FTP refused | | Custom identity provider | Yes - AWS\_LAMBDA / API\_GATEWAY | No - SERVICE\_MANAGED only | | Operations model · who runs it | AWS-managed, transparently scaled | self-operated single deployment | | Transfer logging · audit trail | logging\_role → CloudWatch | gateway stdout → target logging | | API coverage | full | high (SFTP and configuration) | ## On Google Cloud, Azure, OCI, and Private Kubernetes | Operation | Area | Support | Depth | Notes | | --------------------------------------------------------------------------------- | ------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | SSH key (aws\_transfer\_ssh\_key) | Auth | Supported | Common | the user's public key becomes authorized-key configuration loaded at startup; key-based login needs no change | | Managed operations (security\_policy\_name / logging\_role / transparent scaling) | Control plane | Out of scope | Full surface | you configure sshd security, image updates, logging and capacity for the single-instance gateway | | FTPS / FTP protocols | Data plane | Out of scope | Full surface | only SFTP is supported; FTPS or FTP configurations return a build error | | SFTP session + file operations | Data plane | Supported | Common | SFTP over SSH supports list, get, put, rename, mkdir and rm; backing-store filesystem differences apply | | Endpoint type (public / VPC) | Endpoint | Supported | Most usage | public or private exposure uses the host's network endpoint; Kubernetes ClusterIP is cluster-local, so VPC clients need a suitable private access path | | Custom identity provider (AWS\_LAMBDA / API\_GATEWAY) | Identity | Out of scope | Full surface | a self-hosted key-based directory cannot call a Transfer custom-IdP hook; only a SERVICE\_MANAGED directory is supported | | SFTP user (aws\_transfer\_user, SERVICE\_MANAGED) | Identity | Supported | Common | the user name, POSIX uid/gid and home\_directory become SFTP\_USERS startup user configuration; the account is key-only | | SFTP server (aws\_transfer\_server) | Provisioning | Supported | Common | uses atmoz/sftp in the target environment: Kubernetes when a cluster is present, otherwise the cloud-specific VM pool or Container Apps host described below | | Object-store backing | Storage | Supported | Common | Kubernetes and VM hosts mount the mapped object store; Azure Container Apps uses an Azure Files share instead | #### How it works The target uses an `atmoz/sftp` gateway on port 22 for AWS Transfer Family SFTP clients. Usernames, SSH public keys and home-directory configuration come from `aws_transfer_user` and `aws_transfer_ssh_key`. Clients keep their SFTP tools and user credentials; configure trust for the target server's host key. The platform team operates the gateway in the customer's environment. Its host depends on the available compute: Kubernetes, a Google Cloud Managed Instance Group, an OCI Instance Pool or an Azure Container App. Most hosts mount the mapped object store; Azure Container Apps uses Azure Files, as described below.
The target runs an atmoz/sftp gateway. Clients retain SFTP tools, usernames and SSH keys; configure target host-key trust. Most hosts use object storage, while Azure Container Apps uses Azure Files. The target runs an atmoz/sftp gateway. Clients retain SFTP tools, usernames and SSH keys; configure target host-key trust. Most hosts use object storage, while Azure Container Apps uses Azure Files.

The platform team operates an SFTP gateway in the target environment. Clients retain their tools and user keys; server host-key trust and data placement need review.

#### Where the gateway runs, per environment When the environment has GKE, AKS or OKE, the gateway runs as a Kubernetes Deployment behind a Service. Without a cluster, it runs on the cloud-specific compute described below. The no-cluster Google Cloud target uses a Managed Instance Group with one VM; OCI uses an Instance Pool with one VM. Azure uses Container Apps with TCP ingress. Each host runs `atmoz/sftp` and provides storage, network access and restart handling.
Kubernetes environments run a Deployment and Service. Without a cluster, Google Cloud uses a Managed Instance Group, OCI an Instance Pool and Azure a Container App. Kubernetes environments run a Deployment and Service. Without a cluster, Google Cloud uses a Managed Instance Group, OCI an Instance Pool and Azure a Container App.

Use Kubernetes when a cluster exists; otherwise use a Google Managed Instance Group, OCI Instance Pool or Azure Container App.

#### On a Kubernetes cluster A Kubernetes Deployment runs `atmoz/sftp` on port 22. A LoadBalancer Service exposes public endpoints; ClusterIP provides cluster-internal access. Clients elsewhere in the VPC need a private route or suitable internal endpoint rather than assuming ClusterIP is VPC-wide. The pod reads users from a ConfigMap and SSH public keys from a Secret at startup. An object-store CSI driver mounts the mapped bucket below the user's home directory: `gcsfuse.csi` on GKE, `blob.csi` on AKS or the OCI Object Storage CSI driver on OKE. The driver presents object data as files; review its filesystem semantics for the client's upload and rename behavior.
A Kubernetes Service exposes port 22 to an atmoz/sftp Deployment. LoadBalancer can provide public access; ClusterIP is cluster-local. Users and keys load at startup; a CSI driver mounts object data. A Kubernetes Service exposes port 22 to an atmoz/sftp Deployment. LoadBalancer can provide public access; ClusterIP is cluster-local. Users and keys load at startup; a CSI driver mounts object data.

A Deployment serves SFTP behind a Service. ClusterIP is cluster-local; private VPC clients need an appropriate access path.

#### On a no-cluster Google Cloud stack: a Managed Instance Group The Google Cloud target uses a one-instance Managed Instance Group. A `google_compute_instance_template` defines startup of the `atmoz/sftp` container and a `gcsfuse` bucket mount. A network tag selects the firewall rule for port 22. `target_size = 1` provides one gateway instance. A port-22 health check drives autohealing, which replaces an unresponsive instance. The replacement reconnects to the bucket; active SSH sessions are lost. A TCP load balancer with a `google_compute_forwarding_rule` and reserved `google_compute_address` gives clients a stable endpoint during replacement. △ One thing to know about this host * **Object-backed mount behavior:** gcsfuse presents object storage as files. Review listing, rename, write visibility and locking for the selected driver and client workflow; CSI integration alone does not establish stronger filesystem guarantees.
A one-instance Google Managed Instance Group runs atmoz/sftp and gcsfuse. A port-22 health check drives replacement; the load balancer preserves the endpoint while active sessions are interrupted. A one-instance Google Managed Instance Group runs atmoz/sftp and gcsfuse. A port-22 health check drives replacement; the load balancer preserves the endpoint while active sessions are interrupted.

A one-instance Managed Instance Group uses a port-22 health check and a load balancer with a stable endpoint.

#### On a no-cluster OCI stack: an Instance Pool The OCI target uses an Instance Pool with one VM defined by `oci_core_instance_configuration`. Cloud-init starts `atmoz/sftp` and mounts Object Storage through `s3fs` or `rclone` using the S3-compatible endpoint. The pool uses `size = 1` and replacement on failure. A security list or network security group permits port 22. An `oci_network_load_balancer` provides a stable endpoint across instance replacement. An OCI environment with OKE uses the Kubernetes deployment instead.
A one-instance OCI pool runs atmoz/sftp and an s3fs or rclone mount. A network load balancer provides a stable endpoint across replacement. A one-instance OCI pool runs atmoz/sftp and an s3fs or rclone mount. A network load balancer provides a stable endpoint across replacement.

A one-instance OCI pool runs the gateway and object mount behind a network load balancer.

#### On a no-cluster Azure stack: a Container App Without AKS, the Azure target runs `atmoz/sftp` in the shared Container Apps environment. TCP ingress exposes port 22 publicly or internally as required. SSH public keys use a secret block, and `min_replicas = 1` keeps an instance running. That minimum alone does not enforce one instance; the deployment's maximum replica count and rollout settings must also preserve the single-instance design. The Container App mounts Azure Files over SMB for the user's home data. This differs from the Blob container mounted on AKS. Files uploaded here are not automatically visible to applications reading the mapped Blob container. Plan the data location and any required transfer explicitly.
Azure Container Apps runs atmoz/sftp with TCP ingress and an Azure Files volume. min_replicas one keeps an instance running but does not cap scaling; configure a maximum for the single-instance design. Azure Container Apps runs atmoz/sftp with TCP ingress and an Azure Files volume. min_replicas one keeps an instance running but does not cap scaling; configure a maximum for the single-instance design.

Azure Container Apps serves TCP port 22 and mounts Azure Files. Configure replica limits to preserve the single-instance design.

#### The bucket as the SFTP home On Kubernetes and the VM hosts, the gateway exposes the mapped object store as files. File uploads become objects, and object contents can be read through SFTP. The Azure Container Apps target instead stores files in Azure Files. That exception matters when another application expects to read the mapped object bucket. Kubernetes uses an object-store CSI driver; Google Cloud VMs use `gcsfuse`; OCI VMs use `s3fs` or `rclone`. Object-backed mounts can differ from POSIX filesystems in locking, rename atomicity and write visibility. Azure Files is an SMB file share and has its own semantics. Test the relevant mount with the client's transfer workflow.
Kubernetes and VM hosts mount object storage through CSI, gcsfuse, s3fs or rclone. Azure Container Apps uses an SMB Azure Files share. Review filesystem behavior for each mount. Kubernetes and VM hosts mount object storage through CSI, gcsfuse, s3fs or rclone. Azure Container Apps uses an SMB Azure Files share. Review filesystem behavior for each mount.

Object-store mounts expose bucket data on Kubernetes and VM hosts. Azure Container Apps uses Azure Files instead.

#### Users and SSH keys The user name, POSIX UID/GID and home path become an `SFTP_USERS` entry, such as `ada::e:1001:1001:/data`. The SSH public key becomes an authorized key. In atmoz syntax, `e` marks an encrypted password field; it is not a key-only switch. The empty password and supplied public key support key-based login. Configure writable subdirectories below the user's chroot home as required by atmoz. Kubernetes uses a ConfigMap and Secret; the VM hosts use startup or cloud-init configuration; Container Apps uses environment variables and secret entries. The uploaded SSH keys are public keys. Object-store credentials are separate from client authentication: use the host's supported workload identity or credential configuration for mounting storage.
Transfer users and public keys become SFTP_USERS and authorized-key configuration on the selected host. The e marker means encrypted password; an empty password with a public key supports key-based login. Transfer users and public keys become SFTP_USERS and authorized-key configuration on the selected host. The e marker means encrypted password; an empty password with a public key supports key-based login.

User names, numeric identities, home paths and SSH public keys become host-specific gateway configuration.

#### Reachability on :22 Map public endpoints to the host's public Service, forwarding rule, network load balancer or TCP ingress. Private endpoints require the corresponding internal network configuration. On Kubernetes, ClusterIP limits access to the cluster; VPC clients outside it need an additional private access path. Verify reachability from the actual client networks. A public endpoint and a private endpoint require different load-balancer and firewall settings. On VM hosts, the load balancer also preserves the endpoint address while a failed instance is replaced.
Public endpoints use public network configuration. Private clients need the appropriate internal endpoint and routing; Kubernetes ClusterIP alone is cluster-local. Public endpoints use public network configuration. Private clients need the appropriate internal endpoint and routing; Kubernetes ClusterIP alone is cluster-local.

Choose public or private endpoints for the client network. Kubernetes ClusterIP alone does not provide VPC-wide reachability.

#### Availability and concurrent transfers The gateway uses a single-instance design to reduce concurrent writes through object-backed mounts. Kubernetes uses `replicas = 1`, the Google group uses `target_size = 1`, and the OCI pool uses `size = 1`. For Container Apps, configure the maximum replica count as well as `min_replicas = 1`. One server can still handle concurrent sessions, so test conflicting uploads and rename behavior; a single instance does not provide filesystem locking. Recovery replaces the gateway and reconnects it to its backing storage. Active SSH sessions are interrupted, and clients must retry or resume transfers as supported. This is not active-active availability. Size the server for peak connections and throughput, and persist host keys if clients must retain the same server fingerprint after replacement.
The gateway uses a single-instance design and replacement on failure. Sessions are interrupted. Concurrent transfers still need review for the mount semantics; one instance does not guarantee serialized writes. The gateway uses a single-instance design and replacement on failure. Sessions are interrupted. Concurrent transfers still need review for the mount semantics; one instance does not guarantee serialized writes.

Recovery restarts a single gateway against its storage and interrupts SSH sessions. Size the gateway for peak load.

#### Limitations Review protocol support, identity-provider limits, storage semantics, private reachability and gateway operation before migration. △ Where AWS Transfer Family and a self-hosted SFTP gateway diverge * **User updates need restart.** Update the `SFTP_USERS` and SSH-key configuration and restart the gateway; there is no Transfer user-management API. * **Unsupported protocols and identity providers.** FTPS, FTP, `AWS_LAMBDA` and `API_GATEWAY` identity providers return build errors. The target supports SFTP with its configured user directory. * **Security and logging are operational responsibilities.** Configure sshd policy and image updates in place of `security_policy_name`. Collect gateway stdout through the target environment's logging in place of `logging_role` and CloudWatch. #### Other considerations Before cutover, transfer bucket data through the backing-store migration, or provision the separate Azure Files share where applicable. Active SFTP sessions do not transfer. Configure persistent server host keys if clients must keep the same fingerprint across gateway restarts. Assign responsibility for gateway updates, capacity, user changes and log collection. Configure backup, versioning and retention on the actual backing store. A restart can preserve completed files without preserving an in-flight upload. [Service Catalog](/service-adapters/catalog). # Transfer Family (SFTP) (family) Source: https://docs.tensor9.com/service-adapters/aws/other-services/transfer-family-sftp-family An AWS Transfer Family deployment serving files backed by S3 or EFS over SFTP. This entry covers SFTP only, not FTPS, FTP or AS2. This entry uses the gateway mapping described in [Transfer Family SFTP coverage and limitations](/service-adapters/aws/other-services/transfer-family-sftp). Infrastructure only: SFTP with SERVICE\_MANAGED users and startup SSH keys. Cluster deployments use a single-writer server; user changes are not a live Transfer management API. Approved no-cluster designs use mounted object storage on Google Cloud and OCI; Azure Container Apps uses Azure Files rather than Blob storage. Capacity, private client routing and host-key trust require configuration; custom identity providers, multiple home-directory mappings and AWS IAM path-policy enforcement are not reproduced. AWS Transfer management APIs, managed workflows, FTPS, FTP and AS2 are not provided by this mapping. ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## Mapping and limitations Transfer Family's SFTP configuration moves to an SFTP gateway in the target environment. The registered gateway runs atmoz/sftp and serves the SFTP wire protocol. Users, SSH keys, home directories and storage mounts are gateway configuration. AWS Transfer management APIs, managed workflows, FTP, FTPS and AS2 are not provided by this SFTP mapping. These target services provide the mapping described above, within its stated limits. | Cloud | Target service | | ------------------ | -------------- | | Google Cloud | SFTP | | Azure | SFTP | | OCI | SFTP | | Private Kubernetes | SFTP | [Service Catalog](/service-adapters/catalog). # Transfer Server (SFTP) Source: https://docs.tensor9.com/service-adapters/aws/other-services/transfer-server-sftp An AWS Transfer Family server configured with an SFTP endpoint and user access to files in S3 or EFS. This entry covers SFTP only, not FTPS, FTP or AS2. This entry uses the gateway mapping described in [Transfer Family SFTP coverage and limitations](/service-adapters/aws/other-services/transfer-family-sftp). Infrastructure only: SFTP with SERVICE\_MANAGED users and startup SSH keys. Cluster deployments use a single-writer server; user changes are not a live Transfer management API. Approved no-cluster designs use mounted object storage on Google Cloud and OCI; Azure Container Apps uses Azure Files rather than Blob storage. Capacity, private client routing and host-key trust require configuration; custom identity providers, multiple home-directory mappings and AWS IAM path-policy enforcement are not reproduced. AWS Transfer management APIs, managed workflows, FTPS, FTP and AS2 are not provided by this mapping. ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## Mapping and limitations A Transfer server becomes an atmoz/sftp gateway hosted in the target environment. SFTP clients connect to that server using its endpoint and SSH host key. Configure user keys and storage mounts on the gateway, and distribute the replacement host key to clients. This mapping does not reproduce AWS Transfer's management API or add managed workflows, FTP, FTPS or AS2 support. These target services provide the mapping described above, within its stated limits. | Cloud | Target service | | ------------------ | -------------- | | Google Cloud | SFTP | | Azure | SFTP | | OCI | SFTP | | Private Kubernetes | SFTP | [Service Catalog](/service-adapters/catalog). # ACM (certificates) Source: https://docs.tensor9.com/service-adapters/aws/security-identity/acm-certificates Issues and auto-renews TLS certificates for AWS load balancers, CloudFront and API Gateway, keeping the private key inside AWS by default. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure, OCI, and Private Kubernetes](#on-azure-oci-and-private-kubernetes) * [Via cert-manager Certificate](#via-cert-manager-certificate) * [On Azure](#on-azure) * [Via Key Vault](#via-key-vault) * [On OCI](#on-oci) * [Via OCI Certificate](#via-oci-certificate) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of ACM (certificates) with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | ACM (certificates) | Google Cloud | Azure, OCI, and Private Kubernetes · cert-manager Certificate | Azure · Key Vault | OCI · OCI Certificate | | ------------------------------------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | Managed renewal · automatic renewal | Yes - managed renewal for issued certificates | Yes - Google auto-renews the managed certificate | Yes - cert-manager auto-renews ahead of expiry through its renew-before window | Yes - the certificate's lifetime action auto-renews a self-signed or partner-CA certificate | Yes - a renewal rule auto-renews an internally-issued certificate on a set interval | | Public trust · browser-trusted out of the box | Yes - public certificate authority | Yes - Google-managed publicly-trusted certificate | Yes - a Let's Encrypt ACME issuer produces a publicly-trusted certificate | No - a self-signed vault certificate is not publicly trusted | No - issues from a private CA; clients must explicitly trust its root | | DNS validation · prove control with a CNAME | Yes - DNS CNAME domain validation | Yes - a DNS authorization publishes the same kind of CNAME | Yes - an ACME DNS-01 solver proves domain control by publishing a DNS record | No - no DNS-CNAME flow; issuance is governed by the certificate policy | No - no public DNS-validated issuance; the internal CA signs directly | | Key confidentiality · is the private key kept out of reach | Yes - key withheld by default (opt-in export since 2025) | Yes - the managed key is never accessible and an imported key is write-only, so the service does not return private keys | Partial - the private key is written to a Kubernetes Secret, readable in-cluster to anything with access, wider than the default | Partial - the vault certificate is issued exportable today, so its private key is always readable through the backing secret, wider than a default certificate, whose key is never retrievable | Partial - the certificate bundle can return the private key even for an internally-issued certificate, wider than the default | | Wildcard + subject alternative names · multi-domain coverage | Yes - up to one hundred subject alternative names | Yes - the managed certificate names the same domains, and authorizing a domain covers its wildcard | Yes - the certificate's common name and DNS names accept wildcards | Yes - the certificate policy holds the subject and its DNS alternative names, wildcards included | Yes - the certificate holds subject alternative names of DNS and IP types | | Attach model · how the certificate reaches the consumer | attached by reference to a load balancer, CDN, or gateway | bound through a certificate map on the target proxy (in practice it rides the load balancer) | its TLS Secret is referenced by the ingress that terminates TLS | referenced from the vault by the gateway or App Service that terminates TLS | referenced by its certificate OCID from the load balancer that terminates TLS | | Private-CA issuance · issue without reaching a public CA | No - public-certificate issuance in this mapping | No - a Google-managed certificate is issued by Google's hosted CA over the network, not from a private CA | Partial - without reaching a public ACME CA, only a self-signed or private-CA issuer works, which is privately trusted | Partial - a self-signed vault certificate is generated without reaching a public CA, but it is not publicly trusted | Partial - the internal CA signs within your tenancy, so issuance does not depend on a public CA, but the certificate is not publicly trusted | | API coverage | full | high | high | partial | partial | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | ------------------------- | --------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DescribeCertificate | Describe | Supported | Common | returns the certificate's identity and its DNS validation record, realized at install time over the Google-managed certificate | | ListCertificates | Describe | Supported | Most usage | lists the installed certificates, known at install time | | ImportCertificate | Issue | Supported | Most usage | a third-party certificate and its chain are provisioned onto a self-managed certificate; the imported private key is write-only and is never returned on read | | RequestCertificate | Issue | Supported | Common | provisions the requested domain, its subject alternative names, and DNS validation onto a Google-managed certificate: publicly trusted, auto-renewed, its key algorithm Google-chosen (a managed certificate takes no algorithm argument) | | DeleteCertificate | Lifecycle | Supported | Most usage | the managed certificate resource is removed and its consumer's attach reference rewritten so nothing dangles | | UpdateCertificateOptions | Lifecycle | Out of scope | Full surface | certificate-option updates are outside this mapping; ACM has deprecated transparency-logging opt-out for public certificates | | RenewCertificate | Renew | Supported | Most usage | Google renews the managed certificate before expiry | | ExportCertificate | Retrieve | Out of scope | Most usage | a Google-managed key is never accessible and an imported key is write-only, so ExportCertificate is rejected; ACM opt-in managed-key export is not reproduced | | GetCertificate | Retrieve | Supported | Common | returns the certificate and its chain, never the private key | | AddTagsToCertificate | Tags | Supported | Full surface | tags become labels, lowercased to the label character set | | RemoveTagsFromCertificate | Tags | Supported | Full surface | removes labels | | DNS validation | Validate | Supported | Common | a DNS authorization publishes the same kind of CNAME record you add to your zone, and authorizing a domain also covers its wildcard: domain validation through DNS | | Email / HTTP validation | Validate | Out of scope | Most usage | this mapping uses DNS authorization and does not translate ACM email or HTTP validation; Google also supports load-balancer authorization, which is outside this mapping | #### How it works ACM issues and renews TLS certificates, which your infrastructure references by ARN from a load balancer, CDN, or API gateway. That service handles TLS for the application. By default, ACM does not expose the private key. The operation names shown on this page describe infrastructure equivalents configured during installation. This mapping does not serve runtime AWS ACM API requests. Certificate issuance, retrieval, renewal, and deletion use the target service and its access controls. Tensor9 provisions a Google-managed certificate during installation and attaches it to the HTTPS load balancer or proxy serving your application. Google handles certificate issuance and renewal. The adapter does not process application requests.
Tensor9 provisions and attaches a Google-managed certificate during installation. Tensor9 provisions and attaches a Google-managed certificate during installation.

Tensor9 provisions and attaches a Google-managed certificate during installation.

#### The two certificate modes `google_certificate_manager_certificate` supports two mutually exclusive modes. In `managed` mode, you provide `domains` and `dns_authorizations`. Google issues a publicly trusted certificate and renews it, corresponding to ACM `RequestCertificate`. In `self_managed` mode, you provide `pem_certificate`, including its chain, and `pem_private_key`. Google serves this imported certificate, corresponding to `ImportCertificate`, but you must renew it. Google chooses the key algorithm for managed certificates; use an imported certificate if you need to choose the algorithm.
One google_certificate_manager_certificate has exactly one mode: a Google-issued managed block, or a self-supplied self_managed block, the two analogs of ACM's RequestCertificate and ImportCertificate. One google_certificate_manager_certificate has exactly one mode: a Google-issued managed block, or a self-supplied self_managed block, the two analogs of ACM's RequestCertificate and ImportCertificate.

One google\_certificate\_manager\_certificate has exactly one mode: a Google-issued managed block, or a self-supplied self\_managed block, the two analogs of ACM's RequestCertificate and ImportCertificate .

#### The dns\_authorization CNAME For ACM DNS validation, `RequestCertificate` returns the certificate ARN. `DescribeCertificate` supplies its DNS validation CNAME while the certificate is `PENDING_VALIDATION`. Publishing that record proves domain control. ACM then marks the certificate `ISSUED` and uses the DNS authorization for subsequent renewal. A `google_certificate_manager_dns_authorization` similarly returns a `dns_resource_record` CNAME to publish in your DNS zone. The managed certificate references it through `dns_authorizations`. One authorization covers a domain and its wildcard, such as `example.com` and `*.example.com`. The adapter rejects `ValidationMethod=EMAIL` and `HTTP`; DNS is the supported method.
A DNS-validated certificate issues by proving domain control with a CNAME; Google's dns_authorization produces a dns_resource_record CNAME for Google domain validation, added to the same zone. This mapping uses DNS validation. A DNS-validated certificate issues by proving domain control with a CNAME; Google's dns_authorization produces a dns_resource_record CNAME for Google domain validation, added to the same zone. This mapping uses DNS validation.

A DNS-validated certificate issues by proving domain control with a CNAME; Google's dns\_authorization produces a dns\_resource\_record CNAME for Google domain validation, added to the same zone. This mapping uses DNS validation.

#### The certificate map On AWS, a TLS consumer references the certificate by ARN. The load balancer listener, CloudFront distribution, or API gateway performs the TLS handshake. Google Certificate Manager supports a `google_certificate_manager_certificate_map` whose `certificate_map_entry` records associate hostnames with certificates. A target HTTPS proxy selects a certificate using the client's Server Name Indication (SNI) hostname. Tensor9 rewrites the consumer reference to the certificate configuration on the target HTTPS proxy.
On AWS the certificate is an ARN on the listener; on Google it is a certificate_map of hostname→certificate entries the HTTPS proxy consults, with SNI routing modeled as its own resource rather than a listener field. On AWS the certificate is an ARN on the listener; on Google it is a certificate_map of hostname→certificate entries the HTTPS proxy consults, with SNI routing modeled as its own resource rather than a listener field.

On AWS the certificate is an ARN on the listener; on Google it is a certificate\_map of hostname→certificate entries the HTTPS proxy consults, with SNI routing modeled as its own resource rather than a listener field.

#### Private-key access Default ACM public certificates are non-exportable: `GetCertificate` returns the certificate and chain without the key. Since 2025, ACM has also offered opt-in exportable public certificates. Google does not expose managed-certificate private keys. An imported certificate's `pem_private_key` is accepted as input but never returned on read. This preserves non-exportability for the generated managed certificate. It does not reproduce ACM's opt-in managed-key export feature.
Google never returns managed private keys or the private keys supplied with imported certificates. Google never returns managed private keys or the private keys supplied with imported certificates.

Google never returns managed private keys or the private keys supplied with imported certificates.

#### Limitations **Validation methods.** This mapping uses DNS authorization. It does not reproduce ACM email approval or CloudFront HTTP validation. **Certificate attachment.** A certificate map connects hostnames to certificates on the HTTPS proxy. Check each hostname and consumer attachment when replacing the AWS certificate ARN. **Certificate transparency.** Public certificates are logged. ACM has deprecated its transparency-logging opt-out; the adapter does not map that former option. **Key algorithm and export.** Google chooses managed-certificate keys and does not export them. Import a self-managed certificate when you need a particular algorithm; you must then supply renewed certificates. ACM hosted-ACME configuration is outside this mapping. **Private issuance.** Certificates that name an ACM Private CA through certificate\_authority\_arn use the separate private-CA mapping. Google private issuance can use CA Service through issuance\_config. #### Other considerations **Prepare DNS and trust.** Create the DNS authorization records and keep them available for renewal. New Google-managed certificates are issued during installation; existing ACM certificates and keys are not transferred. **Operate the TLS consumer.** The customer controls the Google project, DNS zone, and load balancer. Google issues and renews managed certificates. Verify that the proxy serves the right certificate for every hostname. **Import deliberately.** For an imported certificate, supply the PEM certificate, chain, and private key. Protect that input and arrange replacement before expiry; Google does not renew imported certificates. ## On Azure, OCI, and Private Kubernetes ### Via cert-manager Certificate | Operation | Area | Support | Depth | Notes | | ------------------------- | --------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DescribeCertificate | Describe | Supported | Common | returns the certificate's identity, realized at install time as the name of its TLS Secret | | ListCertificates | Describe | Supported | Most usage | lists the installed certificates, known at install time | | ImportCertificate | Issue | Supported | Most usage | a third-party certificate is brought in as a TLS Secret (or through a certificate-authority issuer), bringing its chain and key | | RequestCertificate | Issue | Supported | Common | realized when the install is built as a cert-manager Certificate bound to a Let's Encrypt ACME cluster issuer with a DNS-01 solver: publicly trusted, DNS-validated, and auto-renewed; the closest analog to a DNS-validated public certificate | | DeleteCertificate | Lifecycle | Supported | Most usage | realized at install time: the Certificate is removed, its readiness resource is folded away, and the consumer's reference is rewritten | | UpdateCertificateOptions | Lifecycle | Out of scope | Full surface | certificate-option updates are outside this mapping; ACM has deprecated transparency-logging opt-out for public certificates | | RenewCertificate | Renew | Supported | Most usage | cert-manager renews ahead of expiry through its renew-before window and the ACME issuer | | ExportCertificate | Retrieve | Supported | Most usage | the export reads the private key from the Kubernetes Secret cert-manager writes, readable in-cluster to anything with access, wider than the certificate's default key-access policy | | GetCertificate | Retrieve | Supported | Common | returns the certificate and its chain, never the private key | | AddTagsToCertificate | Tags | Supported | Full surface | tags become metadata labels and annotations | | RemoveTagsFromCertificate | Tags | Supported | Full surface | removes labels and annotations | | DNS validation | Validate | Supported | Common | an ACME DNS-01 solver proves domain control by publishing a DNS record, domain validation through DNS | | Email / HTTP validation | Validate | Out of scope | Most usage | there is no email or HTTP domain-approval flow that matches the origin's; a request for either validation method is rejected | #### How it works ACM issues and renews TLS certificates, which your infrastructure references by ARN from a load balancer, CDN, or API gateway. That service handles TLS for the application. By default, ACM does not expose the private key. The operation names shown on this page describe infrastructure equivalents configured during installation. This mapping does not serve runtime AWS ACM API requests. Certificate issuance, retrieval, renewal, and deletion use the target service and its access controls. Tensor9 creates a cert-manager `Certificate` during installation and updates the Ingress to reference its TLS `Secret`. cert-manager obtains the certificate through an ACME `ClusterIssuer` and stores the certificate and key in that Secret. The Ingress handles application TLS traffic.
cert-manager stores the certificate in a TLS Secret that the Ingress uses for TLS. cert-manager stores the certificate in a TLS Secret that the Ingress uses for TLS.

cert-manager stores the certificate in a TLS Secret that the Ingress uses for TLS.

#### The Certificate and its ACME ClusterIssuer The `Certificate` resource (`cert-manager.io/v1`) declares `secretName`, `commonName`, `dnsNames`, `duration`, `renewBefore`, and `privateKey` settings. These describe the output Secret, domain names, validity and renewal periods, and key algorithm, size, and rotation policy. Duration defaults to 90 days. `issuerRef` selects the Let's Encrypt ACME `ClusterIssuer`. The issuer's `spec.acme` names the public CA, a contact `email`, and a `solvers` list. A `dns01` solver proves domain control with a DNS record. ACM's hosted ACME service, introduced in 2026, uses the same protocol.
The adapter emits two resources: a Certificate that names the tls Secret to write, and, bound to it by issuerRef, an ACME ClusterIssuer whose dns01 solver proves domain control. The adapter emits two resources: a Certificate that names the tls Secret to write, and, bound to it by issuerRef, an ACME ClusterIssuer whose dns01 solver proves domain control.

The adapter emits two resources: a Certificate that names the tls Secret to write, and, bound to it by issuerRef , an ACME ClusterIssuer whose dns01 solver proves domain control.

#### How the certificate is issued cert-manager opens an ACME `Order` and publishes an `_acme-challenge` DNS record. The CA checks that record and signs the certificate. cert-manager writes the certificate and private key to the TLS Secret, then sets the Certificate's `Ready` condition to true. The `Ready` condition replaces ACM's separate validation-wait resource. This adapter supports `dns01`; requests for `EMAIL` or `HTTP` validation return errors.
The dns01 solver publishes a DNS record, the ACME authority reads it to confirm you control the domain, and the signed certificate and key land in the tls Secret as the Certificate turns Ready. The dns01 solver publishes a DNS record, the ACME authority reads it to confirm you control the domain, and the signed certificate and key land in the tls Secret as the Certificate turns Ready.

The dns01 solver publishes a DNS record, the ACME authority reads it to confirm you control the domain, and the signed certificate and key land in the tls Secret as the Certificate turns Ready .

#### Certificate renewal cert-manager renews the certificate before expiry using the `renewBefore` window. By default it begins renewal roughly two-thirds of the way through the certificate's duration, repeating the ACME validation and issuance process. Renewal updates the certificate and key under the same `secretName`, so the Ingress reference remains valid. Imported certificates supplied as TLS Secrets are not automatically renewed.
Renewal changes the contents of the tls Secret, not its name, so the Ingress reference is stable, the same way an ACM certificate keeps its ARN across a renewal. Renewal changes the contents of the tls Secret, not its name, so the Ingress reference is stable, the same way an ACM certificate keeps its ARN across a renewal.

Renewal changes the contents of the tls Secret, not its name, so the Ingress reference is stable, the same way an ACM certificate keeps its ARN across a renewal.

#### Where the private key lives The Secret contains `tls.crt` and `tls.key`. Any principal permitted to read it can retrieve the private key. Default ACM public certificates do not expose that key. Restrict Secret reads with Kubernetes role-based access control (RBAC), using `Role` and `RoleBinding` permissions for the relevant `ServiceAccount`. ACM's opt-in exportable public certificates, available since 2025, also permit key retrieval; the difference described here is relative to default non-exportable certificates.
Read access to the TLS Secret includes access to the private key in tls.key. Read access to the TLS Secret includes access to the private key in tls.key.

Read access to the TLS Secret includes access to the private key in tls.key .

#### The air-gapped fallback Public ACME issuance requires the cluster to reach the public CA and publish DNS challenge records. The resulting certificate is trusted by clients that trust the CA. For a cluster without public-CA access, configure a `selfSigned` or `ca` issuer. This produces a privately trusted certificate; distribute the private root to every intended client. It does not provide public trust.
Public trust depends on the cluster reaching a public ACME authority; a cluster without public-CA access needs a selfSigned or ca issuer, which is privately trusted, not public. Public trust depends on the cluster reaching a public ACME authority; a cluster without public-CA access needs a selfSigned or ca issuer, which is privately trusted, not public.

Public trust depends on the cluster reaching a public ACME authority; a cluster without public-CA access needs a selfSigned or ca issuer, which is privately trusted, not public.

#### Limitations **Private-key access.** The TLS Secret contains both the certificate and private key. Restrict reads to the identities that require them. Default ACM public certificates do not expose their private keys. **Public-CA connectivity.** Public issuance needs access to the ACME CA and DNS challenge publication. Without that access, a private CA or self-signed issuer requires explicit client trust configuration. **Validation methods.** This mapping uses ACME DNS-01. It does not translate ACM email approval or CloudFront HTTP validation into ACME HTTP-01 challenges. **Other ACM options.** The adapter does not reproduce ACM hosted-ACME endpoint configuration or ManagedBy=CLOUDFRONT. Public certificates are logged; ACM has deprecated the transparency-logging opt-out. **Private issuance.** Certificates naming certificate\_authority\_arn use the separate ACM Private CA mapping. cert-manager CA and Vault issuers have their own trust and operating requirements. #### Other considerations **Operate cert-manager.** The platform team installs, monitors, and updates cert-manager and its ClusterIssuer. Grant the DNS solver access to the required zone and monitor failed Orders and Certificate readiness. **Check renewal at the consumer.** Renewal writes a new certificate to the same Secret. Verify that the Ingress reloads it and serves the renewed certificate. Secret identity remains stable; imported TLS Secrets require separately supplied replacements. **Cut over.** Wait for the new Certificate to become Ready before switching traffic. Existing ACM certificates are not transferred automatically. Import a certificate and key you already hold as a kubernetes.io/tls Secret if they must be retained. ## On Azure ### Via Key Vault | Operation | Area | Support | Depth | Notes | | ------------------------- | --------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DescribeCertificate | Describe | Supported | Common | returns the certificate's identity, realized at install time as a vault certificate reference | | ListCertificates | Describe | Supported | Most usage | lists the installed certificates, known at install time | | ImportCertificate | Issue | Supported | Common | a third-party certificate imports as vault certificate contents (with its chain and passphrase) when you hold the key | | RequestCertificate | Issue | Partial | Common | realized when the install is built as a self-signed generate-policy vault certificate holding the subject and its alternative names; but a self-signed vault certificate is not publicly trusted; public, DNS-validated issuance needs a configured partner CA or the App Service Managed Certificate alternative | | DeleteCertificate | Lifecycle | Supported | Most usage | realized at install time: the vault certificate is removed and its consumer's reference is rewritten; the vault's own soft-delete retention holds it before it is purged | | UpdateCertificateOptions | Lifecycle | Out of scope | Full surface | certificate-option updates are outside this mapping; ACM has deprecated transparency-logging opt-out for public certificates | | RenewCertificate | Renew | Supported | Most usage | the certificate's lifetime action auto-renews a self-signed or partner-CA-integrated certificate; a manual (issuer-unknown) certificate does not auto-renew | | ExportCertificate | Retrieve | Supported | Most usage | the export reads the private key from the backing vault secret; the generated certificate is exportable, so the key is retrievable, wider than the certificate's default key-access policy, whose key is never returned | | GetCertificate | Retrieve | Supported | Common | returns the certificate and its chain, never the private key | | AddTagsToCertificate | Tags | Supported | Full surface | tags become vault-resource tags | | RemoveTagsFromCertificate | Tags | Supported | Full surface | removes tags | | DNS validation | Validate | Out of scope | Common | Key Vault has no DNS-CNAME domain-validation flow; issuance is governed by the certificate policy instead, so DNS validation is rejected | | Email / HTTP validation | Validate | Out of scope | Most usage | there is no email or HTTP domain-approval flow; a request for either validation method is rejected | #### How it works ACM issues and renews TLS certificates, which your infrastructure references by ARN from a load balancer, CDN, or API gateway. That service handles TLS for the application. By default, ACM does not expose the private key. The operation names shown on this page describe infrastructure equivalents configured during installation. This mapping does not serve runtime AWS ACM API requests. Certificate issuance, retrieval, renewal, and deletion use the target service and its access controls. Tensor9 provisions an `azurerm_key_vault_certificate` in the customer deployment's shared Azure Key Vault and updates the service that handles TLS to reference it. This happens during installation. The generated certificate is self-signed, so clients must trust it explicitly; it does not provide ACM public-certificate trust.
During installation, Tensor9 provisions a Key Vault certificate and updates its TLS consumer. During installation, Tensor9 provisions a Key Vault certificate and updates its TLS consumer.

During installation, Tensor9 provisions a Key Vault certificate and updates its TLS consumer.

#### The Key Vault certificate and its policy The `certificate_policy` defines the certificate. `domain_name` becomes `x509_certificate_properties.subject`; `subject_alternative_names` becomes `subject_alternative_names.dns_names`, including wildcards. `key_algorithm` sets `key_properties.key_type` and `key_size`. A `lifetime_action` with `AutoRenew` enables renewal. An existing certificate and key can instead be imported using `certificate.contents` and its passphrase. The generated policy sets `issuer_parameters.name = Self`. Key Vault generates the key and self-signs from the policy. Wait for its certificate operation to complete; no separate DNS approval is required.
The request compiles onto one azurerm_key_vault_certificate; its certificate_policy holds the subject, the alternative names, the key, and the renewal rule; its issuer parameter is Self. The request compiles onto one azurerm_key_vault_certificate; its certificate_policy holds the subject, the alternative names, the key, and the renewal rule; its issuer parameter is Self.

The request compiles onto one azurerm\_key\_vault\_certificate ; its certificate\_policy holds the subject, the alternative names, the key, and the renewal rule; its issuer parameter is Self .

#### The self-signed certificate, and the public-trust gap A self-signed certificate has no `PENDING_VALIDATION` stage or CNAME challenge. Issuance completes directly from the certificate policy. Clients must explicitly trust the self-signed certificate before accepting it. For a publicly trusted certificate, configure a supported partner certificate authority (CA), such as DigiCert or GlobalSign, as the issuer. App Service Managed Certificates provide another option for eligible App Service hostnames.
Key Vault issues the self-signed certificate without a DNS challenge. Clients must be configured to trust it. Key Vault issues the self-signed certificate without a DNS challenge. Clients must be configured to trust it.

Key Vault issues the self-signed certificate without a DNS challenge. Clients must be configured to trust it.

#### The App Service Managed Certificate An `azurerm_app_service_managed_certificate` uses an existing `custom_hostname_binding` to validate the hostname. Azure and DigiCert issue the publicly trusted certificate; Microsoft manages renewal without a certificate charge. App Service Managed Certificates cover one exact hostname, do not support wildcards, and require an internet-reachable App Service for validation. They cannot be attached to an arbitrary load balancer. Use a configured partner CA when these restrictions do not fit your deployment.
App Service Managed Certificates are publicly trusted and renew automatically. They cover one reachable App Service hostname and no wildcards. App Service Managed Certificates are publicly trusted and renew automatically. They cover one reachable App Service hostname and no wildcards.

App Service Managed Certificates are publicly trusted and renew automatically. They cover one reachable App Service hostname and no wildcards.

#### The exportable key `GetCertificate` returns an ACM certificate and chain without the key. Default public certificates are non-exportable; ACM has also offered opt-in exportable public certificates since 2025. Tensor9 currently sets `key_properties.exportable = true`. Anyone allowed to read the backing Key Vault secret can therefore retrieve the private key. To prevent export, configure `exportable = false` or use a non-exportable `RSA-HSM` or `EC-HSM` key. This is a configuration change from the generated default.
The generated Key Vault certificate is exportable: permission to read its backing secret permits reading the private key. The generated Key Vault certificate is exportable: permission to read its backing secret permits reading the private key.

The generated Key Vault certificate is exportable: permission to read its backing secret permits reading the private key.

#### Auto-renew on the certificate's lifetime action The certificate's `lifetime_action` sets `action_type = AutoRenew` and a trigger expressed as `days_before_expiry` or `lifetime_percentage`. Renewal creates a new certificate and secret version. Consumers must use a versionless secret reference, or update their versioned reference, to receive the renewed certificate. Self-signed certificates (`issuer = Self`) and certificates from integrated partner CAs support automatic renewal. Imported or manually managed certificates (`issuer = Unknown`) require you to obtain and import a replacement before expiry.
Automatic renewal creates a new version. A versionless secret reference lets a compatible consumer follow renewed versions. Automatic renewal creates a new version. A versionless secret reference lets a compatible consumer follow renewed versions.

Automatic renewal creates a new version. A versionless secret reference lets a compatible consumer follow renewed versions.

#### Limitations **Public trust.** The generated Key Vault policy uses issuer Self. Clients must explicitly trust that certificate. Public trust requires an integrated partner CA or an eligible App Service Managed Certificate; the latter covers one exact App Service hostname and no wildcard. **Validation.** This mapping does not reproduce ACM DNS, email, or CloudFront HTTP validation. Key Vault issuance follows its certificate policy and configured issuer. **Private-key access.** The generated key is exportable through the backing secret. Set exportable=false or choose an appropriate non-exportable HSM key when export is prohibited, and restrict secret reads. **Private issuance.** A certificate that names certificate\_authority\_arn uses the separate ACM Private CA mapping. It is not automatically recreated by this self-signed Key Vault policy. #### Other considerations **Assign permissions.** The provisioning identity needs certificate-management permissions. The gateway or App Service needs access to the backing secret under its own Azure identity. Grant only the required vault or object scope. **Configure renewal.** Self-signed and integrated partner-CA certificates can renew through lifetime\_action. Use a versionless secret reference where the TLS consumer supports it, or update the consumer to the new version. Imported certificates require you to supply replacements. **Cut over and recover.** Import an existing certificate, chain, and key when you must preserve them; otherwise installation creates a new certificate. Verify client trust and consumer access before switching traffic. Key Vault soft-delete retention affects recovery and name reuse. ## On OCI ### Via OCI Certificate | Operation | Area | Support | Depth | Notes | | ------------------------- | --------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DescribeCertificate | Describe | Supported | Common | returns the certificate's identity, realized at install time as a certificate OCID | | ListCertificates | Describe | Supported | Most usage | lists the installed certificates, known at install time | | ImportCertificate | Issue | Supported | Common | a third-party certificate imports in the imported configuration, bringing its chain and key; import is the alternative for a certificate that must be publicly trusted | | RequestCertificate | Issue | Partial | Common | issuance is provisioned from an internal / private CA holding the subject and its alternative names, but OCI Certificates has no public, DNS-validated, browser-trusted issuance path; clients must explicitly trust the private root; this does not provide public trust | | DeleteCertificate | Lifecycle | Supported | Most usage | realized at install time: the certificate is scheduled for deletion and its consumer's reference is rewritten | | UpdateCertificateOptions | Lifecycle | Out of scope | Full surface | certificate-option updates are outside this mapping; ACM has deprecated transparency-logging opt-out for public certificates | | RenewCertificate | Renew | Supported | Most usage | a certificate renewal rule auto-renews an internally-issued certificate on a set interval with an advance renewal period | | ExportCertificate | Retrieve | Supported | Most usage | the certificate bundle can return the private key even for an internally-issued certificate, wider than the certificate's default key-access policy | | GetCertificate | Retrieve | Supported | Common | returns the certificate and its chain, never the private key | | AddTagsToCertificate | Tags | Supported | Full surface | tags become freeform tags (defined tags additionally take a tag namespace) | | RemoveTagsFromCertificate | Tags | Supported | Full surface | removes tags | | DNS validation | Validate | Out of scope | Common | there is no public, DNS-validated issuance (the internal CA signs directly), so DNS domain validation is rejected | | Email / HTTP validation | Validate | Out of scope | Most usage | there is no email or HTTP domain-approval flow; a request for either validation method is rejected | #### How it works ACM issues and renews TLS certificates, which your infrastructure references by ARN from a load balancer, CDN, or API gateway. That service handles TLS for the application. By default, ACM does not expose the private key. The operation names shown on this page describe infrastructure equivalents configured during installation. This mapping does not serve runtime AWS ACM API requests. Certificate issuance, retrieval, renewal, and deletion use the target service and its access controls. Tensor9 provisions an OCI Certificates resource during installation and updates the OCI load balancer to reference it. OCI issues new certificates from a private CA. Clients must trust that CA explicitly; publicly trusted certificates must be obtained separately and imported.
During installation, Tensor9 provisions an OCI certificate and updates the load balancer reference. During installation, Tensor9 provisions an OCI certificate and updates the load balancer reference.

During installation, Tensor9 provisions an OCI certificate and updates the load balancer reference.

#### The certificate's config type The `certificate_config.config_type` on `oci_certificates_management_certificate` selects issuance. Tensor9 uses `ISSUED_BY_INTERNAL_CA` for a new certificate, or `IMPORTED` with `certificate_pem`, `private_key_pem`, and `cert_chain_pem` for an existing one. OCI also supports `MANAGED_EXTERNALLY_ISSUED_BY_INTERNAL_CA` for external certificate-signing requests (CSRs), but that mode is outside this adapter's coverage. For internal issuance, `DomainName` becomes `subject.common_name`. Subject alternative names become `subject_alternative_names` entries with `DNS` or `IP` types and values. The resource also sets `certificate_profile_type`, such as `TLS_SERVER`, and `key_algorithm`, such as `RSA2048` or `ECDSA_P256`.
The adapter creates an internally issued certificate or imports an existing certificate, according to config_type. The adapter creates an internally issued certificate or imports an existing certificate, according to config_type.

The adapter creates an internally issued certificate or imports an existing certificate, according to config\_type .

#### Internal-CA issuance and public trust With `ISSUED_BY_INTERNAL_CA`, the CA named by `issuer_certificate_authority_id` signs the certificate. Its chain ends at your private root. Distribute that root to each client's trust store; browsers do not trust it by default. For public trust, import a certificate issued by a public CA that the intended clients trust. Importing alone does not make a certificate publicly trusted. The adapter rejects ACM public DNS-validated issuance requests because OCI Certificates does not provide that issuance flow.
An internally issued certificate requires private-root distribution. For public trust, import a certificate from a CA your clients already trust. An internally issued certificate requires private-root distribution. For public trust, import a certificate from a CA your clients already trust.

An internally issued certificate requires private-root distribution. For public trust, import a certificate from a CA your clients already trust.

#### Auto-renewal with certificate\_rules A `CERTIFICATE_RENEWAL_RULE` in `certificate_rules` sets an `advance_renewal_period`, such as `P30D`, and a `renewal_interval`. Before `not_after`, OCI issues a new version under the same certificate OCID. The load balancer keeps that reference. Automatic renewal covers internally issued certificates. For `IMPORTED` certificates, obtain and import a replacement before expiry; OCI cannot renew a certificate issued by another CA.
A certificate_rules renewal rule renews an internally-issued certificate one advance_renewal_period before not_after, in place, keeping the same OCID, so the load balancer reference never has to change. A certificate_rules renewal rule renews an internally-issued certificate one advance_renewal_period before not_after, in place, keeping the same OCID, so the load balancer reference never has to change.

A certificate\_rules renewal rule renews an internally-issued certificate one advance\_renewal\_period before not\_after , in place, keeping the same OCID, so the load balancer reference never has to change.

#### Attaching to the OCI load balancer The load balancer references the certificate by OCID and handles TLS for the application. Certificate provisioning happens during installation; no Tensor9 certificate adapter handles application traffic. Tensor9 rewrites the AWS certificate ARN reference to the provisioned OCI certificate's OCID. This applies to both internally issued and imported certificates.
On OCI a certificate is used by reference (a certificate OCID on the load balancer's listener), the same shape as an ACM ARN on a TLS terminator; the load balancer handles client TLS. On OCI a certificate is used by reference (a certificate OCID on the load balancer's listener), the same shape as an ACM ARN on a TLS terminator; the load balancer handles client TLS.

On OCI a certificate is used by reference (a certificate OCID on the load balancer's listener), the same shape as an ACM ARN on a TLS terminator; the load balancer handles client TLS.

#### Limitations **Public trust.** Internally issued certificates require clients to trust the private CA. For public trust, obtain and import a certificate from a CA the intended clients already trust. Importing a certificate does not itself establish trust. **Validation methods.** OCI internal issuance does not reproduce ACM public DNS, email, or CloudFront HTTP validation. The configured internal CA signs the certificate. **Private-key access.** CERTIFICATE\_CONTENT\_WITH\_PRIVATE\_KEY returns the key in the certificate bundle. Restrict that permission; default non-exportable ACM public certificates do not permit equivalent key retrieval. **Private issuance.** Certificates that name an AWS certificate\_authority\_arn use the separate ACM Private CA mapping. The certificate mapping does not transfer the AWS CA or its history. #### Other considerations **Assign permissions.** Use compartment IAM policies to control certificate management and bundle access. Grant the load balancer its required access, and restrict permission to retrieve bundles containing private keys. **Plan trust and renewal.** Oracle operates the CA service. The customer configures the CA, its key, and client trust. Internally issued certificates renew under certificate\_rules; imported certificates require a replacement from their issuing CA before expiry. **Cut over.** Provision or import the new OCI certificate and update the load balancer reference. Verify client trust, hostname coverage, and renewal at the listener. Existing ACM certificates are not moved automatically. [Service Catalog](/service-adapters/catalog). # ACM Private CA Source: https://docs.tensor9.com/service-adapters/aws/security-identity/acm-private-ca AWS ACM Private CA. Runs a private certificate authority whose root and subordinate CAs sign certificates trusted only by trust stores you control. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure, OCI, and Private Kubernetes](#on-azure-oci-and-private-kubernetes) * [Via cert-manager Issuer](#via-cert-manager-issuer) * [On OCI](#on-oci) * [Via OCI Certificate Authority](#via-oci-certificate-authority) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of ACM Private CA with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | ACM Private CA | Google Cloud | Azure, OCI, and Private Kubernetes · cert-manager Issuer | OCI · OCI Certificate Authority | | ---------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Private certificate authority | Yes | Yes - a CA Service CA in a CA pool | Yes - a cert-manager CA Issuer / ClusterIssuer backed by a CA Secret | Yes - an OCI Certificates CA in a compartment | | Root + subordinate CA hierarchy | Yes | Yes - a SELF\_SIGNED root CA and SUBORDINATE CAs, the CA Service analog of ACM Private CA's ROOT / SUBORDINATE types | Partial - a CA Issuer plus an intermediate Certificate models a subordinate; deep multi-tier hierarchies are assembled by hand, not a managed ROOT / SUBORDINATE type | Yes - OCI root and subordinate CAs, the analog of ACM Private CA's ROOT / SUBORDINATE types | | Certificate issuance from the private CA | Yes | Yes - google\_privateca\_certificate, issued from the pool and chaining to the private root | Yes - cert-manager Certificate resources signed by the CA Issuer, chaining to the private root | Yes - OCI Certificates issues leaf certificates from the CA, chaining to the private root | | Managed CA + certificate rotation | Yes | Yes - CA Service manages the CA lifecycle; certificates rotate by re-issuance from the pool | Partial - cert-manager renews leaf certificates automatically; the operator manages CA certificate rotation | Yes - configured managed leaf certificates renew automatically; the customer initiates CA renewal and rotates the CA key | | Issuance scoping | Yes - acm-pca:TemplateArn IAM condition | Partial - the pool issuance\_policy (allowed key usage / identity constraints), not a 1:1 template-ARN condition | Partial - Kubernetes RBAC on the Certificate / Issuer objects plus the Issuer vs ClusterIssuer scope, not a template-ARN condition | Partial - OCI IAM compartment policy on the CA plus its issuance configuration, not a 1:1 template-ARN condition | | CRL distribution | Yes - customer-owned S3 bucket | Partial - Google-managed or customer-managed Cloud Storage bucket; translate the S3 bucket policy and distribution point | No - the CA operator must provide CRL generation and publication | Partial - configured Object Storage bucket; translate the S3 bucket policy and distribution point | | Cloud-operated / managed CA | Yes | Yes - Google operates CA Service end to end | No - the platform team operates cert-manager and protects the CA Secret, including on AKS | Yes - Oracle operates OCI Certificates end to end | | API coverage | full | partial | partial | partial | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------- | -------------- | -------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Issuance scoping (acm-pca:TemplateArn IAM condition) | Access control | Partial | Most usage | the CA pool issuance\_policy (allowed key usages, identity constraints) plus IAM on the CA Service resources, not a one-to-one template-ARN principal condition | | CSR-signing activation split (aws\_acmpca\_certificate\_authority\_certificate) | CA activation | Adapter-served | Most usage | the mapped infrastructure configures parent signing, CA activation, and the ENABLED state; native CA creation alone does not make a CA ready for pool issuance | | ACM Private CA control plane (CreateCertificateAuthority / issuance API / …) | Control plane | Out of scope | Full surface | CA provisioning happens through your infrastructure-as-code at apply, not runtime ACM Private CA API calls | | CA hierarchy (ROOT / SUBORDINATE types) | Hierarchy | Supported | Most usage | a SELF\_SIGNED root CA plus SUBORDINATE CAs mirror ACM Private CA's ROOT / SUBORDINATE types; a two-tier hierarchy maps directly, deeper trees add more subordinate CAs | | Certificate issuance (aws\_acmpca\_certificate / IssueCertificate) | Issuance | Supported | Common | google\_privateca\_certificate leaves are issued from the pool and chain through a subordinate to the private root, with the subject / SANs / key usage / validity | | Private CA provisioning (aws\_acmpca\_certificate\_authority) | Provisioning | Supported | Common | a google\_privateca\_ca\_pool with a SELF\_SIGNED root CA and SUBORDINATE CAs: a managed private CA that chains issued certificates to its own private root | | CRL distribution (customer-owned S3 bucket) | Revocation | Partial | Full surface | CA Service publishes CRLs to a Google-managed or customer-managed Cloud Storage bucket; AWS S3 bucket policies require translation | #### How it works Tensor9 provisions a private certificate authority (CA) hierarchy on Google Certificate Authority Service. A CA pool groups the root and subordinate CAs; application certificates chain through the issuing CA to that private root. The deployment uses infrastructure as code. Runtime ACM Private CA API calls are outside this mapping.
The ACM Private CA hierarchy is re-expressed as a GCP Certificate Authority Service CA pool with its own private root, so your PKI keeps issuing certificates that chain to a private root you control. The ACM Private CA hierarchy is re-expressed as a GCP Certificate Authority Service CA pool with its own private root, so your PKI keeps issuing certificates that chain to a private root you control.

The ACM Private CA hierarchy is re-expressed as a GCP Certificate Authority Service CA pool with its own private root, so your PKI keeps issuing certificates that chain to a private root you control.

#### Architecture Google operates the CA service and protects the CA keys. The `google_privateca_ca_pool` holds issuance policy; CA resources define the root and subordinates, and `google_privateca_certificate` represents an issued certificate. Client trust stores determine which private roots the application trusts.
A CA pool holds a self-signed root CA and subordinate CAs; the subordinate issues leaf certificates that chain to the private root, with the pool's issuance policy governing what may be issued. A CA pool holds a self-signed root CA and subordinate CAs; the subordinate issues leaf certificates that chain to the private root, with the pool's issuance policy governing what may be issued.

A CA pool holds a self-signed root CA and subordinate CAs; the subordinate issues leaf certificates that chain to the private root, with the pool's issuance policy governing what may be issued.

#### The CA hierarchy A `SELF_SIGNED` root signs subordinate CAs, which issue application certificates. The mapped hierarchy uses a new root and key. Previously issued AWS certificates continue to chain to the old root until they expire or are reissued. Distribute the new root to clients before switching issuance.
ACM Private CA's ROOT and SUBORDINATE types map onto a SELF_SIGNED root CA and SUBORDINATE CAs inside the CA pool. ACM Private CA's ROOT and SUBORDINATE types map onto a SELF_SIGNED root CA and SUBORDINATE CAs inside the CA pool.

ACM Private CA's ROOT and SUBORDINATE types map onto a SELF\_SIGNED root CA and SUBORDINATE CAs inside the CA pool.

#### Issuing certificates ACM Private CA separates signing a certificate-signing request (CSR) from installing the resulting CA certificate. For the mapped Google hierarchy, infrastructure configuration handles parent signing, activation, and the enabled state. Native root CAs start staged; subordinate CAs require activation and then become staged. A pool needs an enabled CA before it can issue certificates. Application certificates specify their subject, subject alternative names (SANs), key usages, and validity; the pool issuance policy constrains what may be issued.
The mapped infrastructure handles CA signing, activation, and enablement before the pool issues leaf certificates. The mapped infrastructure handles CA signing, activation, and enablement before the pool issues leaf certificates.

The mapped infrastructure handles CA signing, activation, and enablement before the pool issues leaf certificates.

#### Issuance scoping The AWS `acm-pca:TemplateArn` IAM condition restricts which templates a principal can request. Google IAM permissions control access to CA resources, while the pool's `issuance_policy` constrains certificate identities and key usages. Review both controls when translating a policy that varies by principal and template.
ACM Private CA's acm-pca:TemplateArn IAM condition maps to the CA pool's issuance policy, not a one-to-one template-ARN condition. ACM Private CA's acm-pca:TemplateArn IAM condition maps to the CA pool's issuance policy, not a one-to-one template-ARN condition.

ACM Private CA's acm-pca:TemplateArn IAM condition maps to the CA pool's issuance policy, not a one-to-one template-ARN condition.

#### Revocation and CRLs A certificate revocation list (CRL) identifies certificates that should no longer be trusted. CA Service publishes CRLs to Cloud Storage, using either a Google-managed or customer-managed bucket. Configure publication, bucket permissions, and the distribution point clients use. Automation that reads or copies the old S3 CRL must use the new location and permissions.
CA Service publishes CRLs to a Google-managed or customer-managed Cloud Storage bucket. CA Service publishes CRLs to a Google-managed or customer-managed Cloud Storage bucket.

CA Service publishes CRLs to a Google-managed or customer-managed Cloud Storage bucket.

#### Limitations The new root does not inherit the AWS private key or issued-certificate history. AWS template conditions and S3 bucket policies need target-specific configuration. These differences affect trust distribution and issuance permissions even when the certificate hierarchy is unchanged. #### Other considerations Distribute the new root before issuing replacement certificates, and retain trust in the old root while existing certificates remain in use. Set CA and certificate validity periods and arrange renewal before expiry. Confirm key algorithms and protection settings at CA creation, then test issuance and client revocation checks from the customer environment. ## On Azure, OCI, and Private Kubernetes ### Via cert-manager Issuer | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------- | -------------- | -------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Issuance scoping (acm-pca:TemplateArn IAM condition) | Access control | Partial | Most usage | Kubernetes RBAC on the Certificate and Issuer objects plus the namespace-scoped Issuer vs cluster-wide ClusterIssuer choice, not an acm-pca:TemplateArn condition | | CSR-signing activation split (aws\_acmpca\_certificate\_authority\_certificate) | CA activation | Adapter-served | Most usage | the CSR-sign-then-install resource pair is implicit in the CA Issuer, which holds the CA certificate and key; there is no separate signing resource | | ACM Private CA control plane (CreateCertificateAuthority / issuance API / …) | Control plane | Out of scope | Full surface | CA provisioning happens through your infrastructure-as-code at apply, not runtime ACM Private CA API calls | | CA hierarchy (ROOT / SUBORDINATE types) | Hierarchy | Partial | Most usage | a root CA Issuer plus an intermediate Certificate that becomes a second Issuer models a subordinate; a two-tier hierarchy maps directly, but deep multi-tier hierarchies are assembled by hand, not a managed ROOT / SUBORDINATE type | | Certificate issuance (aws\_acmpca\_certificate / IssueCertificate) | Issuance | Supported | Common | cert-manager Certificate resources are signed by the CA Issuer and renewed automatically before they expire, with the subject / SANs / key usage / duration | | Private CA provisioning (aws\_acmpca\_certificate\_authority) | Provisioning | Supported | Common | a cert-manager CA Issuer / ClusterIssuer backed by a CA certificate and key in a Kubernetes Secret: a private CA the cluster holds, issuing certificates that chain to its private root | | CRL distribution (customer-owned S3 bucket) | Revocation | Out of scope | Full surface | cert-manager does not generate or publish CRLs; the CA operator must provide revocation services | #### How it works A cert-manager CA `Issuer` or `ClusterIssuer` signs application certificates using a CA certificate and key stored in a Kubernetes Secret. The customer platform team operates this CA, including on AKS. Provisioning uses infrastructure as code; runtime ACM Private CA API calls are outside this mapping.
The ACM Private CA hierarchy is re-expressed as a cert-manager CA Issuer backed by a CA in a Kubernetes Secret, chaining to a private root the cluster holds; your platform team operates it. The ACM Private CA hierarchy is re-expressed as a cert-manager CA Issuer backed by a CA in a Kubernetes Secret, chaining to a private root the cluster holds; your platform team operates it.

The ACM Private CA hierarchy is re-expressed as a cert-manager CA Issuer backed by a CA in a Kubernetes Secret, chaining to a private root the cluster holds; your platform team operates it.

#### Architecture The cert-manager controller watches `Certificate` resources, signs them through the referenced CA Issuer, and writes each certificate and private key to the application's Secret. An Issuer is namespace-scoped; a ClusterIssuer is available across namespaces. Protect the CA Secret with access control, encryption, and backups.
A ClusterIssuer references a CA certificate and key in a Kubernetes Secret and issues Certificate resources; each issued key pair lands in its own Secret, chaining to the private root. A ClusterIssuer references a CA certificate and key in a Kubernetes Secret and issues Certificate resources; each issued key pair lands in its own Secret, chaining to the private root.

A ClusterIssuer references a CA certificate and key in a Kubernetes Secret and issues Certificate resources; each issued key pair lands in its own Secret, chaining to the private root.

#### The CA hierarchy A root CA Issuer can sign an intermediate Certificate. A second Issuer uses that intermediate CA to issue application certificates. Each additional tier needs its own Issuer and CA certificate configuration, including validity and rotation procedures.
A root CA Issuer plus an intermediate Certificate that becomes a second Issuer models a subordinate; deep multi-tier hierarchies are assembled by hand, not a managed ROOT / SUBORDINATE type. A root CA Issuer plus an intermediate Certificate that becomes a second Issuer models a subordinate; deep multi-tier hierarchies are assembled by hand, not a managed ROOT / SUBORDINATE type.

A root CA Issuer plus an intermediate Certificate that becomes a second Issuer models a subordinate; deep multi-tier hierarchies are assembled by hand, not a managed ROOT / SUBORDINATE type.

#### Issuing certificates A Certificate specifies its subject, subject alternative names (SANs), key usages, and duration. cert-manager renews leaf certificates before expiry. The CA certificate supplied in the Issuer's Secret has a separate lifecycle: the operator must rotate it and distribute any changed trust chain to clients.
ACM Private CA's CSR-sign-then-activate pair is implicit in the cert-manager CA Issuer; Certificate resources are signed by the issuer and renewed automatically. ACM Private CA's CSR-sign-then-activate pair is implicit in the cert-manager CA Issuer; Certificate resources are signed by the issuer and renewed automatically.

ACM Private CA's CSR-sign-then-activate pair is implicit in the cert-manager CA Issuer; Certificate resources are signed by the issuer and renewed automatically.

#### Issuance scoping Kubernetes RBAC controls access to Certificate and Issuer resources, together with their namespace scope. It does not reproduce the AWS `acm-pca:TemplateArn` IAM condition. Review which callers can request certificates and which issuer references they can use when translating issuance restrictions.
cert-manager scopes issuance with Kubernetes RBAC on Certificate and Issuer objects; there is no acm-pca:TemplateArn analog. cert-manager scopes issuance with Kubernetes RBAC on Certificate and Issuer objects; there is no acm-pca:TemplateArn analog.

cert-manager scopes issuance with Kubernetes RBAC on Certificate and Issuer objects; there is no acm-pca:TemplateArn analog.

#### Revocation and CRLs cert-manager does not generate or publish certificate revocation lists (CRLs) or Online Certificate Status Protocol (OCSP) responses. The CA operator must supply these services if clients require revocation checking. Short certificate lifetimes reduce the remaining validity of a compromised certificate; they do not revoke it before expiry.
The CA operator must provide CRL generation and publication; cert-manager does not manage them. The CA operator must provide CRL generation and publication; cert-manager does not manage them.

The CA operator must provide CRL generation and publication; cert-manager does not manage them.

#### Limitations The platform team operates cert-manager and protects the CA key. This configuration uses a new private root; AWS private keys and issued-certificate history do not migrate. CA rotation, revocation services, and additional hierarchy tiers require operator configuration. #### Other considerations Distribute the new root to client trust stores before switching certificates, while retaining old trust for the transition. Monitor cert-manager and renewal failures, back up the CA Secret, and rehearse CA rotation. If the CA key requires hardware protection, choose and configure a suitable external signer or HSM-backed issuer rather than storing that key directly in a Kubernetes Secret. ## On OCI ### Via OCI Certificate Authority | Operation | Area | Support | Depth | Notes | | ------------------------------------------------------------------------------- | -------------- | -------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Issuance scoping (acm-pca:TemplateArn IAM condition) | Access control | Partial | Most usage | OCI IAM compartment policy on the CA plus the CA's issuance configuration, not a one-to-one template-ARN principal condition | | CSR-signing activation split (aws\_acmpca\_certificate\_authority\_certificate) | CA activation | Adapter-served | Most usage | the CSR-sign-then-install resource pair is implicit in OCI CA creation; there is no separate signing resource to reference | | ACM Private CA control plane (CreateCertificateAuthority / issuance API / …) | Control plane | Out of scope | Full surface | CA provisioning happens through your infrastructure-as-code at apply, not runtime ACM Private CA API calls | | CA hierarchy (ROOT / SUBORDINATE types) | Hierarchy | Supported | Most usage | a root CA plus subordinate CAs mirror ACM Private CA's ROOT / SUBORDINATE types; a two-tier hierarchy maps directly, deeper trees add more subordinate CAs to the compartment | | Certificate issuance (aws\_acmpca\_certificate / IssueCertificate) | Issuance | Supported | Common | leaf certificates are issued from the CA and chain to the private root, with the subject / SANs / key usage / validity; OCI Certificates rotates managed certificates natively | | Private CA provisioning (aws\_acmpca\_certificate\_authority) | Provisioning | Supported | Common | an oci\_certificates\_management\_certificate\_authority root or subordinate CA in a compartment: a managed private CA that chains issued certificates to its own private root | | CRL distribution (customer-owned S3 bucket) | Revocation | Partial | Full surface | OCI Certificates publishes CRLs to a configured Object Storage bucket; translate bucket permissions and the distribution point | #### How it works Tensor9 provisions root and subordinate certificate authorities (CAs) in OCI Certificates. They issue application certificates that chain to a private root controlled by the customer. The deployment uses infrastructure as code; runtime ACM Private CA API calls are outside this mapping.
The ACM Private CA hierarchy is re-expressed as an OCI Certificates CA in a compartment with its own private root, so your PKI keeps issuing certificates that chain to a private root you control. The ACM Private CA hierarchy is re-expressed as an OCI Certificates CA in a compartment with its own private root, so your PKI keeps issuing certificates that chain to a private root you control.

The ACM Private CA hierarchy is re-expressed as an OCI Certificates CA in a compartment with its own private root, so your PKI keeps issuing certificates that chain to a private root you control.

#### Architecture OCI Certificates manages the CA hierarchy in a compartment. Oracle operates the service; OCI IAM controls who may manage a CA or request certificates. Managed leaf certificate renewal follows configured rules. CA renewal is initiated by the customer; rotate the CA vault key before creating the renewed CA version. Client trust stores determine which private roots the application trusts.
A compartment holds a root CA and subordinate CAs; a subordinate issues leaf certificates that chain to the private root, with OCI IAM compartment policy governing issuance. A compartment holds a root CA and subordinate CAs; a subordinate issues leaf certificates that chain to the private root, with OCI IAM compartment policy governing issuance.

A compartment holds a root CA and subordinate CAs; a subordinate issues leaf certificates that chain to the private root, with OCI IAM compartment policy governing issuance.

#### The CA hierarchy A root CA signs subordinate CAs, which issue application certificates. Additional subordinates require their own CA configuration. The mapped hierarchy uses a new root and key; existing AWS certificates continue to use the old root until they expire or are reissued.
ACM Private CA's ROOT and SUBORDINATE types map onto a root CA and subordinate CAs inside the OCI compartment. ACM Private CA's ROOT and SUBORDINATE types map onto a root CA and subordinate CAs inside the OCI compartment.

ACM Private CA's ROOT and SUBORDINATE types map onto a root CA and subordinate CAs inside the OCI compartment.

#### Issuing certificates ACM Private CA separates signing a certificate-signing request (CSR) from installing the resulting CA certificate. For the mapped OCI hierarchy, CA creation includes signing. Application certificates specify the subject, subject alternative names (SANs), key usages, and validity. Configure rotation for certificates managed by OCI.
ACM Private CA's CSR-sign-then-activate resource pair is implicit in OCI CA creation; leaves issue from the CA and rotate natively. ACM Private CA's CSR-sign-then-activate resource pair is implicit in OCI CA creation; leaves issue from the CA and rotate natively.

ACM Private CA's CSR-sign-then-activate resource pair is implicit in OCI CA creation; leaves issue from the CA and rotate natively.

#### Issuance scoping The AWS `acm-pca:TemplateArn` IAM condition restricts which templates a principal can request. OCI combines IAM permissions on the CA with its issuance configuration. Translate both the permitted callers and the permitted certificate content; compartment access alone does not express an AWS template condition.
ACM Private CA's acm-pca:TemplateArn IAM condition maps to OCI IAM compartment policy on the CA, not a one-to-one template-ARN condition. ACM Private CA's acm-pca:TemplateArn IAM condition maps to OCI IAM compartment policy on the CA, not a one-to-one template-ARN condition.

ACM Private CA's acm-pca:TemplateArn IAM condition maps to OCI IAM compartment policy on the CA, not a one-to-one template-ARN condition.

#### Revocation and CRLs OCI Certificates publishes certificate revocation lists (CRLs) to a configured Object Storage bucket. Configure the bucket, object naming, write permissions for the CA, and distribution-point URL clients use. The service produces the CRL, while the customer controls the storage configuration. Replace automation that reads or copies the old S3 bucket with the corresponding OCI configuration.
OCI Certificates publishes CRLs to a configured Object Storage bucket. OCI Certificates publishes CRLs to a configured Object Storage bucket.

OCI Certificates publishes CRLs to a configured Object Storage bucket.

#### Limitations The new root does not inherit the AWS private key or issued-certificate history. AWS template conditions and S3 bucket policies need target-specific configuration. Previously issued certificates remain dependent on the old root and its revocation information until they expire or are replaced. #### Other considerations Distribute the new root before issuing replacement certificates and retain the old trust root for the transition. Confirm key protection and certificate validity settings, configure managed leaf renewal, schedule CA renewal and key rotation, and test CRL publication and client retrieval. Keep the CRL bucket and its access policy available for as long as clients rely on certificates issued by that CA. [Service Catalog](/service-adapters/catalog). # AWS IAM Source: https://docs.tensor9.com/service-adapters/aws/security-identity/aws-iam Holds the users, groups, roles and JSON policies that decide which principal may call which AWS API on which resource. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Google Cloud, Azure, OCI, and Private Kubernetes](#on-google-cloud-azure-oci-and-private-kubernetes) * [Via Cedar](#via-cedar) * [On Google Cloud](#on-google-cloud) * [Via IAM](#via-iam) * [On Azure](#on-azure) * [Via IAM](#via-iam-2) * [On OCI](#on-oci) * [Via OCI IAM](#via-oci-iam) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of IAM with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | IAM | Google Cloud, Azure, OCI, and Private Kubernetes · Cedar | | ------------------------------------------------ | ------------------------------------ | --------------------------------------------------------------- | | Adaptation mechanism | AWS-managed IAM and STS | Max: IAM and STS requests | | Access keys and secret keys | AWS account and IAM user credentials | Appliance-issued logical-account credentials | | Policy language · authoring | IAM JSON | Supported IAM JSON translated to Cedar | | Evaluation model · enforcement | Explicit deny overrides allow | Deny-wins in enforce mode | | Policy consistency · freshness and revocation | AWS-managed propagation | Eventually consistent | | Explicit deny wins | Yes - native | Yes - Cedar decision; enforce mode blocks | | Condition keys | Yes | Partial - Time and selected DNS keys; separate trust conditions | | Permissions boundaries | Yes | No - Not enforced by local Cedar | | Resource-based policies | Yes | No - No general resource-policy evaluation in this IAM adapter | | Cross-account role assumption | Yes | Partial - Configured local accounts and caller authorization | | Organizations SCPs | Yes | No - Not enforced by this authorizer | | Authorization diagnostics · denial investigation | CloudTrail and Access Analyzer | Denial and would-be-denial diagnostics | | API coverage | full | high | ### Infrastructure-only adaptation | Capability | IAM | Google Cloud · IAM | Azure · IAM | OCI · OCI IAM | | -------------------- | ----------------------- | -------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------- | | Adaptation mechanism | AWS-managed IAM and STS | Infrastructure only: native permission translation | Infrastructure only: native permission translation | Infrastructure only: native permission translation | | API coverage | full | partial | partial | partial | ## On Google Cloud, Azure, OCI, and Private Kubernetes ### Via Cedar | Operation | Area | Support | Depth | Notes | | ----------------------------------------------------------------------------------------------------------------------- | ------------------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | CreatePolicy / GetPolicy / ListPolicies / DeletePolicy | Customer-managed policies | Supported | Common | Uses the deployment's customer-managed policy records; the runtime AWS-managed policy catalog is not supplied. | | CreatePolicyVersion / GetPolicyVersion / ListPolicyVersions / SetDefaultPolicyVersion / DeletePolicyVersion | Customer-managed policies | Supported | Most usage | Maintains policy versions and the default version; up to five versions per policy. | | TagPolicy / UntagPolicy / ListPolicyTags | Customer-managed policies | Supported | Most usage | - | | PutRolePolicy / GetRolePolicy / ListRolePolicies / DeleteRolePolicy | Inline role policies | Supported | Common | Validates supported policy syntax; policy changes propagate eventually and may not affect authorization immediately after success. | | AddRoleToInstanceProfile / RemoveRoleFromInstanceProfile | Instance profiles | Supported | Most usage | Maintains the association used by compute workloads to obtain role credentials. | | CreateInstanceProfile / GetInstanceProfile / ListInstanceProfiles / ListInstanceProfilesForRole / DeleteInstanceProfile | Instance profiles | Supported | Most usage | - | | TagInstanceProfile / UntagInstanceProfile / ListInstanceProfileTags | Instance profiles | Supported | Most usage | - | | CreateOpenIDConnectProvider / GetOpenIDConnectProvider / ListOpenIDConnectProviders / DeleteOpenIDConnectProvider | OIDC providers | Supported | Common | Registers the providers used by web-identity federation. | | TagOpenIDConnectProvider / UntagOpenIDConnectProvider / ListOpenIDConnectProviderTags | OIDC providers | Supported | Most usage | - | | UpdateOpenIDConnectProviderThumbprint / AddClientIDToOpenIDConnectProvider / RemoveClientIDFromOpenIDConnectProvider | OIDC providers | Supported | Most usage | - | | Access Analyzer / credential reports / Access Advisor | Out of scope | Out of scope | Full surface | These AWS IAM tools are not implemented by the adapter. | | Permissions boundaries | Out of scope | Out of scope | Full surface | Role creation that attaches a boundary is rejected unless the operator acknowledges it explicitly. AWS IAM evaluates a boundary as an AND with the identity policy, and this models only the identity-policy layer, so honouring the attach silently would widen the role past what was asked for. | | Users / groups / access-key administration / SAML federation | Out of scope | Out of scope | Full surface | These administration and federation operations are not served. | | AttachRolePolicy / DetachRolePolicy / ListAttachedRolePolicies / ListEntitiesForPolicy | Policy attachments | Supported | Common | Attaches customer-managed policies to roles and returns their recorded relationships. | | CreateRole / GetRole / ListRoles / DeleteRole | Roles | Supported | Common | Creates and reads role records; deletion invalidates sessions bound to that role incarnation. | | TagRole / UntagRole / ListRoleTags | Roles | Supported | Most usage | Stores role tags; this does not enable general PrincipalTag policy conditions. | | UpdateAssumeRolePolicy / UpdateRole / UpdateRoleDescription | Roles | Supported | Most usage | Updates role configuration. Trust is evaluated separately from Cedar identity-policy authorization. | | AssumeRole | STS | Supported | Common | Checks role trust and issues expiring credentials. Role chaining, session policies, ExternalId and MFA inputs are unsupported. | | AssumeRoleWithWebIdentity | STS | Supported | Common | Verifies the OIDC token and role trust before issuing a session. Issuer discovery and signing-key retrieval may need network access. | | GetCallerIdentity | STS | Supported | Common | Returns account, ARN and user ID; an assumed-role session has an assumed-role ARN. | | SimulateCustomPolicy | Simulation | Out of scope | Full surface | The IAM administration endpoint does not implement ad-hoc policy simulation. | | SimulatePrincipalPolicy | Simulation | Partial | Most usage | Only the separate workload-identity endpoint supports simulation for its configured principal, with a supplied policy evaluator and concrete resources; not the IAM administration endpoint. | #### Max IAM and STS Max adaptation serves AWS IAM and Security Token Service (STS) requests within each appliance. The adapter supports 50 IAM administration operations across roles, customer-managed policies, instance profiles and OIDC providers, plus `GetCallerIdentity`, `AssumeRole` and `AssumeRoleWithWebIdentity`. Applications keep the supported AWS request formats. Each appliance has its own identities, policies and authorization state. Appliances do not share policies or distribute policy updates to one another. Policy updates reach only the relevant replicas within the same appliance; changing permissions in one appliance does not change permissions in another. Account IDs in adapted resource ARNs identify logical accounts within the appliance. These account records do not create cloud-provider accounts or billing relationships and do not provide AWS Organizations governance. Cedar is the policy engine underpinning the adapter's authorization decisions. Tensor9's adapter handles IAM administration, STS credential exchanges and the translation of supported IAM policies into Cedar rules. Cedar evaluates those rules for requests handled by receiving adapters. Role trust is evaluated separately when a session is created. Native permission translation is a different choice: it converts declared permissions into the target cloud's grants during provisioning and does not itself serve AWS IAM administration calls. This page is about the IAM in **your application's stack**. The permissions **your control plane** needs inside a customer's environment are a separate subject with its own model; see [Permissions Model](/fundamentals/permissions-model) and [Cross-Cloud IAM](/fundamentals/cross-cloud-iam).
IAM administration, STS trust and request authorization have separate responsibilities. IAM administration, STS trust and request authorization have separate responsibilities.

IAM edits, STS trust checks and downstream authorization are separate steps.

#### Access keys and secret keys Max adaptation supports access keys and secret keys on Google Cloud, Azure, OCI and Private Kubernetes. Use the appliance-issued pair supplied by your appliance administrator with your AWS SDK or CLI. Configure `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, the appliance endpoint and its signing region. Supported service requests use AWS Signature Version 4 (SigV4); the receiving adapter verifies the signature and resolves the configured credential's account. This long-term pair identifies a logical account root within the appliance, not an IAM user or a cloud-provider account. Keep the secret when it is issued; it is shown once. It is not valid at AWS endpoints or in another appliance. Native permission translation does not issue this runtime credential. A long-term pair has no session token. Temporary credentials from the supported STS operations identify a role session and also require `AWS_SESSION_TOKEN`; they expire. Credential use does not provide IAM user access-key administration: `CreateAccessKey`, `ListAccessKeys`, `UpdateAccessKey` and `DeleteAccessKey` are unsupported. The combined IAM and STS management endpoint has the separate protected-access boundary described below. #### Authentication and management access Temporary credentials identify a verified role session. Receiving adapters verify the request signature, session validity and account before using that identity for authorization. `GetCallerIdentity` returns the account, ARN and user ID; an assumed-role session has an assumed-role ARN, not simply its role's IAM ARN. Protect the combined IAM and STS management endpoint with authenticated deployment access. Requests without issued session credentials use its configured bootstrap caller; this endpoint is not a general lookup of arbitrary AWS access keys. The separate public web-identity route accepts only `AssumeRoleWithWebIdentity` and verifies the token before issuing credentials. Configure authorization enforcement on receiving adapters. In enforce mode, a denied decision stops the operation and returns the receiving service's error. Report-only mode records would-be denials without blocking requests. Policy propagation does not turn report-only mode into enforcement. #### Policy authoring and runtime changes Keep authoring supported policies in IAM JSON. Declared stack policies and runtime IAM operations are separate ways to supply them. IAM reads return stored role, policy and relationship records, not documents reconstructed from compiled Cedar rules. Runtime policy edits are validated before acceptance. Accepted changes update IAM records and propagate to the policy state used for authorization. Recording a change and observing its effect on a later request are not the same event. Creating a non-default managed-policy version does not change effective permissions. Making it the default changes the version used by attached roles. Inline policies and policy attachments also affect permissions. Role-trust changes instead govern future session creation; they are evaluated separately from Cedar identity policies. IAM `Allow` statements translate to Cedar `permit` rules and `Deny` statements to `forbid` rules. Supported actions, resource ARNs and conditions determine their scope. On the Cedar authorization path, unsupported restrictions are refused rather than silently dropped. Role trust remains a separate IAM-policy evaluation. #### Eventual consistency for policy changes IAM policy changes are eventually consistent, as in AWS IAM. Policy consistency is not customer-configurable. Changes take time to reach reads and authorization checks, including updates made through another replica in the same appliance. A successful IAM response does not guarantee that subsequent reads or authorization checks already reflect the change. New grants may not be usable immediately, and revoked permissions may remain effective until the change propagates. With enforcement enabled, a request is blocked when the policies used for that check deny it; those policies can still be older than the latest accepted edit. Work already authorized can continue. Other appliances keep their own policies. An error or timeout after commit does not imply rollback. The change may have committed even though the caller did not receive success; do not treat a failed response as proof that the old permissions remain in effect.
Within one appliance, policy propagation may continue after a successful IAM response. Verify the dependent operation; one result does not prove every replica is up to date. Within one appliance, policy propagation may continue after a successful IAM response. Verify the dependent operation; one result does not prove every replica is up to date.

Success can precede propagation. A dependent-operation check is not a freshness guarantee for every replica.

#### Propagation and dependent workflows Verify propagation before starting a workflow that depends on the change; a fixed sleep is not proof that propagation has completed. Check the relevant IAM state and the allowed or denied result needed by that workflow. One observed result does not prove that every replica is up to date or that the next request will observe the same policy state. Avoid putting IAM changes in a critical request path that assumes immediate visibility. There is no fixed propagation deadline. An unavailable replica or policy service can delay visibility or make requests fail. Eventual consistency does not guarantee offline availability, and there is no universal rule that permissions become denied after a fixed cache age. Test propagation, failure and recovery across the appliance's replicas with the required enforcement settings. #### Authorization decisions Cedar evaluates the principal, action, resource and supported request context against the selected policies. Any matching `forbid` overrides a `permit`. With no matching permit, the decision is deny. Policy order does not change that decision. These rules describe supported identity-policy authorization. IAM management also has a privileged account-root path, and STS uses separate trust checks. They do not imply that every AWS policy layer or every receiving service is covered.
In enforce mode, a matching deny blocks the request; otherwise a matching allow permits it, and the default is deny. In enforce mode, a matching deny blocks the request; otherwise a matching allow permits it, and the default is deny.

Explicit deny overrides allow; without a matching allow, the decision is deny.

#### Supported conditions Identity policies support `aws:CurrentTime`, `aws:EpochTime`, and three Route 53 change keys: `route53:ChangeResourceRecordSetsNormalizedRecordNames`, `route53:ChangeResourceRecordSetsRecordTypes` and `route53:ChangeResourceRecordSetsActions`. The DNS subset includes supported set, wildcard, negation and existence tests. Accepted operators depend on the key. `aws:SourceIp`, `aws:PrincipalTag`, `aws:SecureTransport` and MFA identity-policy keys are unsupported. A value being available in the deployment does not itself make its condition supported. Stored role tags do not imply general tag-based authorization. Role-trust conditions are separate. OIDC trust supports issuer-qualified audience and subject comparisons using `StringEquals` and `StringLike`. Pod Identity trust supports those operators on its six verified request-tag keys. Neither capability adds general identity-policy condition coverage. #### Role trust and temporary credentials `AssumeRole` evaluates role trust before creating an expiring session. Conditional cross-account assumption is supported between accounts served by the same appliance when caller-side authorization and target-role trust both permit it. This does not provide cross-appliance trust or policy sharing. Account-root delegation and assumption into arbitrary real AWS accounts are outside this scope. `AssumeRoleWithWebIdentity` verifies the token signature, issuer, audience, subject and expiry against a registered OIDC provider and evaluates role trust. The dedicated Pod Identity exchange verifies the agent and pod token, selects the role from its association and issues a session with attested tags. These are real credential exchanges, not static credentials derived from a role name. Configured replicas within one appliance can share session verification. This does not make credentials valid in another appliance. Sessions expire and are tied to the role's identity, so deleting and recreating a role with the same name does not revive its old sessions. Trust changes govern future assumptions; permission changes affect later authorization checks as they propagate. Neither effect is guaranteed to be visible immediately after a successful IAM response. Normal `AssumeRole` does not support role chaining, session policies, arbitrary session tags, ExternalId, MFA, SourceIdentity or ProvidedContexts. The trusted Pod Identity tag path is separate. Neither session issuance nor `GetCallerIdentity` calls AWS STS, but OIDC discovery and signing-key retrieval may require access to the configured issuer. #### Administration and simulation scope Administration includes role updates and tags; customer-managed policy creation, versions and tags; inline role policies and attachments; instance-profile lifecycle, membership and tags; and OIDC-provider lifecycle, audiences, thumbprints and tags. Policy versioning supports up to five versions per policy. AWS-managed policy documents are not supplied by the runtime catalog; use supported customer-managed policies. The IAM administration endpoint does not implement `SimulatePrincipalPolicy` or `SimulateCustomPolicy`. A separate workload-identity endpoint supports restricted principal simulation when configured with a policy evaluator. That simulation uses its configured workload and explicit actions and resource ARNs; it does not accept arbitrary principals or ad-hoc policy documents. A simulated deny may differ from the allowed response in report-only mode. User, group and access-key administration, SAML federation, service-linked roles, Access Analyzer, credential reports and Access Advisor are outside the implemented API scope. #### Diagnostics and deployment Authorization diagnostics record denials and would-be denials, including the principal, action, resource, result, enforcement mode and readiness. They do not record every successful permit or identify the matched policy statements. Policy validation and offline evaluation help test examples; they are not an Access Analyzer equivalent. The IAM and STS endpoints and Cedar evaluation run within each appliance. There is no shared policy distribution mechanism between appliances. Use the target and deployment configuration supported for that environment; portable policy evaluation does not imply identical native identity provisioning on every cloud. AWS adapter permissions and the target cloud's native grants remain separate. Tensor9 operates the adapter and policy propagation within each appliance. Diagnostic destinations and retention follow the appliance's logging configuration. Before cutover, test administration, denied requests, credential renewal, permission-change propagation and outages across that appliance's replicas. A healthy replica count does not prove that a particular policy change has reached every authorization check. #### Limitations △ Limitations * **The policy language is a supported subset.** Identity-policy NotAction, NotResource and NotPrincipal are refused, as are policy variables, question-mark wildcards and unsupported principal forms. Action wildcards are limited to \* or service:\*; resource patterns support broader star matching. Resolve unsupported restrictions before using the Cedar authorization path. * **Permissions boundaries are not enforced by local Cedar.** Role creation rejects a boundary unless explicitly acknowledged. Acknowledgment retains the attribute but does not enforce it; omitting this restriction can broaden permissions. Native AWS enforcement is a separate domain. * **Resource-policy support belongs to the receiving service.** Do not assume that translating an identity policy also supplies resource-policy APIs or AWS resource/identity-policy combination rules. * **Organizations SCPs are not enforced by this authorizer.** Account and organization restrictions are not added by successful IAM administration or token exchange. * **Propagation and enforcement are separate.** New permissions can take time to become usable, and revoked permissions can remain effective during propagation. Report-only mode does not block denied requests even after the updated policy is visible. * **Each appliance is independent.** Policies, policy updates and authorization state are not shared between appliances. Replica-shared credentials are confined to the same appliance and do not grant global AWS authority. ## On Google Cloud ### Via IAM #### Infrastructure only: native permissions Tensor9 translates the roles, policies, and attachments declared by your stack into Google Cloud IAM grants during provisioning. Google Cloud IAM binds predefined or custom roles to principals. Tensor9 maps supported AWS actions to permissions; the generated scope and supported condition mapping can differ. The resulting grants use predefined or custom role permissions and resource scopes. #### Permission differences AWS policies can restrict individual resources and request conditions. This mapping does not preserve every restriction. Mappings that broaden access require explicit acceptance; incompatible restrictions stop translation. Some mappings grant fewer permissions. Review generated actions and scopes before deployment. The Cedar-based adapter separately serves supported AWS IAM calls and evaluates supported AWS identity policies. #### Application and identity changes This mapping provisions grants; it does not serve runtime AWS IAM calls such as `iam:CreateRole` or `iam:AttachRolePolicy`. Replace references to AWS role ARNs in trust policies, assume-role calls, and resource policies with the appropriate Google identity. New target grants are created during deployment; AWS policy objects are not copied. ## On Azure ### Via IAM #### Infrastructure only: native permissions Tensor9 translates the roles, policies, and attachments declared by your stack into Azure RBAC and Entra grants during provisioning. Azure RBAC assigns built-in or custom roles over a scope hierarchy. Tensor9 maps supported actions and uses target assignment scopes; not every AWS resource or condition restriction is preserved. The resulting grants use role-assignment granularity. #### Permission differences AWS policies can restrict individual resources and request conditions. This mapping does not preserve every restriction. Mappings that broaden access require explicit acceptance; incompatible restrictions stop translation. Some mappings grant fewer permissions. Review generated actions and scopes before deployment. The Cedar-based adapter separately serves supported AWS IAM calls and evaluates supported AWS identity policies. #### Application and identity changes This mapping provisions grants; it does not serve runtime AWS IAM calls such as `iam:CreateRole` or `iam:AttachRolePolicy`. Replace references to AWS role ARNs in trust policies, assume-role calls, and resource policies with the appropriate Azure identity. New target grants are created during deployment; AWS policy objects are not copied. ## On OCI ### Via OCI IAM #### Infrastructure only: native permissions Tensor9 translates the roles, policies, and attachments declared by your stack into OCI IAM grants during provisioning. OCI IAM uses policy statements scoped to compartments and groups. This mapping uses OCI verbs and resource families, which can group AWS actions or resources differently. The resulting grants use group and compartment granularity. #### Permission differences AWS policies can restrict individual resources and request conditions. This mapping does not preserve every restriction. Mappings that broaden access require explicit acceptance; incompatible restrictions stop translation. Some mappings grant fewer permissions. Review generated actions and scopes before deployment. The Cedar-based adapter separately serves supported AWS IAM calls and evaluates supported AWS identity policies. #### Application and identity changes This mapping provisions grants; it does not serve runtime AWS IAM calls such as `iam:CreateRole` or `iam:AttachRolePolicy`. Replace references to AWS role ARNs in trust policies, assume-role calls, and resource policies with the appropriate OCI identity. New target grants are created during deployment; AWS policy objects are not copied. [Service Catalog](/service-adapters/catalog). # KMS Source: https://docs.tensor9.com/service-adapters/aws/security-identity/kms AWS KMS. Creates and stores encryption keys, and performs encrypt and decrypt calls without releasing the key material. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of KMS with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | KMS | Google Cloud | Azure | OCI | Private Kubernetes | | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | Envelope encryption (GenerateDataKey) · a plaintext data key plus its wrapped copy | Yes - GenerateDataKey returns a plaintext data key and its wrapped copy in one call | Partial - synthesized: a random data key wrapped by the key's encrypt. There is no single native data-key op, but the envelope round-trips | Partial - served as RSA wrap-key: a random data key wrapped by an RSA key. The pair round-trips, but the wrapping key is RSA, not a symmetric AES key | Yes - native GenerateDataEncryptionKey: one call returns the plaintext data key and its wrapped copy, one data-key operation | Yes - native Transit datakey endpoint: one call returns the plaintext data key and its wrapped copy, one data-key operation | | Encryption context (AAD) · additional authenticated data bound into the ciphertext | Yes - an encryption-context map bound as AAD; the same map is required to decrypt | Yes - bound natively as Cloud KMS additional authenticated data | No - RSA-OAEP binds no additional data, so the encryption context cannot bind on a standard vault; Managed HSM AES-GCM would provide it | Yes - native OCI associated data | Yes - native Transit associated data | | Symmetric AES · a symmetric encrypt/decrypt key | Yes - a symmetric AES customer master key | Yes - a symmetric encrypt/decrypt crypto key | No - a standard Key Vault holds RSA/EC keys only, so a symmetric AES key has no equivalent here; Managed HSM would provide it | Yes - a native AES key shape (software or HSM protection mode) | Yes - a native aes256-gcm96 Transit key | | Asymmetric sign / verify · RSA / EC signing keys | Yes - asymmetric Sign / Verify with RSA or EC keys | Yes - asymmetric-sign, with verify served against the exported public key | Yes - native Key Vault sign / verify on RSA or EC keys | Yes - native OCI Sign / Verify on RSA or ECDSA keys | Yes - native Transit sign / verify on ed25519, ECDSA, or RSA keys | | HMAC (MAC sign / verify) · a keyed message authentication code | Yes - GenerateMac / VerifyMac with an HMAC key | Yes - native Cloud KMS mac-sign / mac-verify | No - no HMAC keys in this Standard mapping; other Azure configurations differ | No - OCI KMS has no HMAC key type; its shapes are AES, RSA, and ECDSA | Yes - native Transit HMAC endpoint, the only self-hosted target of the four with a native HMAC | | Key rotation · automatic and on-demand rotation | Yes - automatic and on-demand key rotation | Yes - the crypto key's rotation period, plus an on-demand new version | Yes - the key's rotation policy, plus an on-demand rotate | Yes - the key's auto-rotation interval, plus an on-demand new key version | Yes - the key's auto-rotate period, plus a native on-demand rotate endpoint | | Aliases · a mutable name pointing at a key | Yes - a mutable alias pointing at a key id | Yes - the adapter maintains the alias-to-key mapping, since Cloud KMS has no native alias object | Yes - the adapter maintains the alias-to-key mapping, since Key Vault has no separate alias object | Yes - the adapter maintains the alias-to-key mapping, since OCI has no separate alias object | Yes - the adapter maintains the alias-to-key mapping, since a Transit key has no separate alias object | | API coverage | full | high | partial | high | high | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | ----------------------------------- | --------------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | CreateAlias | Aliases | Supported | Most usage | the adapter maintains the alias-to-key mapping and resolves alias references, since Cloud KMS has no native alias object | | DeleteAlias | Aliases | Supported | Full surface | removes an alias from the mapping | | ListAliases | Aliases | Supported | Most usage | lists aliases retained in adapter state | | UpdateAlias | Aliases | Supported | Full surface | changes the key referenced by an alias in adapter state | | DeriveSharedSecret | Asymmetric | Out of scope | Full surface | elliptic-curve shared-secret derivation has no Cloud KMS equivalent op and is rejected | | GetPublicKey | Asymmetric | Supported | Most usage | exports the public half of an asymmetric key | | Sign | Asymmetric | Supported | Most usage | signs a message or digest with an asymmetric key via Cloud KMS asymmetric-sign | | Verify | Asymmetric | Supported | Most usage | served by verifying the signature against the key's exported public key, so a verify does not require an extra native op | | GenerateDataKey | Envelope / data keys | Partial | Common | Cloud KMS has no single data-key op, so the adapter synthesizes it: a fresh random data key is generated, wrapped by the key's encrypt, and returned alongside that wrapped copy | | GenerateDataKeyPair | Envelope / data keys | Partial | Most usage | an asymmetric data-key pair is generated and its private half wrapped by the key; there is no native data-key-pair op | | GenerateDataKeyPairWithoutPlaintext | Envelope / data keys | Partial | Full surface | the same, returning only the wrapped private half | | GenerateDataKeyWithoutPlaintext | Envelope / data keys | Partial | Most usage | the same synthesized path, returning only the wrapped data key for the deferred-decrypt pattern | | CreateGrant | Grants | Out of scope | Most usage | authorization is governed by Cloud IAM bindings on the key rather than KMS grants, so a grant is rejected | | ListGrants | Grants | Out of scope | Most usage | there is no grant to list; access is a Cloud IAM binding | | ListRetirableGrants | Grants | Out of scope | Full surface | there is no grant to list | | RetireGrant | Grants | Out of scope | Full surface | there is no grant to retire | | RevokeGrant | Grants | Out of scope | Full surface | there is no grant to revoke | | GenerateMac | HMAC | Supported | Most usage | computes an HMAC with a MAC key via Cloud KMS mac-sign | | VerifyMac | HMAC | Supported | Most usage | verifies an HMAC via Cloud KMS mac-verify | | DeleteImportedKeyMaterial | Imported key material | Out of scope | Full surface | there is no runtime imported-material lifecycle to drive | | GetParametersForImport | Imported key material | Out of scope | Full surface | imported key material is loaded through Cloud KMS's own import job at provisioning, not through the native import handshake, so the runtime import ops are rejected | | ImportKeyMaterial | Imported key material | Out of scope | Full surface | the import handshake is not served at runtime; material is imported at provisioning | | CancelKeyDeletion | Key management | Supported | Full surface | cancels the adapter recovery window and leaves the key disabled until EnableKey | | CreateKey | Key management | Supported | Common | the equivalent Cloud KMS key is created with the matching purpose and algorithm; the key usage and material spec translate to the crypto-key purpose and version algorithm | | DescribeKey | Key management | Supported | Common | returns AWS key identity and lifecycle metadata from adapter state, with the configured target key purpose and algorithm | | DisableKey | Key management | Supported | Most usage | moves the primary key version to disabled | | EnableKey | Key management | Supported | Most usage | moves the primary key version to enabled | | ListKeys | Key management | Supported | Common | lists the keys in the key ring | | ScheduleKeyDeletion | Key management | Supported | Most usage | records a 7-30-day recovery window in adapter state; final target destruction follows Cloud KMS retention rules | | UpdateKeyDescription | Key management | Supported | Full surface | updates the human label stored on the crypto key | | GetKeyPolicy | Key policy | Out of scope | Most usage | there is no per-key policy document to read | | ListKeyPolicies | Key policy | Out of scope | Full surface | there is no per-key policy document to list | | PutKeyPolicy | Key policy | Out of scope | Most usage | access is governed by Cloud IAM rather than a per-key policy document, so a per-key policy is rejected | | ReplicateKey | Multi-region | Out of scope | Full surface | Cloud KMS locality is the key ring's location; there is no shared-key-material multi-region replica, so a replica request is rejected | | UpdatePrimaryRegion | Multi-region | Out of scope | Full surface | there is no multi-region key set whose primary can be moved | | GenerateRandom | Random | Supported | Full surface | returns cryptographic random bytes from Cloud KMS; no key is involved | | DisableKeyRotation | Rotation | Supported | Full surface | clears the rotation period | | EnableKeyRotation | Rotation | Supported | Most usage | turns on automatic rotation via the crypto key's rotation period | | GetKeyRotationStatus | Rotation | Supported | Most usage | reports whether rotation is on and the configured period | | RotateKeyOnDemand | Rotation | Supported | Full surface | creates a new key version on demand and promotes it to primary | | Decrypt | Symmetric crypto | Supported | Common | decrypts and re-checks the encryption context; a mismatched context fails the decrypt, exactly as it does natively | | Encrypt | Symmetric crypto | Supported | Common | encrypts under a symmetric key; the encryption context binds as Cloud KMS additional authenticated data, so a matching context is required to decrypt | | ReEncrypt | Symmetric crypto | Partial | Most usage | there is no single re-encrypt op; the adapter serves it as a decrypt-then-encrypt across the two keys, and the plaintext is never returned to the caller | | ListResourceTags | Tags | Supported | Full surface | lists source tags from adapter state | | TagResource | Tags | Supported | Full surface | retains source tags in adapter state; provider labels use their own format | | UntagResource | Tags | Supported | Full surface | removes source tags from adapter state | #### How it works Tensor9 runs a KMS adapter in the customer environment and sets `AWS_ENDPOINT_URL_KMS` to its loopback endpoint. The adapter accepts the application's AWS SDK calls, including `Encrypt`, `Decrypt`, and `GenerateDataKey`, and translates them to Google Cloud KMS. The service retains the wrapping key; data-key operations can return a plaintext data key for the application to use. Cloud KMS groups versioned keys in location-specific key rings. It supports symmetric encryption, authenticated context, asymmetric signing, HMAC, and rotation. The adapter constructs `GenerateDataKey` using a locally generated random key and a Cloud KMS encrypt call. KMS key-policy and grant APIs have the restrictions listed below.
The adapter translates AWS KMS requests to Google Cloud KMS. The adapter translates AWS KMS requests to Google Cloud KMS.

The adapter translates AWS KMS requests to Google Cloud KMS.

#### Key identity and lifecycle The adapter keeps durable records for AWS key identities, aliases, tags, and lifecycle state. Key creation provisions the corresponding target key. Later requests resolve the AWS identity to that key; the target service performs cryptographic operations and retains its wrapping key material. An alias is a mutable reference in adapter state. Updating it changes which key subsequent alias-based requests select; it does not move key material or rewrite existing ciphertext. Back up adapter state and retain the target keys and versions needed to decrypt stored data. Scheduled deletion records a recovery window of 7-30 days, with 30 days as the default. Cancelling the request returns the key to a disabled state; enable it explicitly before using it again. Final target destruction follows the target's retention rules, which may delay physical removal beyond the adapter's recovery window. #### The key ring and its crypto keys A Cloud KMS key belongs to a key ring in a location. Each key has immutable versions, with a primary version for symmetric encryption. The adapter resolves AWS key IDs to `google_kms_crypto_key` resources and maintains `alias/…` mappings because Cloud KMS has no native alias object. The key ring's location controls where cryptographic operations run, for example `us-east1` or the `us` multi-region. Symmetric encryption uses the primary version; decryption uses the version recorded in the ciphertext. Signing, MAC, and public-key export operations address a specific version.
A location-specific key ring holds versioned keys. Symmetric encryption uses the primary version. A location-specific key ring holds versioned keys. Symmetric encryption uses the primary version.

A location-specific key ring holds versioned keys. Symmetric encryption uses the primary version.

#### One purpose per key: symmetric, asymmetric, and MAC `CreateKey` translates AWS key usage and specification into a Cloud KMS `purpose` and `version_template.algorithm`. Symmetric `ENCRYPT_DECRYPT` uses `GOOGLE_SYMMETRIC_ENCRYPTION`; signing uses `ASYMMETRIC_SIGN` with an RSA or EC algorithm; HMAC uses `MAC`. Purpose is fixed at creation. Symmetric keys use `cryptoKeys.encrypt` and `decrypt`. Signing uses `asymmetricSign`; verification checks the signature against the public key returned by `getPublicKey`. HMAC uses `macSign` and `macVerify`. A request incompatible with the key's purpose returns an error.
Key purpose selects encryption, signing, or MAC operations and their algorithms. Key purpose selects encryption, signing, or MAC operations and their algorithms.

Key purpose selects encryption, signing, or MAC operations and their algorithms.

#### How GenerateDataKey works `GenerateDataKey` returns a plaintext data key and an encrypted (wrapped) copy. The application uses the plaintext key to encrypt its data locally, then discards that key and stores the wrapped copy beside the encrypted data. To read the data later, it asks the key service to decrypt the wrapped key. The adapter generates a random data key of the requested size and calls `cryptoKeys.encrypt` to wrap it. It returns the AWS-shaped `{Plaintext, CiphertextBlob}` pair and uses `cryptoKeys.decrypt` to unwrap it later. Both calls include the encryption context as AAD. The adapter includes key identity with Cloud KMS ciphertext so Decrypt can locate the key. Applications must treat this blob as opaque.
The adapter generates a random data key and wraps it with cryptoKeys.encrypt. The adapter generates a random data key and wraps it with cryptoKeys.encrypt.

The adapter generates a random data key and wraps it with cryptoKeys.encrypt .

#### Encryption context and authenticated data An encryption context is a non-secret key/value map bound to ciphertext as additional authenticated data (AAD). Decryption requires the same context. Applications can use it to bind encrypted values to a tenant or purpose. Cloud KMS accepts up to 64 KiB of `additionalAuthenticatedData`. The adapter serializes the context map into a stable byte encoding so map insertion order does not affect decryption. It supplies the same encoding for direct encryption and data-key wrapping. A different context causes decryption to fail.
The adapter encodes the encryption context as additionalAuthenticatedData. Decryption requires the same context. The adapter encodes the encryption context as additionalAuthenticatedData. Decryption requires the same context.

The adapter encodes the encryption context as additionalAuthenticatedData . Decryption requires the same context.

#### Rotation without breaking old ciphertexts Key rotation creates a new version for subsequent encryption. Retained, enabled older versions remain available to decrypt existing ciphertext. `rotation_period` schedules new key versions and promotes each to `primary`. `RotateKeyOnDemand` creates and promotes a version immediately; `GetKeyRotationStatus` reports whether a rotation period is configured. Previous enabled versions remain usable until disabled or destroyed.
Rotation creates a primary version for new encryption. Earlier enabled versions can still decrypt existing data. Rotation creates a primary version for new encryption. Earlier enabled versions can still decrypt existing data.

Rotation creates a primary version for new encryption. Earlier enabled versions can still decrypt existing data.

#### Limitations **Access policies and grants.** The mapping uses Google IAM for target-key access and does not reproduce AWS key-policy or grant APIs. Constrained grants are rejected; encryption-context authentication alone does not replace a grant that limits permitted callers or contexts. **Ciphertext format.** The adapter wraps Cloud KMS ciphertext with the key identity needed for a later Decrypt request. Treat CiphertextBlob as opaque. Existing AWS ciphertext is not directly decryptable with a new Google key. **Import and regions.** Import material through the configured Cloud KMS import job. AWS runtime import-handshake APIs are outside this mapping. A key-ring location does not provide independently addressable AWS multi-region replica keys. **Composed operations.** GenerateDataKey generates a random key and wraps it with Cloud KMS. ReEncrypt decrypts and encrypts under the destination key; plaintext exists temporarily in the adapter but is not returned to the caller. DeriveSharedSecret is outside this mapping. #### Other considerations **Configure access and protection.** Grant the deployment identity the required Cloud KMS operations at the appropriate key or key-ring scope. Workload identity supplies credentials. Google operates the key service; choose software or HSM protection for the required keys. **Keep data recoverable.** Retain old key versions while ciphertext requires them. Preserve adapter identity and alias records alongside the target-key inventory. Test disable, cancellation, rotation, and recovery before changing production key state. **Migrate ciphertext.** Use the old AWS key to decrypt ciphertext or unwrap data keys, then encrypt or wrap under the target key. Keep AWS available until migration and target decryption are verified. Provisioning a key does not transform stored ciphertext. ## On Azure | Operation | Area | Support | Depth | Notes | | ----------------------------------- | --------------------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | CreateAlias | Aliases | Supported | Most usage | the adapter maintains the alias-to-key mapping and resolves alias references, since Key Vault addresses a key by name with no separate alias object | | DeleteAlias | Aliases | Supported | Full surface | removes an alias from the mapping | | ListAliases | Aliases | Supported | Most usage | lists aliases retained in adapter state | | UpdateAlias | Aliases | Supported | Full surface | changes the key referenced by an alias in adapter state | | DeriveSharedSecret | Asymmetric | Out of scope | Full surface | elliptic-curve shared-secret derivation has no Key Vault equivalent op and is rejected | | GetPublicKey | Asymmetric | Supported | Most usage | returns the public half of the RSA or EC key | | Sign | Asymmetric | Supported | Most usage | signs natively with an RSA or EC key via Key Vault sign | | Verify | Asymmetric | Supported | Most usage | verifies natively via Key Vault verify | | GenerateDataKey | Envelope / data keys | Partial | Common | the envelope is served as wrap-key: a fresh random data key is generated and wrapped by an RSA key (RSA-OAEP); the returned pair round-trips, but the wrapping key is RSA, not a symmetric AES key | | GenerateDataKeyPair | Envelope / data keys | Partial | Most usage | an asymmetric data-key pair is generated and its private half wrapped by an RSA key | | GenerateDataKeyPairWithoutPlaintext | Envelope / data keys | Partial | Full surface | the same, returning only the wrapped private half | | GenerateDataKeyWithoutPlaintext | Envelope / data keys | Partial | Most usage | the same wrap-key path, returning only the wrapped data key | | CreateGrant | Grants | Out of scope | Most usage | authorization is governed by Azure role assignments rather than KMS grants, so a grant is rejected | | ListGrants | Grants | Out of scope | Most usage | there is no grant to list; access is an Azure role assignment | | ListRetirableGrants | Grants | Out of scope | Full surface | there is no grant to list | | RetireGrant | Grants | Out of scope | Full surface | there is no grant to retire | | RevokeGrant | Grants | Out of scope | Full surface | there is no grant to revoke | | GenerateMac | HMAC | Out of scope | Most usage | Key Vault Standard has no HMAC keys; Managed HSM and Premium oct-HSM preview use separate configurations | | VerifyMac | HMAC | Out of scope | Most usage | there is no HMAC key on a standard vault to verify against | | DeleteImportedKeyMaterial | Imported key material | Out of scope | Full surface | there is no runtime imported-material lifecycle to drive | | GetParametersForImport | Imported key material | Out of scope | Full surface | imported key material is loaded through Key Vault's own key-import flow at provisioning, not through the native import handshake, so the runtime import ops are rejected | | ImportKeyMaterial | Imported key material | Out of scope | Full surface | the import handshake is not served at runtime; material is imported at provisioning | | CancelKeyDeletion | Key management | Supported | Full surface | cancels pending deletion in adapter state; native recovery also applies if target deletion has begun | | CreateKey | Key management | Partial | Common | an RSA or EC key is created natively; symmetric AES keys require a separate Azure configuration and are outside this Standard mapping | | DescribeKey | Key management | Supported | Common | returns the key metadata: identifier, type, size or curve, operations, and enabled/validity attributes | | DisableKey | Key management | Supported | Most usage | clears the key's enabled attribute | | EnableKey | Key management | Supported | Most usage | sets the key's enabled attribute | | ListKeys | Key management | Supported | Common | lists the keys in the vault | | ScheduleKeyDeletion | Key management | Partial | Most usage | records the adapter recovery window; vault retention and purge protection govern final target removal | | UpdateKeyDescription | Key management | Partial | Full surface | a Key Vault key has no description field, so the label is stored as a tag | | GetKeyPolicy | Key policy | Out of scope | Most usage | there is no per-key policy document to read | | ListKeyPolicies | Key policy | Out of scope | Full surface | there is no per-key policy document to list | | PutKeyPolicy | Key policy | Out of scope | Most usage | access is governed by Azure role assignments rather than a per-key policy document, so a per-key policy is rejected | | ReplicateKey | Multi-region | Out of scope | Full surface | a key lives in a single vault and region; cross-region key replication is rejected | | UpdatePrimaryRegion | Multi-region | Out of scope | Full surface | there is no multi-region key set whose primary can be moved | | GenerateRandom | Random | Supported | Full surface | cryptographic random bytes are generated by the adapter; no key is touched | | DisableKeyRotation | Rotation | Supported | Full surface | clears the rotation policy | | EnableKeyRotation | Rotation | Supported | Most usage | turns on rotation via the key's rotation policy | | GetKeyRotationStatus | Rotation | Supported | Most usage | reads the key's rotation policy | | RotateKeyOnDemand | Rotation | Supported | Full surface | rotates the key on demand, creating a new version | | Decrypt | Symmetric crypto | Partial | Common | served as RSA-OAEP decrypt on the RSA key | | Encrypt | Symmetric crypto | Partial | Common | a standard Key Vault holds RSA/EC keys only, so encrypt is served as RSA-OAEP on an RSA key rather than symmetric AES; the payload is bounded by the RSA modulus, and symmetric AES uses a separate Azure configuration | | ReEncrypt | Symmetric crypto | Partial | Most usage | served as a decrypt-then-encrypt across the two RSA keys under the adapter; the plaintext is never returned to the caller | | ListResourceTags | Tags | Supported | Full surface | lists the tags on the key | | TagResource | Tags | Supported | Full surface | attaches key/value tags to the key | | UntagResource | Tags | Supported | Full surface | removes tags | #### How it works Tensor9 runs a KMS adapter in the customer environment and sets `AWS_ENDPOINT_URL_KMS` to its loopback endpoint. The adapter accepts the application's AWS SDK calls, including `Encrypt`, `Decrypt`, and `GenerateDataKey`, and translates them to Azure Key Vault. The service retains the wrapping key; data-key operations can return a plaintext data key for the application to use. A standard Key Vault supports RSA and EC keys. It handles asymmetric signing and RSA encryption, but has no symmetric AES or HMAC key type. The adapter implements data-key generation with RSA wrapping. Requests with an encryption context are rejected because this RSA path cannot bind AAD. Azure Managed HSM is a separate symmetric-key service. Key Vault Premium also offers oct-HSM AES/HMAC in public preview. Neither is part of this Standard RSA/EC mapping.
The adapter translates AWS KMS requests to an Azure Key Vault key. The adapter translates AWS KMS requests to an Azure Key Vault key.

The adapter translates AWS KMS requests to an Azure Key Vault key.

#### Key identity and lifecycle The adapter keeps durable records for AWS key identities, aliases, tags, and lifecycle state. Key creation provisions the corresponding target key. Later requests resolve the AWS identity to that key; the target service performs cryptographic operations and retains its wrapping key material. An alias is a mutable reference in adapter state. Updating it changes which key subsequent alias-based requests select; it does not move key material or rewrite existing ciphertext. Back up adapter state and retain the target keys and versions needed to decrypt stored data. Scheduled deletion records a recovery window of 7-30 days, with 30 days as the default. Cancelling the request returns the key to a disabled state; enable it explicitly before using it again. Final target destruction follows the target's retention rules, which may delay physical removal beyond the adapter's recovery window. #### RSA and EC keys The adapter uses an `azurerm_key_vault_key`. `CreateKey` selects an RSA key size or EC curve through `key_type`, `key_size`, and `curve`. The `key_opts` list restricts the key to operations such as `encrypt`, `decrypt`, `wrapKey`, `unwrapKey`, `sign`, and `verify`. Requests for symmetric AES keys are rejected. `DescribeKey` uses Get Key to report identifier, type, size or curve, permitted operations, and validity. `EnableKey` and `DisableKey` update the enabled attribute. The adapter stores the AWS key description as a tag and maintains `alias/…` mappings. Key Vault retains immutable key versions.
key_opts restricts operations on the RSA or EC key. Standard vaults do not support symmetric AES keys. key_opts restricts operations on the RSA or EC key. Standard vaults do not support symmetric AES keys.

key\_opts restricts operations on the RSA or EC key. Standard vaults do not support symmetric AES keys.

#### Envelope encryption via wrapKey / unwrapKey `GenerateDataKey` returns a plaintext data key and an encrypted (wrapped) copy. The application uses the plaintext key to encrypt its data locally, then discards that key and stores the wrapped copy beside the encrypted data. To read the data later, it asks the key service to decrypt the wrapped key. The adapter generates a random data key, wraps it through RSA-OAEP `wrapKey`, and returns plaintext and encrypted copies. `unwrapKey` recovers it later. This uses an RSA wrapping key; it does not provide a symmetric AES KMS key.
The adapter generates a data key, uses RSA wrapKey to encrypt it, and uses unwrapKey to recover it. The adapter generates a data key, uses RSA wrapKey to encrypt it, and uses unwrapKey to recover it.

The adapter generates a data key, uses RSA wrapKey to encrypt it, and uses unwrapKey to recover it.

#### Encryption-context limitations An encryption context is a non-secret key/value map bound to ciphertext as additional authenticated data (AAD). Decryption requires the same context. Applications can use it to bind encrypted values to a tenant or purpose. The standard-vault RSA-OAEP path has no AAD parameter for an AWS encryption context. Context-bearing requests return errors. Applications that require context binding need a supported AES-GCM service, such as the separate Azure Managed HSM service.
The standard-vault RSA path cannot bind an encryption context, so context-bearing requests are rejected. The standard-vault RSA path cannot bind an encryption context, so context-bearing requests are rejected.

The standard-vault RSA path cannot bind an encryption context, so context-bearing requests are rejected.

#### Supported key types AWS KMS distinguishes symmetric encryption keys, asymmetric RSA or EC keys, and HMAC keys. Standard Azure Key Vault supports the asymmetric family. Its RSA key size limits the amount of data that can be encrypted in one operation. `Sign`, `Verify`, `GetPublicKey`, and RSA `Encrypt`/`Decrypt` use Key Vault operations. Data keys use RSA wrapping. Symmetric AES key creation, encryption-context binding, and `GenerateMac`/`VerifyMac` are unsupported on this standard-vault path.
Key Vault Standard supports RSA and EC keys. Symmetric-key options use other Azure configurations. Key Vault Standard supports RSA and EC keys. Symmetric-key options use other Azure configurations.

Key Vault Standard supports RSA and EC keys. Symmetric-key options use other Azure configurations.

#### Rotation via a key rotation policy Key rotation creates a new version for subsequent encryption. Retained, enabled older versions remain available to decrypt existing ciphertext. The key's rotation policy sets `expire_after` and a `time_after_creation` or `time_before_expiry` trigger. `EnableKeyRotation` configures it, `GetKeyRotationStatus` reads it, and `RotateKeyOnDemand` creates a new version immediately. Previous usable versions remain available for decryption. Removing access to an old key prevents decryption of data encrypted under it.
Rotation creates a key version while retaining older versions for existing ciphertext. Rotation creates a key version while retaining older versions for existing ciphertext.

Rotation creates a key version while retaining older versions for existing ciphertext.

#### Limitations **Standard-vault algorithms.** This mapping uses RSA and EC keys in Key Vault Standard. RSA encryption has a payload limit determined by its modulus and padding. Data-key generation uses RSA wrapping; the wrapping key is not a symmetric AES key. **Authenticated context and HMAC.** The mapped RSA-OAEP operations cannot authenticate an AWS encryption context and reject context-bearing requests. Standard vaults have no HMAC key type. Azure Managed HSM supports symmetric keys; Key Vault Premium also offers oct-HSM AES/HMAC in public preview, outside this Standard mapping. **Authorization and import.** AWS grant and key-policy APIs are not recreated. Use Azure role assignments for target access. Imported material uses the provider key-import procedure; the AWS runtime import handshake is outside this mapping. **Regions and retention.** This mapping does not create shared-material AWS multi-region replica keys. The adapter recovery window and the vault's native soft-delete retention are distinct; native retention and purge protection can delay final removal. #### Other considerations **Choose key protection.** Key Vault Premium can protect RSA and EC keys with an HSM; Managed HSM is a separate service. The Standard RSA/EC profile described here does not claim their additional symmetric-key capabilities. **Grant access.** Use the deployment's Azure identity and narrowly scoped role assignments. Separate permission to manage keys from permission to decrypt or unwrap data keys. **Retain and migrate.** Keep old key versions available while ciphertext requires them. Decrypt or unwrap AWS values with the old key and encrypt or wrap under the new key before retiring AWS access. Test context requirements early: this RSA mapping cannot preserve them. ## On OCI | Operation | Area | Support | Depth | Notes | | ----------------------------------- | --------------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | CreateAlias | Aliases | Supported | Most usage | the adapter maintains the alias-to-key mapping and resolves alias references, since OCI has no separate alias object | | DeleteAlias | Aliases | Supported | Full surface | removes an alias from the mapping | | ListAliases | Aliases | Supported | Most usage | lists aliases retained in adapter state | | UpdateAlias | Aliases | Supported | Full surface | changes the key referenced by an alias in adapter state | | DeriveSharedSecret | Asymmetric | Out of scope | Full surface | elliptic-curve shared-secret derivation has no OCI KMS equivalent op and is rejected | | GetPublicKey | Asymmetric | Supported | Most usage | returns the public half from the key version | | Sign | Asymmetric | Supported | Most usage | signs a message or digest with an RSA or ECDSA key via OCI Sign | | Verify | Asymmetric | Supported | Most usage | verifies natively via OCI Verify | | GenerateDataKey | Envelope / data keys | Supported | Common | served natively by OCI's GenerateDataEncryptionKey with the plaintext key included: one call returns the plaintext data key alongside its wrapped copy, one data-key operation | | GenerateDataKeyPair | Envelope / data keys | Partial | Most usage | the native data-key op is symmetric; an asymmetric data-key pair is generated and its private half wrapped by the key, with no native data-key-pair op | | GenerateDataKeyPairWithoutPlaintext | Envelope / data keys | Partial | Full surface | the same, returning only the wrapped private half | | GenerateDataKeyWithoutPlaintext | Envelope / data keys | Supported | Most usage | the same native op with the plaintext key excluded, returning only the wrapped data key | | CreateGrant | Grants | Out of scope | Most usage | authorization is governed by OCI IAM policies on the compartment rather than KMS grants, so a grant is rejected | | ListGrants | Grants | Out of scope | Most usage | there is no grant to list; access is an OCI IAM policy | | ListRetirableGrants | Grants | Out of scope | Full surface | there is no grant to list | | RetireGrant | Grants | Out of scope | Full surface | there is no grant to retire | | RevokeGrant | Grants | Out of scope | Full surface | there is no grant to revoke | | GenerateMac | HMAC | Out of scope | Most usage | OCI KMS has no HMAC key type (its key shapes are AES, RSA, and ECDSA), so a MAC request is rejected | | VerifyMac | HMAC | Out of scope | Most usage | there is no HMAC key to verify against | | DeleteImportedKeyMaterial | Imported key material | Out of scope | Full surface | there is no runtime imported-material lifecycle to drive | | GetParametersForImport | Imported key material | Out of scope | Full surface | material is imported through OCI wrapping-key and key-import operations at provisioning; EXTERNAL key-manager references are separate | | ImportKeyMaterial | Imported key material | Out of scope | Full surface | the AWS runtime import handshake is outside this mapping; material uses the OCI import workflow at provisioning | | CancelKeyDeletion | Key management | Supported | Full surface | cancels the scheduled deletion and leaves the adapter key disabled until EnableKey | | CreateKey | Key management | Supported | Common | the equivalent OCI key is created with the matching key shape (AES, RSA, or ECDSA) and protection mode; the key usage and material spec translate to the shape's algorithm, length, and curve | | DescribeKey | Key management | Supported | Common | reads the key metadata off the key: identifier, shape, protection mode, state, and rotation | | DisableKey | Key management | Supported | Most usage | sets the key's desired state to disabled | | EnableKey | Key management | Supported | Most usage | sets the key's desired state to enabled | | ListKeys | Key management | Supported | Common | lists the keys in the vault | | ScheduleKeyDeletion | Key management | Partial | Most usage | schedules deletion at an absolute time-of-deletion within the same 7-30-day window as AWS KMS, rather than a relative pending-window duration | | UpdateKeyDescription | Key management | Supported | Full surface | updates the key's display name | | GetKeyPolicy | Key policy | Out of scope | Most usage | there is no per-key policy document to read | | ListKeyPolicies | Key policy | Out of scope | Full surface | there is no per-key policy document to list | | PutKeyPolicy | Key policy | Out of scope | Most usage | access is governed by OCI IAM rather than a per-key policy document, so a per-key policy is rejected | | ReplicateKey | Multi-region | Partial | Full surface | multi-region is served by vault replication, which replicates the whole vault rather than a single key, so the scope differs from a per-key replica | | UpdatePrimaryRegion | Multi-region | Out of scope | Full surface | replication is whole-vault, so there is no per-key primary region to move | | GenerateRandom | Random | Supported | Full surface | cryptographic random bytes are generated by the adapter; no key is touched | | DisableKeyRotation | Rotation | Supported | Full surface | turns off automatic rotation | | EnableKeyRotation | Rotation | Supported | Most usage | turns on automatic rotation via the key's auto-rotation setting | | GetKeyRotationStatus | Rotation | Supported | Most usage | reports whether rotation is on and the configured interval | | RotateKeyOnDemand | Rotation | Supported | Full surface | rotates the key on demand, creating a new key version | | Decrypt | Symmetric crypto | Supported | Common | decrypts and re-checks the associated data; a mismatch fails the decrypt, exactly as it does natively | | Encrypt | Symmetric crypto | Supported | Common | encrypts under a symmetric AES key via the vault's crypto endpoint; the encryption context is passed as OCI associated data, so a matching context is required to decrypt | | ReEncrypt | Symmetric crypto | Partial | Most usage | there is no single re-encrypt op; the adapter serves it as a decrypt-then-encrypt across the two keys, and the plaintext is never returned to the caller | | ListResourceTags | Tags | Supported | Full surface | lists the tags on the key | | TagResource | Tags | Supported | Full surface | attaches key/value tags (as freeform or defined tags) | | UntagResource | Tags | Supported | Full surface | removes tags | #### How it works Tensor9 runs a KMS adapter in the customer environment and sets `AWS_ENDPOINT_URL_KMS` to its loopback endpoint. The adapter accepts the application's AWS SDK calls, including `Encrypt`, `Decrypt`, and `GenerateDataKey`, and translates them to OCI KMS. The service retains the wrapping key; data-key operations can return a plaintext data key for the application to use. OCI KMS supports symmetric AES keys, authenticated encryption context, asymmetric signing, and a native data-key generation operation. HMAC and AWS key-policy and grant APIs are unsupported; key access uses OCI IAM.
The adapter translates AWS KMS requests to OCI KMS. The adapter translates AWS KMS requests to OCI KMS.

The adapter translates AWS KMS requests to OCI KMS.

#### Key identity and lifecycle The adapter keeps durable records for AWS key identities, aliases, tags, and lifecycle state. Key creation provisions the corresponding target key. Later requests resolve the AWS identity to that key; the target service performs cryptographic operations and retains its wrapping key material. An alias is a mutable reference in adapter state. Updating it changes which key subsequent alias-based requests select; it does not move key material or rewrite existing ciphertext. Back up adapter state and retain the target keys and versions needed to decrypt stored data. Scheduled deletion records a recovery window of 7-30 days, with 30 days as the default. Cancelling the request returns the key to a disabled state; enable it explicitly before using it again. Final target destruction follows the target's retention rules, which may delay physical removal beyond the adapter's recovery window. #### The Vault and its master keys An OCI Vault holds master encryption keys. `protection_mode` selects `SOFTWARE`, `HSM` (a hardware security module), or `EXTERNAL` (a reference to an external key manager). `DEFAULT` vaults share an HSM partition; `VIRTUAL_PRIVATE` vaults have a dedicated partition. The adapter uses two OCI clients. Cryptographic requests, including encryption, decryption, data-key generation, and signing, use `crypto_endpoint`. Key creation, rotation configuration, and scheduled deletion use `management_endpoint`.
Cryptographic requests use crypto_endpoint; key management uses management_endpoint. Cryptographic requests use crypto_endpoint; key management uses management_endpoint.

Cryptographic requests use crypto\_endpoint ; key management uses management\_endpoint .

#### The native single-call envelope `GenerateDataKey` returns a plaintext data key and an encrypted (wrapped) copy. The application uses the plaintext key to encrypt its data locally, then discards that key and stores the wrapped copy beside the encrypted data. To read the data later, it asks the key service to decrypt the wrapped key. `GenerateDataEncryptionKey` returns both `plaintext` and `ciphertext` when `include_plaintext_key` is true. With it false, the call returns only the wrapped key, corresponding to `GenerateDataKeyWithoutPlaintext`. The adapter translates these results to AWS response fields without generating the data key locally.
GenerateDataEncryptionKey returns a plaintext data key and its encrypted copy in one call. GenerateDataEncryptionKey returns a plaintext data key and its encrypted copy in one call.

GenerateDataEncryptionKey returns a plaintext data key and its encrypted copy in one call.

#### The encryption context, bound as associated data An encryption context is a non-secret key/value map bound to ciphertext as additional authenticated data (AAD). Decryption requires the same context. Applications can use it to bind encrypted values to a tenant or purpose. The adapter sends the encryption context as OCI `associatedData`. AES-GCM authenticates it with the ciphertext. Decryption with different associated data fails authentication.
OCI authenticates associatedData with the ciphertext. A different context causes decryption to fail. OCI authenticates associatedData with the ciphertext. A different context causes decryption to fail.

OCI authenticates associatedData with the ciphertext. A different context causes decryption to fail.

#### Symmetric and asymmetric keys An OCI key's `key_shape` specifies `algorithm` (`AES`, `RSA`, or `ECDSA`), `length`, and a `curve_id` where applicable. These correspond to AWS symmetric encryption and asymmetric key families. AES keys support `Encrypt`, `Decrypt`, and `GenerateDataEncryptionKey`. RSA and ECDSA keys support signing, verification, and public-key export. OCI has no HMAC key type, so `GenerateMac` and `VerifyMac` return unsupported-operation errors.
OCI supports AES, RSA, and ECDSA key types. HMAC is unsupported. OCI supports AES, RSA, and ECDSA key types. HMAC is unsupported.

OCI supports AES, RSA, and ECDSA key types. HMAC is unsupported.

#### Rotation Key rotation creates a new version for subsequent encryption. Retained, enabled older versions remain available to decrypt existing ciphertext. `is_auto_rotation_enabled` and `rotation_interval_in_days` configure automatic rotation. `RotateKeyOnDemand` creates a version immediately. New encryption uses the new version; decryption of existing ciphertext uses its recorded version. Both AWS and OCI express the rotation interval in days.
Automatic or on-demand rotation creates a version for new encryption and retains previous versions. Automatic or on-demand rotation creates a version for new encryption and retains previous versions.

Automatic or on-demand rotation creates a version for new encryption and retains previous versions.

#### Limitations **Authorization and HMAC.** Target access uses OCI IAM; AWS key-policy and grant APIs are not recreated. OCI key shapes in this mapping are AES, RSA, and ECDSA, without an HMAC key type. **Replication scope.** OCI vault replication copies a vault rather than one independently managed key. ReplicateKey therefore has broader scope; UpdatePrimaryRegion does not move an individual key's primary region. **Deletion timing.** The adapter records the requested 7-30-day recovery window and translates the date to OCI's time\_of\_deletion. Retain the keys and versions required by existing ciphertext before scheduling destruction. **Imported material.** OCI imports externally generated material through its wrapping-key and key-import workflow. That differs from protection\_mode=EXTERNAL, which references a key manager outside OCI. The AWS runtime import handshake is outside this mapping. #### Other considerations **Configure custody and access.** Choose the vault and each key's software, HSM, or external protection configuration. Oracle operates the managed service; the customer configures keys, compartment permissions, and deployment identity. **Retain versions.** Rotation creates a version for new encryption. Existing ciphertext needs its original version. Plan recovery and replication for the vault holding those versions, and verify access from the destination environment. **Migrate ciphertext.** Decrypt AWS ciphertext or unwrap its data keys with the old key, then encrypt or wrap through the target adapter. Keep AWS available until migration and target decryption are verified. Creating an OCI key does not transfer AWS material or ciphertext. ## On Private Kubernetes | Operation | Area | Support | Depth | Notes | | ----------------------------------- | --------------------- | ------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | CreateAlias | Aliases | Supported | Most usage | the adapter maintains the alias-to-key mapping and resolves alias references, since a Transit key is addressed by name with no separate alias object | | DeleteAlias | Aliases | Supported | Full surface | removes an alias from the mapping | | ListAliases | Aliases | Supported | Most usage | lists aliases retained in adapter state | | UpdateAlias | Aliases | Supported | Full surface | changes the key referenced by an alias in adapter state | | DeriveSharedSecret | Asymmetric | Out of scope | Full surface | elliptic-curve shared-secret derivation has no Transit equivalent endpoint and is rejected | | GetPublicKey | Asymmetric | Supported | Most usage | returns the public half of an asymmetric key from the Transit key read | | Sign | Asymmetric | Supported | Most usage | signs server-side with an ed25519, ECDSA, or RSA key via the Transit sign endpoint | | Verify | Asymmetric | Supported | Most usage | verifies natively via the Transit verify endpoint | | GenerateDataKey | Envelope / data keys | Supported | Common | served natively by the Transit plaintext-datakey endpoint: one call returns the plaintext data key alongside its wrapped copy, one data-key operation | | GenerateDataKeyPair | Envelope / data keys | Partial | Most usage | the native datakey endpoint is symmetric; an asymmetric data-key pair is generated and its private half wrapped by the key, with no native data-key-pair endpoint | | GenerateDataKeyPairWithoutPlaintext | Envelope / data keys | Partial | Full surface | the same, returning only the wrapped private half | | GenerateDataKeyWithoutPlaintext | Envelope / data keys | Supported | Most usage | served natively by the Transit wrapped-datakey endpoint, returning only the wrapped data key | | CreateGrant | Grants | Out of scope | Most usage | authorization is governed by Vault ACL policies rather than KMS grants, so a grant is rejected | | ListGrants | Grants | Out of scope | Most usage | there is no grant to list; access is a Vault ACL policy | | ListRetirableGrants | Grants | Out of scope | Full surface | there is no grant to list | | RetireGrant | Grants | Out of scope | Full surface | there is no grant to retire | | RevokeGrant | Grants | Out of scope | Full surface | there is no grant to revoke | | GenerateMac | HMAC | Supported | Most usage | computes an HMAC server-side via the Transit HMAC endpoint; Transit computes the HMAC within Vault | | VerifyMac | HMAC | Supported | Most usage | verifies an HMAC via the Transit verify endpoint | | DeleteImportedKeyMaterial | Imported key material | Out of scope | Full surface | there is no runtime imported-material lifecycle to drive | | GetParametersForImport | Imported key material | Out of scope | Full surface | key material is loaded through Transit's own key-import flow at provisioning, not through the native import handshake, so the runtime import ops are rejected | | ImportKeyMaterial | Imported key material | Out of scope | Full surface | the import handshake is not served at runtime; material is imported at provisioning | | CancelKeyDeletion | Key management | Partial | Full surface | cancels the recovery window the adapter holds, before the delete is applied to Transit | | CreateKey | Key management | Supported | Common | the equivalent Transit key is created with the matching type (aes256-gcm96, RSA, ECDSA, ed25519, or hmac); the key usage and material spec translate to the Transit key type | | DescribeKey | Key management | Supported | Common | returns the key metadata held on the Transit key: type, versions, rotation, and configuration | | DisableKey | Key management | Partial | Most usage | requires adapter state and Vault access controls; minimum-version settings alone do not disable every operation | | EnableKey | Key management | Partial | Most usage | restores permitted adapter operations with the required Vault access; version thresholds govern version selection separately | | ListKeys | Key management | Supported | Common | lists the Transit keys in the mount | | ScheduleKeyDeletion | Key management | Partial | Most usage | Transit deletes immediately once deletion is allowed; the 7-30-day recovery window is held as state by the adapter, not by Transit | | UpdateKeyDescription | Key management | Partial | Full surface | a Transit key has no description field, so the label is held as metadata by the adapter | | GetKeyPolicy | Key policy | Out of scope | Most usage | there is no per-key policy document to read | | ListKeyPolicies | Key policy | Out of scope | Full surface | there is no per-key policy document to list | | PutKeyPolicy | Key policy | Out of scope | Most usage | access is governed by Vault ACL policies rather than a per-key policy document, so a per-key policy is rejected | | ReplicateKey | Multi-region | Out of scope | Full surface | a Transit key lives in a single cluster; cross-region key replication is rejected | | UpdatePrimaryRegion | Multi-region | Out of scope | Full surface | there is no multi-region key set whose primary can be moved | | GenerateRandom | Random | Supported | Full surface | returns cryptographic random bytes from Vault's random endpoint; no key is involved | | DisableKeyRotation | Rotation | Supported | Full surface | clears the auto-rotate period | | EnableKeyRotation | Rotation | Supported | Most usage | turns on automatic rotation via the key's auto-rotate period | | GetKeyRotationStatus | Rotation | Supported | Most usage | reports whether rotation is on and the configured period | | RotateKeyOnDemand | Rotation | Supported | Full surface | served natively by the Transit key-rotate endpoint, creating a new key version | | Decrypt | Symmetric crypto | Supported | Common | decrypts server-side and re-checks the associated data; a mismatch fails the decrypt, exactly as it does natively | | Encrypt | Symmetric crypto | Supported | Common | encrypts server-side via the Transit encrypt endpoint; the encryption context is supplied as Transit associated data, so a matching context is required to decrypt | | ReEncrypt | Symmetric crypto | Supported | Most usage | same-key rotation uses Transit rewrap; a different key or changed context uses adapter decrypt-then-encrypt without returning plaintext to the caller | | ListResourceTags | Tags | Partial | Full surface | lists tags retained in adapter state | | TagResource | Tags | Partial | Full surface | a Transit key has no native tag map, so tags are held as key metadata by the adapter | | UntagResource | Tags | Partial | Full surface | removes a tag from adapter metadata | #### How it works Tensor9 runs a KMS adapter in the customer environment and sets `AWS_ENDPOINT_URL_KMS` to its loopback endpoint. The adapter accepts the application's AWS SDK calls, including `Encrypt`, `Decrypt`, and `GenerateDataKey`, and translates them to HashiCorp Vault Transit. The service retains the wrapping key; data-key operations can return a plaintext data key for the application to use. Vault Transit performs cryptographic operations within Vault and retains its wrapping keys there. It supports data-key generation, rewrapping, signing, verification, and HMAC. You operate the Vault cluster, including storage, unsealing, authentication, and the Transit mount; Tensor9 operates the KMS adapter.
The adapter serves the AWS KMS API using Vault Transit in the customer environment. The adapter serves the AWS KMS API using Vault Transit in the customer environment.

The adapter serves the AWS KMS API using Vault Transit in the customer environment.

#### Key identity and lifecycle The adapter keeps durable records for AWS key identities, aliases, tags, and lifecycle state. Key creation provisions the corresponding target key. Later requests resolve the AWS identity to that key; the target service performs cryptographic operations and retains its wrapping key material. An alias is a mutable reference in adapter state. Updating it changes which key subsequent alias-based requests select; it does not move key material or rewrite existing ciphertext. Back up adapter state and retain the target keys and versions needed to decrypt stored data. Scheduled deletion records a recovery window of 7-30 days, with 30 days as the default. Cancelling the request returns the key to a disabled state; enable it explicitly before using it again. Final target destruction follows the target's retention rules, which may delay physical removal beyond the adapter's recovery window. #### The Transit engine and its versioned keys Transit receives plaintext or ciphertext and returns the result of the requested operation. Encryption and wrapping keys remain in Vault; generated plaintext data keys can be returned to applications. Rotating a Transit key creates a version for new encryption and retains older versions. The `rewrap` operation updates ciphertext to the latest version; `min_decryption_version` controls which older versions may still decrypt.
Transit performs cryptographic operations in Vault and retains versioned wrapping keys there. Transit performs cryptographic operations in Vault and retains versioned wrapping keys there.

Transit performs cryptographic operations in Vault and retains versioned wrapping keys there.

#### The native data key and rewrap `GenerateDataKey` returns a plaintext data key and an encrypted (wrapped) copy. The application uses the plaintext key to encrypt its data locally, then discards that key and stores the wrapped copy beside the encrypted data. To read the data later, it asks the key service to decrypt the wrapped key. Transit implements this with `/transit/datakey/plaintext/:name`; `/transit/datakey/wrapped/:name` returns only the encrypted copy for `GenerateDataKeyWithoutPlaintext`. For rewrapping under the same Transit key, `/transit/rewrap/:name` decrypts the stored value and encrypts it with the current key version within Vault. It does not return plaintext to the caller. A different destination key or changed context requires a decrypt-and-encrypt sequence through the adapter, where plaintext is held temporarily.
The data-key endpoint returns a plaintext key and its encrypted copy. Rewrap updates the encrypted copy without returning plaintext. The data-key endpoint returns a plaintext key and its encrypted copy. Rewrap updates the encrypted copy without returning plaintext.

The data-key endpoint returns a plaintext key and its encrypted copy. Rewrap updates the encrypted copy without returning plaintext.

#### Signing, verification, and HMAC Transit key types include symmetric `aes256-gcm96` and asymmetric `rsa`, `ecdsa`, and `ed25519`. Symmetric keys support encryption, data-key wrapping, rewrap, and HMAC. Asymmetric keys support signing, verification, and public-key export. `GenerateMac` and `VerifyMac` use Transit's server-side HMAC operations. `DeriveSharedSecret` is unsupported because Transit has no corresponding endpoint.
Transit supports encryption, data keys, rewrap, signing, verification, and HMAC. Transit supports encryption, data keys, rewrap, signing, verification, and HMAC.

Transit supports encryption, data keys, rewrap, signing, verification, and HMAC.

#### Encryption context, bound as associated data An encryption context is a non-secret key/value map bound to ciphertext as additional authenticated data (AAD). Decryption requires the same context. Applications can use it to bind encrypted values to a tenant or purpose. The adapter sends encryption context as `associated_data` for encryption, decryption, rewrap, and data-key generation. A mismatch causes decryption to fail. This differs from Transit's `context` parameter, which is used for key derivation.
Transit authenticates the encryption context through associated_data. Decryption requires the same context. Transit authenticates the encryption context through associated_data. Decryption requires the same context.

Transit authenticates the encryption context through associated\_data . Decryption requires the same context.

#### Minimum decryption version Key rotation creates a new version for subsequent encryption. Retained, enabled older versions remain available to decrypt existing ciphertext. Transit's `min_decryption_version` limits which retained versions can decrypt. Its usual value of 1 permits all versions. Raising it prevents decryption of ciphertext using a lower version; rotation alone does not impose that restriction. The adapter keeps `min_decryption_version = 1` on the keys it manages. Raising it or destroying an old version prevents access to affected ciphertext and must be handled as a separate change.
The adapter sets min_decryption_version = 1. Raising it prevents older versions from decrypting their ciphertext. The adapter sets min_decryption_version = 1. Raising it prevents older versions from decrypting their ciphertext.

The adapter sets min\_decryption\_version = 1 . Raising it prevents older versions from decrypting their ciphertext.

#### Limitations **Deletion and disabled state.** Transit has no AWS scheduled-deletion window or single enabled flag. The adapter records the recovery window. Version thresholds restrict particular versions; they do not disable every operation on a key. Coordinate adapter state with Vault access policy. **Authorization and regions.** Vault ACL policies control Transit paths. AWS grant and key-policy APIs are not recreated. Vault Enterprise replication is separate cluster configuration; it does not provide this mapping's ReplicateKey or UpdatePrimaryRegion API. **Re-encryption.** Transit rewrap changes the version of the same named key. ReEncrypt to a different key or with a changed encryption context requires decrypting and encrypting through the adapter. Intermediate plaintext is not returned to the caller. **Version retention and import.** Raising min\_decryption\_version prevents older versions from decrypting. Imported material uses Transit's import workflow; asymmetric data-key pairs require local generation and wrapping. DeriveSharedSecret is outside this mapping. #### Other considerations **Operate Vault.** The platform team runs Vault storage, unsealing, authentication, backups, and the Transit mount. The adapter uses its configured Vault identity; restrict its ACL paths to the required key operations. **Protect both state stores.** Back up adapter key identities and aliases as well as Vault storage. A surviving alias cannot decrypt data if its target key or required version has been lost. **Migrate ciphertext.** Keep AWS available to decrypt values or unwrap data keys, then encrypt or wrap them under Transit before retiring AWS access. Existing AWS ciphertext cannot be used directly with a newly created Transit key. [Service Catalog](/service-adapters/catalog). # Secrets Manager Source: https://docs.tensor9.com/service-adapters/aws/security-identity/secrets-manager AWS Secrets Manager. Keeps credentials and API keys encrypted under KMS, serves them over an API, and rotates them on a schedule with Lambda. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Max adaptation](#max-adaptation) * [On Google Cloud](#on-google-cloud) * [On Azure](#on-azure) * [On OCI](#on-oci) * [On Private Kubernetes](#on-private-kubernetes) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | ✓ | ## How the targets compare Each row compares a capability of Secrets Manager with its adaptation on each target. A dash means this row is not stated for that target. ### Max adaptation | Capability | Secrets Manager | Google Cloud | Azure | OCI | Private Kubernetes | | -------------------------------------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | Version stages · current / pending / previous | Yes | Yes - stage labels and immutable versions are maintained in adapter state | Yes - stage labels and immutable versions are maintained in adapter state | Yes - stage labels and immutable versions are maintained in adapter state | Yes - stage labels and immutable versions are maintained in adapter state | | Secret rotation · function-driven rotation | Yes | Yes - four-step rotation through a connected Lambda adapter and supplied function | Yes - four-step rotation through a connected Lambda adapter and supplied function | Yes - four-step rotation through a connected Lambda adapter and supplied function | Yes - four-step rotation through a connected Lambda adapter and supplied function | | Recovery window · scheduled deletion | Yes | Yes - 7-30 days in adapter state; target cleanup is separate | Yes - 7-30 days in adapter state; target cleanup is separate | Yes - 7-30 days in adapter state; target cleanup is separate | Yes - 7-30 days in adapter state; target cleanup is separate | | Per-secret encryption key · customer-managed key configuration | Yes | Partial - depends on protection of adapter state and target key configuration; KmsKeyId metadata alone does not select an encryption key | Partial - depends on protection of adapter state and target key configuration; KmsKeyId metadata alone does not select an encryption key | Partial - depends on protection of adapter state and target key configuration; KmsKeyId metadata alone does not select an encryption key | Partial - depends on protection of adapter state and target key configuration; KmsKeyId metadata alone does not select an encryption key | | Cross-region replication · physical copies | Yes | Partial - region settings are retained; configure and verify physical target replication separately | Partial - region settings are retained; configure and verify physical target replication separately | Partial - region settings are retained; configure and verify physical target replication separately | Partial - region settings are retained; configure and verify physical target replication separately | | Binary + string values · value payload | Yes | Yes - string and binary values retained in the secret record | Partial - target-encoded value must fit Key Vault's 25 KiB limit | Yes - string and binary values retained in the secret record | Yes - string and binary values retained in the secret record | | Tags · key/value metadata | Yes | Yes - source tags retained in adapter state without provider label normalization | Yes - source tags retained in adapter state without provider label normalization | Yes - source tags retained in adapter state without provider label normalization | Yes - source tags retained in adapter state without provider label normalization | | API coverage | full | high | high | high | high | ## On Google Cloud | Operation | Area | Support | Depth | Notes | | ---------------------------- | --------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------ | | BatchGetSecretValue | Bulk & generate | Supported | Most usage | reads requested secrets from their records with list/filter selection | | GetRandomPassword | Bulk & generate | Supported | Most usage | generates a password using the operating system's secure random source and requested character rules | | ListSecrets | Listing & tags | Supported | Common | lists and filters caller-scoped records, excluding deleted secrets | | TagResource | Listing & tags | Supported | Full surface | retains source tags verbatim; deployment directive tags cannot be mutated | | UntagResource | Listing & tags | Supported | Full surface | removes source tags; deployment directive tags are protected | | RemoveRegionsFromReplication | Replication | Partial | Full surface | removes region entries from the secret record; physical target replicas are managed separately | | ReplicateSecretToRegions | Replication | Partial | Most usage | retains requested region configuration; physical copies require separately configured target replication | | StopReplicationToReplica | Replication | Out of scope | Full surface | this adapter serves primary records and does not promote an independent regional replica | | DeleteResourcePolicy | Resource policy | Supported | Full surface | removes the policy document from the secret record | | GetResourcePolicy | Resource policy | Supported | Full surface | returns the stored source policy document | | PutResourcePolicy | Resource policy | Partial | Full surface | stores and validates the policy; full resource-policy enforcement is separate from policy management | | ValidateResourcePolicy | Resource policy | Partial | Full surface | checks syntax, public access, and administrative lockout; does not promise AWS's complete policy analysis | | CancelRotateSecret | Rotation | Supported | Full surface | disables rotation and withdraws its run; pending labels remain for the caller to resolve | | RotateSecret | Rotation | Supported | Most usage | runs the four-step protocol; the connected Lambda adapter must be able to invoke the supplied function | | CreateSecret | Secret CRUD | Supported | Common | creates durable secret state and the initial version; target-specific value and key constraints still apply | | DeleteSecret | Secret CRUD | Supported | Common | records a 7-30 day recovery window, default 30; force deletion withdraws the record while target cleanup follows | | DescribeSecret | Secret CRUD | Supported | Common | reports the secret record, including version stages, deletion, rotation, and replica configuration | | GetSecretValue | Secret CRUD | Supported | Common | reads the requested version or stage from durable state; scheduled deletion blocks reads | | PutSecretValue | Secret CRUD | Supported | Common | adds an immutable version with idempotency-token checks and updates stages | | RestoreSecret | Secret CRUD | Supported | Most usage | cancels scheduled deletion before the recovery window expires | | UpdateSecret | Secret CRUD | Supported | Most usage | updates metadata or adds a value version; an encryption-key ID must be distinguished from target key configuration | | ListSecretVersionIds | Versioning | Supported | Most usage | lists immutable versions and their stage labels from the record | | UpdateSecretVersionStage | Versioning | Supported | Most usage | moves source stage labels independently of provider-native aliases | #### Requests, versions, and deletion Your application sends AWS Secrets Manager requests to the Tensor9 adapter in the customer environment. The adapter keeps a durable record containing the secret's values, version IDs, stage labels, tags, and lifecycle settings. Reads use that record; they do not select whichever version happens to be latest in the target store. `PutSecretValue` creates an immutable version. `AWSCURRENT`, `AWSPENDING`, `AWSPREVIOUS`, and custom labels select versions in the record. Moving a label changes which value a read returns. A repeated request token with the same value returns the earlier result; reusing the token with a different value is rejected. Scheduled deletion blocks reads for a recoverable period of 7-30 days, with 30 days as the default. `RestoreSecret` cancels that deletion before the window expires. Force deletion removes the secret from the API immediately; removal of any target copy is completed separately.
AWS Secrets Manager requests read and update the adapter's durable secret record. Reconciliation writes the current value to the configured store. AWS Secrets Manager requests read and update the adapter's durable secret record. Reconciliation writes the current value to the configured store.

AWS version history stays in the record; the target copy follows its current value.

#### Rotation and access policies The adapter runs the four rotation steps: create a candidate, update the credential at its source, test it, and make it current. Your rotation function performs the credential-specific work. The deployment must connect that function through the Lambda adapter; a rotation request is rejected when the configured function cannot be reached. Cancelling rotation stops the run but leaves the pending label for the caller to resolve. Resource-policy operations store, return, delete, and validate the AWS policy document. Validation checks syntax, public access, and policies that would lock every caller out of policy administration. Those operations are distinct from complete resource-policy enforcement: the endpoint authorizes the authenticated principal, and target-store access uses the deployment's cloud identity. Review effective permissions when resource policies grant cross-account access or impose restrictions beyond the principal's permissions. #### Target storage, encryption, and replication Google Secret Manager stores the current value under the deployment's Google identity. Its numbered versions are target revisions, not the source of AWS stage selection. Google replication and encryption settings govern that provider copy. The adapter's durable record retains all versions and labels. Reconciliation writes the current value and its revision to Google Secret Manager. Direct edits to that copy do not update AWS version history and can be overwritten. Protect and back up adapter state as well as the target store. Encryption follows the deployment's storage and key configuration. The adapter retains `KmsKeyId` as metadata. Protect the durable state and configure any target copy to meet the workload's encryption-key requirements. Replication requests maintain region configuration in the secret record. They do not by themselves establish a second physical copy or disaster recovery in another region. Configure and verify geographic replication of the underlying storage separately. This adapter serves primary secret records; `StopReplicationToReplica` does not promote an independent regional replica. Google's native replication policy is chosen when its secret is created. AWS stage moves and delayed deletion are handled in the adapter's record, so they do not require corresponding Google aliases or a native secret recovery window. #### Cutover and ongoing operation Create the target deployment, load the required values through the Secrets Manager API, and verify current, previous, and explicitly versioned reads before switching the application. Existing provider values and their history are not automatically imported into the adapter's record. Test credential rotation with the real database or API it updates, including a failed test step and a retried callback. Verify deletion and restore behavior before relying on the recovery window. The customer controls access to secret state, rotation-function permissions, backup retention, and monitoring. ## On Azure | Operation | Area | Support | Depth | Notes | | ---------------------------- | --------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------ | | BatchGetSecretValue | Bulk & generate | Supported | Most usage | reads requested secrets from their records with list/filter selection | | GetRandomPassword | Bulk & generate | Supported | Most usage | generates a password using the operating system's secure random source and requested character rules | | ListSecrets | Listing & tags | Supported | Common | lists and filters caller-scoped records, excluding deleted secrets | | TagResource | Listing & tags | Supported | Full surface | retains source tags verbatim; deployment directive tags cannot be mutated | | UntagResource | Listing & tags | Supported | Full surface | removes source tags; deployment directive tags are protected | | RemoveRegionsFromReplication | Replication | Partial | Full surface | removes region entries from the secret record; physical target replicas are managed separately | | ReplicateSecretToRegions | Replication | Partial | Most usage | retains requested region configuration; physical copies require separately configured target replication | | StopReplicationToReplica | Replication | Out of scope | Full surface | this adapter serves primary records and does not promote an independent regional replica | | DeleteResourcePolicy | Resource policy | Supported | Full surface | removes the policy document from the secret record | | GetResourcePolicy | Resource policy | Supported | Full surface | returns the stored source policy document | | PutResourcePolicy | Resource policy | Partial | Full surface | stores and validates the policy; full resource-policy enforcement is separate from policy management | | ValidateResourcePolicy | Resource policy | Partial | Full surface | checks syntax, public access, and administrative lockout; does not promise AWS's complete policy analysis | | CancelRotateSecret | Rotation | Supported | Full surface | disables rotation and withdraws its run; pending labels remain for the caller to resolve | | RotateSecret | Rotation | Supported | Most usage | runs the four-step protocol; the connected Lambda adapter must be able to invoke the supplied function | | CreateSecret | Secret CRUD | Supported | Common | creates durable secret state and the initial version; target-specific value and key constraints still apply | | DeleteSecret | Secret CRUD | Supported | Common | records a 7-30 day recovery window, default 30; force deletion withdraws the record while target cleanup follows | | DescribeSecret | Secret CRUD | Supported | Common | reports the secret record, including version stages, deletion, rotation, and replica configuration | | GetSecretValue | Secret CRUD | Supported | Common | reads the requested version or stage from durable state; scheduled deletion blocks reads | | PutSecretValue | Secret CRUD | Supported | Common | adds an immutable version with idempotency-token checks and updates stages | | RestoreSecret | Secret CRUD | Supported | Most usage | cancels scheduled deletion before the recovery window expires | | UpdateSecret | Secret CRUD | Supported | Most usage | updates metadata or adds a value version; an encryption-key ID must be distinguished from target key configuration | | ListSecretVersionIds | Versioning | Supported | Most usage | lists immutable versions and their stage labels from the record | | UpdateSecretVersionStage | Versioning | Supported | Most usage | moves source stage labels independently of provider-native aliases | #### Requests, versions, and deletion Your application sends AWS Secrets Manager requests to the Tensor9 adapter in the customer environment. The adapter keeps a durable record containing the secret's values, version IDs, stage labels, tags, and lifecycle settings. Reads use that record; they do not select whichever version happens to be latest in the target store. `PutSecretValue` creates an immutable version. `AWSCURRENT`, `AWSPENDING`, `AWSPREVIOUS`, and custom labels select versions in the record. Moving a label changes which value a read returns. A repeated request token with the same value returns the earlier result; reusing the token with a different value is rejected. Scheduled deletion blocks reads for a recoverable period of 7-30 days, with 30 days as the default. `RestoreSecret` cancels that deletion before the window expires. Force deletion removes the secret from the API immediately; removal of any target copy is completed separately.
AWS Secrets Manager requests read and update the adapter's durable secret record. Reconciliation writes the current value to the configured store. AWS Secrets Manager requests read and update the adapter's durable secret record. Reconciliation writes the current value to the configured store.

AWS version history stays in the record; the target copy follows its current value.

#### Rotation and access policies The adapter runs the four rotation steps: create a candidate, update the credential at its source, test it, and make it current. Your rotation function performs the credential-specific work. The deployment must connect that function through the Lambda adapter; a rotation request is rejected when the configured function cannot be reached. Cancelling rotation stops the run but leaves the pending label for the caller to resolve. Resource-policy operations store, return, delete, and validate the AWS policy document. Validation checks syntax, public access, and policies that would lock every caller out of policy administration. Those operations are distinct from complete resource-policy enforcement: the endpoint authorizes the authenticated principal, and target-store access uses the deployment's cloud identity. Review effective permissions when resource policies grant cross-account access or impose restrictions beyond the principal's permissions. #### Target storage, encryption, and replication Azure Key Vault stores the current value under the deployment's Azure identity. A version tag tracks the adapter revision because Key Vault's native version IDs are opaque. The adapter retains AWS version history and stage labels independently. The adapter's durable record retains all versions and labels. Reconciliation writes the current value and its revision to Azure Key Vault. Direct edits to that copy do not update AWS version history and can be overwritten. Protect and back up adapter state as well as the target store. Encryption follows the deployment's storage and key configuration. The adapter retains `KmsKeyId` as metadata. Protect the durable state and configure any target copy to meet the workload's encryption-key requirements. Replication requests maintain region configuration in the secret record. They do not by themselves establish a second physical copy or disaster recovery in another region. Configure and verify geographic replication of the underlying storage separately. This adapter serves primary secret records; `StopReplicationToReplica` does not promote an independent regional replica. Key Vault limits a stored value to 25 KiB; encoded binary content must fit that limit. This target's deletion and name-reuse workflow requires purge permission. A purge-protected vault can block final removal or reuse of a deleted name. AWS scheduled deletion is handled by the adapter before this target cleanup begins. #### Cutover and ongoing operation Create the target deployment, load the required values through the Secrets Manager API, and verify current, previous, and explicitly versioned reads before switching the application. Existing provider values and their history are not automatically imported into the adapter's record. Test credential rotation with the real database or API it updates, including a failed test step and a retried callback. Verify deletion and restore behavior before relying on the recovery window. The customer controls access to secret state, rotation-function permissions, backup retention, and monitoring. ## On OCI | Operation | Area | Support | Depth | Notes | | ---------------------------- | --------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------ | | BatchGetSecretValue | Bulk & generate | Supported | Most usage | reads requested secrets from their records with list/filter selection | | GetRandomPassword | Bulk & generate | Supported | Most usage | generates a password using the operating system's secure random source and requested character rules | | ListSecrets | Listing & tags | Supported | Common | lists and filters caller-scoped records, excluding deleted secrets | | TagResource | Listing & tags | Supported | Full surface | retains source tags verbatim; deployment directive tags cannot be mutated | | UntagResource | Listing & tags | Supported | Full surface | removes source tags; deployment directive tags are protected | | RemoveRegionsFromReplication | Replication | Partial | Full surface | removes region entries from the secret record; physical target replicas are managed separately | | ReplicateSecretToRegions | Replication | Partial | Most usage | retains requested region configuration; physical copies require separately configured target replication | | StopReplicationToReplica | Replication | Out of scope | Full surface | this adapter serves primary records and does not promote an independent regional replica | | DeleteResourcePolicy | Resource policy | Supported | Full surface | removes the policy document from the secret record | | GetResourcePolicy | Resource policy | Supported | Full surface | returns the stored source policy document | | PutResourcePolicy | Resource policy | Partial | Full surface | stores and validates the policy; full resource-policy enforcement is separate from policy management | | ValidateResourcePolicy | Resource policy | Partial | Full surface | checks syntax, public access, and administrative lockout; does not promise AWS's complete policy analysis | | CancelRotateSecret | Rotation | Supported | Full surface | disables rotation and withdraws its run; pending labels remain for the caller to resolve | | RotateSecret | Rotation | Supported | Most usage | runs the four-step protocol; the connected Lambda adapter must be able to invoke the supplied function | | CreateSecret | Secret CRUD | Supported | Common | creates durable secret state and the initial version; target-specific value and key constraints still apply | | DeleteSecret | Secret CRUD | Supported | Common | records a 7-30 day recovery window, default 30; force deletion withdraws the record while target cleanup follows | | DescribeSecret | Secret CRUD | Supported | Common | reports the secret record, including version stages, deletion, rotation, and replica configuration | | GetSecretValue | Secret CRUD | Supported | Common | reads the requested version or stage from durable state; scheduled deletion blocks reads | | PutSecretValue | Secret CRUD | Supported | Common | adds an immutable version with idempotency-token checks and updates stages | | RestoreSecret | Secret CRUD | Supported | Most usage | cancels scheduled deletion before the recovery window expires | | UpdateSecret | Secret CRUD | Supported | Most usage | updates metadata or adds a value version; an encryption-key ID must be distinguished from target key configuration | | ListSecretVersionIds | Versioning | Supported | Most usage | lists immutable versions and their stage labels from the record | | UpdateSecretVersionStage | Versioning | Supported | Most usage | moves source stage labels independently of provider-native aliases | #### Requests, versions, and deletion Your application sends AWS Secrets Manager requests to the Tensor9 adapter in the customer environment. The adapter keeps a durable record containing the secret's values, version IDs, stage labels, tags, and lifecycle settings. Reads use that record; they do not select whichever version happens to be latest in the target store. `PutSecretValue` creates an immutable version. `AWSCURRENT`, `AWSPENDING`, `AWSPREVIOUS`, and custom labels select versions in the record. Moving a label changes which value a read returns. A repeated request token with the same value returns the earlier result; reusing the token with a different value is rejected. Scheduled deletion blocks reads for a recoverable period of 7-30 days, with 30 days as the default. `RestoreSecret` cancels that deletion before the window expires. Force deletion removes the secret from the API immediately; removal of any target copy is completed separately.
AWS Secrets Manager requests read and update the adapter's durable secret record. Reconciliation writes the current value to the configured store. AWS Secrets Manager requests read and update the adapter's durable secret record. Reconciliation writes the current value to the configured store.

AWS version history stays in the record; the target copy follows its current value.

#### Rotation and access policies The adapter runs the four rotation steps: create a candidate, update the credential at its source, test it, and make it current. Your rotation function performs the credential-specific work. The deployment must connect that function through the Lambda adapter; a rotation request is rejected when the configured function cannot be reached. Cancelling rotation stops the run but leaves the pending label for the caller to resolve. Resource-policy operations store, return, delete, and validate the AWS policy document. Validation checks syntax, public access, and policies that would lock every caller out of policy administration. Those operations are distinct from complete resource-policy enforcement: the endpoint authorizes the authenticated principal, and target-store access uses the deployment's cloud identity. Review effective permissions when resource policies grant cross-account access or impose restrictions beyond the principal's permissions. #### Target storage, encryption, and replication OCI Vault is the target store for the current secret value in this mapping. OCI requires an encryption key for the vault secret and provides its own version stages and replication settings. The adapter owns AWS history and stage selection, independently of those native features. The adapter's durable record retains all versions and labels. Reconciliation writes the current value and its revision to OCI Vault. Direct edits to that copy do not update AWS version history and can be overwritten. Protect and back up adapter state as well as the target store. Encryption follows the deployment's storage and key configuration. The adapter retains `KmsKeyId` as metadata. Protect the durable state and configure any target copy to meet the workload's encryption-key requirements. Replication requests maintain region configuration in the secret record. They do not by themselves establish a second physical copy or disaster recovery in another region. Configure and verify geographic replication of the underlying storage separately. This adapter serves primary secret records; `StopReplicationToReplica` does not promote an independent regional replica. Configure the OCI key and any geographic replication for the provider copy. OCI's native scheduled-deletion rules can delay physical removal after the AWS API has withdrawn a secret. These target rules do not replace the adapter's deletion window or create an independent AWS regional replica. #### Cutover and ongoing operation Create the target deployment, load the required values through the Secrets Manager API, and verify current, previous, and explicitly versioned reads before switching the application. Existing provider values and their history are not automatically imported into the adapter's record. Test credential rotation with the real database or API it updates, including a failed test step and a retried callback. Verify deletion and restore behavior before relying on the recovery window. The customer controls access to secret state, rotation-function permissions, backup retention, and monitoring. ## On Private Kubernetes | Operation | Area | Support | Depth | Notes | | ---------------------------- | --------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------ | | BatchGetSecretValue | Bulk & generate | Supported | Most usage | reads requested secrets from their records with list/filter selection | | GetRandomPassword | Bulk & generate | Supported | Most usage | generates a password using the operating system's secure random source and requested character rules | | ListSecrets | Listing & tags | Supported | Common | lists and filters caller-scoped records, excluding deleted secrets | | TagResource | Listing & tags | Supported | Full surface | retains source tags verbatim; deployment directive tags cannot be mutated | | UntagResource | Listing & tags | Supported | Full surface | removes source tags; deployment directive tags are protected | | RemoveRegionsFromReplication | Replication | Partial | Full surface | removes region entries from the secret record; physical target replicas are managed separately | | ReplicateSecretToRegions | Replication | Partial | Most usage | retains requested region configuration; physical copies require separately configured target replication | | StopReplicationToReplica | Replication | Out of scope | Full surface | this adapter serves primary records and does not promote an independent regional replica | | DeleteResourcePolicy | Resource policy | Supported | Full surface | removes the policy document from the secret record | | GetResourcePolicy | Resource policy | Supported | Full surface | returns the stored source policy document | | PutResourcePolicy | Resource policy | Partial | Full surface | stores and validates the policy; full resource-policy enforcement is separate from policy management | | ValidateResourcePolicy | Resource policy | Partial | Full surface | checks syntax, public access, and administrative lockout; does not promise AWS's complete policy analysis | | CancelRotateSecret | Rotation | Supported | Full surface | disables rotation and withdraws its run; pending labels remain for the caller to resolve | | RotateSecret | Rotation | Supported | Most usage | runs the four-step protocol; the connected Lambda adapter must be able to invoke the supplied function | | CreateSecret | Secret CRUD | Supported | Common | creates durable secret state and the initial version; target-specific value and key constraints still apply | | DeleteSecret | Secret CRUD | Supported | Common | records a 7-30 day recovery window, default 30; force deletion withdraws the record while target cleanup follows | | DescribeSecret | Secret CRUD | Supported | Common | reports the secret record, including version stages, deletion, rotation, and replica configuration | | GetSecretValue | Secret CRUD | Supported | Common | reads the requested version or stage from durable state; scheduled deletion blocks reads | | PutSecretValue | Secret CRUD | Supported | Common | adds an immutable version with idempotency-token checks and updates stages | | RestoreSecret | Secret CRUD | Supported | Most usage | cancels scheduled deletion before the recovery window expires | | UpdateSecret | Secret CRUD | Supported | Most usage | updates metadata or adds a value version; an encryption-key ID must be distinguished from target key configuration | | ListSecretVersionIds | Versioning | Supported | Most usage | lists immutable versions and their stage labels from the record | | UpdateSecretVersionStage | Versioning | Supported | Most usage | moves source stage labels independently of provider-native aliases | #### Requests, versions, and deletion Your application sends AWS Secrets Manager requests to the Tensor9 adapter in the customer environment. The adapter keeps a durable record containing the secret's values, version IDs, stage labels, tags, and lifecycle settings. Reads use that record; they do not select whichever version happens to be latest in the target store. `PutSecretValue` creates an immutable version. `AWSCURRENT`, `AWSPENDING`, `AWSPREVIOUS`, and custom labels select versions in the record. Moving a label changes which value a read returns. A repeated request token with the same value returns the earlier result; reusing the token with a different value is rejected. Scheduled deletion blocks reads for a recoverable period of 7-30 days, with 30 days as the default. `RestoreSecret` cancels that deletion before the window expires. Force deletion removes the secret from the API immediately; removal of any target copy is completed separately.
AWS Secrets Manager requests use durable adapter state without a separate target copy. AWS Secrets Manager requests use durable adapter state without a separate target copy.

The adapter stores values and version history in its durable state.

#### Rotation and access policies The adapter runs the four rotation steps: create a candidate, update the credential at its source, test it, and make it current. Your rotation function performs the credential-specific work. The deployment must connect that function through the Lambda adapter; a rotation request is rejected when the configured function cannot be reached. Cancelling rotation stops the run but leaves the pending label for the caller to resolve. Resource-policy operations store, return, delete, and validate the AWS policy document. Validation checks syntax, public access, and policies that would lock every caller out of policy administration. Those operations are distinct from complete resource-policy enforcement: the endpoint authorizes the authenticated principal, and target-store access uses the deployment's cloud identity. Review effective permissions when resource policies grant cross-account access or impose restrictions beyond the principal's permissions. #### Target storage, encryption, and replication The customer deployment stores the secret entirely in the adapter's durable state. This configuration creates no additional Kubernetes Secret. Version history and reads use the same secret record as cloud-backed deployments. All versions and labels remain in the adapter's durable state. Protect that state store and its backups; no separate cloud secret copy is configured. Encryption follows the deployment's storage and key configuration. The adapter retains `KmsKeyId` as metadata. Protect the durable state and configure any target copy to meet the workload's encryption-key requirements. Replication requests maintain region configuration in the secret record. They do not by themselves establish a second physical copy or disaster recovery in another region. Configure and verify geographic replication of the underlying storage separately. This adapter serves primary secret records; `StopReplicationToReplica` does not promote an independent regional replica. Storage durability, encryption, backups, and recovery depend on the appliance's state store. A Kubernetes namespace or an external synchronization controller does not provide that history. #### Cutover and ongoing operation Create the target deployment, load the required values through the Secrets Manager API, and verify current, previous, and explicitly versioned reads before switching the application. Existing provider values and their history are not automatically imported into the adapter's record. Test credential rotation with the real database or API it updates, including a failed test step and a retried callback. Verify deletion and restore behavior before relying on the recovery window. The customer controls access to secret state, rotation-function permissions, backup retention, and monitoring. [Service Catalog](/service-adapters/catalog). # WAFv2 Source: https://docs.tensor9.com/service-adapters/aws/security-identity/wafv2 The current AWS WAF API: one web ACL model for CloudFront and regional resources, with reusable rule groups, managed rules and capacity units. **On this page** * [Coverage by target cloud](#coverage-by-target-cloud) * [How the targets compare](#how-the-targets-compare) * [Infrastructure-only adaptation](#infrastructure-only-adaptation) * [On Azure](#on-azure) * [On OCI](#on-oci) ## Coverage by target cloud | Target | Available | | ------------------ | --------- | | Google Cloud | ✓ | | Azure | ✓ | | OCI | ✓ | | Private Kubernetes | - | ## How the targets compare Each row compares a capability of WAFv2 with its adaptation on each target. A dash means this row is not stated for that target. ### Infrastructure-only adaptation | Capability | WAFv2 | Azure | OCI | | -------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | IP allow / block | Yes | Yes - RemoteAddr IPMatch custom rule; the ip-set CIDRs inline | Partial - a network address list plus a best-effort access-control condition | | Rate-based rules | Yes | Partial - RateLimitRule; a one- or five-minute window, so other windows round | Yes - request\_rate\_limiting; the AWS window is kept unrounded | | Byte / geo match | Yes | Partial - match\_condition with typed match variables and operators | Partial - access-control rules with a best-effort JMESPATH condition | | Managed rule groups · catalog fidelity | AWS managed rule groups (Common / Bot / SQLi / …) | OWASP Core Rule Set, a different catalog over the same posture | OWASP protection capabilities, a different catalog over the same posture | | Deny-by-default allowlist | Yes | Partial - no whole-policy default deny; add a lowest-priority block rule | Yes - the access-control module's default action preserves it | | API coverage | full | partial | partial | ## On Azure | Operation | Area | Support | Depth | Notes | | -------------------------------------------------------- | ---------------- | ------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Default action (deny-by-default) | Default action | Partial | Most usage | a default of allow maps exactly; a deny-by-default allowlist has no whole-policy analog, so add an explicit lowest-priority block rule to restore it | | IP-set allow / block | IP sets | Supported | Common | ip\_set\_reference → a RemoteAddr IPMatch custom rule; the ip-set CIDRs inline into match\_values | | Logging configuration | Logging | Partial | Full surface | WAF request logging is a diagnostic setting on the Application Gateway rather than a per-web-ACL logging configuration; enable it on the gateway | | AWS managed rule groups | Managed rules | Partial | Most usage | managed\_rule\_group → the OWASP Core Rule Set managed set; the catalogs differ, so a specific AWS-managed rule id does not port 1:1, and an OWASP baseline is added when the web ACL declared none | | Byte-match / geo-match | Match conditions | Partial | Most usage | → a match\_condition; the field\_to\_match component and positional constraint re-author to Azure match variables (RequestUri / QueryString / RequestHeaders) and operators, and geo uses two-letter country codes | | Regex / SQLi / XSS / size / label / composite statements | Match conditions | Out of scope | Full surface | not translated to custom rules; re-express injection and cross-site checks through the OWASP managed set and a regex check as a native Azure Regex custom rule, which the policy supports | | Rate-based rules | Rate limiting | Partial | Common | → a RateLimitRule keyed on the client address; the policy accepts a one- or five-minute window, so a window that is not 60 or 300 seconds rounds | | Rule groups | Rule groups | Partial | Most usage | a reusable rule-group's inlined statements translate with their parent web ACL; a standalone rule-group resource is not emitted on its own | | Web ACL rule tree | Web ACL | Supported | Common | the aws\_wafv2\_web\_acl rule tree is emitted as an azurerm\_web\_application\_firewall\_policy in priority order, evaluated by the Application Gateway WAF engine | #### How it works On AWS, an `aws_wafv2_web_acl` holds a tree of rules that AWS WAF evaluates in front of an ALB or a CloudFront distribution: IP-set allow/block lists, rate-based rules, byte- and geo-match conditions, and AWS-managed rule groups. The customer's Azure has a direct analog, the Application Gateway WAF, whose rules live in an `azurerm_web_application_firewall_policy`. At the build, Tensor9 reads that rule tree and emits the equivalent Azure policy as a real resource. Each statement becomes a `custom_rules` entry with a typed match variable and operator, and the schema-required managed set includes the OWASP Core Rule Set. The policy is a native Azure object the customer attaches to their Application Gateway and inspects in the portal; Azure's WAF engine evaluates every request. Where a statement has no Azure analog, the build says so rather than dropping it silently. The rest of this document walks each mapping and the closing sections collect every place the two engines diverge.
Before: on AWS the aws_wafv2_web_acl rule tree is enforced by AWS WAF at the ALB or CloudFront edge. After: on Azure the same rule tree compiles to a native azurerm_web_application_firewall_policy that the Application Gateway WAF engine evaluates. Before: on AWS the aws_wafv2_web_acl rule tree is enforced by AWS WAF at the ALB or CloudFront edge. After: on Azure the same rule tree compiles to a native azurerm_web_application_firewall_policy that the Application Gateway WAF engine evaluates.

The rule tree compiles to a native Azure WAF policy that Microsoft's Application Gateway WAF engine evaluates; nothing of Tensor9 is in the request path.

#### Architecture An `aws_wafv2_web_acl` maps to exactly one `azurerm_web_application_firewall_policy`. The rules are read in priority order and split by kind. Every custom statement (IP-set, rate-based, byte-match, geo-match) becomes one `custom_rules` entry holding a `match_conditions` block with a typed match variable, an operator, and its match values. AWS-managed rule groups do not become custom rules; they map to the policy's `managed_rules` block, which always includes the OWASP Core Rule Set so the policy is schema-valid and covers the injection and cross-site families out of the box. The policy runs in Prevention mode, so a matched block rule stops the request rather than only logging it. The resource group and region come from the stack's own placement, threaded in as locals so the policy lands in the same resource group as the Application Gateway it protects. Attaching the policy to that gateway is the one manual step, exactly as attaching a web ACL to an ALB is on AWS. * **One policy per web ACL:** the rule tree is read in priority order and emitted into a single `azurerm_web_application_firewall_policy`. * **Custom vs managed.** IP-set, rate, byte- and geo-match statements become `custom_rules`; AWS-managed groups map to the `managed_rules` OWASP set. * **Prevention by default:** the policy enforces (blocks matched requests), it does not run in detection-only mode. * **Placement threaded:** resource group and location come from the stack, so the policy sits with the gateway it guards.
The rule tree fans into two parts of one WAF policy: a custom_rules block holding one entry per translated statement, and a managed_rules block holding the OWASP Core Rule Set. Resource group and location are threaded from the stack. The policy attaches to the Application Gateway. The rule tree fans into two parts of one WAF policy: a custom_rules block holding one entry per translated statement, and a managed_rules block holding the OWASP Core Rule Set. Resource group and location are threaded from the stack. The policy attaches to the Application Gateway.

One web ACL becomes one WAF policy: the rule tree fans into custom\_rules plus the managed OWASP set, and the policy attaches to the Application Gateway.

#### How the rule statements map The translation is statement by statement. An `ip_set_reference_statement` becomes a `MatchRule` whose match variable is `RemoteAddr` and whose operator is `IPMatch`; the referenced ip-set's CIDRs are written directly into the rule's `match_values`, so the block or allow decision reads inline on the Azure policy. A `rate_based_statement` becomes a `RateLimitRule` keyed on the client address, with the AWS request limit as its threshold. A `byte_match_statement` becomes a `match_condition` whose variable is chosen from the AWS `field_to_match` (a URI-path match becomes `RequestUri`, a query-string match becomes `QueryString`, a header match becomes `RequestHeaders`), and its positional constraint becomes the Azure operator (Contains, BeginsWith, EndsWith, Equal). A `geo_match_statement` becomes a geo-match rule on the client address, keeping the two-letter country codes. AWS-managed rule groups are different in kind. Rather than a per-request condition, they are a curated catalog Amazon maintains. Azure's equivalent is the OWASP Core Rule Set, so a `managed_rule_group_statement` maps onto the policy's OWASP `managed_rule_set`. The managed-detection posture survives; the specific rule identifiers do not, which the next sections make precise.
Each AWS WAFv2 statement type maps to an Azure construct: ip_set_reference to a RemoteAddr IPMatch custom rule with the CIDRs inline; rate_based to a RateLimitRule; byte_match to a match_condition with a typed variable; geo_match to a GeoMatch rule; managed_rule_group to the OWASP managed rule set. Each AWS WAFv2 statement type maps to an Azure construct: ip_set_reference to a RemoteAddr IPMatch custom rule with the CIDRs inline; rate_based to a RateLimitRule; byte_match to a match_condition with a typed variable; geo_match to a GeoMatch rule; managed_rule_group to the OWASP managed rule set.

Five statement types have a direct Azure construct; the injection- and bot-family managed groups map onto the OWASP Core Rule Set.

#### IP sets and rate limiting AWS keeps an ip-set as its own resource that rules reference by ARN. Azure has no reference object for a custom rule, so the ip-set's CIDRs are inlined into the rule's `match_values`. The block or allow decision therefore reads on the rule itself. If a single ip-set is shared by several web ACLs, each emitted policy holds its own copy of the CIDRs; there is no shared list to update once, which matters when a list changes. Rate-based rules become `RateLimitRule` entries keyed on the client address, with the AWS request limit as the threshold. The one thing to know is the window. AWS lets a rate rule name any evaluation window; the Application Gateway WAF policy accepts one of two rate windows, one minute or five minutes. A rule already set to 60 or 300 seconds maps exactly. Any other window is rounded to the nearer of the two (a window of 60 seconds or less to one minute, anything larger to five minutes), and the build flags the rounding so the effective rate is understood rather than assumed. * **CIDRs inline, not referenced:** an ip-set's addresses land in the custom rule's `match_values`; a shared ip-set is copied into each policy that uses it. * **Rate key is the client address:** a `rate_based_statement` becomes a `RateLimitRule` on `RemoteAddr` with the AWS limit as its threshold. * **Two windows only.** 60s and 300s map exactly; any other window rounds to the one-minute or five-minute setting and the build flags it.
An aws_wafv2_ip_set with three CIDRs becomes a RemoteAddr IPMatch custom rule whose match_values list holds those three CIDRs inline. A rate_based rule with a 120-second window becomes a RateLimitRule rounded to the FiveMins window because Azure offers only OneMin and FiveMins. An aws_wafv2_ip_set with three CIDRs becomes a RemoteAddr IPMatch custom rule whose match_values list holds those three CIDRs inline. A rate_based rule with a 120-second window becomes a RateLimitRule rounded to the FiveMins window because Azure offers only OneMin and FiveMins.

The ip-set's CIDRs are written straight into the custom rule's match\_values; there is no separate reference resource to keep in sync.

#### Managed rule groups and the OWASP set AWS-managed rule groups are catalogs Amazon curates and versions: the Common Rule Set, the SQL-injection and known-bad-inputs groups, the bot and IP-reputation lists. Azure's managed protection is the OWASP Core Rule Set that Microsoft ships with the Application Gateway WAF. A `managed_rule_group_statement` therefore maps onto the policy's OWASP `managed_rule_set`, and the injection, cross-site-scripting, and protocol-violation coverage comes with it. What changes is the exact rule identity: a specific AWS-managed rule id does not correspond one-to-one to an OWASP rule id, so an alert or an exclusion keyed on a particular AWS rule name needs re-expressing against the OWASP rule it now corresponds to. Azure requires every WAF policy to name at least one managed rule set. If the source web ACL declared no managed group at all (an allowlist built only from custom IP and match rules), the emitted policy still includes the OWASP baseline, because a policy with no managed set is invalid. That baseline is a real behavior change: it can block traffic the AWS web ACL would have passed, so it is called out at the build with a note to verify it does not over-block a legitimate request pattern before the gateway takes traffic. * **Posture survives, ids do not.** AWS-managed groups map onto the OWASP CRS; injection and XSS coverage comes across, but a rule keyed on a specific AWS rule id needs re-expressing. * **The OWASP baseline is always present.** Azure requires a managed set, so a web ACL with none still gets OWASP CRS; verify it does not over-block. * **Managed-rule exclusions are re-authored:** per-rule exclusions on AWS map to OWASP exclusions, not the same rule ids. #### Scope and the default action An AWS web ACL is either REGIONAL (in front of an ALB or an API) or CLOUDFRONT (a global edge). Azure's two analogs split the same way: the Application Gateway WAF is the regional service, and Front Door is the global edge WAF. A regional web ACL maps cleanly to the Application Gateway policy. A CLOUDFRONT-scope web ACL is still emitted as an Application Gateway policy (the rules and match conditions are identical), but the global edge placement is Front Door's job, so the build flags the scope and names Front Door as the alternative if a global edge is what the workload needs. The default action is the other divergence to understand. On AWS a web ACL sets a default of allow (pass-unless-a-rule-blocks) or block (a deny-by-default allowlist, where only explicitly allowed traffic passes). The Application Gateway WAF has no whole-policy default-deny switch: it evaluates rules and, if none matches, the request passes. A web ACL with a default of allow maps exactly. A web ACL built as a deny-by-default allowlist does not: the allow rules are emitted, but a request matching none of them is not denied. That gap is surfaced at the build, with the fix being an explicit lowest-priority block rule that re-creates the deny-by-default behavior. * **Regional maps cleanly:** a REGIONAL web ACL becomes an Application Gateway WAF policy one-for-one. * **CLOUDFRONT is a global edge:** the rules still emit, but a global edge is Front Door's role, and the build names it. * **Default allow maps exactly; default block does not:** a deny-by-default allowlist needs an explicit lowest-priority block rule, which the build calls out. #### Limitations Five statement types translate to Azure constructs; the rest are named at the build rather than dropped quietly. These are the ceilings to check a web ACL against before cutover. △ Where AWS WAFv2 and the Azure WAF policy diverge * **Several statement types are not translated:** regex, size-constraint, and standalone SQLi / XSS match statements, label-match, and and / or / not composites are not emitted as `custom_rules`; each is named at the build rather than dropped quietly. Injection and cross-site checks are re-expressed through the OWASP managed set; a regex check is re-expressed as a native Azure `Regex` custom rule, which the policy does support. * **Deny-by-default allowlists are not enforced whole-policy:** a web ACL whose default action is block emits its allow rules, but a request matching none of them still passes; add an explicit lowest-priority block rule to restore the deny-by-default posture. * **Rate windows round to two values:** a rate rule's evaluation window is one of two settings the policy accepts, one minute or five minutes; a window that is not 60 or 300 seconds is rounded to the nearer, changing the effective rate. * **Managed-rule-group identity does not port.** AWS-managed groups map onto the OWASP Core Rule Set: the protection posture is kept, but a specific AWS rule id has no one-to-one OWASP counterpart, so per-rule alerts and exclusions are re-authored. * **An OWASP baseline is added when none was declared:** because Azure requires a managed set, a custom-only web ACL gains the OWASP baseline, which can block traffic the source ACL passed; verify it before taking traffic. * **Byte-match transforms are dropped:** a byte-match statement's text transformations (lowercase, URL-decode, compress-whitespace) are not applied to the emitted match condition; a match that depended on a transform is re-expressed against the raw field. #### Other considerations Beyond the rule-by-rule mapping, a few operational realities are worth stating plainly. * **Attach the policy to your gateway:** the WAF policy is emitted as a resource; associating it with the Application Gateway that fronts the workload is the one manual step, the same association a web ACL needs on AWS. * **The policy is referenced by name and id:** a WAF policy has no ARN analog, so anything that referenced the web ACL by ARN is re-pointed at the policy's id or name. * **Enforcement is on from the start:** the policy runs in Prevention mode, so matched block rules stop requests; switch to detection in the portal first if a soak period is wanted. * **Logging rides the gateway.** WAF request logging is a diagnostic setting on the Application Gateway rather than a per-web-ACL logging configuration; enable it on the gateway resource. * **Rule counts and sizes differ:** the Application Gateway WAF caps custom-rule count and match-value sizes differently from AWS; a very large ip-set or rule set is validated against the Azure limits at the build. ## On OCI | Operation | Area | Support | Depth | Notes | | -------------------------------------------------------- | ---------------- | ------------ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Default action (deny-by-default) | Default action | Supported | Most usage | the access-control module has its own default action, so both a default of allow and a deny-by-default allowlist map cleanly | | IP-set allow / block | IP sets | Partial | Common | each ip-set → an oci\_waf\_network\_address\_list holding its CIDRs; the source-address to list access-control condition is a best-effort JMESPATH expression, verified at the build | | Logging configuration | Logging | Partial | Full surface | WAF logging is configured on the OCI enforcement point rather than a per-web-ACL logging configuration | | AWS managed rule groups | Managed rules | Partial | Most usage | managed\_rule\_group → a request\_protection OWASP Core Rule Set protection capability; the catalogs differ, so a specific AWS-managed rule id does not port 1:1 | | Byte-match / geo-match | Match conditions | Partial | Most usage | → request\_access\_control rules; OCI's JMESPATH condition language differs from AWS field\_to\_match, so the byte and country conditions become best-effort expressions to verify | | Regex / SQLi / XSS / size / label / composite statements | Match conditions | Out of scope | Full surface | not translated to rules; re-express injection and cross-site checks through the OWASP protection capabilities | | Rate-based rules | Rate limiting | Supported | Common | → a request\_rate\_limiting rule with requests\_limit and period\_in\_seconds; the AWS evaluation window maps unrounded | | Rule groups | Rule groups | Partial | Most usage | a reusable rule-group's inlined statements translate with their parent web ACL; a standalone rule-group resource is not emitted on its own | | Web ACL rule tree | Web ACL | Supported | Common | the aws\_wafv2\_web\_acl rule tree is emitted as an oci\_waf\_web\_app\_firewall\_policy with named actions and access-control / rate-limiting / protection rule modules, evaluated by Oracle's WAF engine | #### How it works On AWS, an `aws_wafv2_web_acl` holds a tree of rules (IP-set lists, rate-based rules, byte- and geo-match conditions, and AWS-managed rule groups) that AWS WAF evaluates in front of an ALB or a CloudFront distribution. The customer's OCI has a direct analog: the OCI Web Application Firewall, whose rules live in an `oci_waf_web_app_firewall_policy`. At the build, Tensor9 reads that rule tree and emits the equivalent OCI policy as a real resource, along with one `oci_waf_network_address_list` for each ip-set the rules reference. Every request condition is written in the policy's JMESPATH condition language, and AWS-managed rule groups map onto OCI's managed protection capabilities, which are drawn from the same OWASP Core Rule Set. The policy is a native OCI object the customer attaches to a load balancer or gateway and inspects in the console; Oracle's WAF engine evaluates every request. Where a condition has no exact OCI form, the build says so rather than pretending the match is identical. The rest of this document walks each mapping and the closing sections collect every divergence.
Before: on AWS the aws_wafv2_web_acl rule tree is enforced by AWS WAF. After: on OCI the same rule tree compiles to a native oci_waf_web_app_firewall_policy plus a network address list per ip-set, which Oracle's WAF engine evaluates. Before: on AWS the aws_wafv2_web_acl rule tree is enforced by AWS WAF. After: on OCI the same rule tree compiles to a native oci_waf_web_app_firewall_policy plus a network address list per ip-set, which Oracle's WAF engine evaluates.

The rule tree compiles to a native OCI WAF policy, with each ip-set becoming its own network address list; Oracle's WAF engine evaluates it, and nothing of Tensor9 is in the request path.

#### Architecture An `aws_wafv2_web_acl` maps to one `oci_waf_web_app_firewall_policy`. OCI structures a policy differently from AWS in two ways worth knowing up front. First, actions are named once at the top of the policy (an allow action, a block action that returns an HTTP 403, and a check action that logs and continues), and every rule refers to one of them by name, rather than each rule declaring its own inline action. Second, rules are grouped into modules by what they inspect: access-control rules (IP, byte, and geo conditions), rate-limiting rules, and protection rules (the managed OWASP capabilities). Each ip-set the rules reference becomes its own `oci_waf_network_address_list` holding the CIDRs, a separate resource the access-control rules point at. The compartment comes from the stack's own placement, threaded in as a variable so the policy and its address lists land in the compartment the customer runs in. Attaching the finished policy to the load balancer or gateway that fronts the workload is the one manual step, the same association a web ACL needs on AWS. * **One policy per web ACL:** the rule tree is read in priority order and emitted into a single `oci_waf_web_app_firewall_policy`. * **Actions are named, then referenced:** an allow, a block (HTTP 403), and a check action are declared once; every rule refers to one by name. * **Three rule modules:** access-control (IP / byte / geo), rate-limiting, and protection (managed OWASP capabilities). * **Ip-sets are their own resource:** each becomes an `oci_waf_network_address_list` the access-control rules reference; the compartment is threaded from the stack.
The policy declares three named actions (allowAction, blockAction, checkAction) and three rule modules: request access control for IP, byte and geo rules; request rate limiting for rate rules; request protection for managed groups. Each ip-set is a separate network address list. Compartment is threaded from the stack. The policy declares three named actions (allowAction, blockAction, checkAction) and three rule modules: request access control for IP, byte and geo rules; request rate limiting for rate rules; request protection for managed groups. Each ip-set is a separate network address list. Compartment is threaded from the stack.

The policy declares three named actions and three rule modules; each ip-set becomes its own network address list, and the compartment comes from the stack.

#### How the rule statements map The translation is statement by statement, into whichever rule module fits. An `ip_set_reference_statement` becomes a network address list holding the CIDRs plus an access-control rule that references it. A `rate_based_statement` becomes a rate-limiting rule keyed on the client connection, keeping the AWS request limit and its evaluation window unchanged. A `byte_match_statement` becomes an access-control rule with a JMESPATH condition over the request field, and a `geo_match_statement` becomes an access-control rule that tests the request's country code. OCI's condition language is JMESPATH, which is expressive but shaped differently from AWS's `field_to_match` plus positional-constraint model. The byte- and geo-match conditions therefore land as a best-effort JMESPATH expression: the intent is preserved, but the exact predicate should be checked against the OCI condition before the policy takes traffic. AWS-managed rule groups map onto OCI's request protection, whose capabilities are drawn from the OWASP Core Rule Set, so the injection and cross-site coverage holds while the specific rule identity does not.
Each AWS WAFv2 statement maps to an OCI construct: ip_set_reference to a network address list plus an access-control rule; rate_based to a request rate-limiting rule; byte_match to an access-control rule with a JMESPATH condition; geo_match to an access-control rule on the country code; managed_rule_group to a request-protection OWASP capability. Each AWS WAFv2 statement maps to an OCI construct: ip_set_reference to a network address list plus an access-control rule; rate_based to a request rate-limiting rule; byte_match to an access-control rule with a JMESPATH condition; geo_match to an access-control rule on the country code; managed_rule_group to a request-protection OWASP capability.

IP-set and rate rules map cleanly; byte- and geo-match conditions become best-effort JMESPATH access rules to verify; managed groups map onto the OWASP protection capabilities.

#### IP sets and network address lists OCI keeps a list of source addresses as its own resource, so an ip-set maps to an `oci_waf_network_address_list` of type ADDRESSES holding the CIDRs. This is closer to the AWS model than Azure's inline approach: the list is a single resource an access-control rule references, so a shared list lives in one place. The rule that consumes it is an access-control rule whose JMESPATH condition tests the request's source address against the list. That source-address condition is the one thing to verify. OCI expresses a source-against-list test through a specific JMESPATH function over the request's connection address, and the emitted condition is a best-effort rendering of the AWS ip-set match. The address list itself is exact (the CIDRs are copied verbatim), but the condition wiring should be confirmed against OCI's condition syntax before the policy enforces, which the build calls out so it is not assumed correct. * **An ip-set is its own resource:** it becomes an `oci_waf_network_address_list` of type ADDRESSES, referenced rather than inlined, so a shared list lives in one place. * **The CIDRs are exact:** the address list holds the exact prefixes from the ip-set. * **Verify the source-address condition:** the access-control rule's JMESPATH test is best-effort and is flagged at the build to confirm against OCI's condition syntax.
An aws_wafv2_ip_set becomes an oci_waf_network_address_list of type ADDRESSES holding the CIDRs, referenced by an access-control rule whose JMESPATH condition tests the request source address against the list. An aws_wafv2_ip_set becomes an oci_waf_network_address_list of type ADDRESSES holding the CIDRs, referenced by an access-control rule whose JMESPATH condition tests the request source address against the list.

Unlike Azure, an ip-set stays a standalone resource on OCI: its own network address list, referenced by the access-control rule's source-address condition.

#### Rate limiting and protection capabilities Rate-based rules become request rate-limiting rules with the AWS request limit as the `requests_limit` and the AWS evaluation window as the `period_in_seconds`, passed through unchanged. This is a cleaner mapping than Azure's: OCI takes an arbitrary window in seconds, so a 90-second or 120-second rate rule keeps its exact window rather than rounding to a fixed set. The rate rule references the block action, so exceeding the limit returns the policy's HTTP 403. AWS-managed rule groups map onto OCI's request protection. OCI ships managed protection capabilities drawn from the OWASP Core Rule Set (the same injection, cross-site-scripting, and protocol-violation families), so the managed-detection posture is preserved. What is lost is the exact rule identity: a specific AWS-managed rule id has no one-to-one OCI capability, so an alert or exclusion keyed on a particular AWS rule needs re-expressing against the OWASP capability it now corresponds to. OCI's protection capabilities are tunable in the console after the build (collaborative thresholds, per-capability exclusions), which is where a false-positive is dialed out. * **Rate windows pass through unrounded:** the AWS limit and evaluation window become `requests_limit` and `period_in_seconds`; an arbitrary window keeps its exact value. * **Exceeding the limit returns HTTP 403:** the rate rule references the policy's block action. * **Managed groups map onto OWASP capabilities:** the protection posture is preserved; a rule keyed on a specific AWS id is re-expressed against the OWASP capability. * **Protection is tunable after the build:** collaborative thresholds and exclusions are adjusted in the console to dial out a false positive. #### Named actions and the default action OCI declares its actions once at the top of the policy and lets rules reference them by name. Three are emitted: an allow action, a block action that returns an HTTP 403, and a check action that logs the match and lets the request continue. An AWS rule whose action is allow, block, or count maps onto the allow, block, or check action respectively, so the per-rule decision is kept intact. The default action is where OCI is the stronger fit of the two targets. An AWS web ACL sets a default of allow (pass-unless-blocked) or block (a deny-by-default allowlist). OCI's access-control module has its own required default action, so both map cleanly: a default of allow becomes the allow action, and a deny-by-default web ACL becomes the block action, so a request matching no rule is denied, and the allowlist posture is preserved rather than needing a hand-authored catch-all rule. This is the one place OCI matches a web ACL more closely than the Azure WAF policy, which has no whole-policy default deny. * **Three named actions:** allow, block (HTTP 403), and check (log and continue); each rule references one by name. * **Per-rule decisions line up.** AWS allow / block / count map onto allow / block / check. * **Deny-by-default is preserved:** a block-default web ACL maps to the access-control module's default block action, so unmatched requests are denied, with no catch-all rule needed. #### Limitations Five statement types translate to OCI constructs; the rest are named at the build rather than dropped quietly. These are the ceilings to check a web ACL against before cutover. △ Where AWS WAFv2 and the OCI WAF policy diverge * **Several statement types are not translated:** regex, size-constraint, and standalone SQLi / XSS match statements, label-match, and and / or / not composites are not emitted as rules; each is named at the build. Injection and cross-site checks are re-expressed through the OWASP protection capabilities, and a bespoke match is re-expressed as a JMESPATH access-control condition. * **Byte- and geo-match conditions are best-effort.** OCI's JMESPATH condition language differs from the AWS field-plus-positional model, so a byte-match or country condition lands as a best-effort expression to verify against OCI's condition syntax, not a byte-for-byte equivalent. * **The source-address condition on an ip-set rule is verified, not assumed:** the address list is exact, but the access-control rule's source-against-list test is a best-effort JMESPATH rendering flagged at the build. * **Managed-rule-group identity does not port.** AWS-managed groups map onto OCI's OWASP protection capabilities: the protection posture holds, but a specific AWS rule id has no one-to-one OCI capability, so per-rule alerts and exclusions are re-authored. * **There is no dedicated geo operator.** OCI matches a country through a JMESPATH comparison on the request's country code rather than a dedicated geo operator, so a large country list is expressed as a condition rather than a single match block. #### Other considerations Beyond the rule-by-rule mapping, a few operational realities are worth stating plainly. * **Attach the policy to an enforcement point:** the WAF policy is emitted as a resource; associating it with the load balancer or gateway that fronts the workload is the one manual step, the same association a web ACL needs on AWS. * **The policy is referenced by name and id:** an OCI WAF policy has no ARN analog, so anything that referenced the web ACL by ARN is re-pointed at the policy's id or name. * **The compartment comes from the stack:** the policy and its network address lists land in the compartment the stack runs in, threaded in as a variable. * **Enforcement is on from the start:** the block action returns an HTTP 403 on a match; switch a rule to the check action first if a soak period is wanted. * **Verify conditions before taking traffic:** the best-effort JMESPATH conditions (source address, byte-match, country) are the shape to review in the console once, since OCI's condition language is where the divergence lives. [Service Catalog](/service-adapters/catalog). # Azure AI Search Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-ai-search Azure AI Search indexes content for keyword, vector and semantic queries; configured processing steps can extract or add information during indexing. Azure AI Search is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure API Management Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-api-management Azure API Management applies access and traffic policies to APIs through a gateway, with subscriptions and a developer portal for API users. Azure API Management is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure App Service Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-app-service Azure App Service hosts web applications and APIs on a shared plan, offering deployment slots, scaling rules and built-in authentication. Azure App Service is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure Application Insights Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-application-insights Azure Application Insights collects application requests, dependencies and exceptions, and shows how application components communicate. Azure Application Insights is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure Cosmos DB Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-cosmos-db Azure Cosmos DB is a multi-model database that partitions by key, meters throughput in request units and offers five consistency levels. Azure Cosmos DB is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure Data Factory Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-data-factory Azure Data Factory runs pipelines that copy and transform data, using configured connections to data sources and runtimes that execute the work. Azure Data Factory is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure Databricks Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-databricks Azure Databricks provides Databricks workspaces on Azure, with Unity Catalog governance, Delta Lake tables and the Databricks Runtime. Azure Databricks is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure DNS Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-dns Azure DNS hosts authoritative records in public or private zones, with private zones resolvable from linked virtual networks. Azure DNS is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure Event Grid Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-event-grid Azure Event Grid routes events from Azure services and custom publishers to subscribers, filtering on event type and subject. Azure Event Grid is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure Event Hubs Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-event-hubs Azure Event Hubs ingests event streams into partitions that consumers read by offset, with Capture optionally writing the same stream to storage. Azure Event Hubs is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure Front Door Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-front-door Azure Front Door is a global entry point that routes, caches and inspects traffic at Microsoft's edge locations before it reaches your origin. Azure Front Door is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure Functions Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-functions Azure Functions runs event-driven code on a hosting plan, with triggers and output bindings that connect functions to other services. Azure Functions is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure Logic Apps Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-logic-apps Azure Logic Apps runs workflows defined as JSON, wiring managed connectors to Azure and third-party services without custom code. Azure Logic Apps is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure Managed Redis Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-managed-redis Azure Managed Redis provides a Redis-compatible cache on selected tiers, with clustering and optional persistence to storage. Azure Managed Redis is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure Monitor Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-monitor Azure Monitor collects metrics and logs and evaluates alert rules. Azure Monitor Metrics stores time-series metrics; Log Analytics workspaces store logs queried with Kusto Query Language. Azure Monitor is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure Service Bus Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-service-bus Azure Service Bus is a message broker offering queues and topics with sessions, transactions, duplicate detection and dead-letter subqueues. Azure Service Bus is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure SignalR Service Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-signalr-service Azure SignalR Service holds real-time client connections and fans messages out to hubs, groups and individual users on the application's behalf. Azure SignalR Service is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure SQL Database Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-sql-database Azure SQL Database runs the SQL Server engine as a managed service, with elastic pools, Hyperscale storage and automatic tuning. Azure SQL Database is available on Azure only. [Service Catalog](/service-adapters/catalog). # Azure Stream Analytics Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/azure-stream-analytics Azure Stream Analytics runs continuous queries over streaming input, written in a SQL dialect with windowing and pattern matching. Azure Stream Analytics is available on Azure only. [Service Catalog](/service-adapters/catalog). # Blob Storage Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/blob-storage Azure Blob Storage keeps objects in containers inside a storage account, with access tiers, lifecycle rules and optional immutability policies. Blob Storage is available on Azure only. [Service Catalog](/service-adapters/catalog). # Container Apps Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/container-apps Azure Container Apps runs containers in a managed environment, scales them in response to events using KEDA, and provides Dapr components for service-to-service calls. Container Apps is available on Azure only. [Service Catalog](/service-adapters/catalog). # Container Registry (ACR) Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/container-registry-acr Azure Container Registry stores container images and Helm charts per registry, with geo-replication and Entra ID authentication. Container Registry (ACR) is available on Azure only. [Service Catalog](/service-adapters/catalog). # Microsoft Entra ID Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/microsoft-entra-id Microsoft Entra ID is the tenant directory holding users, groups and app registrations, and it issues the tokens Azure resources accept. Microsoft Entra ID is available on Azure only. [Service Catalog](/service-adapters/catalog). # Microsoft Foundry Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/microsoft-foundry Microsoft Foundry, formerly Azure AI Foundry, hosts model deployments and agents behind Microsoft-operated endpoints with content filtering applied in the service. Microsoft Foundry is available on Azure only. [Service Catalog](/service-adapters/catalog). # MySQL Flexible Server Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/mysql-flexible-server Azure Database for MySQL Flexible Server runs managed MySQL with a chosen compute tier, configurable maintenance windows and zone-redundant options. MySQL Flexible Server is available on Azure only. [Service Catalog](/service-adapters/catalog). # PostgreSQL Flexible Server Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/postgresql-flexible-server Azure Database for PostgreSQL Flexible Server runs managed PostgreSQL with server parameters you set, automated backups and zone-redundant high availability. PostgreSQL Flexible Server is available on Azure only. [Service Catalog](/service-adapters/catalog). # Virtual Machine Scale Sets Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/virtual-machine-scale-sets Azure Virtual Machine Scale Sets run a set of identical VMs from one model, resizing on metrics or schedule and spreading them over fault domains. Virtual Machine Scale Sets is available on Azure only. [Service Catalog](/service-adapters/catalog). # Virtual Machines Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/virtual-machines An Azure virtual machine booted from an image onto a VM size, joined to a virtual network subnet with managed disks attached. Virtual Machines is available on Azure only. [Service Catalog](/service-adapters/catalog). # Virtual Network Source: https://docs.tensor9.com/service-adapters/azure/available-on-azure-only/virtual-network An Azure virtual network with your own address space, subnets, route tables and network security groups filtering traffic per subnet or interface. Virtual Network is available on Azure only. [Service Catalog](/service-adapters/catalog). # Service Catalog Source: https://docs.tensor9.com/service-adapters/catalog Find the services your application uses under its origin cloud, then check the columns for your customer's target cloud. Open a service page for its supported operations and limitations. For an explanation of the tiers and the Tensor9 runtime, see [how service adapters work](/service-adapters/overview). ## How to read this table **Tier** describes the runtime dependency on Tensor9. *Infrastructure only* means your application calls the target service directly. *Max* serves supported application API calls through Tensor9. Resource-management API support depends on the service and target. The service row shows the highest tier offered across its published targets; individual target and operation details determine what your application needs. A stack uses the highest tier required by any of its services: one service at Max makes the stack's tier Max. See [adaptation tiers](/service-adapters/overview). **The cloud columns** show targets for each origin service. The origin cloud is omitted because using a service on its own cloud needs no adaptation. ✓ means a supported or approved intended target; - means no adaptation is offered on that cloud. A checkmark does not promise every API operation: the service page states the supported scope. A dash in the tier column means no adaptation tier applies or no tier is specified. The service page explains any capability supplied by the target environment. ## Coverage by target cloud ### AWS | Service | Tier | Google Cloud | Azure | OCI | Private Kubernetes | | ------------------------------------------------------------------------------------------------------------------------------ | ------------------- | ------------ | ----- | --- | ------------------ | | [ACM (certificates)](/service-adapters/aws/security-identity/acm-certificates) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [ACM Private CA](/service-adapters/aws/security-identity/acm-private-ca) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [Amazon MQ RabbitMQ Cluster](/service-adapters/aws/messaging-streaming/amazon-mq-rabbitmq-cluster) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [Amazon MQ RabbitMQ Single](/service-adapters/aws/messaging-streaming/amazon-mq-rabbitmq-single) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [Amplify](/service-adapters/aws/available-on-aws-only/amplify) | - | - | - | - | - | | [API Gateway (REST)](/service-adapters/aws/networking-traffic/api-gateway-rest) Preview | - | - | - | - | - | | [API Gateway v2 (HTTP/WebSocket)](/service-adapters/aws/networking-traffic/api-gateway-v2-http-websocket) Preview | - | - | - | - | - | | [AppConfig](/service-adapters/aws/other-services/appconfig) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [Application Load Balancer](/service-adapters/aws/networking-traffic/application-load-balancer) | Max | ✓ | ✓ | ✓ | - | | [AppSync](/service-adapters/aws/available-on-aws-only/appsync) | - | - | - | - | - | | [Athena](/service-adapters/aws/available-on-aws-only/athena) | - | - | - | - | - | | [Aurora PostgreSQL](/service-adapters/aws/databases-storage/aurora-postgresql) | Max | ✓ | ✓ | ✓ | ✓ | | [AWS Backup](/service-adapters/aws/available-on-aws-only/aws-backup) | - | - | - | - | - | | [Batch](/service-adapters/aws/available-on-aws-only/batch) | - | - | - | - | - | | [Bedrock (LLM inference)](/service-adapters/aws/ai-machine-learning/bedrock-llm-inference) | Max | ✓ | ✓ | ✓ | ✓ | | [Cloud Map (Service Discovery)](/service-adapters/aws/available-on-aws-only/cloud-map-service-discovery) | - | - | - | - | - | | [CloudFormation](/service-adapters/aws/available-on-aws-only/cloudformation) | - | - | - | - | - | | [CloudFront](/service-adapters/aws/networking-traffic/cloudfront) | Infrastructure only | ✓ | ✓ | - | - | | [CloudWatch](/service-adapters/aws/available-on-aws-only/cloudwatch) | - | - | - | - | - | | [CloudWatch Alarms](/service-adapters/aws/available-on-aws-only/cloudwatch-alarms) | - | - | - | - | - | | [CloudWatch Dashboards](/service-adapters/aws/available-on-aws-only/cloudwatch-dashboards) | - | - | - | - | - | | [CloudWatch Logs](/service-adapters/aws/available-on-aws-only/cloudwatch-logs) | - | - | - | - | - | | [CloudWatch Metrics](/service-adapters/aws/available-on-aws-only/cloudwatch-metrics) | - | - | - | - | - | | [CodeBuild](/service-adapters/aws/available-on-aws-only/codebuild) | - | - | - | - | - | | [CodePipeline](/service-adapters/aws/available-on-aws-only/codepipeline) | - | - | - | - | - | | [Cognito](/service-adapters/aws/available-on-aws-only/cognito) | - | - | - | - | - | | [Cognito Identity Pools](/service-adapters/aws/available-on-aws-only/cognito-identity-pools) | - | - | - | - | - | | [DMS (Database Migration)](/service-adapters/aws/available-on-aws-only/dms-database-migration) | - | - | - | - | - | | [DocumentDB (MongoDB)](/service-adapters/aws/databases-storage/documentdb-mongodb) | Max | ✓ | ✓ | ✓ | ✓ | | [DynamoDB (control)](/service-adapters/aws/databases-storage/dynamodb-control) | Max | ✓ | ✓ | ✓ | ✓ | | [EC2](/service-adapters/aws/compute-containers/ec2) | Max | ✓ | ✓ | ✓ | ✓ | | [EC2 Auto Scaling](/service-adapters/aws/compute-containers/ec2-auto-scaling) | Max | ✓ | ✓ | ✓ | ✓ | | [ECR](/service-adapters/aws/compute-containers/ecr) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [ECR Public](/service-adapters/aws/compute-containers/ecr-public) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [ECS](/service-adapters/aws/compute-containers/ecs) | Max | ✓ | ✓ | ✓ | ✓ | | [EFS](/service-adapters/aws/databases-storage/efs) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [EKS](/service-adapters/aws/compute-containers/eks) | Max | ✓ | ✓ | ✓ | ✓ | | [EKS ALB Ingress](/service-adapters/aws/compute-containers/eks-alb-ingress) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [EKS IRSA](/service-adapters/aws/compute-containers/eks-irsa) | Max | ✓ | ✓ | ✓ | ✓ | | [EKS NLB Service](/service-adapters/aws/compute-containers/eks-nlb-service) | Infrastructure only | - | - | - | ✓ | | [EKS NLB Service (TLS)](/service-adapters/aws/compute-containers/eks-nlb-service-tls) | Infrastructure only | - | - | - | ✓ | | [EKS Pod Identity](/service-adapters/aws/compute-containers/eks-pod-identity) | Max | ✓ | ✓ | ✓ | ✓ | | [ElastiCache (Valkey/Redis)](/service-adapters/aws/databases-storage/elasticache-valkey-redis) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [ElastiCache Serverless (Valkey/Redis)](/service-adapters/aws/databases-storage/elasticache-serverless-valkey-redis) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [EMR](/service-adapters/aws/other-services/emr) | Infrastructure only | ✓ | - | - | - | | [EventBridge](/service-adapters/aws/messaging-streaming/eventbridge) | Max | ✓ | ✓ | ✓ | ✓ | | [Glue](/service-adapters/aws/available-on-aws-only/glue) | - | - | - | - | - | | [GuardDuty](/service-adapters/aws/other-services/guardduty) | Infrastructure only | ✓ | ✓ | ✓ | - | | [GuardDuty Detector](/service-adapters/aws/other-services/guardduty-detector) | Infrastructure only | ✓ | ✓ | ✓ | - | | [AWS IAM](/service-adapters/aws/security-identity/aws-iam) | Max | ✓ | ✓ | ✓ | ✓ | | [IAM Identity Center (SSO)](/service-adapters/aws/available-on-aws-only/iam-identity-center-sso) | - | - | - | - | - | | [IoT Core](/service-adapters/aws/available-on-aws-only/iot-core) | - | - | - | - | - | | [Kinesis](/service-adapters/aws/messaging-streaming/kinesis) | Max | ✓ | ✓ | ✓ | ✓ | | [Kinesis Data Firehose](/service-adapters/aws/messaging-streaming/kinesis-data-firehose) | Max | ✓ | ✓ | ✓ | ✓ | | [KMS](/service-adapters/aws/security-identity/kms) | Max | ✓ | ✓ | ✓ | ✓ | | [Lake Formation](/service-adapters/aws/available-on-aws-only/lake-formation) | - | - | - | - | - | | [Lambda](/service-adapters/aws/compute-containers/lambda) | Max | ✓ | ✓ | ✓ | ✓ | | [Managed Service for Apache Flink](/service-adapters/aws/other-services/managed-service-for-apache-flink) Preview | - | - | - | - | - | | [MemoryDB](/service-adapters/aws/other-services/memorydb) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [MSK (Kafka)](/service-adapters/aws/messaging-streaming/msk-kafka) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [MWAA (Managed Airflow)](/service-adapters/aws/other-services/mwaa-managed-airflow) | Infrastructure only | ✓ | - | - | - | | [Neptune (graph)](/service-adapters/aws/available-on-aws-only/neptune-graph) | - | - | - | - | - | | [Network Firewall](/service-adapters/aws/networking-traffic/network-firewall) | Infrastructure only | ✓ | ✓ | ✓ | - | | [Network Load Balancer](/service-adapters/aws/networking-traffic/network-load-balancer) | Max | ✓ | ✓ | ✓ | - | | [Network Manager](/service-adapters/aws/available-on-aws-only/network-manager) | - | - | - | - | - | | [OpenSearch Domain](/service-adapters/aws/databases-storage/opensearch-domain) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [OpenSearch Serverless](/service-adapters/aws/databases-storage/opensearch-serverless) Preview | - | - | - | - | - | | [Organizations](/service-adapters/aws/available-on-aws-only/organizations) | - | - | - | - | - | | [Pinpoint](/service-adapters/aws/available-on-aws-only/pinpoint) | - | - | - | - | - | | [RDS MySQL](/service-adapters/aws/databases-storage/rds-mysql) | Max | ✓ | ✓ | ✓ | ✓ | | [RDS PostgreSQL](/service-adapters/aws/databases-storage/rds-postgresql) | Max | ✓ | ✓ | ✓ | ✓ | | [Redshift](/service-adapters/aws/available-on-aws-only/redshift) | - | - | - | - | - | | [Resource Access Manager](/service-adapters/aws/available-on-aws-only/resource-access-manager) | - | - | - | - | - | | [Resource Groups](/service-adapters/aws/available-on-aws-only/resource-groups) | - | - | - | - | - | | [Route 53](/service-adapters/aws/networking-traffic/route-53) | Max | ✓ | ✓ | ✓ | ✓ | | [Route 53 Resolver](/service-adapters/aws/networking-traffic/route-53-resolver) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [S3](/service-adapters/aws/databases-storage/s3) | Max | ✓ | ✓ | ✓ | ✓ | | [S3 Glacier](/service-adapters/aws/databases-storage/s3-glacier) | Max | ✓ | ✓ | ✓ | ✓ | | [S3 Tables](/service-adapters/aws/available-on-aws-only/s3-tables) | - | - | - | - | - | | [S3 Vectors](/service-adapters/aws/available-on-aws-only/s3-vectors) | - | - | - | - | - | | [SageMaker (Inference)](/service-adapters/aws/other-services/sagemaker-inference) | Max | ✓ | ✓ | - | - | | [SageMaker AI (Training and Notebooks)](/service-adapters/aws/available-on-aws-only/sagemaker-ai-training-and-notebooks) | - | - | - | - | - | | [Secrets Manager](/service-adapters/aws/security-identity/secrets-manager) | Max | ✓ | ✓ | ✓ | ✓ | | [SNS](/service-adapters/aws/messaging-streaming/sns) | Max | ✓ | ✓ | ✓ | ✓ | | [SQS (FIFO)](/service-adapters/aws/messaging-streaming/sqs-fifo) | Max | ✓ | ✓ | ✓ | ✓ | | [SQS (Standard)](/service-adapters/aws/messaging-streaming/sqs-standard) | Max | ✓ | ✓ | ✓ | ✓ | | [Step Functions](/service-adapters/aws/other-services/step-functions) Preview | - | - | - | - | - | | [Systems Manager (SSM)](/service-adapters/aws/configuration-management/systems-manager-ssm) | Max | ✓ | ✓ | ✓ | ✓ | | [Transfer Family (SFTP)](/service-adapters/aws/other-services/transfer-family-sftp) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [Transfer Server (SFTP)](/service-adapters/aws/other-services/transfer-server-sftp) | Infrastructure only | ✓ | ✓ | ✓ | ✓ | | [Verified Permissions](/service-adapters/aws/available-on-aws-only/verified-permissions) | - | - | - | - | - | | [VPC](/service-adapters/aws/networking-traffic/vpc) | Max | ✓ | ✓ | ✓ | ✓ | | [VPC Flow Logs](/service-adapters/aws/networking-traffic/vpc-flow-logs) Preview | - | - | - | - | - | | [VPC Lattice](/service-adapters/aws/available-on-aws-only/vpc-lattice) | - | - | - | - | - | | [WAFv2](/service-adapters/aws/security-identity/wafv2) | Infrastructure only | ✓ | ✓ | ✓ | - | ### Google Cloud Google Cloud services deploy on Google Cloud only; cross-cloud adaptation is not offered. | Service | Tier | AWS | Azure | OCI | Private Kubernetes | | ---------------------------------------------------------------------------------------------------------------------- | ---- | --- | ----- | --- | ------------------ | | [AlloyDB for PostgreSQL](/service-adapters/google-cloud/available-on-google-cloud-only/alloydb-for-postgresql) | - | - | - | - | - | | [Apigee API Management](/service-adapters/google-cloud/available-on-google-cloud-only/apigee-api-management) | - | - | - | - | - | | [App Engine](/service-adapters/google-cloud/available-on-google-cloud-only/app-engine) | - | - | - | - | - | | [Artifact Registry](/service-adapters/google-cloud/available-on-google-cloud-only/artifact-registry) | - | - | - | - | - | | [BigQuery](/service-adapters/google-cloud/available-on-google-cloud-only/bigquery) | - | - | - | - | - | | [Cloud Bigtable](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-bigtable) | - | - | - | - | - | | [Cloud DNS](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-dns) | - | - | - | - | - | | [Cloud KMS](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-kms) | - | - | - | - | - | | [Cloud Load Balancing](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-load-balancing) | - | - | - | - | - | | [Cloud Logging](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-logging) | - | - | - | - | - | | [Cloud Monitoring](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-monitoring) | - | - | - | - | - | | [Cloud Run](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-run) | - | - | - | - | - | | [Cloud Run functions](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-run-functions) | - | - | - | - | - | | [Cloud Spanner](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-spanner) | - | - | - | - | - | | [Cloud SQL for MySQL](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-sql-for-mysql) | - | - | - | - | - | | [Cloud SQL for PostgreSQL](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-sql-for-postgresql) | - | - | - | - | - | | [Cloud Storage](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-storage) | - | - | - | - | - | | [Cloud Tasks](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-tasks) | - | - | - | - | - | | [Cloud Workflows](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-workflows) | - | - | - | - | - | | [Compute Engine](/service-adapters/google-cloud/available-on-google-cloud-only/compute-engine) | - | - | - | - | - | | [Dataflow](/service-adapters/google-cloud/available-on-google-cloud-only/dataflow) | - | - | - | - | - | | [Firestore](/service-adapters/google-cloud/available-on-google-cloud-only/firestore) | - | - | - | - | - | | [Google Kubernetes Engine](/service-adapters/google-cloud/available-on-google-cloud-only/google-kubernetes-engine) | - | - | - | - | - | | [Google Pub/Sub](/service-adapters/google-cloud/available-on-google-cloud-only/google-pub-sub) | - | - | - | - | - | | [Google Cloud IAM](/service-adapters/google-cloud/available-on-google-cloud-only/google-cloud-iam) | - | - | - | - | - | | [Identity Platform](/service-adapters/google-cloud/available-on-google-cloud-only/identity-platform) | - | - | - | - | - | | [Memorystore for Redis](/service-adapters/google-cloud/available-on-google-cloud-only/memorystore-for-redis) | - | - | - | - | - | | [Secret Manager](/service-adapters/google-cloud/available-on-google-cloud-only/secret-manager) | - | - | - | - | - | | [Virtual Private Cloud (VPC)](/service-adapters/google-cloud/available-on-google-cloud-only/virtual-private-cloud-vpc) | - | - | - | - | - | ### Azure Azure services deploy on Azure only; cross-cloud adaptation is not offered. | Service | Tier | AWS | Google Cloud | OCI | Private Kubernetes | | -------------------------------------------------------------------------------------------------------- | ---- | --- | ------------ | --- | ------------------ | | [Azure AI Search](/service-adapters/azure/available-on-azure-only/azure-ai-search) | - | - | - | - | - | | [Azure API Management](/service-adapters/azure/available-on-azure-only/azure-api-management) | - | - | - | - | - | | [Azure App Service](/service-adapters/azure/available-on-azure-only/azure-app-service) | - | - | - | - | - | | [Azure Application Insights](/service-adapters/azure/available-on-azure-only/azure-application-insights) | - | - | - | - | - | | [Azure Cosmos DB](/service-adapters/azure/available-on-azure-only/azure-cosmos-db) | - | - | - | - | - | | [Azure Data Factory](/service-adapters/azure/available-on-azure-only/azure-data-factory) | - | - | - | - | - | | [Azure Databricks](/service-adapters/azure/available-on-azure-only/azure-databricks) | - | - | - | - | - | | [Azure DNS](/service-adapters/azure/available-on-azure-only/azure-dns) | - | - | - | - | - | | [Azure Event Grid](/service-adapters/azure/available-on-azure-only/azure-event-grid) | - | - | - | - | - | | [Azure Event Hubs](/service-adapters/azure/available-on-azure-only/azure-event-hubs) | - | - | - | - | - | | [Azure Front Door](/service-adapters/azure/available-on-azure-only/azure-front-door) | - | - | - | - | - | | [Azure Functions](/service-adapters/azure/available-on-azure-only/azure-functions) | - | - | - | - | - | | [Azure Logic Apps](/service-adapters/azure/available-on-azure-only/azure-logic-apps) | - | - | - | - | - | | [Azure Managed Redis](/service-adapters/azure/available-on-azure-only/azure-managed-redis) | - | - | - | - | - | | [Azure Monitor](/service-adapters/azure/available-on-azure-only/azure-monitor) | - | - | - | - | - | | [Azure Service Bus](/service-adapters/azure/available-on-azure-only/azure-service-bus) | - | - | - | - | - | | [Azure SignalR Service](/service-adapters/azure/available-on-azure-only/azure-signalr-service) | - | - | - | - | - | | [Azure SQL Database](/service-adapters/azure/available-on-azure-only/azure-sql-database) | - | - | - | - | - | | [Azure Stream Analytics](/service-adapters/azure/available-on-azure-only/azure-stream-analytics) | - | - | - | - | - | | [Blob Storage](/service-adapters/azure/available-on-azure-only/blob-storage) | - | - | - | - | - | | [Container Apps](/service-adapters/azure/available-on-azure-only/container-apps) | - | - | - | - | - | | [Container Registry (ACR)](/service-adapters/azure/available-on-azure-only/container-registry-acr) | - | - | - | - | - | | [Microsoft Entra ID](/service-adapters/azure/available-on-azure-only/microsoft-entra-id) | - | - | - | - | - | | [Microsoft Foundry](/service-adapters/azure/available-on-azure-only/microsoft-foundry) | - | - | - | - | - | | [MySQL Flexible Server](/service-adapters/azure/available-on-azure-only/mysql-flexible-server) | - | - | - | - | - | | [PostgreSQL Flexible Server](/service-adapters/azure/available-on-azure-only/postgresql-flexible-server) | - | - | - | - | - | | [Virtual Machine Scale Sets](/service-adapters/azure/available-on-azure-only/virtual-machine-scale-sets) | - | - | - | - | - | | [Virtual Machines](/service-adapters/azure/available-on-azure-only/virtual-machines) | - | - | - | - | - | | [Virtual Network](/service-adapters/azure/available-on-azure-only/virtual-network) | - | - | - | - | - | Each service page explains the supported operations and limitations for its target clouds. If the page does not answer your deployment question, ask us. ## Services we do not adapt yet Tensor9 does not yet adapt these services from their native cloud to another cloud. If your application depends on one, we work with you on the options: adding the adapter, changing the application so it no longer depends on that service, or bypassing Tensor9 for that service and writing code directly against the target environment. The direction matters: a service listed here can still be a target for an adapter from another cloud. For example, Tensor9 can adapt SQS onto Azure Service Bus; it does not adapt an application using the Service Bus API onto AWS. If your application needs one of these, [tell us about your use case](https://www.tensor9.com/contact/). ### AWS * [Amplify](/service-adapters/aws/available-on-aws-only/amplify) * [AppSync](/service-adapters/aws/available-on-aws-only/appsync) * [Athena](/service-adapters/aws/available-on-aws-only/athena) * [AWS Backup](/service-adapters/aws/available-on-aws-only/aws-backup) * [Batch](/service-adapters/aws/available-on-aws-only/batch) * [Cloud Map (Service Discovery)](/service-adapters/aws/available-on-aws-only/cloud-map-service-discovery) * [CloudFormation](/service-adapters/aws/available-on-aws-only/cloudformation) * [CloudWatch](/service-adapters/aws/available-on-aws-only/cloudwatch) * [CloudWatch Alarms](/service-adapters/aws/available-on-aws-only/cloudwatch-alarms) * [CloudWatch Dashboards](/service-adapters/aws/available-on-aws-only/cloudwatch-dashboards) * [CloudWatch Logs](/service-adapters/aws/available-on-aws-only/cloudwatch-logs) * [CloudWatch Metrics](/service-adapters/aws/available-on-aws-only/cloudwatch-metrics) * [CodeBuild](/service-adapters/aws/available-on-aws-only/codebuild) * [CodePipeline](/service-adapters/aws/available-on-aws-only/codepipeline) * [Cognito](/service-adapters/aws/available-on-aws-only/cognito) * [Cognito Identity Pools](/service-adapters/aws/available-on-aws-only/cognito-identity-pools) * [DMS (Database Migration)](/service-adapters/aws/available-on-aws-only/dms-database-migration) * [Glue](/service-adapters/aws/available-on-aws-only/glue) * [IAM Identity Center (SSO)](/service-adapters/aws/available-on-aws-only/iam-identity-center-sso) * [IoT Core](/service-adapters/aws/available-on-aws-only/iot-core) * [Lake Formation](/service-adapters/aws/available-on-aws-only/lake-formation) * [Neptune (graph)](/service-adapters/aws/available-on-aws-only/neptune-graph) * [Network Manager](/service-adapters/aws/available-on-aws-only/network-manager) * [Organizations](/service-adapters/aws/available-on-aws-only/organizations) * [Pinpoint](/service-adapters/aws/available-on-aws-only/pinpoint) * [Redshift](/service-adapters/aws/available-on-aws-only/redshift) * [Resource Access Manager](/service-adapters/aws/available-on-aws-only/resource-access-manager) * [Resource Groups](/service-adapters/aws/available-on-aws-only/resource-groups) * [S3 Tables](/service-adapters/aws/available-on-aws-only/s3-tables) * [S3 Vectors](/service-adapters/aws/available-on-aws-only/s3-vectors) * [SageMaker AI (Training and Notebooks)](/service-adapters/aws/available-on-aws-only/sagemaker-ai-training-and-notebooks) * [Verified Permissions](/service-adapters/aws/available-on-aws-only/verified-permissions) * [VPC Lattice](/service-adapters/aws/available-on-aws-only/vpc-lattice) ### Google Cloud * [AlloyDB for PostgreSQL](/service-adapters/google-cloud/available-on-google-cloud-only/alloydb-for-postgresql) * [Apigee API Management](/service-adapters/google-cloud/available-on-google-cloud-only/apigee-api-management) * [App Engine](/service-adapters/google-cloud/available-on-google-cloud-only/app-engine) * [Artifact Registry](/service-adapters/google-cloud/available-on-google-cloud-only/artifact-registry) * [BigQuery](/service-adapters/google-cloud/available-on-google-cloud-only/bigquery) * [Cloud Bigtable](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-bigtable) * [Cloud DNS](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-dns) * [Cloud KMS](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-kms) * [Cloud Load Balancing](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-load-balancing) * [Cloud Logging](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-logging) * [Cloud Monitoring](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-monitoring) * [Cloud Run](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-run) * [Cloud Run functions](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-run-functions) * [Cloud Spanner](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-spanner) * [Cloud SQL for MySQL](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-sql-for-mysql) * [Cloud SQL for PostgreSQL](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-sql-for-postgresql) * [Cloud Storage](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-storage) * [Cloud Tasks](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-tasks) * [Cloud Workflows](/service-adapters/google-cloud/available-on-google-cloud-only/cloud-workflows) * [Compute Engine](/service-adapters/google-cloud/available-on-google-cloud-only/compute-engine) * [Dataflow](/service-adapters/google-cloud/available-on-google-cloud-only/dataflow) * [Firestore](/service-adapters/google-cloud/available-on-google-cloud-only/firestore) * [Google Kubernetes Engine](/service-adapters/google-cloud/available-on-google-cloud-only/google-kubernetes-engine) * [Google Pub/Sub](/service-adapters/google-cloud/available-on-google-cloud-only/google-pub-sub) * [Google Cloud IAM](/service-adapters/google-cloud/available-on-google-cloud-only/google-cloud-iam) * [Identity Platform](/service-adapters/google-cloud/available-on-google-cloud-only/identity-platform) * [Memorystore for Redis](/service-adapters/google-cloud/available-on-google-cloud-only/memorystore-for-redis) * [Secret Manager](/service-adapters/google-cloud/available-on-google-cloud-only/secret-manager) * [Virtual Private Cloud (VPC)](/service-adapters/google-cloud/available-on-google-cloud-only/virtual-private-cloud-vpc) ### Azure * [Azure AI Search](/service-adapters/azure/available-on-azure-only/azure-ai-search) * [Azure API Management](/service-adapters/azure/available-on-azure-only/azure-api-management) * [Azure App Service](/service-adapters/azure/available-on-azure-only/azure-app-service) * [Azure Application Insights](/service-adapters/azure/available-on-azure-only/azure-application-insights) * [Azure Cosmos DB](/service-adapters/azure/available-on-azure-only/azure-cosmos-db) * [Azure Data Factory](/service-adapters/azure/available-on-azure-only/azure-data-factory) * [Azure Databricks](/service-adapters/azure/available-on-azure-only/azure-databricks) * [Azure DNS](/service-adapters/azure/available-on-azure-only/azure-dns) * [Azure Event Grid](/service-adapters/azure/available-on-azure-only/azure-event-grid) * [Azure Event Hubs](/service-adapters/azure/available-on-azure-only/azure-event-hubs) * [Azure Front Door](/service-adapters/azure/available-on-azure-only/azure-front-door) * [Azure Functions](/service-adapters/azure/available-on-azure-only/azure-functions) * [Azure Logic Apps](/service-adapters/azure/available-on-azure-only/azure-logic-apps) * [Azure Managed Redis](/service-adapters/azure/available-on-azure-only/azure-managed-redis) * [Azure Monitor](/service-adapters/azure/available-on-azure-only/azure-monitor) * [Azure Service Bus](/service-adapters/azure/available-on-azure-only/azure-service-bus) * [Azure SignalR Service](/service-adapters/azure/available-on-azure-only/azure-signalr-service) * [Azure SQL Database](/service-adapters/azure/available-on-azure-only/azure-sql-database) * [Azure Stream Analytics](/service-adapters/azure/available-on-azure-only/azure-stream-analytics) * [Blob Storage](/service-adapters/azure/available-on-azure-only/blob-storage) * [Container Apps](/service-adapters/azure/available-on-azure-only/container-apps) * [Container Registry (ACR)](/service-adapters/azure/available-on-azure-only/container-registry-acr) * [Microsoft Entra ID](/service-adapters/azure/available-on-azure-only/microsoft-entra-id) * [Microsoft Foundry](/service-adapters/azure/available-on-azure-only/microsoft-foundry) * [MySQL Flexible Server](/service-adapters/azure/available-on-azure-only/mysql-flexible-server) * [PostgreSQL Flexible Server](/service-adapters/azure/available-on-azure-only/postgresql-flexible-server) * [Virtual Machine Scale Sets](/service-adapters/azure/available-on-azure-only/virtual-machine-scale-sets) * [Virtual Machines](/service-adapters/azure/available-on-azure-only/virtual-machines) * [Virtual Network](/service-adapters/azure/available-on-azure-only/virtual-network) # AlloyDB for PostgreSQL Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/alloydb-for-postgresql AlloyDB is a PostgreSQL-compatible database on Google Cloud, with a columnar query engine, read-only instances and storage that scales separately from compute. AlloyDB for PostgreSQL is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Apigee API Management Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/apigee-api-management Apigee on Google Cloud manages API traffic through proxies with configurable policies and reusable processing steps. Apigee API Management is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # App Engine Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/app-engine Google Cloud App Engine hosts applications as services and versions, with instance classes and scaling settings declared per version. App Engine is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Artifact Registry Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/artifact-registry Google Cloud Artifact Registry stores container images and language packages in per-region repositories, with IAM permissions set per repository. Artifact Registry is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # BigQuery Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/bigquery Google Cloud BigQuery is a serverless data warehouse that separates table storage from query compute, billed either per slot or per byte scanned. BigQuery is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Cloud Bigtable Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/cloud-bigtable Bigtable on Google Cloud stores rows ordered by key, groups related columns into column families, and distributes ranges of rows across storage partitions. Cloud Bigtable is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Cloud DNS Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/cloud-dns Google Cloud DNS serves authoritative records from managed zones, public or private to chosen VPCs, with DNSSEC and split-horizon views. Cloud DNS is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Cloud KMS Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/cloud-kms Google Cloud KMS holds encryption keys in key rings scoped to a location, and encrypts or decrypts without releasing the key material. Cloud KMS is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Cloud Load Balancing Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/cloud-load-balancing Cloud Load Balancing on Google Cloud can route traffic to backends across regions through a global IP address, using health checks and URL-based routing rules. Cloud Load Balancing is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Cloud Logging Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/cloud-logging Cloud Logging on Google Cloud stores log entries in buckets, routes them to configured destinations and provides a query language for searching them. Cloud Logging is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Cloud Monitoring Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/cloud-monitoring Google Cloud Monitoring collects metric time series and evaluates alerting policies; metrics scopes determine which projects can be monitored together. Cloud Monitoring is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Cloud Run Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/cloud-run Cloud Run on Google Cloud runs containers that scale on request volume, splitting traffic across revisions and scaling to zero when idle. Cloud Run is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Cloud Run functions Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/cloud-run-functions Cloud Run functions, formerly Cloud Functions, builds and runs functions from source on Google Cloud, invoked by HTTP requests or events. Cloud Run functions is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Cloud Spanner Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/cloud-spanner Spanner is a Google Cloud relational database that distributes data across servers and supports SQL queries and transactions across regions. Cloud Spanner is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Cloud SQL for MySQL Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/cloud-sql-for-mysql Google Cloud SQL for MySQL runs managed MySQL, taking backups, applying patches and offering a regional standby that fails over automatically. Cloud SQL for MySQL is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Cloud SQL for PostgreSQL Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/cloud-sql-for-postgresql Google Cloud SQL for PostgreSQL runs managed PostgreSQL with automated backups, point-in-time recovery, read replicas and a choice of supported extensions. Cloud SQL for PostgreSQL is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Cloud Storage Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/cloud-storage Google Cloud Storage holds objects in buckets with a single global namespace, storage classes per bucket or object, and strong consistency on read. Cloud Storage is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Cloud Tasks Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/cloud-tasks Cloud Tasks on Google Cloud queues HTTP requests for later delivery, with per-queue rate limits and a schedule set per task. Cloud Tasks is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Cloud Workflows Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/cloud-workflows Workflows runs step-by-step orchestrations defined in YAML, calling Google Cloud services and HTTP endpoints and waiting on callbacks. Cloud Workflows is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Compute Engine Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/compute-engine A virtual machine on Google Cloud, booted from an image onto a machine type, attached to a VPC subnet and persistent disks. Compute Engine is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Dataflow Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/dataflow Google Cloud Dataflow runs Apache Beam batch and streaming pipelines as managed jobs, with Streaming Engine and Shuffle handled by the service. Dataflow is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Firestore Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/firestore Google Cloud Firestore is a document database whose client SDKs connect to it directly, with snapshot listeners, offline caching and server-side Security Rules. Firestore is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Google Cloud IAM Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/google-cloud-iam Google Cloud IAM grants roles on organizations, folders, projects and resources, with allow policies inheriting down that hierarchy. IAM is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Google Kubernetes Engine Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/google-kubernetes-engine Google Kubernetes Engine runs managed Kubernetes clusters on Google Cloud: Standard mode lets you size node pools, while Autopilot manages the nodes for you. Google Kubernetes Engine is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Google Pub/Sub Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/google-pub-sub Google Cloud Google Pub/Sub. Pub/Sub delivers messages from topics to subscriptions, with optional ordering by key and exactly-once delivery for pull subscriptions. Google Pub/Sub is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Identity Platform Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/identity-platform Google Cloud Identity Platform signs end users in across federated and password providers, issuing Google-signed tokens per tenant. Identity Platform is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Memorystore for Redis Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/memorystore-for-redis Google Cloud Memorystore for Redis provides a managed Redis instance on chosen capacity tiers, with an optional replica for automatic failover. Memorystore for Redis is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Secret Manager Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/secret-manager Google Cloud Secret Manager stores secrets as immutable versions, served over an API and encrypted with a Google or customer-managed key. Secret Manager is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # Virtual Private Cloud (VPC) Source: https://docs.tensor9.com/service-adapters/google-cloud/available-on-google-cloud-only/virtual-private-cloud-vpc A Google Cloud VPC: a global network whose subnets each belong to one region, with firewall rules applied by target tags and service accounts. Virtual Private Cloud (VPC) is available on Google Cloud only. [Service Catalog](/service-adapters/catalog). # How Service Adapters Work Source: https://docs.tensor9.com/service-adapters/overview A **service adapter** lets your application use an equivalent service in your customer's environment. You define your application and its infrastructure once. Tensor9 adapts that infrastructure to each customer's cloud and, where supported, handles the API differences so your application can keep its existing client libraries. The [Service Catalog](/service-adapters/catalog) describes the service mappings available for each target cloud. Your application can, for example, manage a database through the RDS API while sending SQL queries directly to the target PostgreSQL server. A VPC adapter handles network-management calls; the customer's cloud handles the application's network traffic. Choose the cloud your application is written for, then look for your customer's cloud in the target columns. Adaptation has a direction: support for SQS on Azure Service Bus does not imply support for moving a Service Bus application to AWS. Each service page explains its target services, supported operations and differences that affect applications.
Example builds from an AWS origin stack using DynamoDB and S3: AWS keeps those services; Google Cloud uses Firestore and Cloud Storage; Azure uses Cosmos DB and Blob Storage. Each deployment runs in its customer's environment. Adapted deployments answer supported cloud API calls through the service adapter, and data persists in the target services. Arrows show deployment builds, not runtime traffic. Example builds from an AWS origin stack using DynamoDB and S3: AWS keeps those services; Google Cloud uses Firestore and Cloud Storage; Azure uses Cosmos DB and Blob Storage. Each deployment runs in its customer's environment. Adapted deployments answer supported cloud API calls through the service adapter, and data persists in the target services. Arrows show deployment builds, not runtime traffic.
Example builds from an AWS origin stack using DynamoDB and S3: AWS keeps those services; Google Cloud uses Firestore and Cloud Storage; Azure uses Cosmos DB and Blob Storage. Each deployment runs in its customer's environment. Adapted deployments answer supported cloud API calls through the service adapter, and data persists in the target services. Arrows show deployment builds, not runtime traffic. Example builds from an AWS origin stack using DynamoDB and S3: AWS keeps those services; Google Cloud uses Firestore and Cloud Storage; Azure uses Cosmos DB and Blob Storage. Each deployment runs in its customer's environment. Adapted deployments answer supported cloud API calls through the service adapter, and data persists in the target services. Arrows show deployment builds, not runtime traffic.
## Adaptation Tiers An adaptation tier describes how much of a service Tensor9 takes on when your application runs in your customer's cloud. It covers the infrastructure Tensor9 provisions in the customer's environment and whether a service adapter answers your application's origin-cloud API calls at runtime. Tiers apply per service, so you choose service by service how far to adapt. The [Service Catalog](/service-adapters/catalog) lists the tiers available for each mapping, and your application's own API calls determine which of them it needs. | Tier | What Tensor9 provides | How the application uses it | | ----------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | **Max** | Equivalent infrastructure plus an adapter for supported cloud API calls. | Existing cloud clients send those calls to the adapter. Native protocols and application traffic can still go directly to the target service. | | **Infrastructure only** | Native cloud resources defined in Terraform you own and manage. The resources run in your customer's environment. | The application uses the target service's native API or protocol. Tensor9 does not answer its origin-cloud API calls at runtime. | Where both tiers are available, Infrastructure only is an option for applications that do not need the runtime adapter. For example, a PostgreSQL application can connect directly to Cloud SQL after Tensor9 updates its endpoint. If it also calls the RDS API to create or modify databases, it needs Max support for those operations. S3 illustrates a different case. Even when the target supports the S3 protocol, an AWS client signs requests for AWS. The adapter directs those requests to the target and signs them with the customer's credentials. Protocol compatibility alone does not remove this runtime dependency. When the origin and target clouds are the same, the stack runs natively. ## A stack's tier depends on its services and API calls Your stack requires the highest tier needed by any of its services. Consider an application with these requirements on Google Cloud: | Requirement | Adaptation | Required tier | | ---------------------------------------------------- | ----------------------------------------- | ------------------- | | Deploy Kubernetes workloads to a provisioned cluster | EKS infrastructure becomes GKE | Infrastructure only | | Query a provisioned PostgreSQL database | RDS infrastructure becomes Cloud SQL | Infrastructure only | | Resolve names in a provisioned DNS zone | Route 53 infrastructure becomes Cloud DNS | Infrastructure only | | Publish notifications through the SNS API | The adapter publishes to Pub/Sub | Max | This stack requires Max for SNS. Adding runtime EKS or RDS management calls also requires their Max adapters; the first two rows describe workloads that only use the provisioned cluster or database. You can reduce the required tier by removing or replacing a service that needs Max. An optional alerting topic may be a reasonable tradeoff; a database central to the application may not be. Changes to the origin stack affect every customer deployment, so assess that choice for the whole application. Declaring IAM roles and policies does not by itself require Max. At Infrastructure only, Tensor9 translates them into the target cloud's permissions at build time. Those permissions can be broader than the original AWS policies. See [AWS IAM](/service-adapters/aws/security-identity/aws-iam) for the differences between native permission translation and runtime authorization. Send us your origin stack and target cloud, and we will report its tier and the services that determine it. Selecting a tier yourself and previewing the resulting deployment is **(private beta)**. ## How Tensor9 adapters work A service adapter is a small server that runs inside your customer's appliance, in their cloud account or private environment. There is one adapter per service your application uses: a DynamoDB adapter, an S3 adapter, and so on. Each one serves the origin cloud's API for that service alone. Tensor9 points your application's SDK at the adapter, so those calls reach it inside the appliance instead of the origin cloud, with no change to your code. The adapter authenticates supported origin-cloud requests, translates them into target operations and returns the response format the client expects.
An application in the customer's Google Cloud environment keeps using the AWS SDK. A Tensor9 DynamoDB adapter serves the DynamoDB API and holds the tables in Cloud SQL for PostgreSQL; a Tensor9 S3 adapter serves the S3 API and holds the objects in Cloud Storage. Both adapters run beside the application, inside the customer's environment. An application in the customer's Google Cloud environment keeps using the AWS SDK. A Tensor9 DynamoDB adapter serves the DynamoDB API and holds the tables in Cloud SQL for PostgreSQL; a Tensor9 S3 adapter serves the S3 API and holds the objects in Cloud Storage. Both adapters run beside the application, inside the customer's environment.
An application in the customer's Google Cloud environment keeps using the AWS SDK. A Tensor9 DynamoDB adapter serves the DynamoDB API and holds the tables in Cloud SQL for PostgreSQL; a Tensor9 S3 adapter serves the S3 API and holds the objects in Cloud Storage. Both adapters run beside the application, inside the customer's environment. An application in the customer's Google Cloud environment keeps using the AWS SDK. A Tensor9 DynamoDB adapter serves the DynamoDB API and holds the tables in Cloud SQL for PostgreSQL; a Tensor9 S3 adapter serves the S3 API and holds the objects in Cloud Storage. Both adapters run beside the application, inside the customer's environment.
For an S3 request on Google Cloud, the objects live in the customer's Cloud Storage bucket. For an adapter built on a database, the database stores the service's records in a schema managed by the adapter. The service page explains which resources hold the data and who operates them. Management operations can take time. An adapter may record a requested change, create or update native resources, and report progress through subsequent API reads. The service's operation table describes which calls are supported and where the target behaves differently. Supported API-compatible mappings keep the application's existing client libraries. Some mappings require a different client or offer only part of the origin service's behavior; their pages state those differences. The adaptation tier describes the runtime components involved, not a guarantee that every operation is equivalent. At Max, cloud API calls depend on the adapter as well as the backing service. That does not put every database query or network packet through the adapter. Each service's architecture explains the request paths it handles. ## Find an adaptation Compare service mappings, adaptation tiers and target clouds. The [Service Catalog](/service-adapters/catalog#services-we-do-not-adapt-yet) also lists services we do not adapt yet. If a mapping you need is absent, [tell us about your use case](https://www.tensor9.com/contact/). ## Terms used here * **[Origin stack](/fundamentals/origin-stacks)**: the infrastructure configuration your application starts with, such as Terraform, CloudFormation, Helm or Docker Compose. * **[Appliance](/fundamentals/appliances)**: the system Tensor9 deploys into the customer's cloud or private environment. * **Service adapter**: the runtime component that answers supported origin-cloud API calls, one per service, running beside your application in the customer's environment. * **Customer**: the organization hosting your software in its own infrastructure. * **Vendor**: you, the software vendor defining the application, its origin stack and the target environments you offer to customers. ## Related topics * [Deployments](/fundamentals/deployments): how compilation uses service adapters * [Form factors](/fundamentals/key-concepts#form-factor): which services an environment can provide * [Appliances](/fundamentals/appliances): what gets deployed into the customer's environment * [How Tensor9 works](/fundamentals/how-tensor9-works): how Tensor9 prepares and deploys an application