> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aireserve.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Repoint Amazon ECS to AI Reserve

> Four client settings move an existing ECS setup onto AI Reserve. Every command, SDK call, task definition, and deploy script stays as it is.

Four settings, no code changes. Whatever you use to drive ECS today — the AWS
CLI, a shell script, boto3, the JavaScript, Go, or Java SDK, Terraform — takes
these and keeps working:

| Setting | Value | Notes |
| - | - | - |
| ECS endpoint | `https://api.aireserve.com` | Set it for the **ECS service**, plus EC2 and CloudWatch Logs if you want the [read-only lookups](#network-ids-and-logs) below; other AWS services keep their normal endpoints. |
| `aws_access_key_id` | your ECS credential's access key id (`AIRA…`, 20 characters) | Create one in the portal under **Containers & Orchestration › Elastic Container Service › Create credential**, or let your coding agent mint it over [MCP](/connect/mcp-tools#containers-containers-query-containers-action). |
| `aws_secret_access_key` | its secret access key (40 characters) | Shown once when the credential is created. Signs every request, exactly as an AWS secret does. |
| region | a real AWS region, e.g. `us-east-1` | Where your clusters, services, and tasks run. Use the one your resources are in. |

**The pair works like an AWS one.** Every AWS SDK signs requests with SigV4
and puts the access key id in the `Authorization` header's `Credential` field.
AI Reserve looks the id up, recomputes the signature with the secret that
pairs with it, and refuses the request with AWS's own
`InvalidSignatureException` if they differ — so the secret never crosses the
wire, and a wrong secret fails the way it would against AWS. The region in
that same field tells us which regional endpoint you meant.

> **Using an aireserve\_api\_ key instead**
>
> Any of your organization's `aireserve_api_…` keys still works as the `aws_access_key_id` with any non-empty
> `aws_secret_access_key` (the key itself is the bearer secret, carried over TLS). The pair form above is the one the
> portal issues and the one to use in new setups; nothing you have configured the old way stops working.

## Step by step

**1. Create a credential**

In the portal, open **Containers & Orchestration › Elastic Container Service**, click **Create credential**, name it after the
environment that will use it, and copy both values. The secret access key is shown once.

```bash theme={"dark"}
export AWS_ACCESS_KEY_ID="AIRA…"
export AWS_SECRET_ACCESS_KEY="…"
```

**2. Repoint your client**

Pick the one that matches how you call ECS today.

**AWS CLI**

One-off, per command:

```bash theme={"dark"}
aws ecs list-clusters --region us-east-1 --endpoint-url https://api.aireserve.com
```

For a whole shell session, the service-specific endpoint variable keeps every other AWS service on its normal
endpoint:

```bash theme={"dark"}
export AWS_ENDPOINT_URL_ECS=https://api.aireserve.com
export AWS_ENDPOINT_URL_EC2=https://api.aireserve.com    # optional: subnet / security-group lookups
export AWS_ENDPOINT_URL_CLOUDWATCH_LOGS=https://api.aireserve.com   # optional: read your tasks' logs
export AWS_ENDPOINT_URL_SECRETS_MANAGER=https://api.aireserve.com   # optional: secrets your tasks inject at start
export AWS_ENDPOINT_URL_SSM=https://api.aireserve.com               # optional: Parameter Store
export AWS_ENDPOINT_URL_ECR=https://api.aireserve.com               # optional: your private image registry
export AWS_DEFAULT_REGION=us-east-1

aws ecs list-clusters
aws ecs describe-services --cluster <your-cluster> --services <your-service>
```

**Shared config / profile**

A named profile makes the switch per command (`--profile aireserve`) or per environment
(`AWS_PROFILE=aireserve`), and every SDK honors it too.

`~/.aws/config`:

```ini theme={"dark"}
[profile aireserve]
region = us-east-1
services = aireserve

[services aireserve]
ecs =
  endpoint_url = https://api.aireserve.com
ec2 =
  endpoint_url = https://api.aireserve.com
cloudwatch_logs =
  endpoint_url = https://api.aireserve.com
secrets_manager =
  endpoint_url = https://api.aireserve.com
ssm =
  endpoint_url = https://api.aireserve.com
ecr =
  endpoint_url = https://api.aireserve.com
```

The variable and key names come from each service id: "CloudWatch Logs" is `CLOUDWATCH_LOGS` /
`cloudwatch_logs` (not `LOGS`), and "Secrets Manager" is `SECRETS_MANAGER` / `secrets_manager`
(not `SECRETSMANAGER`). A mis-named one is silently ignored and the CLI calls AWS itself, which answers
`UnrecognizedClientException`.

`~/.aws/credentials`:

```ini theme={"dark"}
[aireserve]
aws_access_key_id = AIRA…
aws_secret_access_key = …
```

```bash theme={"dark"}
aws ecs list-clusters --profile aireserve
```

**Python (boto3)**

```diff theme={"dark"}
 import boto3

-ecs = boto3.client("ecs", region_name="us-east-1")
+ecs = boto3.client(
+    "ecs",
+    endpoint_url="https://api.aireserve.com",
+    aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
+    aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
+    region_name="us-east-1",
+)

 # every call site is unchanged
 ecs.run_task(cluster="demo", taskDefinition="web:7", launchType="FARGATE", ...)
 for page in ecs.get_paginator("list_tasks").paginate(cluster="demo"):
     ...
 ecs.get_waiter("services_stable").wait(cluster="demo", services=["web"])
```

**JavaScript / TypeScript**

```diff theme={"dark"}
 import { ECSClient, RunTaskCommand } from "@aws-sdk/client-ecs";

-const ecs = new ECSClient({ region: "us-east-1" });
+const ecs = new ECSClient({
+  endpoint: "https://api.aireserve.com",
+  region: "us-east-1",
+  credentials: {
+    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
+    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
+  },
+});

 await ecs.send(new RunTaskCommand({ cluster: "demo", taskDefinition: "web:7", launchType: "FARGATE", /* … */ }));
```

**Go**

```go theme={"dark"}
cfg, err := config.LoadDefaultConfig(ctx,
    config.WithRegion("us-east-1"),
    config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
        os.Getenv("AWS_ACCESS_KEY_ID"), os.Getenv("AWS_SECRET_ACCESS_KEY"), "")),
)
if err != nil { log.Fatal(err) }

client := ecs.NewFromConfig(cfg, func(o *ecs.Options) {
    o.BaseEndpoint = aws.String("https://api.aireserve.com")
})

out, err := client.ListClusters(ctx, &ecs.ListClustersInput{})
```

**Java**

```java theme={"dark"}
EcsClient ecs = EcsClient.builder()
    .region(Region.US_EAST_1)
    .endpointOverride(URI.create("https://api.aireserve.com"))
    .credentialsProvider(StaticCredentialsProvider.create(
        AwsBasicCredentials.create(System.getenv("AWS_ACCESS_KEY_ID"), System.getenv("AWS_SECRET_ACCESS_KEY"))))
    .build();

ListClustersResponse clusters = ecs.listClusters();
```

**Terraform**

Use a dedicated provider alias for your `aws_ecs_*` resources so the rest of your configuration keeps
talking to AWS. The `skip_*` flags stop the provider from calling STS, which is not served yet.

```hcl theme={"dark"}
provider "aws" {
  alias  = "aireserve_ecs"
  region = "us-east-1"

  access_key = var.aireserve_access_key_id
  secret_key = var.aireserve_secret_access_key

  skip_credentials_validation = true
  skip_requesting_account_id  = true
  skip_metadata_api_check     = true

  endpoints {
    ecs  = "https://api.aireserve.com"
    ec2  = "https://api.aireserve.com"   # data "aws_subnets" / "aws_security_group" lookups
    logs = "https://api.aireserve.com"   # data "aws_cloudwatch_log_group"
  }
}

resource "aws_ecs_cluster" "demo" {
  provider = aws.aireserve_ecs
  name     = "demo"
}
```

**3. Verify**

```bash theme={"dark"}
aws ecs list-clusters --endpoint-url https://api.aireserve.com --region us-east-1
```

An empty `clusterArns` list is a success — you are talking to ECS through AI Reserve. An
`InvalidSignatureException` means the secret access key does not match the access key id (or
your clock is more than 15 minutes off); an `AccessDeniedException` means the credential is
unknown, revoked, or paused — check it under **Containers & Orchestration › Elastic Container Service** in the portal.

**4. Run what you already run**

Your existing `register-task-definition`, `create-service`, `update-service`, `run-task`, `describe-*`,
`list-*`, `execute-command`, waiters, and paginators work unchanged. See [what to expect](#what-to-expect)
for the handful of things that differ from your own AWS account.

## Deploy a container and get a URL

The fastest path from an image to a running HTTPS service is Amazon ECS
**Express Mode**: one call, and ECS creates the load balancer, target group,
security groups, CPU autoscaling and rollback alarms in your account and gives
the service a URL at `https://<name>.ecs.<region>.on.aws/` on an AWS-managed
certificate. The portal's **Deploy a container** button is this call; from the
CLI it is:

```bash theme={"dark"}
aws ecs create-express-gateway-service \
  --service-name storefront \
  --primary-container image=public.ecr.aws/nginx/nginx:latest,containerPort=80 \
  --execution-role-arn arn:aws:iam::<your account id>:role/AireserveEcsTaskExecution \
  --infrastructure-role-arn arn:aws:iam::<your account id>:role/AireserveEcsInfrastructure \
  --health-check-path / \
  --cpu 1024 --memory 2048 \
  --scaling-target minTaskCount=1,maxTaskCount=4,autoScalingMetric=AVERAGE_CPU,autoScalingTargetValue=60

aws ecs describe-express-gateway-service --service-arn <the ARN it returned>   # until status ACTIVE; the URL is under ingressPaths
```

The two roles already exist in your account: `AireserveEcsTaskExecution` pulls
the image, writes logs and reads secrets at start; `AireserveEcsInfrastructure`
is what ECS assumes to create the load balancer and friends (the AWS console's
"Create new role" button, pre-created). Where the AWS console asks you to create
them, you do not.

Everything an Express service runs on is an ordinary ECS resource in your
account: the service appears in `list-services`, its tasks in `list-tasks`,
its logs under the log group ECS created, so the rest of this page and all of
`aws ecs` apply to it unchanged. Change the image, size, variables or scaling
with `update-express-gateway-service` (the portal's service page has a button
for each); ECS rolls the change out and rolls back on its own if the new tasks
fail health checks or raise 5xx. `delete-express-gateway-service` removes the
load balancer and URL with the service.

The Fargate task-hours are billed as described under [Billing](#billing). The
load balancer ECS creates is an AWS resource in your account too (AWS lists it
at \$0.0225 per hour plus traffic); it is metered on the same hourly basis.

## Infrastructure as code

**Terraform** works through the provider alias above: the AWS provider calls
the ECS API directly, so pointing its `ecs` endpoint at AI Reserve moves
`aws_ecs_cluster`, `aws_ecs_task_definition`, `aws_ecs_service`, and the other
`aws_ecs_*` resources. Keep references to roles, subnets, security groups, and
log groups set to the identifiers AI Reserve provisions for you.

**CloudFormation and the AWS CDK** deploy through AWS's own control plane,
which calls ECS from inside your AWS account — there is no endpoint to
repoint. Drive ECS with the CLI, an SDK, or Terraform instead, or keep using
CloudFormation for the stacks that stay in your account.

## What to expect

**The whole ECS API.** AI Reserve does not keep a list of supported
operations; it forwards whatever you send and returns whatever ECS answers.
New ECS features work the day AWS ships them.

### Network ids and logs

A Fargate task needs a subnet and a security group, and once it runs you
will want its output. Both live in your container account, so point the EC2
and CloudWatch Logs services at the same endpoint and call them as you would
against AWS. AI Reserve forwards every operation of both services the same
way it forwards ECS; what your account's role permits is what succeeds, and
anything it does not is AWS's own `UnauthorizedOperation` (EC2) or
`AccessDeniedException` (Logs). Today the role grants the reads a Fargate
tenant needs:

| Service | Granted today | Typical use |
| - | - | - |
| EC2 | `DescribeSubnets`, `DescribeVpcs`, `DescribeSecurityGroups` | `aws ec2 describe-subnets --filters Name=default-for-az,Values=true` |
| CloudWatch Logs | `DescribeLogGroups`, `DescribeLogStreams`, `GetLogEvents`, `FilterLogEvents` | `aws logs tail /your/group --follow` |

Your account comes with a default VPC in every region, so the subnets and the
`default` security group those commands return are ready to use in
`run-task`'s `awsvpcConfiguration`. Set `awslogs-create-group=true` in a task
definition's log configuration and the group is created on first run.

The portal's **Elastic Container Service** page shows the same clusters, services,
tasks and logs (per task and per service, with a live follow), and every row
there carries the exact `aws ecs …` / `aws logs …` command that reproduces
it. Streams are named `<awslogs-stream-prefix>/<container name>/<task id>`.

**Responses are Amazon ECS's.** Pagination tokens, waiters, and error types
are the real ones, so SDK code written against ECS behaves identically —
including retries, which key off the status codes ECS returns.

**Resources live in an AWS account that is yours alone.** When an admin
enables Containers in the portal (Compute › Containers), AI Reserve creates
an AWS account for your organization inside its AWS Organization — no ticket,
no waiting on us; it is active within a few minutes and the page shows its
12-digit id. Every ECS call you make is signed in that account, so ARNs carry
its id, `list-clusters` returns only your clusters, and nothing of any other
organization's is reachable, listable, or even nameable from it. That is
AWS's own account boundary doing the isolating, not a filter in front of it.
A service-control policy caps the account to ECS on Fargate and what tasks
need (image pulls, logs, networking), and an `AireserveEcsTaskExecution`
role is ready to reference as your task definitions' `executionRoleArn`.
Public images (Docker Hub, ECR Public, GitHub Container Registry) pull as-is.

**Pick the region you mean.** The region in your client configuration is the
region your clusters and tasks run in, exactly as with AWS.

**ECS Exec works.** `aws ecs execute-command` goes through AI Reserve; the
session it returns connects your Session Manager plugin directly to AWS.

**Errors from AI Reserve arrive in the AWS envelope.** Your SDK raises its
normal typed exceptions:

| Situation | Error type | Status |
| - | - | - |
| Unknown, revoked, or paused credential; IP/country policy | `AccessDeniedException` | 403 |
| Secret access key does not match the id; clock off by more than 15 minutes | `InvalidSignatureException` | 403 |
| Containers not enabled for your organization yet | `AccessDeniedException` (names Compute › Containers) | 403 |
| Rate limit or spending cap | `ThrottlingException` (`Retry-After` set) | 429 |
| Request is not an ECS operation | `UnknownOperationException` | 400 |
| Region in the credential isn't an AWS region | `InvalidParameterException` | 400 |
| ECS unreachable / slow | `ServiceUnavailableException` / `RequestTimeoutException` | 503 / 504 |

Everything else — `ClusterNotFoundException`, `InvalidParameterException`
from a bad task definition, `ServerException` — is ECS's own answer, relayed.

## Open the AWS console

Your container account is a real AWS account, and the portal can sign you
into its AWS Management Console. On **Containers & Orchestration › Elastic
Container Service**, choose a region and click **Open AWS Console**: a new tab
lands on the ECS clusters page of your account, signed in as you. The session
lasts an hour, then AWS sends you back to the portal.

This is AWS's own federated sign-in. The portal is the identity provider: an
organization admin's portal session is exchanged for temporary credentials in
your account, with your email as the session name, so every console action
is attributed to you in your account's CloudTrail. No AWS user or password
exists for you, nothing to rotate, and leaving the organization removes the
access. Members can read everything in the portal; opening the console is an
admin action, because a console session can run, scale and delete.

What you see is your account and nothing else: no other organization's
resources, no AI Reserve platform resources. The console shows the same
permission boundary the API has. Pages outside the ECS product (EC2
instances, IAM, S3) answer **Access denied**; that is the account's ceiling,
not a fault.

## Billing

Fargate tasks you run through AI Reserve are billed to your organization's
wallet at AWS's on-demand list price for the region — per vCPU-hour and per
GB-hour of the task's requested `cpu` and `memory`, plus ephemeral storage
above the included 20 GiB and the Windows licence fee where they apply —
from image pull until the task stops, with AWS's one-minute minimum.

Each task-hour appears as its own line in **Observe › Event Log** and in
**Financials**, model `ecs:fargate-task`, provider `aws-ecs`, with the task
and cluster ARN in its details, so what you see in the console matches what
`aws ecs describe-tasks` tells you. Metering enumerates your organization's
own AWS account every five minutes, so tasks started by an ECS service
scheduler — which never pass through AI Reserve — are billed like any other,
and nothing another organization runs can ever land on your bill. A task
that stopped may take one cycle to settle its last partial hour.

If your wallet cannot cover an hour, the hour stays recorded as unpaid and
is retried as funds arrive; your running tasks are not stopped by AI
Reserve. Tasks on the `EC2` or `EXTERNAL` launch types are not billed per
task (the instances are) — talk to your account team before using them.

> [!NOTE]
>
> Containers & orchestration is in early access. Talk to your account team before pointing production traffic at it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.