aws - AWS service bindings for Tcl, modelled on the AWS CLI
package require aws ?2.0a31?
package require aws::<service>
aws service operation ?-option value …?
aws foreach varlist service operation ?-option value
…? body
aws lmap varlist service operation ?-option value …?
body
aws endpoint -service service ?-region region?
::aws::helpers::set_creds -access_key key -secret secret
?-token token?
The aws package is an AWS SDK generated from the botocore service
definitions that back the AWS CLI. Operation names, option names, and
semantics are deliberately close to the CLI so that knowledge of
aws <service> <operation> carries over directly; this manpage
documents only where a Tcl script must be written differently from the
CLI, and refers the reader to the AWS CLI reference for per-operation
documentation at https://docs.aws.amazon.com/cli/latest/reference/.
Configuration (profiles, credentials, region, retries, endpoint overrides, TLS trust) follows the AWS CLI’s resolution rules — see CONFIGURATION below for the full list of env vars and profile keys honored, and the places where behaviour differs.
Each AWS service lives in its own sub-package. The top-level aws
ensemble auto-loads them on first use, so for most code a single
package require aws
aws s3 list_bucketsis sufficient.
The CLI uses --double-dashed options in kebab-case. The Tcl SDK uses
single-dashed options in snake_case, matching Tcl convention:
| AWS CLI | Tcl SDK |
|---|---|
--function-name foo |
-function_name foo |
--max-items 10 |
-max_items 10 |
--cli-input-json file://req.json |
not supported — pass arguments directly |
Operation names convert the same way: list-functions →
list_functions, get-caller-identity → get_caller_identity.
Every operation returns a JSON document (a Tcl string containing valid JSON). The rl_json package handles navigation and extraction — the conventional alias at the top of a script is:
package require rl_json
interp alias {} json {} ::rl_json::jsonwhich makes json get, json extract, json foreach, etc. resolve
without the namespace prefix. Inside aws::* service namespaces the
alias is unnecessary — namespace path already imports rl_json.
Where the CLI takes a space-separated list:
aws ec2 describe-instances --instance-ids i-aaa i-bbbthe Tcl SDK takes a Tcl list:
aws ec2 describe_instances -instance_ids {i-aaa i-bbb}Services using the json, rest-json, or json_1.0 protocols (dynamodb,
lambda, ecs, and many others) accept nested values as JSON fragments,
not as Tcl dicts. Use the ~J:, ~S:, ~N: substitution forms from
rl_json’s json template:
set tags [json template {["~S:a", "~S:b"]}]
set item [json template {
{
"pk": "~S:pk",
"sk": "~S:sk",
"count": "~N:count",
"tags": "~J:tags"
}
}]
aws dynamodb put_item \
-table_name Users \
-item $itemThis is a deliberate design choice: nested JSON is frequently constructed by composition, and round-tripping through Tcl dicts loses type information (boolean vs string, number vs string). Query, ec2, and rest-xml protocols continue to take Tcl-native nested values because those protocols serialise to XML-flavoured wire formats.
Binary outputs (e.g., s3 get_object, lambda invoke response payload)
are returned as byte-string Tcl values. Use binary format /
binary scan / chan write to handle them; do not puts them to a
text channel without a -translation binary configuration.
Replace aws ... | jq .NextToken loops with aws foreach or aws
lmap. These iterators drive the paginator definitions shipped with
botocore (the same ones the CLI uses for --max-items / --page-size)
and hide the continuation-token plumbing:
aws foreach bucket s3 list_buckets {
puts [json get $bucket Name]
}
set names [aws lmap bucket s3 list_buckets {
json get $bucket Name
}]The first argument after foreach / lmap is the loop variable name;
the body runs once per item across all pages. There is no
--no-paginate equivalent — if you want a single page, just call the
underlying operation (aws s3 list_buckets -max_buckets 100) and
iterate the result yourself.
Paginator metadata (what counts as a page, which key is the continuation
token, which key is the items array) comes from paginators-1.json in
botocore and is baked into the generated service modules at build time.
Resolution order matches the CLI:
- A per-call
-regionoption (aws s3 list_buckets -region eu-west-1). AWS_REGIONenvironment variable.AWS_DEFAULT_REGIONenvironment variable.region =in the active profile (fromAWS_PROFILE→AWS_DEFAULT_PROFILE→default).us-east-1as a last-resort fallback.
The process-level default is cached in $::aws::default_region at
package-load time from sources 2 – 5; set it explicitly if you need to
change it for the rest of the process without touching env vars or
writing to the config file.
The following environment variables and profile-file keys are honored with AWS-CLI-compatible semantics. See the AWS CLI reference at https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html for the authoritative meaning of each — this section lists only what aws-tcl implements, and notes the places where behaviour differs.
| Purpose | Env var | Config key |
|---|---|---|
| Active profile | AWS_PROFILE, AWS_DEFAULT_PROFILE |
n/a |
| Config file path | AWS_CONFIG_FILE |
n/a |
| Credentials file path | AWS_SHARED_CREDENTIALS_FILE |
n/a |
AWS_PROFILE wins over AWS_DEFAULT_PROFILE; both fall back to
default. Config-file section naming matches the CLI: bare profile
names in the credentials file ([myprofile]), [profile NAME] in the
config file (except for the default profile, which is bare).
Credential providers are tried in this order; the first that yields a credential wins:
- A static override set by ::aws::helpers::set_creds.
AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY(+ optionalAWS_SESSION_TOKEN).AWS_ROLE_ARN+AWS_WEB_IDENTITY_TOKEN_FILE— callssts:AssumeRoleWithWebIdentity. Used by EKS IRSA and Pod Identity.- The active profile. Within the profile, these providers are tried in
order:
credential_process— runs the configured command, parses its JSON stdout (withVersion,AccessKeyId,SecretAccessKey, optionalSessionToken, optionalExpiration).role_arn+web_identity_token_file—sts:AssumeRoleWithWebIdentity.role_arn+source_profile(orcredential_source=Environment/Ec2InstanceMetadata/EcsContainer) —sts:AssumeRolechain;external_id,role_session_name,duration_secondsall honored.sso_session =(modern sso-session form) orsso_start_url(legacy form): reads the token cache thataws sso loginmaintains at~/.aws/sso/cache/<sha1>.jsonand callssso:GetRoleCredentials. If the access token is expired and arefreshTokenis present, asso-oidc:CreateTokenrefresh is attempted and written back to the cache.- Static
aws_access_key_id/aws_secret_access_key/aws_session_token.
- Container credentials via
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI(ECS task role) orAWS_CONTAINER_CREDENTIALS_FULL_URI+AWS_CONTAINER_AUTHORIZATION_TOKEN/..._TOKEN_FILE(EKS Pod Identity Agent). - EC2 instance metadata (IMDSv2 preferred, v1 as fallback unless
AWS_EC2_METADATA_V1_DISABLED=true).AWS_EC2_METADATA_DISABLED,AWS_METADATA_SERVICE_TIMEOUT,AWS_METADATA_SERVICE_NUM_ATTEMPTS, andAWS_EC2_METADATA_SERVICE_ENDPOINTare honored.
Supported via the CLI’s token cache — aws-tcl reads
~/.aws/sso/cache/<sha1(session_name or start_url)>.json, exchanges the
access token for short-lived role credentials via
sso:GetRoleCredentials, and refreshes the access token via
sso-oidc:CreateToken when a refresh token is present.
aws-tcl does not implement the interactive device-code login flow.
Run aws sso login (optionally with --profile NAME) from the AWS CLI
to establish a session; aws-tcl picks up the resulting token the next
time a request is signed. If the session has expired beyond what refresh
can recover, a clear {AWS SSO_TOKEN_EXPIRED} error is raised advising
you to re-run aws sso login.
AWS_ENDPOINT_URL_<SERVICE> (per-service) and AWS_ENDPOINT_URL
(global), plus endpoint_url = in the active profile. Useful for
LocalStack, VPC endpoints, and private-link testing. Only the scheme /
host / port of the URL are used; request paths built from the
operation’s httpRequestUri are preserved.
Service name matching for AWS_ENDPOINT_URL_<SERVICE> uses uppercase
with hyphens converted to underscores
(e.g. AWS_ENDPOINT_URL_TRANSCRIBE_STREAMING).
| Setting | Env var | Config key |
|---|---|---|
| FIPS endpoints | AWS_USE_FIPS_ENDPOINT |
use_fips_endpoint |
| Dualstack | AWS_USE_DUALSTACK_ENDPOINT |
use_dualstack_endpoint |
| STS regional | AWS_STS_REGIONAL_ENDPOINTS |
sts_regional_endpoints |
Endpoint-rule builtins feed these through to the per-service endpoint resolution.
AWS_CA_BUNDLE or profile ca_bundle = selects a PEM bundle that
replaces the system trust store for service requests — matching CLI
semantics, not the additive behaviour of some other SDKs.
Matching the AWS SDK v2 / v3 conventions:
| Setting | Env var | Config key | Default |
|---|---|---|---|
| Retry mode | AWS_RETRY_MODE |
retry_mode |
standard |
| Attempt cap | AWS_MAX_ATTEMPTS |
max_attempts |
3 |
legacy / standard / adaptive have the same meaning as the AWS SDKs
— adaptive adds client-side adaptive throttling on top of the standard
backoff.
The retry classifier recognises:
- AWS throttling codes (
Throttling,SlowDown,ProvisionedThroughputExceededException, etc.) - Transient codes (
InternalError,ServiceUnavailable,RequestTimeout, …) - HTTP statuses
408,425,429,500,502,503,504,509
Rate-limit state (the adaptive-mode bucket) is kept in process-scoped
tsv arrays, so throttling observed on one thread influences subsequent
sends from other threads that share the rl_http keepalive pool.
Idempotency tokens on operations that declare one (ubiquitous on
Create* ops that accept a ClientToken) are auto-populated with a
UUIDv4 if the caller doesn’t supply one, so a retry reaches the same
logical request.
Three HTTP budgets, mirroring the AWS SDK v2 shape. Because the
governing env var names differ slightly from the CLI’s (which has no
direct equivalents), the aws-tcl prefix AWSTCL_ is used to avoid
colonising the AWS_ namespace:
| Setting | Env var | Default |
|---|---|---|
| Request budget | AWSTCL_REQUEST_TIMEOUT |
60 s |
| Connect budget | AWSTCL_CONNECT_TIMEOUT |
5 s |
| Read gap budget | AWSTCL_READ_TIMEOUT |
30 s |
Request budget is a hard ceiling on overall wall-clock time per attempt (not per retry cycle). Connect budget caps the DNS + TCP + TLS handshake. Read budget caps the inter-chunk silence during a body stream (so a 30-second read gap fails, even if total bytes are still flowing steadily).
The connection-pool parking age, AWSTCL_MAX_KEEPALIVE_AGE (default 60
s), matches the Java SDK v2 convention and balances keepalive efficiency
against S3/DynamoDB partition-scale opacity.
AWS service errors are raised as Tcl exceptions with an errorCode of
the form:
AWS <SERVICE_ID_UPPER> <type> <Code>
where:
- SERVICE_ID_UPPER is the service ID in upper case (e.g.
S3,DYNAMODB,STS). - type is
Sender,Server, orunknown. - Code is the service-defined error code (e.g.
NoSuchBucket,ValidationException).
Typical pattern:
try {
aws dynamodb get_item -table_name Users -key $key
} trap {AWS DYNAMODB Sender ResourceNotFoundException} {} {
# table doesn't exist — create it or surface a user-friendly error
} trap {AWS DYNAMODB} {msg opts} {
# any other DynamoDB-side failure
log warn "dynamodb: $msg"
}Transport-level failures (DNS, TCP reset, TLS) propagate with an
errorCode starting with AWS POSIX, matching the classifier used by the
retry logic.
Credential-resolution errors have their own taxonomy:
AWS NO_CREDENTIALS— no provider yielded a credential.AWS SSO_NO_TOKEN/AWS SSO_TOKEN_EXPIRED/AWS SSO_TOKEN_INVALID/AWS SSO_REFRESH_FAILED— SSO failures.AWS CREDENTIAL_PROCESS—credential_processinvocation failure.AWS PROFILE_INVALID/AWS PROFILE_CYCLE— malformed profile config.AWS UNSUPPORTED— the profile asks for something we don’t implement (currently:mfa_serial).
Things a CLI user might reach for that aren’t in this SDK:
--query/ JMESPath filtering. Use rl_json’sjson get,json extract,json foreach— JMESPath’s niche overlaps substantially with rl_json’s navigation.--output table/--output text/--output yaml. Every operation returns JSON. Format to taste from there.--cli-input-json file://…. The Tcl SDK accepts all input as typed option arguments; serialise JSON yourself if that’s your input format (aws s3 put_object -body [readfile doc.json]).- Interactive SSO login (device code flow). Run
aws sso loginfrom the CLI. - MFA-gated AssumeRole.
mfa_serialin a profile raises{AWS UNSUPPORTED}— there is no hook for the SDK to prompt. Obtain a session token withaws sts get-session-tokenand export through env vars instead. - Shell completion helpers.
package require aws
package require rl_json
namespace import ::rl_json::json
# aws sts get-caller-identity
set ident [aws sts get_caller_identity]
puts "account: [json get $ident Account]"
# aws s3api head-object --bucket assets --key foo/bar.jpg
set meta [aws s3 head_object -bucket assets -key foo/bar.jpg]
puts "size: [json get $meta ContentLength]"
# aws ec2 describe-instances \
# --instance-ids i-aaa i-bbb \
# --filters "Name=tag:Env,Values=prod"
set instances [aws ec2 describe_instances \
-instance_ids {i-aaa i-bbb} \
-filters [list [dict create Name tag:Env Values prod]]]
# aws dynamodb put-item \
# --table-name Users \
# --item '{"pk":{"S":"user#42"},"count":{"N":"7"}}'
set pk user#42
set count 7
set item [json template {
{
"pk": { "S": "~S:pk" },
"count": { "N": "~S:count" }
}
}]
aws dynamodb put_item -table_name Users -item $item
# aws lambda invoke --function-name worker --payload '{"op":"reindex"}' out.json
set resp [aws lambda invoke \
-function_name worker \
-payload [json template {{"op":"~S:op"}} {op reindex}]]
set fh [open out.json wb]
try {
puts -nonewline $fh [json get $resp Payload]
} finally {
close $fh
}package require aws
package require rl_json
namespace import ::rl_json::json
# Iterate every object in a bucket — even across paginator boundaries.
# The CLI equivalent is `aws s3api list-objects-v2 --bucket assets`
# plus a NextContinuationToken loop.
aws foreach obj \
s3 list_objects_v2 \
-bucket assets \
-prefix images/ \
{
puts "[json get $obj Key] ([json get $obj Size] bytes)"
}
# Same pattern with aws lmap: produce a Tcl list by transforming each
# item. lmap runs the body once per item and collects return values.
set keys [aws lmap obj s3 list_objects_v2 -bucket assets {
json get $obj Key
}]
# A non-S3 example: list every CloudWatch log group across pages.
aws foreach group logs describe_log_groups {
puts [json get $group logGroupName]
}
# If you want per-page metadata that some services return outside of each item
aws foreach item \
-page page \
-itemtype type \
s3 list_objects_v2 \
-bucket assets \
-delimiter / \
{
if {[info exists page]} {
puts "Page keycount: [json get $page KeyCount]"
unset page ;# Will only be set again when the next page arrives
}
# Some paginated services return multiple types, like s3 given -delimiter
switch -- $type {
CommonPrefix {
puts "Folder: [json get $item Prefix]"
}
Object {
puts "[json get $item Key] ([json get $item Size] bytes)"
}
}
}
# If you want to iterate over the raw items yourself, once per page:
aws foreach item \
-page page \
-itemtype type \
s3 list_objects_v2 \
-bucket assets \
-delimiter / \
{
json foreach object [json extract $page Contents] {
puts "key: [json get $object Key]"
}
# Signal to the iteration orchestrator that we're done with the items in this page
throw {AWS FOREACH NEXT_PAGE} {}
}package require aws
package require rl_json
namespace import ::rl_json::json
# Service error codes are part of the Tcl errorCode, so you can pattern
# match on them with `try ... trap`. This is the preferred shape —
# catching by string match on the error message is brittle across
# service wording changes.
proc get_user user_id {
try {
aws dynamodb get_item \
-table_name Users \
-key [json template {{"pk":{"S":"~S:user_id"}}}]
} trap {AWS DYNAMODB Sender ResourceNotFoundException} {} {
# Table doesn't exist. Different from "item not found" — an
# empty Item in the response means the row is absent.
throw {APP TABLE_MISSING} "Users table is not provisioned"
} trap {AWS DYNAMODB Sender ProvisionedThroughputExceededException} {} {
# The SDK already retried this per the configured retry policy;
# if it still bubbles up the bucket is exhausted for real.
throw {APP OVERLOADED} "DynamoDB throttling — try again shortly"
}
}
# Catch all errors from a given service in one block. Tcl's trap
# pattern matching is prefix-based, so {AWS S3} matches any s3 error.
try {
aws s3 put_object -bucket private -key secrets.txt -body $data
} trap {AWS S3} {msg opts} {
log error "s3 upload failed: [dict get $opts -errorcode] $msg"
}
# Credential-resolution errors are distinct from service errors.
try {
aws sts get_caller_identity
} trap {AWS SSO_TOKEN_EXPIRED} msg {
puts stderr "Run 'aws sso login' to refresh your session."
exit 1
} trap {AWS NO_CREDENTIALS} {} {
puts stderr "No AWS credentials found in env, profile, or metadata."
exit 1
}AWS-CLI-compatible (see CONFIGURATION):
AWS_PROFILE, AWS_DEFAULT_PROFILE, AWS_CONFIG_FILE,
AWS_SHARED_CREDENTIALS_FILE, AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_REGION,
AWS_DEFAULT_REGION, AWS_ENDPOINT_URL, AWS_ENDPOINT_URL_<SERVICE>,
AWS_USE_FIPS_ENDPOINT, AWS_USE_DUALSTACK_ENDPOINT,
AWS_STS_REGIONAL_ENDPOINTS, AWS_RETRY_MODE, AWS_MAX_ATTEMPTS,
AWS_CA_BUNDLE, AWS_ROLE_ARN, AWS_WEB_IDENTITY_TOKEN_FILE,
AWS_ROLE_SESSION_NAME, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
AWS_CONTAINER_CREDENTIALS_FULL_URI,
AWS_CONTAINER_AUTHORIZATION_TOKEN,
AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, AWS_EC2_METADATA_DISABLED,
AWS_EC2_METADATA_V1_DISABLED, AWS_EC2_METADATA_SERVICE_ENDPOINT,
AWS_METADATA_SERVICE_TIMEOUT, AWS_METADATA_SERVICE_NUM_ATTEMPTS.
aws-tcl-specific (not defined by the CLI):
AWSTCL_REQUEST_TIMEOUT, AWSTCL_CONNECT_TIMEOUT,
AWSTCL_READ_TIMEOUT, AWSTCL_MAX_KEEPALIVE_AGE,
AWSTCL_EXTRA_TM_PATH.
- AWS CLI reference: https://docs.aws.amazon.com/cli/latest/reference/
- AWS CLI configuration guide: https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html
- rl_json(n) — the JSON library used for inputs and outputs
- reuri(n) — the URI parser used internally
Report issues at https://github.com/cyanogilvie/aws-tcl.