diff --git a/alchemy/src/aws/control/client.ts b/alchemy/src/aws/control/client.ts index 986353c06d..f07bc185b1 100644 --- a/alchemy/src/aws/control/client.ts +++ b/alchemy/src/aws/control/client.ts @@ -3,6 +3,7 @@ import { loadConfig } from "@smithy/node-config-provider"; import { AwsClient } from "aws4fetch"; import { logger } from "../../util/logger.ts"; import { safeFetch } from "../../util/safe-fetch.ts"; +import { awsEndpoint } from "../utils.ts"; import { AlreadyExistsError, CloudControlError, @@ -355,7 +356,7 @@ export class CloudControlClient { while (true) { try { const args = [ - `https://cloudcontrolapi.${this.region}.amazonaws.com/?Action=${action}&Version=2021-09-30`, + `${awsEndpoint("cloudcontrolapi", this.region)}/?Action=${action}&Version=2021-09-30`, { method: "POST", headers: { diff --git a/alchemy/src/aws/ec2/utils.ts b/alchemy/src/aws/ec2/utils.ts index 6f4f98d489..dff1f4e375 100644 --- a/alchemy/src/aws/ec2/utils.ts +++ b/alchemy/src/aws/ec2/utils.ts @@ -6,7 +6,7 @@ import { logger } from "../../util/logger.ts"; import { flattenParams } from "../../util/params.ts"; import type { AwsClientProps } from "../client-props.ts"; import { resolveAwsCredentials } from "../credentials.ts"; -import { getRegion } from "../utils.ts"; +import { awsEndpoint, getRegion } from "../utils.ts"; /** * Shared EC2 utilities for all EC2 resources @@ -100,8 +100,10 @@ export async function callEC2Api( // Try the API call, and retry once with fresh credentials on auth failure for (let attempt = 1; attempt <= 2; attempt++) { try { - // Use the client's region instead of the global region - const url = `https://ec2.${client.region}.amazonaws.com/`; + // Use the client's region instead of the global region. + // Honour AWS_ENDPOINT_URL / AWS_ENDPOINT_URL_EC2 for emulators + // (LocalStack, moto, etc.) — matches AWS SDK v3 behaviour. + const url = `${awsEndpoint("ec2", client.region ?? "us-east-1")}/`; const body = new URLSearchParams({ Action: action, diff --git a/alchemy/src/aws/utils.ts b/alchemy/src/aws/utils.ts index b2603b964a..66407a6139 100644 --- a/alchemy/src/aws/utils.ts +++ b/alchemy/src/aws/utils.ts @@ -14,13 +14,29 @@ export const getRegion: Provider = loadConfig({ default: "us-east-1", }); +/** + * Resolve the endpoint URL for an aws4fetch service request, honouring the + * same `AWS_ENDPOINT_URL_` / `AWS_ENDPOINT_URL` env vars as the AWS + * SDK / CLI so aws4fetch-based resources can be pointed at LocalStack, moto, + * or any other emulator. + */ +export function awsEndpoint(service: string, region: string): string { + return ( + process.env[ + `AWS_ENDPOINT_URL_${service.toUpperCase().replace(/-/g, "_")}` + ] ?? + process.env.AWS_ENDPOINT_URL ?? + `https://${service}.${region}.amazonaws.com` + ).replace(/\/+$/, ""); +} + /** * AWS Service Configuration */ export interface AwsServiceConfig { service: string; version: string; - endpoint: (region: string) => string; + endpoint?: (region: string) => string; } /** @@ -63,7 +79,9 @@ export function getAwsApiCaller( for (let attempt = 1; attempt <= 2; attempt++) { try { const region = await getRegion(); - const url = config.endpoint(region); + const url = config.endpoint + ? config.endpoint(region) + : `${awsEndpoint(config.service, region)}/`; const body = new URLSearchParams({ Action: action,