Skip to content

Compute developer guide

Developer-focused information about using the compute API: how a request is made, how it is authenticated and signed, what comes back, and what to do when what comes back is an error.

For what the service is and how to get your first machine running, read the user guide. For every operation and its parameters, the API reference.

https://ec2.shelfcs.com

Region hel1. The region is not in the hostname but it is in the credential scope of every signature, so every client must be told it explicitly. Getting it wrong returns SignatureDoesNotMatch and nothing in the message says why.

There is no endpoint discovery service. Hosts are constructed from the table in Service endpoints.

HTTPS only. HTTP is refused, never redirected — a redirect would invite a client to send a signed request unencrypted first.

An access key pair, minted from the console or POST /v1/access-keys. The signature is verified by the identity service, so the key and your cloud password are two faces of one identity: revoking the user revokes both.

Full detail, including the one way these do not behave like AWS: Access keys.

There are no temporary credentials, no role assumption, and no instance profile — a machine cannot authenticate as itself. See Identity, as deployed.

The API speaks the EC2 query protocol: POST, form-encoded, Action and Version in the body, signed with Signature Version 4.

You do not have to build that by hand, and should not. Every AWS SDK, the AWS CLI and the Terraform AWS provider already speak it; point them at the endpoint.

POST / HTTP/1.1
Host: ec2.shelfcs.com
Content-Type: application/x-www-form-urlencoded
Authorization: AWS4-HMAC-SHA256 Credential=…/20260904/hel1/ec2/aws4_request, …
Action=DescribeInstances&Version=2016-11-15

Details of the wire format, headers and the request-id contract: Making requests and Requests and responses.

SigV4, with the credential scope <date>/hel1/ec2/aws4_request. The service name is ec2 and the region is hel1.

This is why a single global endpoint_url override in a client configuration is wrong: it sends every service’s calls to one host, and those calls are signed for the service they were meant for.

See Authentication.

Terminal window
export AWS_ACCESS_KEY_ID=…
export AWS_SECRET_ACCESS_KEY=…
aws --endpoint-url https://ec2.shelfcs.com --region hel1 ec2 describe-instances

Put it in a profile so you stop typing the flags:

~/.aws/config
[profile shelf]
region = hel1
endpoint_url = https://ec2.shelfcs.com
import boto3
ec2 = boto3.client(
"ec2",
endpoint_url="https://ec2.shelfcs.com",
region_name="hel1",
)
for page in ec2.get_paginator("describe_instances").paginate():
for res in page["Reservations"]:
for i in res["Instances"]:
print(i["InstanceId"], i["InstanceType"], i["State"]["Name"])

Use the paginator rather than reading one page — the API returns a nextToken and a client that ignores it silently sees a partial answer.

import { EC2Client, DescribeInstancesCommand } from "@aws-sdk/client-ec2";
const ec2 = new EC2Client({
endpoint: "https://ec2.shelfcs.com",
region: "hel1",
});
const out = await ec2.send(new DescribeInstancesCommand({ MaxResults: 100 }));
cfg, _ := config.LoadDefaultConfig(ctx,
config.WithRegion("hel1"),
config.WithBaseEndpoint("https://ec2.shelfcs.com"),
)
svc := ec2.NewFromConfig(cfg)
out, err := svc.DescribeInstances(ctx, &ec2.DescribeInstancesInput{})
provider "aws" {
region = "hel1"
access_key = var.access_key
secret_key = var.secret_key
skip_credentials_validation = true
skip_requesting_account_id = true
skip_metadata_api_check = true
endpoints { ec2 = "https://ec2.shelfcs.com" }
}

The three skip_* lines are required: the provider otherwise calls STS to validate credentials and find an account id, and there is no STS here.

Paginated actions return a nextToken. Keep asking until it is absent — do not assume one page is the whole answer.

Three actions are deliberately not paginated, because their Amazon models are not: DescribeRegions, DescribeAvailabilityZones, DescribeAccountAttributes. Every SDK builds no paginator for them and would discard a token, so returning one would silently truncate a location list. They return everything in one response, always.

Filters are applied after a page is assembled, so an empty page can still carry a token. See Pagination.

ActionRetry behaviour
RunInstancesIdempotent via ClientToken. Always send one
TerminateInstances, StartInstances, StopInstancesNaturally idempotent
CreateTagsSets values; repeating changes nothing
CreateVolumeNot idempotent, and no ClientToken exists — a retry after a lost response creates a second volume, and bills for it
CreateKeyPair, ImportKeyPairNot idempotent

Tag resources at creation and reconcile by tag if you retry automatically. See Idempotency.

Errors are the EC2 shape: an HTTP status, an error Code, a Message, and a RequestID. Quote the request id in any support case.

ClassExamplesDo
Malformed inputInvalidParameterValue, InvalidParameterCombinationFix the call. Retrying will not help
Not found / not yoursInvalidInstanceID.NotFound, InvalidVolume.NotFoundAnother account’s id is byte-identical to one that never existed. Treat as not-found
StateIncorrectState, VolumeInUseWait for the resource to reach the state you need, then retry
QuotaInstanceLimitExceeded, VolumeLimitExceededAsk for more quota
CapacityInsufficientInstanceCapacity, InsufficientVolumeCapacityThe box is full. A quota increase will not help; take a smaller shape or wait
AuthSignatureDoesNotMatch, UnauthorizedOperationCheck the region first. It is hel1
UnsupportedInvalidAction, UnsupportedOperationThe call or that parameter is not implemented. See the action list

DryRun on any action evaluates permissions and does nothing else: DryRunOperation if permitted, UnauthorizedOperation if not. It is how you test before running something destructive.

Full tables: Common errors and Errors.

Differences worth knowing before you write code

Section titled “Differences worth knowing before you write code”
  • InvalidAction rather than approximation. An action not on the action list errors instead of doing something nearly right.
  • Rejected parameters, not ignored ones. Encrypted, Iops, Throughput, MultiAttachEnabled and gp3 are refused rather than silently dropped, so a module that asks for encryption fails at plan rather than shipping unencrypted.
  • Responses name our instance types, not the AWS alias you launched with — write ours in Terraform or you get a perpetual diff.
  • No IMDSv2. HttpTokens=required is refused, not downgraded.
  • No rate limiting exists yet. Do not read that as permission; it will arrive, and code that hammers the API will notice.