---
name: azion-migrate-from-aws-to-azion
description: >-
  Move an AWS project to Azion: deploy it, recreate Lambda, CloudFront, and WAF rules, move S3, DynamoDB, and RDS data, then switch DNS.
---

# Migrate from AWS to Azion

A project on AWS spreads across CloudFront distributions, Lambda functions, API Gateway routes, S3 buckets, DynamoDB tables, RDS databases, WAF rules, Route 53 zones, and CloudWatch. Over time, each one gains its own cache behaviors, dependencies, access policies, capacity settings, health checks, and alarms. Moving the project means recreating each of these on Azion, then confirming that the project answers correctly before DNS changes.

On Azion, an [application](/en/documentation/platform/applications/) and its rules take over delivery, routing, and cache, and [Connectors](/en/documentation/platform/connectors/) reach the origins. [Functions](/en/documentation/platform/functions/) runs the Lambda code. [KV Store](/en/documentation/platform/kv-store/), [Object Storage](/en/documentation/platform/object-storage/), and [SQL Database](/en/documentation/platform/sql-database/) hold the data. [Firewall](/en/documentation/platform/firewall/) filters traffic, a [workload](/en/documentation/platform/workloads/) serves the domain, and [Edge DNS](/en/documentation/platform/edge-dns/) answers for the zone. [Real-Time Metrics](/en/documentation/platform/real-time-metrics/), [Real-Time Events](/en/documentation/platform/real-time-events/), and [Data Stream](/en/documentation/platform/data-stream/) show what happens to each request.

Each stage of this guide moves one layer, in the order a migration runs: inventory, deployment, code and rules, data, security, monitoring, and DNS. Lambda code moves with focused changes: the handler shape, the runtime language, and how the code opens storage and calls a model. When near-zero downtime is not a requirement, migrate in phases with maintenance windows. Writes stop during each step, so the data needs no parallel synchronization.

---

The prerequisites and the procedures on this page switch with the interface you select:

## Prerequisites

- An Azion account. To open one, [sign up in Azion Console](https://console.azion.com/signup). For more information, refer to [Create an account](/en/documentation/fundamentals/creating-account/).
- The AWS account, with access to its distributions, functions, data stores, and hosted zones.
- Access to the DNS records or the registrar of each domain you move.
- `curl` and `dig`, to check responses and DNS answers.

**Console**

- Access to Azion Console. To sign in, refer to [Access Azion Console](/en/documentation/guides/platform/account-and-billing/how-to-access-azion-console/).

**CLI**

- The [Azion CLI](/en/documentation/devtools/cli/), installed and authorized with your account. The commands on this page match Azion CLI 4.23.0.

**API**

- A personal token, sent in the `Authorization` header as `Token [TOKEN VALUE]`. To create one, refer to [Manage a personal token](/en/documentation/guides/platform/account-and-billing/personal-tokens/). Every request on this page goes to `https://api.azion.com/v4`.

---

## Inventory the AWS account

Do not start with the most complex application in the portfolio. Pick a project that matters enough to test the whole path and is small enough to move quickly. A good first project has a CloudFront distribution, a few Lambda functions, an S3 bucket, and perhaps a DynamoDB table. Use it to document the process and the patterns your team reuses. Then move more cache behaviors, functions, data, observability, and security rules, in the same order. Keep the first deployment small, and prove that it builds and runs on Azion before you move domains, storage, or databases.

Before you create anything on Azion, list what the project uses:

- SAM templates, CDK apps, and CloudFormation stacks, with their build and deploy commands.
- Environment variables and secrets, from the Lambda configuration, AWS Secrets Manager, Systems Manager Parameter Store, Elastic Beanstalk environment properties, CodePipeline and CodeBuild settings, and the source code.
- Lambda functions, their runtimes, triggers, and event sources.
- API Gateway APIs, their routes, authorizers, stages, and usage plans.
- Step Functions state machines and EventBridge rules.
- CloudFront distributions: origins, cache behaviors, cache and origin request policies, response headers policies, CloudFront Functions, and Lambda\@Edge triggers.
- Load balancers and their target groups.
- Image transformations and Bedrock calls.
- S3 buckets, DynamoDB tables, RDS and Aurora databases, and ElastiCache clusters.
- Route 53 hosted zones, records, routing policies, DNSSEC, and ACM certificates.
- WAF web ACLs, managed rule groups, IP sets, rate-based rules, Bot Control, Shield settings, and GuardDuty, Inspector, and Security Hub findings.
- CloudWatch dashboards, alarms, log groups, metric filters, X-Ray traces, Firehose streams, and RUM monitors.

Each item in the list maps to a stage of this guide. The table in Map each AWS product to Azion names the destination of each one.

---

## Map each AWS product to Azion

Every AWS product in the inventory has a destination on Azion. Find the product in the first column, then move it with the stage that names its destination. A dash (`-`) in the last column means that Azion has no direct equivalent.

| AWS product                              | What it covers                                                                   | Destination on Azion                                                                                                                                                               |
| ---------------------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Amazon CloudFront                        | Delivery of static and dynamic content, APIs, applications, and video            | Applications, served by a workload, with Connectors for the origins                                                                                                                |
| CloudFront distributions                 | Domains, origins, cache behavior, certificates, and security of one delivery     | Applications, Connectors, and [Workloads](/en/documentation/platform/workloads/)                                                                                                   |
| CloudFront alternate domain names        | Custom domains of a distribution                                                 | The **Domains** of a workload                                                                                                                                                      |
| CloudFront cache behaviors               | Per-path cache policy, origin request policy, methods, and function associations | [Rules Engine for Applications](/en/documentation/platform/applications/rules-engine/)                                                                                             |
| CloudFront cache policies                | Cache keys, TTLs, headers, cookies, query strings, and compression               | [Cache settings](/en/documentation/platform/applications/cache/cache-settings/)                                                                                                    |
| CloudFront origin request policies       | The request values forwarded to the origin                                       | Rules Engine for Applications and Connectors                                                                                                                                       |
| CloudFront response headers policies     | Security headers and CORS headers on responses                                   | Rules Engine for Applications                                                                                                                                                      |
| CloudFront Origin Shield                 | A central cache layer in front of the origin                                     | [Tiered Cache](/en/documentation/platform/applications/cache/tiered-cache/), a feature of Cache                                                                                    |
| CloudFront invalidation                  | Removal of content from cache before it expires                                  | [Real-Time Purge](/en/documentation/platform/applications/cache/real-time-purge/)                                                                                                  |
| CloudFront Functions                     | Lightweight JavaScript for redirects, rewrites, headers, and cache keys          | Functions                                                                                                                                                                          |
| Lambda\@Edge                             | Node.js or Python functions on CloudFront events                                 | Functions                                                                                                                                                                          |
| Lambda\@Edge for security logic          | Request validation, authentication, and blocking                                 | [Functions for Firewall](/en/documentation/platform/firewall/functions/)                                                                                                           |
| CloudFront device detection              | Device type passed to the origin                                                 | [Device Groups](/en/documentation/platform/applications/device-groups/)                                                                                                            |
| AWS Elemental MediaLive                  | Real-time encoding of live video                                                 | [Live Ingest](/en/documentation/platform/connectors/#live-ingest), a module of Connectors                                                                                          |
| AWS Elemental MediaPackage               | Packaging and origination of live streams for HLS and DASH                       | Live Ingest and Applications                                                                                                                                                       |
| CloudFront for live streaming            | Live video delivery with AWS Media Services                                      | Applications and Live Ingest                                                                                                                                                       |
| AWS Lambda                               | Serverless compute for event-driven code                                         | Functions                                                                                                                                                                          |
| Amazon API Gateway                       | REST, HTTP, and WebSocket APIs with backend integrations                         | Applications and Functions                                                                                                                                                         |
| API Gateway validation and throttling    | Request controls, throttling, and validation                                     | Rules Engine for Applications and Firewall                                                                                                                                         |
| AWS Amplify Hosting                      | Git-based CI/CD and hosting for static and server-rendered apps                  | Applications, served by a workload, and the [Azion CLI](/en/documentation/devtools/cli/)                                                                                           |
| AWS App Runner                           | Managed web applications from source code or container images                    | Applications and [Orchestrator](/en/documentation/platform/orchestrator/)                                                                                                          |
| Amazon Bedrock                           | Foundation models and generative AI development                                  | [AI Inference](/en/documentation/platform/ai-inference/)                                                                                                                           |
| Bedrock model inference                  | Text, image, and embedding model calls                                           | AI Inference                                                                                                                                                                       |
| Bedrock fine-tuning                      | Customization of foundation models                                               | [LoRA Fine-Tune](https://www.azion.com/en/learning/ai/what-is-lora-fine-tuning/)                                                                                                   |
| Bedrock custom model import              | Import of customized foundation models                                           | AI Inference and LoRA Fine-Tune. AI Inference runs a catalog of open-source models and imports none                                                                                |
| SageMaker AI real-time inference         | Managed real-time inference endpoints                                            | AI Inference                                                                                                                                                                       |
| Amazon S3                                | Object storage for assets, backups, archives, and websites                       | Object Storage                                                                                                                                                                     |
| S3 static website hosting                | Static websites served from a bucket                                             | Object Storage and Applications, through a storage connector                                                                                                                       |
| S3 Object Lambda                         | Objects transformed by Lambda on the way out                                     | Object Storage and Functions                                                                                                                                                       |
| S3 Object Lambda for images              | Resizing, watermarking, and redaction of retrieved content                       | [Image Processor](/en/documentation/platform/applications/image-processor/quickstart/) and Functions                                                                               |
| Amazon Aurora DSQL                       | Serverless distributed SQL for transactions                                      | SQL Database                                                                                                                                                                       |
| Amazon DynamoDB                          | Serverless key-value and document database                                       | KV Store, or SQL Database for queries                                                                                                                                              |
| DynamoDB global tables                   | Multi-Region replication of NoSQL data                                           | KV Store                                                                                                                                                                           |
| AWS WAF                                  | Web application firewall for applications and APIs                               | [Web Application Firewall](/en/documentation/platform/firewall/waf/quickstart/), a module of Firewall                                                                              |
| AWS Managed Rules for AWS WAF            | Managed rule groups for common vulnerabilities                                   | [WAF rule sets](/en/documentation/platform/firewall/waf/rules-set/)                                                                                                                |
| AWS WAF custom rules                     | Custom match conditions and actions                                              | [Rules Engine for Firewall](/en/documentation/platform/firewall/rules-engine/)                                                                                                     |
| AWS WAF Bot Control                      | Managed bot detection and mitigation                                             | [Bot Manager](/en/documentation/platform/firewall/bot-manager/quickstart/) and [Bot Manager Lite](/en/documentation/guides/application-development/integrations/bot-manager-lite/) |
| AWS WAF rate-based rules                 | Rate limits keyed on IP, headers, URI, or method                                 | Firewall and Functions                                                                                                                                                             |
| AWS WAF IP sets                          | Reusable IP and CIDR lists for allow or block rules                              | [Network lists](/en/documentation/platform/firewall/network-shield/network-lists/)                                                                                                 |
| AWS Shield Standard                      | Standard DDoS protection                                                         | [DDoS Protection](/en/documentation/platform/workloads/#ddos-protection)                                                                                                           |
| AWS Shield Advanced                      | DDoS protection with more visibility and mitigation options                      | DDoS Protection                                                                                                                                                                    |
| Shield Advanced automatic mitigation     | WAF protections created during application-layer attacks                         | DDoS Protection and Web Application Firewall                                                                                                                                       |
| AWS Network Firewall                     | Managed network firewall for a VPC                                               | Firewall and [Network Shield](/en/documentation/platform/firewall/network-shield/quickstart/)                                                                                      |
| Elastic Load Balancing                   | Traffic distributed across targets and zones                                     | [Load Balancer](/en/documentation/platform/connectors/load-balancer/quickstart/), a module of Connectors                                                                           |
| Application Load Balancer                | Layer 7 HTTP and HTTPS load balancing                                            | Load Balancer                                                                                                                                                                      |
| Network Load Balancer                    | Layer 4 TCP, UDP, and TLS load balancing                                         | Load Balancer, for HTTP and HTTPS origins only                                                                                                                                     |
| AWS Global Accelerator                   | Global anycast IP addresses and routing over the AWS network                     | Applications and Load Balancer                                                                                                                                                     |
| CloudFront origin access control         | S3 origins reachable only through CloudFront                                     | Connectors and Origin Shield, a module of Connectors, with Origin IP ACL and HMAC                                                                                                  |
| CloudFront signed URLs and cookies       | Access to private content                                                        | Rules Engine for Applications, Functions, and the [Secure Token](/en/documentation/guides/application-development/integrations/secure-token/) integration on a firewall            |
| CloudFront geographic restrictions       | Access allowed or blocked by country                                             | Rules Engine for Applications and Network Shield                                                                                                                                   |
| Amazon Route 53                          | Authoritative DNS, registration, routing policies, and health checks             | Edge DNS                                                                                                                                                                           |
| AWS Certificate Manager                  | TLS certificates: provisioning, import, deployment, and renewal                  | [Certificate Manager](/en/documentation/platform/workloads/certificate-manager/certificates/)                                                                                      |
| AWS Private CA                           | Private certificate authorities                                                  | Certificate Manager, which holds Trusted CA certificates                                                                                                                           |
| AWS Private CA for client authentication | Certificate-based client authentication                                          | [mTLS](/en/documentation/platform/workloads/mtls/)                                                                                                                                 |
| AWS IoT Greengrass                       | Managed components and local processing on edge devices                          | Orchestrator                                                                                                                                                                       |
| AWS IoT Greengrass deployments           | Components and configurations sent to devices                                    | Orchestrator                                                                                                                                                                       |
| Amazon ECS Anywhere                      | On-premises servers registered in ECS clusters                                   | Orchestrator                                                                                                                                                                       |
| AWS Outposts                             | AWS infrastructure on customer premises                                          | Orchestrator                                                                                                                                                                       |
| AWS Systems Manager hybrid activations   | Management of non-EC2 machines                                                   | Orchestrator                                                                                                                                                                       |
| Amazon CloudWatch metrics                | Metrics collected, queried, charted, and alarmed on                              | Real-Time Metrics                                                                                                                                                                  |
| Amazon CloudWatch Logs                   | Logs collected, monitored, and queried                                           | Real-Time Events and Data Stream                                                                                                                                                   |
| CloudWatch Logs Insights                 | Interactive log queries                                                          | Real-Time Events                                                                                                                                                                   |
| Amazon Data Firehose                     | Streaming data delivered to S3, Redshift, OpenSearch, and HTTP                   | Data Stream                                                                                                                                                                        |
| CloudWatch RUM                           | Real user monitoring for browser and mobile apps                                 | [Edge Pulse](/en/documentation/platform/edge-pulse/)                                                                                                                               |
| AWS X-Ray                                | Distributed tracing across services                                              | Data Stream and Functions                                                                                                                                                          |
| AWS Step Functions                       | Workflow orchestration with state machines                                       | Functions. Azion has no workflow service                                                                                                                                           |
| Amazon EventBridge                       | Event routing with rules and targets                                             | -                                                                                                                                                                                  |
| Amazon ElastiCache                       | In-memory caching with Redis or Memcached                                        | Cache and KV Store                                                                                                                                                                 |
| Amazon RDS and Aurora                    | Managed relational databases                                                     | SQL Database                                                                                                                                                                       |
| Amazon GuardDuty                         | Threat detection                                                                 | Firewall and network lists                                                                                                                                                         |
| Amazon Inspector                         | Vulnerability scanning                                                           | -                                                                                                                                                                                  |
| AWS Security Hub                         | Centralized security findings                                                    | Real-Time Events and Data Stream                                                                                                                                                   |
| AWS Marketplace                          | Third-party software, data, and services                                         | [Marketplace](/en/documentation/platform/marketplace/)                                                                                                                             |
| AWS Management Console                   | Web interface to AWS services                                                    | [Azion Console](https://console.azion.com/)                                                                                                                                        |
| AWS APIs                                 | Programmatic access to AWS services                                              | [Azion API](/en/documentation/devtools/api/)                                                                                                                                       |
| CloudWatch APIs                          | Programmatic access to metrics, logs, and alarms                                 | [GraphQL API](/en/documentation/devtools/graphql/first-steps/)                                                                                                                     |
| AWS CLI                                  | Command-line management of AWS services                                          | Azion CLI                                                                                                                                                                          |
| AWS SDK for JavaScript                   | JavaScript libraries for AWS services                                            | [Azion Lib](/en/documentation/devtools/azion-lib/)                                                                                                                                 |
| AWS CloudFormation                       | Infrastructure as code for AWS resources                                         | [Terraform Provider](/en/documentation/devtools/terraform/)                                                                                                                        |
| AWS Cloud Development Kit                | Infrastructure defined in code and synthesized to CloudFormation                 | Terraform Provider and Azion API                                                                                                                                                   |
| Terraform AWS Provider                   | Terraform management of AWS resources                                            | Terraform Provider                                                                                                                                                                 |
| Lambda runtime environment               | The runtime layer of Lambda functions                                            | [Azion Runtime](/en/documentation/devtools/runtime/)                                                                                                                               |
| CloudFront edge runtime                  | The runtime of CloudFront Functions and Lambda\@Edge                             | Azion Runtime                                                                                                                                                                      |

Azion holds a SOC 2 Type 2 report and a SOC 3 report, and is a PCI DSS 4.0.1 Level 1 Service Provider. For the attestations, refer to [SOC 2 and SOC 3](/en/documentation/fundamentals/soc/) and [PCI DSS certification](/en/documentation/fundamentals/pci-dss-certification/).

---

## Deploy the project on Azion

On Azion, a project becomes an application, and a workload serves that application on a domain. AWS keeps the build and infrastructure in `template.yaml` for SAM or `cdk.json` for CDK. Azion keeps them in [`azion.config.js`](/en/documentation/devtools/cli/azion-config-js/), which can also be `azion.config.mjs` or `azion.config.cjs`, depending on the preset.

| Task        | AWS                                     | Azion CLI                                                                              |
| ----------- | --------------------------------------- | -------------------------------------------------------------------------------------- |
| Install     | `pip install awscli`                    | `curl -fsSL https://cli.azion.app/install.sh \| bash`, or `brew install azion`         |
| Sign in     | `aws configure`                         | `azion login`                                                                          |
| Run locally | `sam local start-api`                   | `azion dev`, which serves on `http://localhost:3333`                                   |
| Deploy      | `sam deploy --guided` or `cdk deploy`   | `azion link`, then `azion deploy`                                                      |
| View logs   | `aws logs tail /aws/lambda/my-function` | `azion logs cells` for function console logs, or `azion logs http` for HTTP event logs |
| Roll back   | Stack rollback                          | `azion rollback`, which serves the static files of an earlier deploy again             |

| Aspect                 | AWS SAM or CDK               | Azion                                        |
| ---------------------- | ---------------------------- | -------------------------------------------- |
| Infrastructure as code | YAML or TypeScript templates | A JavaScript configuration file              |
| Local testing          | SAM local emulator           | `azion dev`                                  |
| Deployment             | CloudFormation stacks        | Resources created directly by `azion deploy` |
| Frameworks             | Configured by hand           | 19 frameworks and 5 generic presets          |

`azion logs cells` and `azion logs http` return the logs of the last 5 minutes, and `--tail` keeps printing new ones. `azion rollback` takes the `--connector-id` of the storage connector that delivers the site. For the flags, refer to [azion logs](/en/documentation/devtools/cli/logs/) and [azion rollback](/en/documentation/devtools/cli/rollback/).

Azion Console imports a repository with one of six presets, and the Azion CLI asks for the preset in a picker. Neither detects the framework for you.

**Console**

To import the repository in Azion Console:

1. **Open the create dialog**

   Access [Azion Console](https://console.azion.com/) > **Create**. The **New** dialog opens.

2. **Open the Import from GitHub tab**

   In the **New** dialog, select the **Import from GitHub** tab, then select the card.

3. **Connect your GitHub account**

   In the **GitHub Connection** section, select **Connect with GitHub**, and install the Azion GitHub App for the repository.

4. **Select the repository**

   In **Git Scope**, select the GitHub account. In **Repository**, select the repository to move.

5. **Select the preset**

   In **Preset**, select the framework: *Next.js*, *Angular*, *Astro*, *Hexo*, *React*, or *Vue*.

6. **Enter the install command**

   In **Install Command**, enter the command that installs the project. For example: `npm install`.

7. **Select Deploy**

Azion builds the project and creates its application and workload. For every field of the page, refer to [Import a project from GitHub](/en/documentation/guides/application-development/automation/import-an-existing-project-from-github/).

**CLI**

The Azion CLI deploys the project from your machine. In the root of the project, link it to Azion, and select the preset when the CLI asks for it:

```bash
azion link
```

Then deploy. `azion deploy` needs the project settings that `azion link` writes:

```bash
azion deploy
```

The CLI builds the project and deploys it to Azion. To set the preset in code, put it in `build.preset` of `azion.config.js`:

```javascript
import { defineConfig } from '@aziontech/config'

export default defineConfig({
  build: {
    preset: 'javascript',
    polyfills: true
  }
})
```

The preset of a Next.js project is `next`. The `azion` package that older samples import is deprecated: import `defineConfig` from `@aziontech/config`. Install that package in the project first, with `npm install -D @aziontech/config`. Without it, the CLI fails with `Failed to load configuration file`. For the commands, refer to [Azion CLI quickstart](/en/documentation/devtools/cli/quickstart/) and [azion deploy](/en/documentation/devtools/cli/deploy/).

**API**

The API creates the application, its rules, and the workload one resource at a time. To build that chain, refer to [Applications quickstart](/en/documentation/platform/applications/quickstart/).

The deployment answers on a workload domain that Azion assigns under `map.azionedge.net`. Before you move any production domain, send a request to the root path, with that domain in place of `<your-workload-domain>`:

```bash
curl -i https://<your-workload-domain>/
```

The response carries the status, the headers, and the body the project returns for `/`. Send the same request to each critical route. A build that passes can still behave differently at run time, so compare the responses with the ones AWS returns.

If the build fails on Azion, compare the preset with the framework of the project, and check `build.preset`, `build.entry`, and `build.bundler` in `azion.config.js`. The `build` block has no command field.

---

## Move environment variables

Environment variables hold API keys, database credentials, authentication secrets, service endpoints, feature flags, and per-environment settings. A variable that does not reach Azion breaks the project at run time, even when the deployment succeeds.

On AWS, a Lambda function reads a variable from `process.env`, and secrets often live in AWS Secrets Manager or Parameter Store. On Azion, variables belong to the account, up to 100 of them, and a function reads them with `Azion.env.get()`. Each variable has a key, a value, and a flag that marks it as a secret.

| Aspect  | AWS                                                     | Azion                                                  |
| ------- | ------------------------------------------------------- | ------------------------------------------------------ |
| Access  | `process.env.VARIABLE`                                  | `Azion.env.get('VARIABLE')`, or `process.env.VARIABLE` |
| Secrets | AWS Secrets Manager and Systems Manager Parameter Store | Variables with the secret flag on                      |
| Size    | Set per function                                        | 32 KB for all the variables of one function            |

**Console**

To create the variables in Azion Console, open the **Variables** page of the **Account** menu, and create each variable with its key and its value. Turn a variable that holds a credential into a secret.

**CLI**

To create each variable with the Azion CLI:

```bash
azion create variables --key API_KEY --value <your-value> --secret false
```

The CLI answers with the UUID of the new variable:

```text
Created variable with UUID 00000000-0000-0000-0000-000000000001
```

Set `--secret true` for a credential. To list the variables, run `azion list variables`. For the other commands, refer to [variables](/en/documentation/devtools/cli/resources/variables/).

**API**

The Azion CLI and Azion Console create the variables. For the interfaces that create, list, and change a variable, refer to [Environment variables](/en/documentation/platform/functions/environment-variables/).

Then change the code that reads the variables. A value fetched from Secrets Manager becomes a variable read:

```javascript diff
-// Before: AWS Lambda
-const apiKey = process.env.API_KEY;
-const dbHost = process.env.DB_HOST;
 
-// Before: AWS Secrets Manager
-const { SecretsManager } = require('@aws-sdk/client-secrets-manager');
-const client = new SecretsManager();
-const secret = await client.getSecretValue({ SecretId: 'my-secret' });
-const secretValue = JSON.parse(secret.SecretString);
 
+// After: Azion
+const apiKey = Azion.env.get('API_KEY');
+const dbHost = Azion.env.get('DB_HOST');
```

Code that reads `process.env.API_KEY` keeps working on a deployed function, because Azion Runtime supports `process.env` for Node.js compatibility. Under `azion dev`, a function reads the project `.env` file instead of the account variables, or the whole shell environment when there is no `.env` file. A key that does not exist returns `undefined`, not an error. If a function reads a variable as `undefined`, confirm that the variable exists on the account.

> **Caution**
>
> Keep secrets in approved systems, and limit access to the processes that need them. Never copy a secret into local notes, tickets, chat messages, or temporary documents.

---

## Move Lambda functions to Functions

Lambda functions often carry the most critical logic of a project: authentication, personalization, API orchestration, and integrations with third-party services. On Azion, this code runs in [Functions](/en/documentation/platform/functions/). A function holds the JavaScript code, a [function instance](/en/documentation/platform/applications/functions-instances/) runs it on an application, and a rule decides which requests reach it.

| Aspect     | AWS Lambda                                   | Azion Functions                                                   |
| ---------- | -------------------------------------------- | ----------------------------------------------------------------- |
| Handler    | `exports.handler = async (event) => {}`      | `export default { async fetch(request, env, ctx) {} }`            |
| Runtimes   | Node.js, Python, Java, Go, .NET, and Ruby    | JavaScript, in V8 isolates                                        |
| Memory     | 128 MB - 10,240 MB                           | 512 MB per isolate, on every plan                                 |
| Timeout    | 1 sec - 15 min                               | 2 s of CPU time and 5 minutes of wall-clock time per invocation   |
| Cold start | Common, especially in a VPC                  | None                                                              |
| Triggers   | SQS, SNS, Kinesis, DynamoDB, and API Gateway | HTTP requests that a rule of an application or a firewall matches |

A Lambda function written in Java, Python, or another runtime has to be rewritten in JavaScript. Framework builds can also produce WebAssembly. A function makes at most 50 outbound `fetch()` calls in one invocation. For every limit, refer to [Functions limits](/en/documentation/platform/functions/limits/).

| Scenario          | AWS Lambda cold start | Azion Functions cold start |
| ----------------- | --------------------- | -------------------------- |
| Node.js, no VPC   | 100-300ms             | None                       |
| Node.js, with VPC | 500ms-2s              | None                       |
| Java or Spring    | 1-5s                  | None                       |
| Python            | 100-500ms             | None                       |

The handler receives a standard `Request` and returns a `Response`. Read the body, the query string, and the path from the request instead of from the event:

```javascript diff
-// Before: AWS Lambda
-exports.handler = async (event) => {
-  const body = JSON.parse(event.body);
-  const pathParams = event.pathParameters;
-  const queryParams = event.queryStringParameters;
-
-  return {
-    statusCode: 200,
-    headers: { 'Content-Type': 'application/json' },
-    body: JSON.stringify({ message: 'Hello', data: body })
-  };
-};
 
+// After: Azion
+export default {
+  async fetch(request, env, ctx) {
+    const body = await request.json();
+    const url = new URL(request.url);
+    const queryParams = Object.fromEntries(url.searchParams);
+
+    return new Response(JSON.stringify({ message: 'Hello', data: body }), {
+      status: 200,
+      headers: { 'Content-Type': 'application/json' }
+    });
+  }
+};
```

`env` is an empty object on a deployed function, and `ctx` carries `args` and `waitUntil`. The Lambda context and the API Gateway request context move to `request.metadata`:

```javascript diff
-// Before: AWS Lambda
-exports.handler = async (event, context) => {
-  const requestId = context.awsRequestId;
-  const functionName = context.functionName;
-  const remainingTime = context.getRemainingTimeInMillis();
-  const sourceIp = event.requestContext.identity.sourceIp;
-  const userAgent = event.requestContext.identity.userAgent;
-  const country = event.headers['CloudFront-Viewer-Country'];
-  return { statusCode: 200, body: 'OK' };
-};
 
+// After: Azion
+export default {
+  async fetch(request, env, ctx) {
+    const requestId = request.metadata['request_id'];
+    const sourceIp = request.metadata['remote_addr'];
+    const userAgent = request.headers.get('user-agent');
+    const country = request.metadata['geoip_country_code'];
+    const city = request.metadata['geoip_city'];
+    const region = request.metadata['geoip_region'];
+    return new Response('OK', { status: 200 });
+  }
+};
```

`request_id` holds the same value as the `x-azion-request-id` response header. `geoip_city` and `geoip_region` hold codes. Under `azion dev`, `request.metadata` is `undefined`, so test code that reads it on a deployed function. For every field, refer to [Metadata](/en/documentation/devtools/runtime/api-reference/metadata/).

### Map CloudFront triggers to rule phases

An application processes each request in two phases. The Request Phase handles what the user sent, and the Response Phase handles what the application sends back. Lambda\@Edge triggers map to those phases:

| CloudFront trigger | Azion equivalent                                                     |
| ------------------ | -------------------------------------------------------------------- |
| Viewer request     | A Request Phase rule, which can run a function                       |
| Origin request     | A Request Phase rule. No phase runs between the cache and the origin |
| Origin response    | A Response Phase rule with header, cookie, or redirect behaviors     |
| Viewer response    | A Response Phase rule with header, cookie, or redirect behaviors     |

A JavaScript function runs in the Request Phase. For the Response Phase, the Console function instance form states `Only Lua functions can be used in the Response phase.` Move response logic to Response Phase behaviors, such as *Add Request Header*, which adds a response header in that phase.

Code that reads KV, SQL, or a model also changes its calls. The stages on KV Store, SQL Database, and AI Inference show the new calls. For the runtime APIs, refer to [Web APIs](/en/documentation/devtools/runtime/api-reference/javascript/).

---

## Route API Gateway paths to functions

API Gateway routes each method and path to a backend, with authorizers, throttling, and stages around it. On Azion, a rule of the application matches the method and the path, and its *Run Function* behavior runs a function instance. *Run Function* requires Application Accelerator and Functions on the application.

| Aspect         | AWS API Gateway                      | Azion                                                       |
| -------------- | ------------------------------------ | ----------------------------------------------------------- |
| API types      | REST APIs and HTTP APIs              | Functions, reached through Rules Engine rules               |
| Routes         | Resource paths and methods           | Criteria on `${uri}` and `${request_method}`                |
| Authorizers    | Lambda, Cognito, and JWT authorizers | A function on a firewall, which runs before the application |
| Stages         | Stage variables and deployments      | -                                                           |
| Throttling     | Usage plans and rate limiting        | *Set Rate Limit* on a firewall rule                         |
| Custom domains | Domain mappings                      | The **Domains** of a workload                               |

A function becomes reachable in three steps: create the function, create an instance of it on the application, and create the rule.

**Console**

To route a path to a function in Azion Console:

1. **Open the Functions page**

   Access [Azion Console](https://console.azion.com/) > **Products Menu** > **Libraries** > **Functions**.

2. **Select + Function**

3. **Name the function**

   Enter a name. For example: `api-handler`.

4. **Paste the code**

   In the **Code** tab, paste the code of the handler, and select **Save**.

5. **Turn on Functions on the application**

   Go to **Applications**, select the application, and turn on the **Functions** module in the **Main Settings** tab. Select **Save**.

6. **Create the instance**

   In the **Functions Instances** tab, select **+ Function Instance**. Enter a name, select the function, and select **Save**.

7. **Create the rule**

   In the **Rules Engine** tab, select **+ Rule**, and select **Request Phase**. Set the criterion `${uri}` *starts with* `/api/`.

8. **Add Run Function**

   In the **Behaviors** section, select **Run Function** and the instance. Select **Save**.

The rule runs the instance on every request whose path starts with `/api/`. If **Run Function** is not in the list, turn on Application Accelerator on the application.

**CLI**

To create the function with the Azion CLI, save the code to `index.js`, then run:

```bash
azion create function --name api-handler --code ./index.js --active true
```

```text
Created function with ID <function-id>
```

Create the instance on the application, with the function ID:

```bash
azion create function-instance --application-id <application-id> --function-id <function-id> --name "api-handler instance"
```

```text
Created Function Instance with ID <function-instance-id>
```

Then create the rule from a JSON file with `azion create rules-engine --application-id <application-id> --phase request --file rule.json`. The file carries the body of the API panel. For the full procedure, refer to [Functions quickstart](/en/documentation/platform/functions/quickstart/).

**API**

To create the function, send a `POST` request to the functions endpoint. The code is a JSON string, so newlines are escaped as `\n`:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/functions \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "api-handler",
  "code": "export default {\n  async fetch(request, env, ctx) {\n    return new Response(JSON.stringify({ message: \"Hello\" }), { headers: { \"Content-Type\": \"application/json\" } });\n  },\n};"
}'
```

The response carries `202`, the function `id`, `"runtime": "azion_js"`, and `"execution_environment": "application"`. Create the instance on the application with the function `id`:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/applications/<application-id>/functions \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "api-handler instance",
  "function": <function-id>,
  "args": {},
  "active": true
}'
```

The response carries `202` and the instance `id`. Save this rule body as `rule.json`, with the instance `id` in `attributes.value`:

```json
{
  "name": "api-route",
  "active": true,
  "criteria": [[{ "variable": "${uri}", "conditional": "if", "operator": "starts_with", "argument": "/api/" }]],
  "behaviors": [{ "type": "run_function", "attributes": { "value": <function-instance-id> } }]
}
```

Send it to the request rules of the application:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/applications/<application-id>/request_rules \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data @rule.json
```

The response carries `202` and the rule with its `order`. `run_function` takes the ID of the instance, not of the function.

New rules can take a few minutes to propagate. On an unexpected response, wait and retry before diagnosing.

To route each method and path to its own function, create one function and one instance per route, as the previous procedure shows. Then give each route its own rule. A rule that matches a method joins `${request_method}` to the path criterion with `and`:

```json
[
  {
    "name": "GET /users/:id",
    "active": true,
    "criteria": [[
      { "variable": "${uri}", "conditional": "if", "operator": "matches", "argument": "^/users/([0-9]+)$" },
      { "variable": "${request_method}", "conditional": "and", "operator": "is_equal", "argument": "GET" }
    ]],
    "behaviors": [{ "type": "run_function", "attributes": { "value": <get-user-instance-id> } }]
  },
  {
    "name": "POST /users",
    "active": true,
    "criteria": [[
      { "variable": "${uri}", "conditional": "if", "operator": "is_equal", "argument": "/users" },
      { "variable": "${request_method}", "conditional": "and", "operator": "is_equal", "argument": "POST" }
    ]],
    "behaviors": [{ "type": "run_function", "attributes": { "value": <create-user-instance-id> } }]
  }
]
```

Send each object to the request rules of the application, as the API panel shows. These two rules replace the API Gateway routes `GET /users/{id}` and `POST /users`. Add a rule of the same shape for `PUT /users/{id}` and `DELETE /users/{id}`. To declare the same functions, instances, and rules as code, refer to [azion.config.js](/en/documentation/devtools/cli/azion-config-js/).

### Move an authorizer to a firewall function

A Lambda authorizer returns an IAM policy that allows or denies the call:

```javascript
// Before: AWS Lambda authorizer
exports.handler = async (event) => {
  const token = event.authorizationToken;
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    return {
      principalId: decoded.sub,
      policyDocument: {
        Version: '2012-10-17',
        Statement: [{ Action: 'execute-api:Invoke', Effect: 'Allow', Resource: event.methodArn }]
      }
    };
  } catch (err) {
    throw new Error('Unauthorized');
  }
};
```

On Azion, a function on a [firewall](/en/documentation/platform/firewall/) decides before the application runs. Every function on a firewall must end with a finishing outcome: `event.continue()` lets the request proceed, and `event.deny()` answers `403 Forbidden`. This function lets through only requests that carry the expected bearer token:

```javascript
addEventListener('firewall', (event) => {
  const authHeader = event.request.headers.get('Authorization') || '';
  const token = authHeader.replace('Bearer ', '');
  if (token !== '' && token === Azion.env.get('API_TOKEN')) {
    event.continue();
  } else {
    event.deny();
  }
});
```

Put the verification your authorizer performs, such as a JWT check, in place of the comparison. A request without a valid token receives `403`. To answer `401` instead, call `event.respondWith(new Response('Unauthorized', { status: 401 }))`.

The function needs the `firewall` execution environment: `execution_environment` in the API, or `--execution-environment` in the Azion CLI. A firewall with the **Functions** module runs it through an instance and a rule with *Run Function*. For the steps, refer to [Functions for Firewall](/en/documentation/platform/firewall/functions/) and [Functions instances](/en/documentation/platform/firewall/functions-instances/).

---

## Replace Step Functions and EventBridge

Azion has no workflow orchestration service and no event bus. A Step Functions workflow becomes code that calls each step in sequence inside one function. An EventBridge rule becomes a function route that the producer of the event calls over HTTP.

| Aspect              | AWS Step Functions         | Azion Functions                                                                     |
| ------------------- | -------------------------- | ----------------------------------------------------------------------------------- |
| Workflow definition | Amazon States Language     | JavaScript                                                                          |
| State               | Built-in state machine     | Your code. Persistent state goes to KV Store or SQL Database                        |
| Error handling      | Retry, catch, and fallback | `try` and `catch`                                                                   |
| Visualization       | Workflow Studio            | The code                                                                            |
| Bounds              | Set by the state machine   | 50 outbound calls, 2 s of CPU time, and 5 minutes of wall-clock time per invocation |

| Aspect         | Amazon EventBridge | Azion                                                            |
| -------------- | ------------------ | ---------------------------------------------------------------- |
| Event routing  | Rules and targets  | A rule of the application that runs a function on the event path |
| Event patterns | Pattern matching   | Conditions in the function code                                  |
| Targets        | 100+ AWS services  | HTTP endpoints that the function calls with `fetch()`            |

This Step Functions definition validates an order, charges it, and fulfills it, with a failure branch:

```json
{
  "Comment": "Order processing workflow",
  "StartAt": "ValidateOrder",
  "States": {
    "ValidateOrder": { "Type": "Task", "Resource": "arn:aws:lambda:us-east-1:123456789:function:validate-order", "Next": "ProcessPayment" },
    "ProcessPayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:us-east-1:123456789:function:process-payment",
      "Catch": [{ "ErrorEquals": ["PaymentFailed"], "Next": "NotifyFailure" }],
      "Next": "FulfillOrder"
    },
    "FulfillOrder": { "Type": "Task", "Resource": "arn:aws:lambda:us-east-1:123456789:function:fulfill-order", "End": true },
    "NotifyFailure": { "Type": "Task", "Resource": "arn:aws:lambda:us-east-1:123456789:function:notify-failure", "End": true }
  }
}
```

The same flow runs as one function. Each step is a function call, and the `catch` block takes the failure branch:

```javascript
async function validateOrder(order) {
  if (!order.items || order.items.length === 0) {
    throw new Error('Invalid order: no items');
  }
  return { ...order, validated: true };
}

async function processPayment(order) {
  const response = await fetch('https://payment-api.example.com/charge', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ amount: order.total, currency: 'USD' })
  });
  if (!response.ok) {
    throw new Error('PaymentFailed');
  }
  return { ...order, paid: true };
}

async function fulfillOrder(order) {
  return { ...order, fulfilled: true, fulfilledAt: new Date().toISOString() };
}

async function notifyFailure(details) {
  await fetch('https://notifications.example.com/failure', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(details)
  });
}

export default {
  async fetch(request, env, ctx) {
    const order = await request.json();
    try {
      const validated = await validateOrder(order);
      const payment = await processPayment(validated);
      const fulfillment = await fulfillOrder(payment);
      return new Response(JSON.stringify(fulfillment), {
        headers: { 'Content-Type': 'application/json' }
      });
    } catch (error) {
      await notifyFailure({ order, error: error.message });
      return new Response(JSON.stringify({ error: error.message }), {
        status: 500,
        headers: { 'Content-Type': 'application/json' }
      });
    }
  }
};
```

A workflow that waits for hours, or that must survive a failed invocation, does not fit in one invocation. Keep its state in [KV Store](/en/documentation/platform/kv-store/) or [SQL Database](/en/documentation/platform/sql-database/), and let each call advance one step.

An EventBridge rule matches events by source, type, and content:

```json
{
  "source": ["com.mycompany.orders"],
  "detail-type": ["OrderCreated"],
  "detail": { "amount": [{ "numeric": [">", 100] }] }
}
```

On Azion, the producer posts the events to a function route, and the function applies the pattern:

```javascript
async function processHighValueOrder(data) {
  await fetch('https://orders.example.com/high-value', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  });
}

export default {
  async fetch(request, env, ctx) {
    const events = await request.json();
    for (const event of events) {
      if (event.source === 'com.mycompany.orders' && event.type === 'OrderCreated' && event.data.amount > 100) {
        await processHighValueOrder(event.data);
      }
    }
    return new Response('OK');
  }
};
```

The function answers `OK` after it handles every event in the batch. [Data Stream](/en/documentation/platform/data-stream/) cannot replace the bus: it sends only Azion's own logs, from Activity History, Applications, Functions, and WAF Events.

---

## Recreate CloudFront distributions

A CloudFront distribution holds the origins, the cache behaviors, the certificate, and the domains in one resource. On Azion, a [connector](/en/documentation/platform/connectors/) holds each origin, an application holds the rules and the cache, and a workload holds the domains and the certificate.

| Aspect                    | AWS CloudFront                | Azion                                                                               |
| ------------------------- | ----------------------------- | ----------------------------------------------------------------------------------- |
| Configuration             | XML or JSON in CloudFormation | Azion Console, the API, the Azion CLI, or `azion.config.js`                         |
| Origins                   | S3, ALB, and custom origins   | Connectors of type `http`, `storage` for an Object Storage bucket, or `live_ingest` |
| Behaviors                 | Cache behaviors per path      | Rules with criteria, and *Set Connector* to pick the origin                         |
| Certificates              | ACM, in us-east-1 only        | Certificate Manager, on the workload                                                |
| Edge functions            | Lambda\@Edge, on 4 triggers   | Functions, on Request Phase rules                                                   |
| Response headers policies | Security and CORS headers     | Response Phase rules with *Add Request Header*                                      |

A new application has Cache and Functions on, and Application Accelerator and Image Processor off. A rule with *Set Connector* sends the requests it matches to a connector. When several matching rules carry *Set Connector*, only the last one runs, so keep a per-path rule after the catch-all rule.

**Console**

To recreate an origin and its behavior in Azion Console:

1. **Open the Connectors page**

   Access [Azion Console](https://console.azion.com/) > **Connectors**. The **Create Connector** page opens from it.

2. **Name the connector**

   In **General**, enter a **Name**. For example: `api-origin`.

3. **Select the type**

   In **Connector Type**, select *HTTP* for a server, or *Object Storage* for a bucket.

4. **Enter the origin**

   For *HTTP*, enter the host of the origin in **Address**, without a protocol or a port. In **Host**, enter the name the origin answers for.

5. **Select Create**

6. **Open the application**

   Go to **Applications**, select the application, and select the **Rules Engine** tab.

7. **Create the rule**

   Select **+ Rule**, enter a **Name**, and select *Request Phase*. Set the criterion `${uri}` *starts with* `/api/`.

8. **Select the connector**

   Under **Behaviors**, select *Set Connector* and the connector. Select **Save**.

Requests whose path starts with `/api/` reach the origin of the connector. For the full chain, refer to [Applications quickstart](/en/documentation/platform/applications/quickstart/) and [Connectors quickstart](/en/documentation/platform/connectors/quickstart/).

**CLI**

To create the connector with the Azion CLI, save the body of the API panel as `connector.json`, then run:

```bash
azion create connector --type http --file connector.json
```

```text
Created Connector with ID <connector-id>
```

Create the *Set Connector* rule from a file with `azion create rules-engine`, as the [Connectors quickstart](/en/documentation/platform/connectors/quickstart/) shows.

**API**

To create the application, send a `POST` request to the applications endpoint:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/applications \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{"name": "my-app", "active": true}'
```

The response carries the application `id` and its `modules`. Then create the connector:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/connectors \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "api-origin",
  "type": "http",
  "attributes": {
    "addresses": [{ "address": "origin.example.com" }],
    "connection_options": { "transport_policy": "force_https", "host": "origin.example.com" }
  }
}'
```

The response carries `202` and the connector `id`. Send a request rule with `{ "type": "set_connector", "attributes": { "value": <connector-id> } }` to `/v4/workspace/applications/<application-id>/request_rules`. Cache settings are a separate resource, covered in Recreate cache settings.

A connector change reaches Azion's infrastructure in several minutes, with no new deployment. To serve a bucket prefix with a one-day cache and send `/api/` to a function with no cache, the distribution takes these resources:

- A connector of type `storage` with the bucket and the prefix, such as `assets/`. The build refuses a `storage` connector without `prefix`.
- A cache setting with a browser TTL of 3,600 seconds, a cache TTL of 86,400 seconds, stale cache on, Tiered Cache off, and no variation by cookie or query string.
- A request rule on `${uri}` *starts with* `/` with *Set Connector* and *Set Cache Policy*.
- A request rule on `${uri}` *starts with* `/api/` with *Bypass Cache* and *Run Function*.

`bypass_cache` and `run_function` require Application Accelerator. *Bypass Cache* leaves the browser cache and Tiered Cache unchanged. To declare these resources as code, refer to [azion.config.js](/en/documentation/devtools/cli/azion-config-js/).

To add the security headers or the CORS headers of a response headers policy, create a Response Phase rule with `add_response_header`. Its value takes the form `Name: value`, such as `Access-Control-Allow-Origin: https://example.com` or `Access-Control-Allow-Methods: GET, POST`. The rule adds nothing to a `404` that Azion generates with no origin.

CloudFront origin access control keeps an S3 origin private. On Azion, [Origin Shield](/en/documentation/platform/connectors/origin-shield/origin-ip-acl-and-hmac/) on an `http` connector does the same with Origin IP ACL and HMAC. HMAC signs each request with `aws4_hmac_sha256`, a region, a service, and a key pair. For a bucket in your Azion account, use a `storage` connector instead.

---

## Recreate cache settings

On Azion, a [cache setting](/en/documentation/platform/applications/cache/cache-settings/) holds how long a response stays in cache and what makes two requests share one cached copy. A rule with *Set Cache Policy* applies the setting to the requests it matches. The rule selects a setting, and the setting holds the TTL and the cache key.

| Aspect        | AWS CloudFront                                         | Azion                                                                                                 |
| ------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| Cache levels  | Regional caches and the caches of each CloudFront site | Cache, [Tiered Cache](/en/documentation/platform/applications/cache/tiered-cache/), and browser cache |
| Cache key     | Cache policy and origin request policy                 | **Cache vary by** controls of the cache setting, which require Application Accelerator                |
| TTL           | Default 24h, max 1 year                                | **Max Age** of each cache setting, from 0 to 31,536,000 seconds                                       |
| Purge         | Path-based invalidations                               | URL, cache key, and wildcard                                                                          |
| Stale content | Origin Shield and `stale-while-revalidate`             | **Stale cache**, which serves an expired copy when revalidation fails                                 |

Map each CloudFront managed cache policy to a setting or a behavior:

| CloudFront cache policy | Azion equivalent                                                                                                                            |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| CachingOptimized        | A cache setting with *Override cache behavior* and a high **Max Age**                                                                       |
| CachingDisabled         | The *Bypass Cache* behavior in a Request Phase rule                                                                                         |
| Elemental-MediaPackage  | *Enforce HLS cache*, which Azion adds when you select a Live Ingest source: 5 seconds for `.m3u8` playlists and 60 seconds for `.ts` chunks |
| A custom policy         | A cache setting and a rule with *Set Cache Policy*                                                                                          |

**Max Age** defaults to 60 seconds. A value below 60 requires Application Accelerator, and a cache setting with Tiered Cache on needs at least 3 seconds and *Override cache behavior*. **Stale cache** honors the `stale-while-revalidate` the origin sends, or keeps a 300-second window under *Override cache behavior*. It is on by default in Azion Console and off in the API and the CLI.

**Console**

To create the cache setting in Azion Console:

1. **Open the application**

   Access [Azion Console](https://console.azion.com/) > **Applications**, then select the application.

2. **Go to the Cache Settings tab**

3. **Select + Cache**

4. **Name the cache setting**

   In **Name**, enter `static-assets`.

5. **Keep Override cache behavior selected**

   Under **Cache**, keep *Override cache behavior* selected, so **Max Age** replaces the TTL the origin sends.

6. **Set Max Age**

   In **Max Age**, enter the TTL in seconds. For example: `300`.

7. **(Optional) Turn on Tiered Cache**

   Turn on **Tiered Cache**, and select the **Tiered Cache Region**.

8. **Select Save**

The new setting appears in the **Cache Settings** list. To apply it, create a Request Phase rule in the **Rules Engine** tab. Use criteria such as `${uri}` *starts with* `/static/`, and the behavior **Set Cache Policy** set to `static-assets`.

**CLI**

The CLI flags cannot set the cache TTL, the cache behavior, or Tiered Cache. Send the full cache setting body from a file with `--file`, as the [Cache quickstart](/en/documentation/platform/applications/cache/quickstart/) shows.

**API**

To create the cache setting, send a `POST` request to the cache settings of the application:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/applications/<application-id>/cache_settings \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "static-assets",
  "browser_cache": { "behavior": "override", "max_age": 3600 },
  "modules": {
    "cache": {
      "behavior": "override",
      "max_age": 86400,
      "stale_cache": { "enabled": true },
      "tiered_cache": { "enabled": true, "topology": "nearest-region" }
    }
  }
}'
```

Cache settings are sub-resources of an application. To change one, send the same fields in a `PATCH` request to `/v4/workspace/applications/<application-id>/cache_settings/<cache-setting-id>`. Then apply the setting with a request rule:

```json
{
  "name": "apply-static-assets-cache",
  "active": true,
  "criteria": [[{ "variable": "${uri}", "operator": "starts_with", "conditional": "if", "argument": "/static/" }]],
  "behaviors": [{ "type": "set_cache_policy", "attributes": { "value": <cache-setting-id> } }]
}
```

The rule applies `static-assets` to every request whose path starts with `/static/`.

To vary the cache by query string, cookie, or device, use the **Cache vary by** controls of the cache setting: **Cache vary by Query String**, **Cache vary by Cookies**, and **Cache vary by Devices**. They require Application Accelerator on the application. In `azion.config.js`, a cache setting that varies only by the `version` and `lang` parameters carries `cacheByQueryString: { option: 'allowlist', list: ['version', 'lang'] }`. The options are `ignore`, `all`, `allowlist`, and `denylist`. For the controls, refer to [Cache variation](/en/documentation/platform/applications/application-accelerator/cache-variation/).

A CloudFront invalidation names a distribution and paths:

```bash
aws cloudfront create-invalidation \
  --distribution-id E123456789ABCD \
  --paths "/images/*" "/css/*"
```

On Azion, purge is a top-level endpoint, not nested under an application. A wildcard purge takes one expression, so each path pattern is its own request:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/purge/wildcard \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{
  "items": ["https://example.com/images/*"],
  "layer": "cache"
}'
```

Send a second request with `https://example.com/css/*`. A URL purge, at `/v4/workspace/purge/url`, takes up to 50 items. Only a cache key purge, at `/v4/workspace/purge/cachekey`, reaches Tiered Cache with `"layer": "tiered_cache"`. A URL or wildcard purge with that layer fails with `30001`. A purge takes time to propagate, and it appears in the purge history when it is complete. To check a purged object, request it with `curl -I` and read the response headers. For the purge types, refer to [Real-Time Purge](/en/documentation/platform/applications/cache/real-time-purge/).

---

## Balance traffic across origins

On Azion, [Load Balancer](/en/documentation/platform/connectors/load-balancer/balancing-methods/) is a module of a connector, not a separate resource. One connector of type `http` holds every origin as an address, up to 15 addresses with Load Balancer on, and one address without it. A rule with *Set Connector* sends the requests of the application to the connector.

| Aspect            | AWS ALB and NLB                                   | Azion Load Balancer                                                                                                                              |
| ----------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Balancing methods | Round-robin, least connections, and IP hash       | *Round Robin*, *Least Connections*, and *IP Hash*, which are `round_robin`, `least_conn`, and `ip_hash` in the API. No method steers by location |
| Health checks     | HTTP, HTTPS, and TCP                              | None. Failover is passive: **Max Retries** and the timeouts handle a failed connection                                                           |
| Failover          | Target group failover                             | Several *Primary* addresses with weights, and *Backup* addresses that receive traffic only when every primary fails                              |
| Session affinity  | Cookie and IP hash                                | *IP Hash* only, which maps each client IP address to one address                                                                                 |
| Targets           | EC2 instances, Lambda functions, and IP addresses | Addresses of one connector                                                                                                                       |
| Protocol          | Layer 4 for NLB, layer 7 for ALB                  | HTTP and HTTPS origins, on connectors of type `http`                                                                                             |

An ALB target group becomes one connector. Its health check path, interval, and healthy and unhealthy thresholds have no equivalent field.

Each address has a **Weight** from 1 to 100, which sets its share of the traffic. *IP Hash* refuses *Backup* addresses, with `28005` in the API. **Max Retries** takes 0 to 20, **Connection Timeout** 1 to 300 seconds, and **Read/Write Timeout** 1 to 600 seconds. These fields exist only with Load Balancer on. When you turn it on in Azion Console, the form fills in *Round Robin*, `3`, `30`, and `60`. The API defaults are `0`, `60`, and `120`.

**Console**

To turn on Load Balancer in Azion Console:

1. **Open the Connectors page**

   Access [Azion Console](https://console.azion.com/) > **Connectors**.

2. **Open the connector of the origin**

3. **Turn on Load Balancer**

   In **Modules**, turn on **Load Balancer**.

4. **Select the balancing method**

   In **Load Balancer Configuration**, set **Method** to *Round Robin*, *Least Connections*, or *IP Hash*.

5. **Set the first address**

   Under **Address Management**, on the existing address, set **Server Role** and **Weight**.

6. **Select Add Address**

7. **Enter the next origin**

   In the new **Address**, enter the host of the origin, without a protocol or a port. Then set its **Server Role** and **Weight**.

8. **Select Save**

The Console shows `Connector has been updated`. The connector has Load Balancer on and one address for each origin.

**CLI**

The update command needs the full connector body. Put it in a JSON file and send it with `--file`, as the [Load Balancer quickstart](/en/documentation/platform/connectors/load-balancer/quickstart/) shows.

**API**

To turn on Load Balancer, send a `PATCH` request to the connector. The address carries `server_role` and `weight`, and the method, the retries, and the timeouts sit in `attributes.modules.load_balancer.config`:

```bash
curl --request PATCH \
  --url https://api.azion.com/v4/workspace/connectors/<connector-id> \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{
  "attributes": {
    "addresses": [
      { "address": "us-east-origin1.example.com", "active": true, "modules": { "load_balancer": { "server_role": "primary", "weight": 3 } } },
      { "address": "us-east-origin2.example.com", "active": true, "modules": { "load_balancer": { "server_role": "primary", "weight": 1 } } }
    ],
    "modules": {
      "load_balancer": {
        "enabled": true,
        "config": { "method": "round_robin", "max_retries": 3, "connection_timeout": 10, "read_write_timeout": 30 }
      }
    }
  }
}'
```

The response carries `"state": "pending"`. Two addresses with Load Balancer off are refused with `28004`, and `"enabled": true` with an empty `config` is refused with `28014`. `"active": false` takes an address out of rotation.

A connector without Load Balancer has no configurable timeout. If an origin times out, check the address and its ports, and turn on Load Balancer to set the timeouts. For the connector fields, refer to [Connector settings](/en/documentation/platform/connectors/settings/).

---

## Serve optimized images

On AWS, image transformations usually need Lambda\@Edge or a custom origin. [Image Processor](/en/documentation/platform/applications/image-processor/quickstart/) resizes, crops, converts, and filters images on request, with no code. It stores nothing: it reads the source image from the origin of the application, which can be an Object Storage bucket behind a connector.

| Aspect          | AWS CloudFront and Lambda\@Edge | Azion Image Processor                                                                                                             |
| --------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Storage         | An S3 origin                    | The origin of the application, such as Object Storage                                                                             |
| Transformations | Custom Lambda\@Edge code        | Resize, format, quality, crop, rotate, fill, and watermark                                                                        |
| URL format      | A custom implementation         | `/image.png?ims=<OPTIONS>`                                                                                                        |
| Formats         | A custom implementation         | WebP, AVIF, JPEG, GIF, and PNG                                                                                                    |
| Signed URLs     | CloudFront signed URLs          | Through the [Secure Token](/en/documentation/guides/application-development/integrations/secure-token/) integration on a firewall |

Image Processor works in two steps: turn on the module on the application, then create a rule with the *Optimize Images* behavior. A request that no such rule matches is delivered unprocessed.

**Console**

To turn on Image Processor in Azion Console:

1. **Open the application**

   Access [Azion Console](https://console.azion.com/) > **Applications**, then select the application.

2. **Turn on Image Processor**

   In the **Main Settings** tab, under **Modules**, turn on **Image Processor**. For more information, refer to [Main Settings](/en/documentation/platform/applications/main-settings/).

3. **Select Save**

4. **Add the Optimize Images rule**

   In the **Rules Engine** tab, create a Request Phase rule with `${uri}` *matches* `\.(jpg|jpeg|gif|bmp|png|ico|webp|avif)` and the *Optimize Images* behavior.

Image requests that match the rule are now processed.

**CLI**

To turn on Image Processor with the Azion CLI, follow the CLI panel of the [Image Processor quickstart](/en/documentation/platform/applications/image-processor/quickstart/).

**API**

To turn on the module, send a `PATCH` request to the application. `image_processor` sits under `modules`, at the level of the application:

```bash
curl --request PATCH \
  --url https://api.azion.com/v4/workspace/applications/<application-id> \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{
  "modules": {
    "image_processor": { "enabled": true }
  }
}'
```

Then create a request rule with the `optimize_images` behavior, as the [Image Processor quickstart](/en/documentation/platform/applications/image-processor/quickstart/) shows.

Image Processor reads the transformation from the `ims` query parameter:

```text
# AWS, with a custom Lambda@Edge implementation
https://d12345.cloudfront.net/images/photo.jpg?w=400&q=80

# Azion
https://example.com/images/photo.jpg?ims=400x/filters:quality(80)
```

| Syntax                                | Result                                                             | Example                                   |
| ------------------------------------- | ------------------------------------------------------------------ | ----------------------------------------- |
| `?ims=WxH`                            | Resizes to the width and height, cropping to fit when both are set | `?ims=400x300`                            |
| `?ims=Wx`                             | Resizes to the width, with the height in proportion                | `?ims=400x`                               |
| `?ims=xH`                             | Resizes to the height, with the width in proportion                | `?ims=x300`                               |
| `?ims=fit-in/WxH`                     | Fits the image inside the dimensions, never enlarging it           | `?ims=fit-in/400x300`                     |
| `?ims=fit-in/WxH/filters:fill(Color)` | Fits the image and fills the rest of the canvas with a color       | `?ims=fit-in/400x300/filters:fill(white)` |

Image Processor converts to WebP when the `Accept` header of the client allows it. AVIF needs `?ims=filters:format(avif)` and a client that accepts `image/avif`. To cache one copy for each `ims` value, turn on Application Accelerator and vary the cache by query string. For every parameter, refer to [URL parameters](/en/documentation/platform/applications/image-processor/url-parameters/).

---

## Move Bedrock to AI Inference

[AI Inference](/en/documentation/platform/ai-inference/) runs a catalog of open-source [models](/en/documentation/platform/ai-inference/models/): large language models, vision language models, an embedding model, and a reranker. A model is not an object you create, and Azion hosts no inference endpoint for it. A function calls a model by its ID with `Azion.AI.run()`, and needs no credential.

| Aspect      | Amazon Bedrock                                                                | Azion AI Inference                                                      |
| ----------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Models      | Managed foundation models, such as Claude, Llama, Titan, and Stable Diffusion | A catalog of open-source models. To adapt a model, use LoRA fine-tuning |
| Interface   | Bedrock Runtime API                                                           | `Azion.AI.run()` inside a function                                      |
| Model types | Text, image, and embedding models                                             | LLMs, vision language models, an embedding model, and a reranker        |
| Billing     | Per token or per image                                                        | Compute Time and Requests                                               |

A Bedrock call names a model and sends a provider-specific body:

```javascript
// Before: Amazon Bedrock
import { BedrockRuntime } from '@aws-sdk/client-bedrock-runtime';

const client = new BedrockRuntime({ region: 'us-east-1' });

const response = await client.invokeModel({
  modelId: 'anthropic.claude-3-sonnet-20240229-v1:0',
  contentType: 'application/json',
  accept: 'application/json',
  body: JSON.stringify({
    anthropic_version: 'bedrock-2023-05-31',
    max_tokens: 1024,
    messages: [{ role: 'user', content: 'Hello, world!' }]
  })
});
```

On Azion, the function passes the model ID and a chat body:

```javascript
const modelResponse = await Azion.AI.run("Qwen/Qwen3-30B-A3B-Instruct-2507-FP8", {
  "stream": false,
  "messages": [
    { "role": "system", "content": "You are a helpful assistant." },
    { "role": "user", "content": "Name three European capitals." }
  ]
})
const answer = modelResponse?.choices?.[0]?.message?.content
```

`answer` holds the text of the reply. Fields such as `max_tokens` and `temperature` shape the output. Under `azion dev`, `Azion.AI` is `undefined`, so test the call on a deployed function. For the request fields, refer to [Model invocation](/en/documentation/platform/ai-inference/model-invocation/) and the [AI runtime API](/en/documentation/devtools/runtime/api-reference/ai/).

For an OpenAI-compatible `/v1/chat/completions` endpoint, deploy the [AI Inference Starter Kit](/en/documentation/guides/application-development/frameworks/ai-inference-starter-kit/) template. It creates an application and a function that serve that endpoint. To deploy it, access [Azion Console](https://console.azion.com/) > **Create**, select the template, and select **Deploy**. The Azion CLI has no template flag.

---

## Move DynamoDB key-value data to KV Store

DynamoDB serves key-value and document data. Key lookups, sessions, feature flags, and configuration move to [KV Store](/en/documentation/platform/kv-store/). Queries with filters, secondary indexes, aggregations, and joins move to SQL Database, in the stage Move DynamoDB queries and RDS databases to SQL Database.

| Aspect      | Amazon DynamoDB                 | Azion KV Store                                            | Azion SQL Database                  |
| ----------- | ------------------------------- | --------------------------------------------------------- | ----------------------------------- |
| Data model  | Key-value and document          | Key-value                                                 | Relational, in SQLite's dialect     |
| Queries     | Key lookups, queries, and scans | Key lookups                                               | Full SQL                            |
| Replication | Global tables across Regions    | Azion's distributed infrastructure                        | One main instance and read replicas |
| Consistency | Eventual or strong              | Eventual. A write is visible everywhere within 60 seconds | ACID                                |
| Indexes     | GSI and LSI                     | None                                                      | SQL indexes                         |
| Capacity    | On-demand or provisioned        | Serverless                                                | Serverless                          |

| DynamoDB use                    | Destination on Azion |
| ------------------------------- | -------------------- |
| Primary key lookups             | KV Store             |
| Simple key-value operations     | KV Store             |
| Session storage                 | KV Store             |
| Feature flags and configuration | KV Store             |
| Queries with filters            | SQL Database         |
| Secondary index queries         | SQL Database         |
| Aggregations and joins          | SQL Database         |
| Relational data                 | SQL Database         |

DynamoDB global tables resolve conflicts with last writer wins and charge for each replicated write. KV Store has no Region to choose: a write applies where it arrives, and other parts of the infrastructure converge within 60 seconds.

A function opens a namespace with `Azion.KV.open()`, a runtime global that needs no import line:

```javascript diff
-// Before: DynamoDB
-// const result = await dynamodb.getItem({ TableName: 'Users', Key: { userId: { S: '123' } } }).promise();
-// await dynamodb.putItem({ TableName: 'Users', Item: { userId: { S: '123' }, name: { S: 'John' } } }).promise();
 
+// After: Azion
+const kv = await Azion.KV.open('my-namespace');
+await kv.put('user:123', { name: 'John' }, { expirationTtl: 3600 });
+const userData = await kv.get('user:123', 'json');
```

`put` serializes an object to JSON, and `get` with `'json'` parses it back. `Azion.KV.open()` is the only entry point, and it is asynchronous. The namespace must exist first, or `open()` throws `NotFound`. `get()` returns `null` for a missing key, the same as for an expired one.

KV Store has no bulk import, and no API, CLI command, or Console screen reads or writes keys. To move the data:

1. Export the table from AWS:

   ```bash
   aws dynamodb export-table-to-point-in-time \
     --table-name Users \
     --s3-bucket my-export-bucket \
     --export-format DYNAMODB_JSON
   ```

2. Transform each item into a key and a value.

3. Create the namespace with a `POST` request to `https://api.azion.com/v4/workspace/kv/namespaces`, with `{"name": "my-namespace"}`. The name takes 3 to 63 characters, is case-sensitive, and is permanent: a namespace cannot be renamed or deleted. For the fields and the errors, refer to [Namespaces](/en/documentation/platform/kv-store/namespaces/).

4. Write the keys from a deployed function with `kv.put()`.

The function writes a key at most once per second. A value takes up to 25 MB, a key up to 512 bytes, and the metadata up to 1,024 bytes. Map each DynamoDB TTL to the `expiration` option, in Unix seconds, or to `expirationTtl`, in seconds with a minimum of 60. A write becomes visible everywhere within 60 seconds, or within the `cacheTtl` of the read. Before the import, review the key prefixes, the value formats, and the code that handles a missing key. For the client, refer to [KV Store runtime API](/en/documentation/devtools/runtime/api-reference/kv-store/).

---

## Replace ElastiCache

ElastiCache keeps data in memory, behind Redis or Memcached. On Azion, HTTP responses stay in Cache, and the values a function reads and writes move to KV Store.

| Aspect          | Amazon ElastiCache                            | Azion Cache               | Azion KV Store                                                        |
| --------------- | --------------------------------------------- | ------------------------- | --------------------------------------------------------------------- |
| Type            | In-memory cache                               | HTTP cache                | Distributed key-value store                                           |
| Engines         | Redis and Memcached                           | HTTP responses            | `Azion.KV`, from inside a function                                    |
| Persistence     | Optional, with Redis AOF or RDB               | Expires with **Max Age**  | Persists until the key expires or is deleted                          |
| Data structures | Strings, hashes, lists, sets, and sorted sets | HTTP responses            | One value per key: a string, an object, an `ArrayBuffer`, or a stream |
| Replication     | Cluster mode and replication groups           | Tiered Cache              | Azion's distributed infrastructure                                    |
| Access          | TCP connection                                | HTTP request and response | Runtime API                                                           |

`put` refuses a `Map`, a `Set`, a `WeakMap`, a `WeakSet`, a `RegExp`, and a `SharedArrayBuffer` with `INVALID_VALUE_TYPE`. Convert them to arrays first.

| ElastiCache use              | Destination on Azion |
| ---------------------------- | -------------------- |
| HTTP response caching        | Cache                |
| Session storage              | KV Store             |
| Rate-limit counters          | KV Store             |
| Feature flags                | KV Store             |
| Database query caching       | Cache and Functions  |
| Pub/Sub messaging            | -                    |
| Leaderboards and sorted sets | SQL Database         |
| Complex data structures      | SQL Database         |

Redis commands become KV Store calls:

```javascript diff
 const kv = await Azion.KV.open('sessions');
 const sessionData = { userId: 42, cart: [] };
 
 // Redis SET with expiration
-await redis.set('session:abc123', JSON.stringify(sessionData), 'EX', 3600);
+await kv.put('session:abc123', sessionData, { expirationTtl: 3600 });
 
 // Redis GET
-const data = await redis.get('session:abc123');
+const data = await kv.get('session:abc123', 'json');
 
 // Redis DEL
-await redis.del('session:abc123');
+await kv.delete('session:abc123');
```

`delete` resolves whether or not the key existed. For HTTP caching, refer to Recreate cache settings and to [Cache settings](/en/documentation/platform/applications/cache/cache-settings/).

---

## Move S3 buckets to Object Storage

[Object Storage](/en/documentation/platform/object-storage/) holds images, documents, static assets, media, uploads, and generated files. It speaks the S3 protocol, so S3 tools and SDKs reach it with a new endpoint, a region, and a key pair.

| Aspect              | Amazon S3                                  | Azion Object Storage                                                                                                    |
| ------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| Endpoint            | `https://s3.amazonaws.com/bucket`          | `https://s3.us-east-005.azionstorage.net`                                                                               |
| Region              | Multiple regions                           | `us-east-005`                                                                                                           |
| Data transfer       | Charged per GB                             | No data-transfer charge on Object Storage. Delivery to users goes through a workload, billed as Workloads data transfer |
| Storage classes     | Standard, Intelligent-Tiering, and Glacier | -                                                                                                                       |
| Buckets per account | 100 by default                             | 100                                                                                                                     |

The key pair comes from an Object Storage credential. The `secret_key` comes back only in the create response, so store it then. To migrate, the credential needs at least `listBuckets`, `listFiles`, and `writeFiles`, plus `listAllBucketNames` to list the buckets. `deleteFiles` requires `writeFiles`.

**Console**

To create the credential in Azion Console, use **Create Credential** in Object Storage. Select the capabilities **List Files**, **Read Files**, **Write Files**, **List All Bucket Names**, and **List Buckets**, then save the keys it shows.

**CLI**

The Azion CLI creates the destination buckets. For the commands, refer to [Create and modify a bucket](/en/documentation/guides/application-development/data/create-and-modify-bucket/). Create the credential in Azion Console or through the API.

**API**

To create the credential, send a `POST` request to the credentials endpoint:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/storage/credentials \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "migration-credential",
  "capabilities": ["listFiles", "readFiles", "writeFiles", "listAllBucketNames", "listBuckets"],
  "buckets": ["my-bucket"]
}'
```

The response carries `201`, the `access_key`, and the `secret_key`. Without `buckets`, the credential reaches every bucket in the account.

A Node.js migration script reaches Object Storage with the AWS SDK. Only the region, the endpoint, and the keys change:

```javascript diff
 import { S3Client } from '@aws-sdk/client-s3';
 
-// Before: Amazon S3
-const awsClient = new S3Client({
-  region: 'us-east-1',
-  credentials: {
-    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
-    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
-  }
-});
 
+// After: Azion Object Storage
+const azionClient = new S3Client({
+  region: 'us-east-005',
+  endpoint: 'https://s3.us-east-005.azionstorage.net',
+  credentials: {
+    accessKeyId: process.env.AZION_ACCESS_KEY,
+    secretAccessKey: process.env.AZION_SECRET_KEY
+  }
+});
```

Create the destination bucket before you copy, in Azion Console, the API, or the CLI. S3 tools cannot create or remove a bucket on Azion: `s3cmd mb` and `s3cmd rb` are refused with `403 AccessDenied`. A bucket name takes 6 to 63 characters, is unique across all accounts, and cannot start with `azion`.

To copy the data with [s3cmd](https://s3tools.org/s3cmd):

1. Run `s3cmd --configure -c ~/.s3cfg-azion`, and enter these values:

   - **Access Key** and **Secret Key**: the key pair of the credential.
   - **Default Region**: `us-east-005`.
   - **S3 Endpoint**: `s3.us-east-005.azionstorage.net`.
   - **DNS-style bucket+hostname:port template**: `%(bucket).s3.us-east-005.azionstorage.net`.
   - **Use HTTPS protocol**: `true`.

2. Configure a second file, `~/.s3cfg-aws`, with the keys of the AWS account. One s3cmd configuration holds one endpoint.

3. Download the S3 objects:

   ```bash
   s3cmd -c ~/.s3cfg-aws sync s3://aws-bucket/ ./export/
   ```

4. Upload them to Azion:

   ```bash
   s3cmd -c ~/.s3cfg-azion sync ./export/ s3://azion-bucket/
   ```

The objects are in the Azion bucket. To check, list them with `s3cmd -c ~/.s3cfg-azion ls s3://azion-bucket/`. `s3cmd ls` with no bucket lists every bucket, which needs `listAllBucketNames`. `s3cmd put file.png s3://my-bucket/` uploads an object, and `s3cmd get s3://my-bucket/file.png` downloads one.

The AWS CLI reaches Azion through a profile and `--endpoint-url`. One command reaches one endpoint, so the copy also goes through a local folder:

```bash
aws configure --profile azion
aws s3 ls --profile azion --endpoint-url https://s3.us-east-005.azionstorage.net
aws s3 sync s3://source-bucket/ ./export/
aws s3 sync ./export/ s3://dest-bucket/ --profile azion --endpoint-url https://s3.us-east-005.azionstorage.net
```

In `aws configure`, enter the Azion access key, the secret key, and `us-east-005` as the region. The last command uploads the folder to the Azion bucket.

[rclone](https://rclone.org) holds one remote for each provider, so it copies from S3 to Azion in one command. Create the remotes with `rclone config`:

- `aws-s3`: storage `s3`, provider `AWS`, and `env_auth` set to `true`.
- `azion`: storage `s3`, provider `Other`, endpoint `https://s3.us-east-005.azionstorage.net`, and `acl` set to `private`.

Then sync:

```bash
rclone sync aws-s3:source-bucket azion:dest-bucket --progress
```

`--progress` prints the transfer as it runs. Azion Console refuses a single upload over 300 MB. The API and S3 tools are not bound by that limit. Before the move, map the buckets, the object prefixes, the public and private assets, the access patterns, and the signed URL logic. Bucket access to workloads is `read_only`, `read_write`, or `restricted`, and a credential works independently of it. For the S3 operations, refer to [S3 compatibility](/en/documentation/platform/object-storage/s3-compatibility/) and [Use S3-compatible tools](/en/documentation/guides/application-development/data/use-s3-compatible-tools-with-object-storage/).

### Serve a bucket through an application

Public delivery goes through a connector and an application, not through the S3 endpoint. A connector of type `storage` reads a bucket of your account, narrowed to a prefix.

**Console**

To create the storage connector in Azion Console:

1. **Open the Connectors page**

   Access [Azion Console](https://console.azion.com/) > **Connectors**.

2. **Select the Object Storage type**

   On the **Create Connector** page, enter a **Name**, and select *Object Storage* in **Connector Type**.

3. **Select the bucket**

   In *Select a Bucket*, select the bucket. In **Prefix**, enter the prefix, such as `assets/`. The Console requires it.

4. **Select Create**

The connector reads the bucket. Send requests to it with a *Set Connector* rule in the application.

**CLI**

To create the connector with the Azion CLI, save the body of the API panel as `connector.json`, and run `azion create connector --type storage --file connector.json`. For the other fields, refer to [Connector settings](/en/documentation/platform/connectors/settings/#storage).

**API**

To create the connector, send a `POST` request to the connectors endpoint:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/connectors \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "assets-origin",
  "type": "storage",
  "attributes": { "bucket": "my-bucket", "prefix": "assets/" }
}'
```

The response carries `202` and the connector. A bucket that does not exist is refused with `28007`. To store no prefix, leave `prefix` out: an empty string is refused with `10018`.

For the buckets and their objects, refer to [Create and modify a bucket](/en/documentation/guides/application-development/data/create-and-modify-bucket/), [Upload and download objects](/en/documentation/guides/application-development/data/upload-and-download-objects-from-bucket/), [Use a bucket as origin](/en/documentation/guides/application-development/data/use-bucket-as-origin/), and the [Storage library](/en/documentation/devtools/azion-lib/storage/).

---

## Move DynamoDB queries and RDS databases to SQL Database

[SQL Database](/en/documentation/platform/sql-database/) uses the SQLite dialect and is fully ACID-compliant. One main instance takes every write, and read replicas answer reads. SQL Database is in Preview on every plan.

| Aspect           | Amazon RDS and Aurora                              | Azion SQL Database                                                                                                                                        |
| ---------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Engines          | MySQL, PostgreSQL, MariaDB, Oracle, and SQL Server | SQLite's dialect                                                                                                                                          |
| Architecture     | Primary and read replicas                          | One main instance and read replicas                                                                                                                       |
| Connection       | Connection string and connection pooling           | `Database.open()` from the `Azion.Sql` global, with no pool to manage                                                                                     |
| Writes from code | Through the connection                             | Through the API. A runtime connection is read-only                                                                                                        |
| Transactions     | ACID                                               | ACID                                                                                                                                                      |
| Scaling          | Vertical scaling and read replicas                 | Distributed architecture                                                                                                                                  |
| Vector search    | Engine-specific                                    | Supported, with vector columns, `libsql_vector_idx`, and `vector_top_k`. Refer to [Vector search](/en/documentation/platform/sql-database/vector-search/) |

A function reads the database through a read-only replica connection. `Azion.Sql` is a global, and the `azion:sql` import fails the build. A query parameter must be a number: a JavaScript string is refused with ``TypeError: unknown variant `String` ``. Write a text value as a literal in the SQL instead:

```javascript diff
-// Before: PostgreSQL
-// const result = await pgClient.query('SELECT id, name, email FROM users WHERE status = $1', ['active']);
 
-// Before: DynamoDB query on an index
-// const result = await dynamodb.query({ TableName: 'Orders', IndexName: 'CustomerIdIndex',
-//   KeyConditionExpression: 'customerId = :cid', FilterExpression: 'orderDate > :date' }).promise();
 
+// After: Azion
+const { Database } = Azion.Sql;
+
+const connection = await Database.open('my-database');
+const rows = await connection.query(
+  "SELECT id, name, email FROM users WHERE status = 'active' AND customer_id = ?",
+  [123]
+);
+let row = await rows.next();
+while (row) {
+  const id = row.getString(0);
+  const name = row.getString(1);
+  const email = row.getString(2);
+  row = await rows.next();
+}
```

`rows.next()` returns the next row, or `null` after the last one, and `row.getString(i)` reads a column by index. An `insert` or `delete` through the connection fails with `attempt to write a readonly database`. Under `azion dev`, `Azion.Sql` is `undefined`, so test the code on a deployed function. For server-side scripts, the [`@aziontech/sql`](/en/documentation/devtools/azion-lib/sql/) package manages databases through the API.

**Console**

To create the database in Azion Console:

1. **Open the database list**

   Access [Azion Console](https://console.azion.com/) > **SQL Database**.

2. **Start a new database**

   Open the create form with the **SQL Database** control of the list.

3. **Name the database**

   Under **General**, in **Name**, enter a name.

4. **Select Save**

The database appears in the list with the status `creating`, which turns to `created` in about 15 seconds.

**CLI**

The Azion CLI has no SQL Database command. Create the database in Azion Console or through the API.

**API**

To create the database, send a `POST` request to the databases endpoint:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/sql/databases \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{"name": "production-db"}'
```

The response carries `202` and the database with `"status": "creating"`. A create accepts only `name` and `active`. Then send the schema in the `statements` array of a `POST` request to the query endpoint:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/sql/databases/<database-id>/query \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{"statements": ["CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT, email TEXT, created_at TEXT);"]}'
```

The response carries one entry in `data` for each statement.

A database name takes 6 to 50 characters of letters, numbers, and hyphens, and it cannot change after creation. For the steps, refer to [Create the database](/en/documentation/guides/application-development/data/manage-sql-database/).

To move the data, export it from AWS. A PostgreSQL database exports to `INSERT` statements:

```bash
pg_dump -h my-db.xxxx.region.rds.amazonaws.com \
  -U admin \
  -d mydb \
  --data-only \
  --inserts \
  > export.sql
```

An Aurora cluster can also export a snapshot to S3:

```bash
aws rds start-export-task \
  --export-task-identifier my-export \
  --source-arn arn:aws:rds:region:account:cluster:my-aurora-cluster \
  --s3-bucket-name my-export-bucket \
  --export-only data
```

A DynamoDB export becomes `INSERT` statements after you transform its items. Then run the statements through one of these paths:

- **The API**: send the statements in the `statements` array of a `POST` request to `https://api.azion.com/v4/workspace/sql/databases/<database-id>/query`. One call takes 100 statements. A failed statement still returns HTTP `200`, with an `error` key in place of `results`. When nothing can run, the request returns `422` with `14005`.
- **The EdgeSQL Shell**: `.read export.sql` runs the script. The shell does not start on a clean install, so check [EdgeSQL Shell](/en/documentation/platform/sql-database/edgesql-shell/) before you rely on it. To install it, refer to [Install SQL Shell](/en/documentation/guides/application-development/data/install-edge-sql-shell/).

Azion has no import API. For the other import formats, refer to [Import data into SQL Database](/en/documentation/guides/application-development/data/import-data-sql-database/). For the runtime API, refer to [SQL Database runtime API](/en/documentation/devtools/runtime/api-reference/sql-database/).

`pg_dump` writes PostgreSQL types, and SQLite stores each value in its own type classes. Convert the column types in the schema before the import:

| PostgreSQL type           | SQLite type                        |
| ------------------------- | ---------------------------------- |
| INTEGER, BIGINT, SMALLINT | INTEGER                            |
| SERIAL, BIGSERIAL         | INTEGER, with auto-increment       |
| VARCHAR(n), CHAR(n), TEXT | TEXT                               |
| BOOLEAN                   | INTEGER, `0` or `1`                |
| REAL, DOUBLE PRECISION    | REAL                               |
| DECIMAL, NUMERIC          | REAL                               |
| DATE, TIME, TIMESTAMP     | TEXT, in ISO 8601 format           |
| JSON, JSONB               | TEXT, read with the JSON functions |
| UUID                      | TEXT                               |
| BYTEA                     | BLOB                               |

For the SQL each statement accepts, refer to the [SQLite language reference](https://www.sqlite.org/lang.html).

---

## Protect the application with WAF

[Web Application Firewall](/en/documentation/platform/firewall/waf/quickstart/) scores requests against eight threat families: cross-site scripting, directory traversal, evading tricks, file upload, identified attack, remote file inclusion, SQL injection, and unwanted access. A WAF rule set holds a sensitivity for each family, and a firewall rule applies it with *Set WAF*.

| Aspect        | AWS WAF                                 | Azion WAF                                                                      |
| ------------- | --------------------------------------- | ------------------------------------------------------------------------------ |
| Rule language | JSON rules in a web ACL                 | Criteria of Rules Engine for Firewall                                          |
| Managed rules | AWS Managed Rules and Marketplace rules | One managed ruleset, scored per threat family                                  |
| Custom rules  | Rules of a web ACL                      | [Rules Engine for Firewall](/en/documentation/platform/firewall/rules-engine/) |
| Scope         | Regional or CloudFront                  | The workloads the firewall is bound to                                         |
| Modes         | Count and Block                         | *Logging* and *Blocking*                                                       |

Map each AWS managed rule group to the families of the one Azion ruleset:

| AWS managed rule group       | Azion equivalent                             |
| ---------------------------- | -------------------------------------------- |
| AWSManagedRulesSQLiRuleSet   | The `sql_injection` threat family            |
| AWSManagedRulesXSSRuleSet    | The `cross_site_scripting` threat family     |
| AWSManagedRulesCommonRuleSet | The managed ruleset, with all eight families |
| AWSManagedRulesLinuxRuleSet  | -                                            |
| AWSManagedRulesPHPAppRuleSet | -                                            |

`mode` is required on every *Set WAF* behavior, and it has no default. Start in *Logging*, which matches the AWS Count action, to check what the rule set would block. Then switch to *Blocking*. In *Blocking* mode, a request the rule set blocks receives `400`. To keep a legitimate request from matching, add a WAF exception or use the **Tuning** tab, and review the blocked requests in Real-Time Events.

**Console**

To set up WAF in Azion Console:

1. **Create the rule set**

   Access [Azion Console](https://console.azion.com/) > **Edge Libraries** > **WAF Rules**, and create a rule set. Set the sensitivity of each family in **Threat Type Configuration**.

2. **Open the firewall**

   Go to **Secure** > **Firewalls**, and select or create the firewall.

3. **Turn on Web Application Firewall**

   In the **Main Settings** tab, under **Modules**, turn on **Web Application Firewall**, and select **Save**.

4. **Apply the rule set**

   In the **Rules Engine** tab, create a rule with the *Set WAF* behavior. In **Select a WAF**, select the rule set. In **Select a WAF mode**, select *Logging*.

5. **Bind the firewall to the workload**

   In the workload, under **Deployment Settings**, select the firewall in **Firewall**.

The firewall applies the rule set to the requests of the workload.

**CLI**

To bind a firewall with the Azion CLI, pass `--firewall-id` to the workload deployment. For the rule set and the rule, follow the [WAF quickstart](/en/documentation/platform/firewall/waf/quickstart/).

**API**

To create the rule set, send a `POST` request to the WAF endpoint:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/wafs \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{
  "active": true,
  "name": "My WAF",
  "product_version": "1.0",
  "engine_settings": {
    "engine_version": "2021-Q3",
    "type": "score",
    "attributes": {
      "rulesets": [1],
      "thresholds": [
        { "threat": "sql_injection", "sensitivity": "medium" }
      ]
    }
  }
}'
```

`engine_settings` holds the ruleset and the sensitivity of each family. `rulesets` accepts only `[1]`. Then apply the rule set with a firewall rule whose behavior is `{ "type": "set_waf", "attributes": { "waf_id": <waf-rule-set-id>, "mode": "logging" } }`. The API refuses `learning` as a mode.

Convert each AWS WAF custom rule to firewall criteria. Firewall variables differ from application variables: the path is `${request_uri}`, and an address range goes in a network list matched with `${network}`.

```json
{
  "Name": "BlockAdminAccess",
  "Priority": 1,
  "Statement": {
    "AndStatement": {
      "Statements": [
        { "ByteMatchStatement": { "SearchString": "/admin", "FieldToMatch": { "UriPath": {} }, "PositionalConstraint": "CONTAINS" } },
        { "NotStatement": { "Statement": { "IPSetReferenceStatement": { "IPSet": "allowed-ips" } } } }
      ]
    }
  },
  "Action": { "Block": {} }
}
```

The same rule as Azion criteria, with the IP set moved to a network list:

```text
${request_uri}  starts with     /admin
and
${network}      is not in list  <network-list-id>   (a network list that holds the allowed addresses)
Behavior: Deny (403 Forbidden)
```

The `${network}` criterion requires Network Shield on the firewall. For the variables and operators, refer to [Rules Engine for Firewall](/en/documentation/platform/firewall/rules-engine/). For code that inspects requests, refer to [Functions for Firewall](/en/documentation/platform/firewall/functions/).

---

## Recreate IP sets and threat lists

AWS WAF IP sets and the threat findings of GuardDuty become [network lists](/en/documentation/platform/firewall/network-shield/network-lists/) on Azion. A network list holds IP addresses and CIDR ranges, ASNs, or countries, with 1 to 20,000 items. A firewall rule matches the client address against it through the `${network}` criterion, which requires Network Shield.

| Aspect                 | AWS security services  | Azion                                                                             |
| ---------------------- | ---------------------- | --------------------------------------------------------------------------------- |
| Threat detection       | GuardDuty              | Firewall rules and network lists                                                  |
| Vulnerability scanning | Inspector              | -                                                                                 |
| Centralized findings   | Security Hub           | Real-Time Events, with WAF fields on **HTTP Requests**, and Data Stream to a SIEM |
| Threat intelligence    | GuardDuty threat feeds | The `Azion IP Tor Exit Nodes` list, ID `2`                                        |
| Audit logs             | CloudTrail             | The Activity History data source, kept for 2 years                                |

Azion maintains one list for every account: `Azion IP Tor Exit Nodes`, with ID `2`. No account can change it, and a write to it is refused with `22004`. A country list is a list you create with the `countries` type.

Move the findings in this order:

1. Export the GuardDuty findings, and put the addresses of IP-based threats in a network list.
2. Fix the Inspector findings in the application before the move. WAF protects the application at run time.
3. Read the CloudTrail equivalents in the Activity History data source of Real-Time Events.
4. Send the logs that Security Hub collected to a SIEM, such as IBM QRadar or Splunk, with Data Stream.

**Console**

To create a network list in Azion Console:

1. **Open the Network Lists page**

   Access [Azion Console](https://console.azion.com/) > **Edge Libraries** > **Network Lists**.

2. **Start a list**

   Select **Network List**.

3. **Name the list**

   In the **General** section, enter a **Name**. For example: `blocked-ips`.

4. **Select the type**

   In the **Network List Settings** section, select *IP/CIDR*, *ASN*, or *Countries*. The form opens with *ASN* selected.

5. **Enter the items**

   In the **List** field, enter one address or range per line. For *Countries*, select the countries in **Countries**.

6. **Select Save**

Azion Console shows the message `Your network list has been created`. The list appears in **Network Lists**, next to the Azion-maintained lists.

**CLI**

To create a list with the Azion CLI:

```bash
azion create network-list --name "Blocked countries" --type countries --items "BR,US"
```

`--add-item` and `--remove-item` change single items of an existing list. For the flags, refer to [Network lists](/en/documentation/platform/firewall/network-shield/network-lists/).

**API**

To create a list, send a `POST` request to the network lists endpoint:

```bash
curl -X POST https://api.azion.com/v4/workspace/network_lists \
  -H "Authorization: Token [TOKEN VALUE]" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"name":"Blocked IPs","type":"ip_cidr","items":["192.0.2.0/24","198.51.100.10"]}'
```

The response carries `201`, `"state": "executed"`, and the list with its `id`. Exact duplicates are removed. A `PATCH` that sends `items` replaces the whole array.

Then reference the list in a request rule of the firewall, at `/v4/workspace/firewalls/<firewall-id>/request_rules`:

```json
{
  "name": "Block listed addresses",
  "active": true,
  "criteria": [[{ "variable": "${network}", "conditional": "if", "operator": "is_in_list", "argument": <network-list-id> }]],
  "behaviors": [{ "type": "deny" }]
}
```

The list ID is a JSON integer. An ID sent as a string is refused with `25042`, and a firewall without Network Shield refuses the rule with `25047`. A list deletes only when no rule references it.

---

## Rely on DDoS Protection

[DDoS Protection](/en/documentation/platform/workloads/#ddos-protection) is on for every workload, with nothing to create and nothing to configure. It mitigates volumetric, protocol, and application-layer attacks, such as UDP and ICMP floods, SYN floods, packet fragmentation, HTTP floods, and slowloris, on layers 3, 4, 6, and 7.

| Aspect              | AWS Shield                             | Azion DDoS Protection                                                            |
| ------------------- | -------------------------------------- | -------------------------------------------------------------------------------- |
| Standard protection | Automatic, at no cost                  | Automatic, and it cannot be turned off                                           |
| Advanced protection | Shield Advanced, at an additional cost | Custom firewall rules                                                            |
| Layers              | 3, 4, and 7                            | 3, 4, 6, and 7                                                                   |
| Response team       | Shield Advanced only                   | The Security Response Team, an add-on to Enterprise and Mission-Critical support |
| Billing             | Cost protection with Shield Advanced   | Unmetered for layers 3 and 4. Layer 7 mitigation can generate chargeable traffic |

A firewall shows the **DDoS Protection Unmetered** switch in **Main Settings** > **Modules**, always on. In the API, `modules.ddos_protection` is read-only. DDoS Protection has no thresholds, no per-rule switches, and no alerts. For targeted mitigation, write custom rules on the firewall bound to the workload. For the attack types, refer to [Attack mitigation](/en/documentation/platform/workloads/ddos-protection/ddos-mitigation/).

[Network Shield](/en/documentation/platform/firewall/network-shield/quickstart/) is a different module of the firewall. It matches the client address against a network list of IP addresses, CIDR ranges, ASNs, or countries, through the `${network}` criterion. Use it to block or allow sets of clients, restrict countries, or rate-limit a set of clients.

---

## Recreate bot management

[Bot Manager](/en/documentation/platform/firewall/bot-manager/quickstart/) scores each request and acts on the score. Bot Manager Lite is the Marketplace function included on every plan, and the full Bot Manager is available on Enterprise.

| Aspect       | AWS WAF Bot Control                              | Azion Bot Manager                                                                                                    |
| ------------ | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| Detection    | Machine learning, heuristics, and fingerprinting | Static rules, a dynamic behavioral method in the full Bot Manager, device fingerprints, and reputation network lists |
| Challenge    | CAPTCHA and silent challenge                     | A JavaScript Tag for fingerprinting, and ALTCHA through the `redirect` action                                        |
| Actions      | Allow, Count, Block, and CAPTCHA                 | `allow`, `custom_html`, `deny`, `drop`, `hold_connection`, `random_delay`, and `redirect`                            |
| Lite version | Not available                                    | Bot Manager Lite                                                                                                     |

Bot Manager Lite scores a request with 26 static rules, against a `threshold` that defaults to 30, and takes the `deny` action by default. It can also check the client against reputation network lists. Tolerance levels belong to the dynamic rules of the full Bot Manager, which Bot Manager Lite does not have.

To set up Bot Manager Lite in Azion Console:

1. **Install the integration**

   Access [Azion Console](https://console.azion.com/) > **Marketplace**, search for **Bot Manager Lite**, and select **Install**. The installation takes effect at once.

2. **Open the firewall**

   Go to **Firewalls**, and select a firewall with the **Functions** module on.

3. **Create the function instance**

   In the **Functions Instances** tab, create an instance of Bot Manager Lite. In its JSON arguments, set `threshold` and `action`.

4. **Run the function**

   In the **Rules Engine** tab, create a rule with the *Run Function* behavior and the instance.

5. **Bind the firewall to the workload**

The firewall scores the requests of the workload. For every argument, refer to [Install Bot Manager Lite](/en/documentation/guides/application-development/integrations/bot-manager-lite/) and [Bot Manager Lite](/en/documentation/platform/firewall/bot-manager/bot-manager-lite/). For how a function runs on a firewall, refer to [Functions for Firewall](/en/documentation/platform/firewall/functions/). Bot protection from a third party is also available through the [Radware Bot Manager](/en/documentation/guides/application-development/integrations/radware-bot-manager/) integration.

To block a client by its user agent, add a firewall rule:

```text
Criteria: ${header_user_agent} matches BadBot
Behavior: Deny (403 Forbidden)
```

`${header_user_agent}` requires the WAF module on the firewall and supports only *matches* and *does not match*. The firewall has no allow behavior. To exempt a client, such as Googlebot, add a *does not match* criterion to the deny rule, or order the rules. A client can send any user agent, so verify a good bot another way. To check the rule:

```bash
curl -A "BadBot/1.0" https://<your-domain>/
```

The response is `403`, with the Forbidden error page. A request with a browser user agent, such as `Mozilla/5.0`, receives the normal response.

---

## Recreate rate limits

Azion limits request rates in two ways, and each covers a different part of what AWS WAF rate-based rules do. AWS counts per IP, header, URI, or method, over a window of 1 minute to 1 hour, and blocks or counts. Use the native *Set Rate Limit* behavior of a firewall rule to cap the requests per second or per minute. It counts per client IP address or across all clients. Use the [Upstash Rate Limiting](/en/documentation/guides/application-development/integrations/upstash-rate-limiting-integration/) integration, a rate limit with penalty run as a firewall function, for custom keys, custom windows, or a penalty period.

| Capability      | Native *Set Rate Limit*                   | Upstash Rate Limiting function                                                       |
| --------------- | ----------------------------------------- | ------------------------------------------------------------------------------------ |
| Count key       | Client IP address or global               | Any combination of request metadata, headers, and the hostname                       |
| Window          | Per second or per minute                  | Any interval in seconds or minutes, with different limits for different times of day |
| Algorithm       | Leaky bucket, counted in each data center | Fixed window, sliding window, or token bucket, counted globally                      |
| Response        | `429`, with no rate-limit header          | `429` at the limit, and `403` during a penalty                                       |
| Log-only action | None                                      | None                                                                                 |
| Requirements    | None                                      | An Upstash account and Global Database                                               |

### Use the native rate limit

A *Set Rate Limit* behavior counts the requests its rule matches. The criteria of the rule scope the limit, such as the path with `${request_uri}`, or the method, which requires WAF on the firewall. **Rate Limit Type** is *Req/s* or *Req/min*, and **Limit By** is *Client IP address* or *Global*. **Average Rate Limit** takes at least 1, and **Maximum Burst Size** takes at least 1 and applies to *Req/s* only. No behavior can follow *Set Rate Limit* in a rule. A rule whose criteria join several paths with `or` shares one count across all of them.

**Console**

To create the rate limit in Azion Console:

1. **Open the firewall**

   Access [Azion Console](https://console.azion.com/) > **Secure** > **Firewalls**, and select the firewall.

2. **Go to the Rules Engine tab**

3. **Select + Rule**

4. **Set the criteria**

   Under **Criteria**, select `${request_uri}`, the *starts with* operator, and `/api/` as the argument.

5. **Add the Set Rate Limit behavior**

   Under **Behaviors**, select *Set Rate Limit*. Set **Rate Limit Type** to *Req/s*, **Limit By** to *Client IP address*, **Average Rate Limit** to `10`, and **Maximum Burst Size** to `10`.

6. **Select Save**

The rule appears in the list of firewall rules.

**CLI**

To create the rule with the Azion CLI, save the rule body of the API panel in a file. Then pass the file with `--file` to the firewall rule command. For the commands, refer to [Firewall quickstart](/en/documentation/platform/firewall/quickstart/).

**API**

To create the rate limit, send a `POST` request to the request rules of the firewall:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/firewalls/<firewall-id>/request_rules \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "api rate limit",
  "active": true,
  "criteria": [
    [{ "variable": "${request_uri}", "conditional": "if", "operator": "starts_with", "argument": "/api/" }]
  ],
  "behaviors": [
    { "type": "set_rate_limit", "attributes": { "type": "second", "limit_by": "client_ip", "average_rate_limit": 10, "maximum_burst_size": 10 } }
  ]
}'
```

The response carries `"state": "pending"` and the rule. Firewall rules follow the same workspace path pattern as application rules.

A request beyond the rate and the burst receives `429`, with the error page titled Too Many Requests. For how the rate and the burst admit requests, refer to [Set Rate Limit](/en/documentation/platform/firewall/rules-engine/#set-rate-limit). For the instances a firewall runs, refer to [Functions instances](/en/documentation/platform/firewall/functions-instances/).

### Use the rate limit with penalty

The Upstash Rate Limiting function keeps its counters in an Upstash Global Database. It therefore counts every request across the network, not in each data center. A request during a penalty receives `403 Forbidden`. A valid request is counted, and the function returns `429 Too Many Requests` when the count reaches the limit.

To set it up in Azion Console:

1. **Install the integration**

   Access [Azion Console](https://console.azion.com/) > **Marketplace**, search for `Upstash Rate Limiting`, and select **Install**.

2. **Open the firewall**

   Go to **Firewalls**, and open a firewall with **Functions** turned on in **Modules**.

3. **Create the function instance**

   In the **Functions Instances** tab, create an instance. In **Function**, select the Upstash Rate Limiting function, and edit the JSON **Arguments**.

4. **Run the function**

   In the **Rules Engine** tab, create a rule with criteria such as `Host` *matches* `yourdomain.com`, and the *Run Function* behavior with the instance.

5. **Bind the firewall to the workload**

   Run the CLI command that creates the workload deployment with the firewall:

   ```bash
   azion create workload-deployment --workload-id <workload-id> --name <deployment-name> --application-id <application-id> --firewall-id <firewall-id> --strategy-type default --active true --current true
   ```

The function counts the requests the rule matches. These arguments set a sliding window of 2 requests per 20 seconds from midnight to noon UTC, with a 45-second penalty:

```json
{
  "upstash_redis_rest_url": "https://your-database.upstash.io",
  "upstash_redis_rest_token": "<your-upstash-token>",
  "rate_limit_prefix": "my_rate_limit",
  "rate_limit_key_metadata": ["remote_addr"],
  "rate_limit_key_header": ["x-a-custom-header"],
  "rate_limit_key_hostname": true,
  "rate_limit_repenalize": true,
  "rate_limits": [
    {
      "algorithm": "sliding_window",
      "requests": 2,
      "interval": "20 s",
      "start": "00:00",
      "end": "12:00",
      "penalty_in_seconds": 45
    }
  ]
}
```

| Argument                                             | Description                                                                                                             |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `upstash_redis_rest_url`, `upstash_redis_rest_token` | The REST URL and the token of the Upstash database that stores the counters and the penalties                           |
| `rate_limit_prefix`                                  | A prefix for every key, which keeps two instances of the function apart                                                 |
| `rate_limit_key_metadata`                            | The request metadata that forms the key, such as `remote_addr`                                                          |
| `rate_limit_key_header`                              | The headers that form the key                                                                                           |
| `rate_limit_key_hostname`                            | When `true`, the hostname is part of the key                                                                            |
| `rate_limit_repenalize`                              | When `true`, every request during a penalty restarts it                                                                 |
| `rate_limits`                                        | The windows, at least one. When two windows overlap, the first one in the list applies                                  |
| `algorithm`                                          | `fixed_window`, `sliding_window`, or `token_bucket`                                                                     |
| `requests`                                           | The requests allowed in the interval                                                                                    |
| `interval`                                           | The window, as a number and `s` or `m`. For example: `"120 s"`                                                          |
| `start`, `end`                                       | The time of day the window covers, in 24-hour UTC. They default to `00:00` and `23:59`                                  |
| `penalty_in_seconds`                                 | How long a client that exceeds the limit receives `403`. Without it, the window is a plain rate limit                   |
| `max_tokens`, `refil_rate`                           | The bucket size and the refill per interval of a `token_bucket` window. `refil_rate` is the spelling the function reads |

The key joins the prefix and every value the arguments select. In this example, it is `my_rate_limit + client IP + x-a-custom-header value + hostname`, such as `my_rate_limit_127.0.0.1_Value_azion.com`. For the full setup, refer to [Install the Upstash Rate Limiting integration](/en/documentation/guides/application-development/integrations/upstash-rate-limiting-integration/).

---

## Rebuild monitoring

Azion splits observability across three products. [Real-Time Metrics](/en/documentation/platform/real-time-metrics/) charts aggregates over time, [Real-Time Events](/en/documentation/platform/real-time-events/) answers queries about individual requests, and [Data Stream](/en/documentation/platform/data-stream/) sends the logs to external destinations.

### Real-Time Metrics

| Aspect         | Amazon CloudWatch metrics                       | Azion Real-Time Metrics                                                                                     |
| -------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Data freshness | 1-5 minutes                                     | Up to 10 minutes to aggregate                                                                               |
| Retention      | 15 months                                       | 2 years, except 90 days for `httpBreakdownMetrics` and 60 days for `botManagerBreakdownMetrics`             |
| Query method   | GetMetricStatistics API and CloudWatch Insights | Dashboards, **Copy query**, **Export CSV**, and the GraphQL API                                             |
| Metrics        | Custom metrics in namespaces                    | Requests, data transferred, status codes, cache offload, average request time, WAF threats, and DNS queries |
| Granularity    | 1 second to 1 day                               | 1 minute under 2.5 days, 1 hour up to 60 days, and 1 day beyond                                             |
| Dashboards     | CloudWatch dashboards                           | Real-Time Metrics and the Grafana plugin                                                                    |
| Alarms         | CloudWatch alarms                               | Alerts in the destination of a Data Stream stream, such as Datadog or Splunk                                |

The dashboards cover these products:

- **Applications**: **Requests**, with total requests, requests by method, and **Average Request Time**, the average time in seconds Azion takes to process and answer a request. **Status Codes**, with the 2XX, 3XX, 4XX, and 5XX responses. **Data Transferred**, **Bandwidth Saving**, and **Edge Offload**.
- **Functions**: **Total Invocations**, split into invocations on applications and on firewalls.
- **Image Processor**: **Total Requests** and **Total Requests per Second**.
- **Tiered Cache**: the **Caching Offload** dashboard, with the **Tiered Cache** and **Tiered Cache Offload** charts.
- **WAF**: **Threats vs Requests**, the cross-site scripting, remote file inclusion, and SQL injection threats, and the threats by country, family, and host.
- **Edge DNS**: **Total Queries**.
- **Bot Manager**: **Bad Bot Hits**, **Good Bot Hits**, **Bot Hits**, and **Transactions**. The tab needs a Bot Manager subscription.
- **Data Stream**: **Total Data Streamed** and **Total Requests**.

Real-Time Metrics reports no latency, time to first byte, or origin response time. To read the cache status of the requests, filter a dashboard by **Upstream Cache Status**, whose values include `HIT`, `MISS`, `STALE`, and `EXPIRED`. To find origin errors, filter by **Upstream Status**, which is `0` when the origin did not answer.

To open the dashboards, access [Azion Console](https://console.azion.com/) > **Real-Time Metrics**. It opens on **Build** > **Applications** > **Data Transferred**, over the **Last 5 minutes**. Select a category tab, **Build**, **Secure**, or **Observe**, then a product tab. To narrow a dashboard to one workload, add the **Domain** or **Workload** filter. To export a chart, open its **More options** menu and select **Export CSV**.

To query the same data, send a GraphQL query to `https://api.azion.com/v4/metrics/graphql`. This query counts the requests by status code:

```graphql
query {
  workloadMetrics(limit: 20, filter: { tsRange: {begin: "2026-10-01T00:00:00", end: "2026-10-02T00:00:00"} }, aggregate: { sum: requests }, groupBy: [status], orderBy: [sum_DESC]) {
    status
    sum
  }
}
```

This query sums the bytes sent for one host, per time bucket:

```graphql
query {
  workloadMetrics(limit: 500, filter: { tsRange: {begin: "2026-10-01T00:00:00", end: "2026-10-07T00:00:00"}, host: "www.example.com" }, aggregate: { sum: bytesSent }, groupBy: [ts], orderBy: [ts_DESC]) {
    ts
    sum
  }
}
```

Replace the dates with a range inside the retention period. A range past it returns an empty array. `limit` takes up to 10,000 rows and defaults to 10. A calculated field, such as `dataTransferredOut`, cannot be aggregated. The `httpMetrics` dataset of older queries still works, but it is deprecated. To query a WAF chart, such as the threats by country, open its **More options** menu and select **Copy query**.

For every field, refer to [Real-Time Metrics GraphQL fields](/en/documentation/devtools/graphql/gql-real-time-metrics-fields/) and [Build dashboards](/en/documentation/platform/real-time-metrics/build-dashboards/). For dashboards in Grafana, refer to [Grafana plugin custom dashboards](/en/documentation/guides/platform/observability/azion-plugin-grafana-custom-dash/), [pre-built dashboards](/en/documentation/guides/platform/observability/azion-plugin-grafana-pre-built-dash/), and the [Azion Grafana plugin](https://github.com/aziontech/grafana-plugin) repository. To read the dashboards, refer to [Analyze metrics](/en/documentation/guides/platform/observability/analyze-metrics/).

### Real-Time Events

| Aspect         | Amazon CloudWatch Logs                        | Azion Real-Time Events                                                                      |
| -------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Access         | GetLogEvents API and CloudWatch Logs Insights | Queries in Azion Console or the GraphQL API                                                 |
| Delay          | Seconds to minutes                            | Up to 30 seconds                                                                            |
| Retention      | 1 day to 10 years                             | 7 days, or 168 hours. Activity History keeps 2 years. For longer retention, use Data Stream |
| Query language | CloudWatch Logs Insights                      | GraphQL, with the fields you select                                                         |
| Organization   | Log groups with log streams                   | Data sources by product                                                                     |
| Metric filters | Metrics created from logs                     | Data Stream to an external destination                                                      |

Real-Time Events needs no setup. Each CloudWatch log group maps to a data source:

| CloudWatch log group  | Real-Time Events data source                    |
| --------------------- | ----------------------------------------------- |
| `/aws/cloudfront/...` | **HTTP Requests**                               |
| `/aws/lambda/...`     | **Functions**                                   |
| `/aws/waf/...`        | **HTTP Requests**, which carries the WAF fields |
| `/aws/route53/...`    | **Edge DNS**                                    |
| CloudTrail            | **Activity History**                            |

The other data sources are **Functions Console**, **Image Processor**, **Tiered Cache**, and **Data Stream**.

To query the events in Azion Console:

1. **Open Real-Time Events**

   Access [Azion Console](https://console.azion.com/) > **Products menu** > **Observe** > **Real-Time Events**.

2. **Select the data source**

   Select the data source, such as **HTTP Requests**.

3. **Set the time and the filters**

   Set the **Time Filter**, which opens on the last 15 minutes. In **Filter by**, add conditions such as the host, the status, or the remote address.

4. **Select Refresh**

The results table lists the events. Select a row to open the **More details** view, with every variable of the record. A custom time range stays inside the last 168 hours.

A CloudWatch Logs Insights query filters and sorts the log lines:

```text
fields @timestamp, @message
| filter @logGroup = "/aws/cloudfront/distribution"
| filter status >= 500
| sort @timestamp desc
| limit 100
```

To query the same data, send a GraphQL query to `https://api.azion.com/v4/events/graphql`. The `workloadEvents` dataset holds the HTTP requests:

```graphql
query {
  workloadEvents(
    limit: 100
    filter: { tsRange: { begin: "2026-10-07T00:00:00", end: "2026-10-07T01:00:00" }, statusGte: 500 }
    orderBy: [ts_DESC]
  ) {
    ts
    remoteAddress
    requestUri
    requestMethod
    host
    status
    upstreamStatus
    upstreamResponseTime
  }
}
```

Replace the dates with a range inside the last 7 days. `statusGte: 500` keeps the server errors, and `statusEq: 500` keeps one status. `upstreamResponseTime` reads `-` for a response served from cache. The Activity History data source answers queries over 2 years.

The datasets carry these fields, among others:

- **HTTP Requests**: `ts`, `remoteAddress`, `remotePort`, `host`, `requestUri`, `requestMethod`, and `status`. Timing: `requestTime`, `upstreamResponseTime`, and `upstreamHeaderTime`. Bytes: `bytesSent`, `requestLength`, `upstreamBytesReceived`, and `upstreamBytesSent`. Cache: `upstreamCacheStatus`. WAF: `wafBlock`, `wafMatch`, `wafScore`, and `wafLearning`, which reports the *Logging* mode. Location: `geolocCountryName`, `geolocRegionName`, and `geolocAsn`. TLS: `sslCipher` and `sslProtocol`.
- **Functions**: `functionsInstanceIdList`, `functionsList`, `functionsTime`, and `functionLanguage`.
- **Edge DNS**: `level`, `qtype`, `resolutionType`, `statusCode`, and `zoneId`.

For every field, refer to [Real-Time Events GraphQL fields](/en/documentation/devtools/graphql/gql-real-time-events-fields/) and [Investigate requests with the GraphQL API](/en/documentation/guides/platform/observability/investigate-requests-graphql-api/).

### Data Stream

| Aspect       | Amazon Data Firehose                         | Azion Data Stream                                                  |
| ------------ | -------------------------------------------- | ------------------------------------------------------------------ |
| Access       | Delivery to configured destinations          | Push to an external destination                                    |
| Delay        | Buffered delivery                            | Batches of 2,000 records or 60 seconds, delivered within 3 minutes |
| Retention    | Set by the destination                       | Set by the destination                                             |
| Format       | Records, with optional Lambda transformation | Templates that select the fields                                   |
| Destinations | S3, Redshift, OpenSearch, and HTTP endpoints | 11 types, listed below                                             |

A stream sends one data source to one destination:

- **Storage**: Amazon S3, Azure Blob Storage, and Azion Object Storage, through the S3 type.
- **Monitoring**: Datadog, Splunk, Elasticsearch, and Azure Monitor.
- **Streaming**: AWS Kinesis Data Firehose and Apache Kafka.
- **Analytics**: Google BigQuery.
- **Security**: IBM QRadar.
- **Custom**: Standard HTTP/HTTPS POST.

Each Firehose destination maps to an endpoint type:

| Firehose destination  | Data Stream endpoint                                                               |
| --------------------- | ---------------------------------------------------------------------------------- |
| S3                    | S3, for any S3-compatible storage                                                  |
| Redshift              | Google BigQuery, or Standard HTTP/HTTPS POST                                       |
| Elasticsearch         | Elasticsearch                                                                      |
| HTTP endpoint         | Standard HTTP/HTTPS POST                                                           |
| Lambda transformation | A custom template that selects the fields. Data Stream runs no code on the records |

A CloudWatch metric filter or alarm becomes a stream to a monitoring destination, such as Datadog or Splunk, which counts and alerts there. A stream needs exactly one of sampling or a workload filter. Saving an active sampled stream deactivates every other stream on the account. Filtered streams coexist.

**Console**

To create a stream in Azion Console:

1. **Open Data Stream**

   Access [Azion Console](https://console.azion.com/) > **Data Stream**.

2. **Select + Stream**

3. **Select the data source**

   In **Input**, select the **Data Source**: *Activity History*, *Applications*, *Functions*, or *WAF Events*.

4. **Select the template**

   In **Render Template**, select the **Template**. To choose the fields, select **Create Custom Template**.

5. **Select the destination**

   In **Output**, select the **Connector**, such as *Datadog* or *Simple Storage Service (S3)*, and enter its credentials.

6. **Turn on Active and select Save**

The stream starts sending after 1 to 2 minutes.

**CLI**

To create a stream with the Azion CLI, follow the CLI panel of the [Data Stream quickstart](/en/documentation/platform/data-stream/quickstart/).

**API**

To create a stream, send a `POST` request to `https://api.azion.com/v4/workspace/stream/streams`. The body has `inputs`, `transform`, and `outputs`. This stream sends the requests of one workload to an S3 bucket:

```json
{
  "name": "s3-archive",
  "active": true,
  "inputs": [
    { "type": "raw_logs", "attributes": { "data_source": "workloads" } }
  ],
  "transform": [
    { "type": "filter_workloads", "attributes": { "workloads": [<workload-id>] } },
    { "type": "render_template", "attributes": { "template": 2 } }
  ],
  "outputs": [
    {
      "type": "s3",
      "attributes": {
        "host_url": "https://s3.amazonaws.com",
        "bucket_name": "my-logs-bucket",
        "region": "us-east-1",
        "access_key": "[ACCESS KEY]",
        "secret_key": "[SECRET KEY]",
        "object_key_prefix": "azion-logs",
        "content_type": "plain/text"
      }
    }
  ]
}
```

A `POST` returns `201`. Template `2` is the *Applications Event Collector*. Without sampling or a workload filter, the create fails with `32002`, and with both it fails with `32007`. A Datadog output takes `url`, such as `https://http-intake.logs.datadoghq.com/v1/input`, and `api_key`.

For an Object Storage destination, the credential needs `listAllBucketNames`, `listBuckets`, `listFiles`, and `writeFiles`, or every send fails with `503`. For the fields, refer to [Stream settings](/en/documentation/platform/data-stream/stream-settings/) and [Endpoints](/en/documentation/platform/data-stream/endpoints/). For destination guides, refer to [Amazon S3](/en/documentation/guides/platform/observability/endpoint-amazon-s3/), [Azion Object Storage](/en/documentation/guides/platform/observability/connector-azion-object-storage/), [Datadog](/en/documentation/guides/platform/observability/endpoint-datadog/), [Splunk](/en/documentation/guides/platform/observability/endpoint-splunk/), [Elasticsearch](/en/documentation/guides/platform/observability/endpoint-elasticsearch/), [Kinesis](/en/documentation/guides/platform/observability/endpoint-amazon-kinesis/), [BigQuery](/en/documentation/guides/platform/observability/endpoint-google-bigquery/), and [Configure sampling](/en/documentation/guides/platform/observability/configure-sampling/).

---

## Replace X-Ray tracing

Azion collects no traces and draws no service map. Each request log carries a unique ID, `$request_id`, which a tracing tool joins with the logs of your other services. Data Stream sends those logs to the tool, and a function passes a trace ID to the origin.

| Aspect           | AWS X-Ray                    | Azion                                                |
| ---------------- | ---------------------------- | ---------------------------------------------------- |
| Trace collection | Automatic with the X-Ray SDK | Request logs sent by Data Stream to an external tool |
| Service map      | Built-in service map         | The external tool, such as Datadog or Splunk         |
| Trace analysis   | X-Ray console                | The external tool                                    |
| Annotations      | Custom key-value pairs       | The variables of a custom template                   |
| Sampling         | Sampling rules               | A sampling rate from 1 to 100 percent                |
| Destinations     | X-Ray console and CloudWatch | 11 endpoint types                                    |

| X-Ray concept        | Azion equivalent                                                              |
| -------------------- | ----------------------------------------------------------------------------- |
| Segment              | A request log entry, from the **Applications** data source                    |
| Subsegment           | A function log entry, from a second stream with the **Functions** data source |
| Service graph        | The service map of the external tool                                          |
| Trace ID propagation | A header that a function adds to the request it forwards                      |
| Annotation           | A variable of the custom template                                             |

Data Stream reaches these tracing tools: Datadog, Splunk, Elasticsearch, Apache Kafka, which feeds Jaeger or Zipkin, Google BigQuery, AWS Kinesis Data Firehose, and Standard HTTP/HTTPS POST for a custom system.

To send the trace fields, create a custom template with a `POST` request to `https://api.azion.com/v4/workspace/stream/templates`. The `data_set` is a JSON object, sent as a string, whose values are variables:

```json
{
  "name": "trace-template",
  "data_set": "{\"request_id\": \"$request_id\", \"time\": \"$time\", \"host\": \"$host\", \"request_uri\": \"$request_uri\", \"request_method\": \"$request_method\", \"status\": \"$status\", \"upstream_addr\": \"$upstream_addr\", \"upstream_response_time\": \"$upstream_response_time\", \"upstream_status\": \"$upstream_status\", \"remote_addr\": \"$remote_addr\"}"
}
```

The response carries the template `id`. Use it in `render_template` of a stream with the `workloads` data source, as the Data Stream stage shows, and a Datadog or Splunk output. In Azion Console, **Create Custom Template** in the **Render Template** section opens the same form. A stream holds one data source, so function logs go in a second stream with the `functions_console` data source.

To pass a trace ID to the origin, run a function that forwards the request. It reads the incoming ID, or uses the Azion request ID:

```javascript
export default {
  async fetch(request, env, ctx) {
    const traceId = request.headers.get('x-trace-id') || request.metadata['request_id'];
    const headers = new Headers(request.headers);
    headers.set('X-Trace-Id', traceId);
    return fetch(new Request(request, { headers }));
  }
};
```

The origin receives `X-Trace-Id`, and the client receives the same request ID in the `x-azion-request-id` response header. For the variables of each data source, refer to [Data sources and variables](/en/documentation/platform/data-stream/data-sources-and-variables/).

---

## Replace CloudWatch RUM with Edge Pulse

[Edge Pulse](/en/documentation/platform/edge-pulse/) measures how real visitors reach Azion's infrastructure from their browsers: navigation, availability, latency, and bandwidth. Every account has it active on every plan, at no additional charge, so there is nothing to create.

| Aspect           | Amazon CloudWatch RUM                             | Azion Edge Pulse                                                                |
| ---------------- | ------------------------------------------------- | ------------------------------------------------------------------------------- |
| Collection       | JavaScript SDK                                    | A JavaScript tag on each page, the **Default Tag** or the **Pre-loading Tag**   |
| Metrics          | Page load, Core Web Vitals, and JavaScript errors | Navigation, availability, latency, and bandwidth against Azion's infrastructure |
| Settings         | Sampling and custom events                        | None. The tag has no sampling rate and no custom events                         |
| Reading the data | The RUM console                                   | Queries to the GraphQL API of Real-Time Events, on the `pulseEvents` dataset    |

To add the tag:

1. **Open the Edge Pulse page**

   Access [Azion Console](https://console.azion.com/) > **Products menu** > **Observe** > **Edge Pulse**.

2. **Select the tag your pages need**

   Select the **Pre-loading Tag** for pages whose Content Security Policy rules out inline JavaScript. Otherwise, select the **Default Tag**.

3. **Select Copy to Clipboard**

4. **Paste the tag into the page**

   Paste it before the closing `body` tag of each page you want measured, and publish the pages.

Each visitor runs one test every 30 minutes. Azion Console has no page that charts the results: read them with the GraphQL API of Real-Time Events. For the steps, refer to [Edge Pulse quickstart](/en/documentation/platform/edge-pulse/quickstart/).

---

## Prepare the certificate

[Certificate Manager](/en/documentation/platform/workloads/certificate-manager/quickstart/) holds the certificates that workloads serve. Prepare the certificate before the domain points to Azion, so users reach the project over HTTPS from the first request.

| Area                | AWS Certificate Manager                | Azion Certificate Manager                                                                                               |
| ------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Certificate types   | Public, private, and imported          | Azion SAN, Let's Encrypt, custom certificates, and Trusted CA certificates for mTLS                                     |
| Validation          | DNS and email                          | Let's Encrypt HTTP-01 or DNS-01 challenges                                                                              |
| Renewal             | Automatic                              | Let's Encrypt certificates renew from 30 days before their 90-day expiry. Custom certificates follow your own lifecycle |
| Scope               | Regional, and us-east-1 for CloudFront | TLS certificates for workloads                                                                                          |
| mTLS                | Supported, with AWS Private CA         | Trusted CA certificates. Azion SAN does not support mTLS                                                                |
| Cost                | Free for AWS resources                 | Let's Encrypt at no additional cost                                                                                     |
| Default certificate | -                                      | Azion SAN covers the `azionedge.net` workload domain and the `azion.app` hostname                                       |
| Custom certificates | Imported certificates                  | Upload of a certificate and its private key, single-domain or SAN, with RSA 2048 or P-256 keys                          |
| Origin encryption   | Origin protocol policy                 | **Transport Protocol Policy** of the connector: *Preserve*, *Force HTTPS*, or *Force HTTP*                              |

Azion issues a Let's Encrypt certificate once you choose a Let's Encrypt preset. Pick the challenge by where DNS answers:

- **HTTP-01** needs the hostname, and every alternative name, to already point to Azion.
- **DNS-01** works before the move. At an external DNS provider, such as Route 53, add a CNAME from `_acme-challenge.<domain>` to `<domain>.letsencrypt.azion.com`. In Edge DNS, the record is automatic.

For a move from AWS, use DNS-01.

**Console**

To request the certificate in Azion Console:

1. **Open the workload**

   Access [Azion Console](https://console.azion.com/) > **Workloads**, then select or create the workload.

2. **Enter the domains**

   In **Domains**, enter each hostname the project answers to. Wildcards are refused.

3. **Turn on HTTPS**

   In **Protocol Settings**, turn on **HTTPS support**.

4. **Select the certificate**

   In **Digital Certificate**, select *New Let's Encrypt Certificate (DNS-01)*.

5. **Select Save**

The first attempt runs up to 5 minutes after you save, and retries follow at 5, 10, 15, 20, and 30 minutes, then on a slower schedule. The status moves from `pending` to `challenge_verification`, then to `active` or `failed`.

To upload a certificate exported from ACM, access **Certificate Manager** and create a digital certificate with the **Server Certificate** preset. Paste the PEM certificate and the private key. Intermediate certificates go in the same field as the certificate. Then select it in the **Digital Certificate** field of the workload. For the steps, refer to [Upload a digital certificate](/en/documentation/guides/application-security/tls-and-certificates/digital-certificates/), and for every certificate field, to [Certificates](/en/documentation/platform/workloads/certificate-manager/certificates/).

**CLI**

To set mTLS or the certificate of a workload with the Azion CLI, run `azion update workload --file` with the workload body. For the certificate commands, refer to [Certificate Manager quickstart](/en/documentation/platform/workloads/certificate-manager/quickstart/).

**API**

To upload a certificate, send a `POST` request to the certificates endpoint:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/tls/certificates \
  --header 'Accept: application/json' \
  --header 'Authorization: Token [TOKEN VALUE]' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "my-certificate",
  "type": "edge_certificate",
  "certificate": "-----BEGIN CERTIFICATE-----\n<certificate-body>\n-----END CERTIFICATE-----\n",
  "private_key": "-----BEGIN PRIVATE KEY-----\n<private-key-body>\n-----END PRIVATE KEY-----\n"
}'
```

The API answers `201`. Each PEM is one JSON string with `\n` line breaks, and intermediates follow the certificate in the same string. To request a Let's Encrypt certificate instead, send a `POST` request to `/v4/workspace/tls/certificates/request` with `name`, `"authority": "lets_encrypt"`, `challenge` (`http` or `dns`), `common_name`, and optional `alternative_names`.

To serve the certificate, set its ID in `tls.certificate` of the workload, or `null` for Azion SAN:

```json
{
  "name": "my-workload",
  "active": true,
  "infrastructure": 1,
  "domains": ["www.example.com"],
  "tls": { "certificate": 12345, "ciphers": 4, "minimum_version": "tls_1_2" }
}
```

`infrastructure` `1` is production. `minimum_version` takes `tls_1_0`, `tls_1_1`, `tls_1_2`, or `tls_1_3`, and defaults to `tls_1_3`. `ciphers` takes 1 to 8, and defaults to 7. Send the body to `https://api.azion.com/v4/workspace/workloads`.

If the certificate does not become active, read its `status` and `status_detail`. For HTTP-01, check that the hostname points to Azion. For DNS-01, check the `_acme-challenge` CNAME. Retries continue on schedule, so a fixed record issues the certificate later. On a TLS handshake failure, check that the certificate covers the hostname and that the intermediates are in the certificate field. Then check the `minimum_version` of the workload. For the issuance rules, refer to [Issuance and renewal](/en/documentation/platform/workloads/certificate-manager/issuance-and-renewal/).

mTLS needs a Trusted CA certificate, and an Azion-generated certificate cannot be the Trusted CA. An AWS Private CA certificate can be exported and uploaded with the **Trusted CA Certificate** preset. Sales activates mTLS on the account. Then set `mtls.enabled`, `mtls.config.certificate`, and `verification`, `enforce` or `permissive`, on the workload through the API or `azion update workload --file`. mTLS works over HTTPS only. For the steps, refer to [Configure mTLS on a workload](/en/documentation/guides/application-security/tls-and-certificates/associate-an-mtls-certificate/) and [mTLS](/en/documentation/platform/workloads/mtls/).

---

## Move Route 53 zones to Edge DNS

Moving the zone to [Edge DNS](/en/documentation/platform/edge-dns/) gives Azion every record of the domain, including the apex. Every zone uses the same three nameservers, `ns1.aziondns.net`, `ns2.aziondns.com`, and `ns3.aziondns.org`. Skip this stage when you keep Route 53 and point only subdomains, as Point the domain to the workload shows.

| Aspect           | Amazon Route 53                                      | Azion Edge DNS                                                                |
| ---------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------- |
| Nameservers      | Assigned per hosted zone                             | `ns1.aziondns.net`, `ns2.aziondns.com`, and `ns3.aziondns.org` for every zone |
| Record types     | A, AAAA, CNAME, MX, TXT, SRV, NS, SOA, PTR, and CAA  | A, AAAA, ANAME, CAA, CNAME, DS, MX, NS, PTR, SRV, and TXT                     |
| Routing policies | Simple, weighted, latency, failover, and geolocation | *Simple* and *Weighted*                                                       |
| Health checks    | Route 53 health checks                               | None                                                                          |
| DNSSEC           | Supported                                            | Supported                                                                     |
| API              | REST API                                             | `/v4/workspace/dns/zones`                                                     |

Map each routing policy:

| Route 53 policy | Azion equivalent                                                                                            |
| --------------- | ----------------------------------------------------------------------------------------------------------- |
| Simple          | *Simple* (`simple`), which answers with every value of the record                                           |
| Weighted        | *Weighted* (`weighted`): several records share a name and a type, each answered in proportion to its weight |
| Latency         | -                                                                                                           |
| Failover        | -. For origins, Load Balancer sends traffic to *Backup* addresses when every primary fails                  |
| Geolocation     | -                                                                                                           |

A weight takes 0 to 255, and `0` keeps the record without answering it. The API defaults to `255`, and the Console fills in `100`. TXT and NS records refuse the *Weighted* policy with `19017`.

Recreate each Route 53 record with its type:

| Record | Use on Azion                                                                            |
| ------ | --------------------------------------------------------------------------------------- |
| A      | IPv4 address                                                                            |
| AAAA   | IPv6 address                                                                            |
| ANAME  | Alias of the apex to an Azion hostname, such as the workload domain. Its TTL must be 20 |
| CNAME  | Alias to another name. It holds exactly one value and cannot sit at the apex            |
| MX     | Mail exchange, with its priority                                                        |
| TXT    | Text, such as SPF and DKIM                                                              |
| SRV    | Service records, one per name                                                           |
| CAA    | Certificate authorities allowed to issue for the domain                                 |
| NS     | Delegation of a subdomain                                                               |
| DS     | Delegation signer of a signed child zone                                                |
| PTR    | Reverse lookup                                                                          |

Edge DNS refuses other types, such as SOA. A, AAAA, ANAME, DS, MX, and NS records hold up to 10 values each.

**Console**

To create the zone in Azion Console:

1. **Open Edge DNS**

   Access [Azion Console](https://console.azion.com/) > **Edge DNS**.

2. **Select + Zone**

3. **Enter the zone**

   Enter a **Name** for the zone, and the **Domain Name**. The domain cannot change after the zone is created.

4. **(Optional) Turn on DNSSEC**

   Under **DNSSEC**, turn on **Enable DNSSEC**.

5. **Select Save**

6. **Add the records**

   In the **Records** tab, create each record of the Route 53 zone.

The zone answers on the Azion nameservers. Then change the nameservers of the domain at the registrar, not in Route 53.

**CLI**

To create zones and records with the Azion CLI, follow the CLI panel of the [Edge DNS quickstart](/en/documentation/platform/edge-dns/quickstart/). Boolean flags need `=`, such as `--active=false`.

**API**

To create the zone and a record, send `POST` requests to the zones endpoint:

```bash
curl -X POST https://api.azion.com/v4/workspace/dns/zones \
  -H "Authorization: Token [TOKEN VALUE]" \
  -H "Content-Type: application/json" \
  -d '{"name":"example-zone","domain":"example.com","active":true}'
```

```bash
curl -X POST https://api.azion.com/v4/workspace/dns/zones/<zone-id>/records \
  -H "Authorization: Token [TOKEN VALUE]" \
  -H "Content-Type: application/json" \
  -d '{"name":"www","type":"A","rdata":["192.0.2.1"],"ttl":3600}'
```

A zone needs `name`, `domain`, and `active`. A record takes its `name` relative to the zone, its `type`, and `rdata` as an array of strings, such as `["192.0.2.1"]` for an A record or `["mail.example.com"]` for a CNAME. To turn on DNSSEC, send `{"enabled": true}` in a `PATCH` request to `/v4/workspace/dns/zones/<zone-id>/dnssec`.

With DNSSEC on, Edge DNS shows four DS values: **Key Tag**, **Algorithm** `13`, **Digest Type** `2`, and **Digest**. Reload the page after you save to see them. Add them at the registrar, which can take up to 48 hours to publish them. For the steps, refer to [DNSSEC](/en/documentation/platform/edge-dns/dnssec/).

To check the move, query the nameservers and the records, and compare the answer of Route 53 with the answer of Edge DNS:

```bash
dig example.com NS +short
dig www.example.com A +short
dig @ns-1234.awsdns-12.com example.com A
dig @ns1.aziondns.net example.com A
dig +dnssec example.com DNSKEY
```

The first command lists the three Azion nameservers once the registrar change propagates, which can take up to 48 hours. The second shows the answer the resolver returns now. The third and the fourth show the answers of Route 53, with your own Route 53 nameserver, and of Edge DNS. With DNSSEC on, the last returns two DNSKEY records, with flags `257` and `256` and algorithm `13`. Do not query a new name before its record exists: Edge DNS caches the negative answer for one hour. For more commands, refer to [Run the dig command](/en/documentation/guides/application-security/dns/run-the-dig-command/) and [Run the traceroute command](/en/documentation/guides/application-security/dns/run-the-traceroute-command/).

---

## Point the domain to the workload

The DNS change is the switch: once the domain resolves to the workload, users reach the project through Azion. A domain change affects users, search rankings, and availability, so treat it as a controlled cutover. Before you switch, confirm that:

- The certificate is active.
- The hostname is in the **Domains** of the workload.
- The DNS records are ready.
- The critical routes and the redirects answer as expected on the workload domain. To test them under the real hostname before the switch, refer to [Test an application through the hosts file](/en/documentation/guides/application-development/getting-started/stage-applications-through-hosts-file/).
- Monitoring is ready to watch the traffic after the switch.

Point each name with the record its zone allows:

| Strategy    | Use it for                                                          | Record                                      |
| ----------- | ------------------------------------------------------------------- | ------------------------------------------- |
| CNAME       | A subdomain, keeping Route 53 as the DNS provider                   | `www CNAME <your-workload-domain>`          |
| Nameservers | The apex and every other name, with Edge DNS answering for the zone | An ANAME at the apex to the workload domain |

To check a CNAME:

```bash
dig www.example.com CNAME +short
```

The answer is the workload domain, such as `xxxxxxxxxx.map.azionedge.net`. DNS changes take time to propagate. For the Console settings of the custom domain, refer to [Point a domain to a workload](/en/documentation/guides/platform/migration/point-domain-to-azion/). To move the nameservers, refer to [Migrate the nameservers to Azion](/en/documentation/guides/platform/migration/migrate-ns-to-azion/).

---

## Next steps

- [Real-Time Metrics](/en/documentation/platform/real-time-metrics/quickstart.md): Watch requests, data transferred, status codes, and cache offload after the switch.
- [Real-Time Events](/en/documentation/platform/real-time-events/quickstart.md): Query individual requests, function logs, and WAF results from the last 7 days.
- [Web Application Firewall](/en/documentation/platform/firewall/waf/quickstart.md): Move the rule set from Logging to Blocking once the traffic is clean.
- [Data Stream](/en/documentation/platform/data-stream/quickstart.md): Send the logs to your SIEM or analytics tools, and alert there.
- [Azion community](https://discord.gg/azion): Ask the Azion community on Discord how others run their projects on Azion.
- [Azion Support](/en/documentation/support.md): Open a ticket with the support team when the migration needs help.
