Use when writing boto3 or AWS SDK v3 code — configuring IAM auth, reading/writing S3, designing DynamoDB access patterns, writing Lambda handlers, processing SQS batches, or troubleshooting credential and throttling errors.
Scanned 9/9/2026
Install to Claude Code
npx -y skills add kid-sid/codex-spellbook --skill aws --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Aws?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/kid-sid-aws)More formats (shields.io, HTML) on the badges page.
---
name: aws
description: Use when writing boto3 or AWS SDK v3 code — configuring IAM auth, reading/writing S3, designing DynamoDB access patterns, writing Lambda handlers, processing SQS batches, or troubleshooting credential and throttling errors.
---
# AWS SDK Patterns
Production patterns for AWS services using boto3 (Python) and AWS SDK v3 (TypeScript).
## When to Activate
- Writing code that imports `boto3`, `@aws-sdk/*`, or `aws-sdk`
- Configuring authentication for AWS services (IAM roles, instance profiles, environment credentials)
- Reading or writing objects in S3 with presigned URLs or multipart upload
- Designing a DynamoDB table or writing query/scan access patterns
- Writing Lambda handler functions or connecting them to SQS/S3/API Gateway triggers
- Processing SQS messages with partial batch failure handling
- Retrieving secrets from Secrets Manager or Parameter Store
- Troubleshooting `ClientError`, credential resolution, or throttling
## Authentication
### Credential Chain (always prefer role-based auth)
boto3 and AWS SDK v3 resolve credentials in this order — the same code works locally and in production without changes:
```
1. Explicit credentials passed to client (avoid — hardcodes secrets)
2. Environment variables: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN
3. AWS config file: ~/.aws/credentials
4. IAM instance profile (EC2) / task role (ECS) / execution role (Lambda)
5. IAM Roles Anywhere / container credentials
```
```python
# Python — boto3 picks up credentials automatically
import boto3
s3 = boto3.client("s3", region_name="us-east-1") # uses credential chain
# BAD: hardcoded credentials
s3 = boto3.client("s3", aws_access_key_id="AKIA...", aws_secret_access_key="...")
# GOOD: explicit profile for local dev only
session = boto3.Session(profile_name="dev")
s3 = session.client("s3")
```
```typescript
// TypeScript — SDK v3 uses same chain automatically
import { S3Client } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: "us-east-1" }); // no credentials needed
// Local dev with named profile
import { fromIni } from "@aws-sdk/credential-providers";
const s3 = new S3Client({
region: "us-east-1",
credentials: fromIni({ profile: "dev" }),
});
```
### IAM Role Decision Matrix
| Environment | Auth method | How to set up |
|---|---|---|
| Local dev | Named profile (`~/.aws/credentials`) | `aws configure --profile dev` |
| GitHub Actions | OIDC + IAM role (no long-lived keys) | `aws-actions/configure-aws-credentials` with `role-to-assume` |
| Lambda | Execution role (auto-injected) | Attach IAM role to function in console/IaC |
| ECS / Fargate | Task role | `taskRoleArn` in task definition |
| EC2 | Instance profile | Attach IAM role to instance |
| Local → assume role | `AWS_PROFILE` + role ARN | Add `role_arn` to `~/.aws/config` |
## Configuration
```python
from pydantic_settings import BaseSettings
class AWSSettings(BaseSettings):
aws_region: str = "us-east-1"
s3_bucket: str
dynamodb_table: str
sqs_queue_url: str
secrets_manager_prefix: str = "/myapp/prod"
class Config:
env_file = ".env"
settings = AWSSettings()
```
Never store `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` in `.env` files committed to source control.
## S3
### Upload and Download
```python
import boto3
from botocore.exceptions import ClientError
s3 = boto3.client("s3", region_name="us-east-1")
def upload_object(bucket: str, key: str, data: bytes, content_type: str = "application/octet-stream") -> str:
s3.put_object(Bucket=bucket, Key=key, Body=data, ContentType=content_type)
return f"s3://{bucket}/{key}"
def download_object(bucket: str, key: str) -> bytes:
response = s3.get_object(Bucket=bucket, Key=key)
return response["Body"].read()
def list_objects(bucket: str, prefix: str = "") -> list[str]:
paginator = s3.get_paginator("list_objects_v2")
keys = []
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
keys.extend(obj["Key"] for obj in page.get("Contents", []))
return keys
```
```typescript
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { Readable } from "stream";
const s3 = new S3Client({ region: "us-east-1" });
async function uploadObject(bucket: string, key: string, body: Buffer, contentType: string) {
await s3.send(new PutObjectCommand({ Bucket: bucket, Key: key, Body: body, ContentType: contentType }));
return `s3://${bucket}/${key}`;
}
async function downloadObject(bucket: string, key: string): Promise<Buffer> {
const res = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
return Buffer.from(await res.Body!.transformToByteArray());
}
```
### Presigned URLs
```python
from datetime import timedelta
def get_presigned_url(bucket: str, key: str, expiry_seconds: int = 3600) -> str:
return s3.generate_presigned_url(
"get_object",
Params={"Bucket": bucket, "Key": key},
ExpiresIn=expiry_seconds,
)
def get_presigned_upload_url(bucket: str, key: str, content_type: str, expiry_seconds: int = 900) -> str:
return s3.generate_presigned_url(
"put_object",
Params={"Bucket": bucket, "Key": key, "ContentType": content_type},
ExpiresIn=expiry_seconds,
)
```
### Multipart Upload (files > 100 MB)
```python
import boto3
from boto3.s3.transfer import TransferConfig
s3_resource = boto3.resource("s3")
config = TransferConfig(
multipart_threshold=100 * 1024 * 1024, # 100 MB
multipart_chunksize=50 * 1024 * 1024, # 50 MB chunks
max_concurrency=10,
)
def upload_large_file(bucket: str, key: str, file_path: str):
s3_resource.Object(bucket, key).upload_file(file_path, Config=config)
```
## DynamoDB
### Single-Table Design
```
Table: MyApp
PK SK Attributes
USER#u1 PROFILE name, email, plan
USER#u1 ORDER#2024-01 status, total
USER#u1 ORDER#2024-02 status, total
ORDER#o1 METADATA customer_id, created_at
ORDER#o1 ITEM#sku-a qty, unit_price
GSI1: GSI1PK=customer_id, GSI1SK=created_at → query all orders for a customer
```
Access patterns map to key structure — define all access patterns before writing schema.
### CRUD Operations
```python
import boto3
from boto3.dynamodb.conditions import Key, Attr
from decimal import Decimal
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
table = dynamodb.Table("MyApp")
def put_item(item: dict) -> None:
table.put_item(Item=item)
def get_item(pk: str, sk: str) -> dict | None:
response = table.get_item(Key={"PK": pk, "SK": sk})
return response.get("Item")
def query_items(pk: str, sk_prefix: str) -> list[dict]:
response = table.query(
KeyConditionExpression=Key("PK").eq(pk) & Key("SK").begins_with(sk_prefix),
)
return response["Items"]
def update_item(pk: str, sk: str, updates: dict) -> None:
expr = "SET " + ", ".join(f"#{k} = :{k}" for k in updates)
table.update_item(
Key={"PK": pk, "SK": sk},
UpdateExpression=expr,
ExpressionAttributeNames={f"#{k}": k for k in updates},
ExpressionAttributeValues={f":{k}": v for k, v in updates.items()},
)
def delete_item(pk: str, sk: str) -> None:
table.delete_item(Key={"PK": pk, "SK": sk})
```
### Batch Operations
```python
def batch_write(items: list[dict]) -> None:
with table.batch_writer() as batch: # auto-handles 25-item limit and unprocessed items
for item in items:
batch.put_item(Item=item)
def batch_get(keys: list[dict]) -> list[dict]:
response = dynamodb.meta.client.batch_get_item(
RequestItems={table.name: {"Keys": keys[:100]}} # max 100 per call
)
return response["Responses"].get(table.name, [])
```
### DynamoDB vs RDS Decision
| Factor | DynamoDB | RDS (Postgres) |
|---|---|---|
| Access patterns | Known, finite, key-based | Ad-hoc queries, complex joins |
| Scale | Millions of req/s, auto-scale | Vertical + read replicas |
| Schema | Flexible, item-level | Strict, table-level |
| Consistency | Eventually consistent (default) | ACID |
| Operational cost | ~Zero ops | Patching, backups, failover |
| Cost model | Pay-per-request or provisioned | Instance hours |
## Lambda
### Handler Patterns
```python
# Python — structured handler with typed events
import json, logging
from typing import Any
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def handler(event: dict, context: Any) -> dict:
logger.info("invocation", extra={"request_id": context.aws_request_id, "event": event})
try:
result = process(event)
return {"statusCode": 200, "body": json.dumps(result)}
except ValueError as e:
return {"statusCode": 400, "body": json.dumps({"error": str(e)})}
except Exception:
logger.exception("unhandled_error")
raise # let Lambda retry / send to DLQ
def process(event: dict) -> dict:
...
```
```typescript
// TypeScript — API Gateway proxy event
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from "aws-lambda";
export const handler = async (
event: APIGatewayProxyEvent,
context: Context,
): Promise<APIGatewayProxyResult> => {
const body = JSON.parse(event.body ?? "{}");
try {
const result = await process(body);
return { statusCode: 200, body: JSON.stringify(result) };
} catch (err) {
console.error({ requestId: context.awsRequestId, err });
return { statusCode: 500, body: JSON.stringify({ error: "internal error" }) };
}
};
```
### Lambda Environment Best Practices
```python
import os
import boto3
# Initialize clients outside the handler — reused across warm invocations
_s3 = None
_table = None
def get_s3():
global _s3
if _s3 is None:
_s3 = boto3.client("s3")
return _s3
def get_table():
global _table
if _table is None:
dynamodb = boto3.resource("dynamodb")
_table = dynamodb.Table(os.environ["DYNAMODB_TABLE"])
return _table
# Read config from environment, not hardcoded
BUCKET = os.environ["S3_BUCKET"]
REGION = os.environ.get("AWS_REGION", "us-east-1")
```
### Lambda Event Source Comparison
| Trigger | Invocation | Retry behavior | Batch |
|---|---|---|---|
| API Gateway / ALB | Sync | None (caller handles) | No |
| SQS | Async | Redrive to DLQ after maxReceiveCount | Yes (up to 10000) |
| S3 | Async | 2 retries then discard | No (one event per object) |
| DynamoDB Streams | Async | Retry until success or record expires | Yes (per shard) |
| EventBridge | Async | Configurable retry + DLQ | No |
| SNS | Async | 3 retries then DLQ | No |
## SQS
### Send and Receive
```python
sqs = boto3.client("sqs", region_name="us-east-1")
QUEUE_URL = os.environ["SQS_QUEUE_URL"]
def send_message(body: dict, deduplication_id: str | None = None) -> str:
params = {"QueueUrl": QUEUE_URL, "MessageBody": json.dumps(body)}
if deduplication_id: # required for FIFO queues
params["MessageDeduplicationId"] = deduplication_id
params["MessageGroupId"] = body.get("group_id", "default")
response = sqs.send_message(**params)
return response["MessageId"]
def send_batch(messages: list[dict]) -> None:
entries = [
{"Id": str(i), "MessageBody": json.dumps(msg)}
for i, msg in enumerate(messages[:10]) # max 10 per batch
]
response = sqs.send_message_batch(QueueUrl=QUEUE_URL, Entries=entries)
if response.get("Failed"):
raise RuntimeError(f"batch send failed: {response['Failed']}")
```
### Lambda SQS Consumer with Partial Batch Failure
```python
def handler(event: dict, context: Any) -> dict:
batch_item_failures = []
for record in event["Records"]:
message_id = record["messageId"]
try:
body = json.loads(record["body"])
process_message(body)
except Exception:
logger.exception("message_failed", extra={"message_id": message_id})
batch_item_failures.append({"itemIdentifier": message_id})
# Return only failed IDs — SQS retries these, deletes the rest
return {"batchItemFailures": batch_item_failures}
```
Enable **partial batch failure** (`FunctionResponseTypes: [ReportBatchItemFailures]`) in the event source mapping — otherwise one failure requeues the entire batch.
## Secrets Manager
```python
import json
import boto3
from functools import lru_cache
_sm = boto3.client("secretsmanager", region_name="us-east-1")
@lru_cache(maxsize=None) # cache per Lambda warm instance
def get_secret(secret_name: str) -> dict:
response = _sm.get_secret_value(SecretId=secret_name)
raw = response.get("SecretString") or response["SecretBinary"].decode()
try:
return json.loads(raw)
except json.JSONDecodeError:
return {"value": raw}
# Usage
db_creds = get_secret("/myapp/prod/db")
password = db_creds["password"]
```
### Secrets Manager vs Parameter Store
| Factor | Secrets Manager | Parameter Store (SSM) |
|---|---|---|
| Secret rotation | Built-in (Lambda-based) | Manual |
| Versioning | Yes | Yes |
| Cost | $0.40/secret/month | Free (standard), $0.05/10K API calls advanced |
| Size limit | 64 KB | 4 KB (standard), 8 KB (advanced) |
| Best for | DB passwords, API keys, rotation | Config values, feature flags, non-sensitive config |
```python
# Parameter Store (cheaper for non-secrets)
ssm = boto3.client("ssm")
def get_parameter(name: str, with_decryption: bool = True) -> str:
response = ssm.get_parameter(Name=name, WithDecryption=with_decryption)
return response["Parameter"]["Value"]
```
## Retry and Error Handling
### botocore Retry Config
```python
from botocore.config import Config
retry_config = Config(
retries={
"max_attempts": 5,
"mode": "adaptive", # adaptive > standard > legacy; backs off on throttle
},
connect_timeout=5,
read_timeout=30,
)
s3 = boto3.client("s3", config=retry_config)
dynamodb = boto3.client("dynamodb", config=retry_config)
```
### ClientError Handling
```python
from botocore.exceptions import ClientError, NoCredentialsError
def safe_get_object(bucket: str, key: str) -> bytes | None:
try:
return s3.get_object(Bucket=bucket, Key=key)["Body"].read()
except ClientError as e:
code = e.response["Error"]["Code"]
match code:
case "NoSuchKey" | "404":
return None
case "AccessDenied" | "403":
logger.error("s3_access_denied", bucket=bucket, key=key)
raise
case "ThrottlingException" | "RequestLimitExceeded" | "SlowDown":
raise # botocore retry handles this
case _:
logger.error("s3_error", code=code, bucket=bucket, key=key)
raise
except NoCredentialsError:
logger.critical("no_aws_credentials")
raise
```
### Common Error Codes
| Service | Error Code | Meaning |
|---|---|---|
| S3 | `NoSuchKey` | Object doesn't exist |
| S3 | `NoSuchBucket` | Bucket doesn't exist or no access |
| DynamoDB | `ConditionalCheckFailedException` | Optimistic lock / condition failed |
| DynamoDB | `ProvisionedThroughputExceededException` | Throttled — retry with backoff |
| DynamoDB | `ResourceNotFoundException` | Table doesn't exist |
| Secrets Manager | `ResourceNotFoundException` | Secret not found |
| All | `AccessDeniedException` | IAM permissions missing |
| All | `ThrottlingException` | Rate limited — botocore retries |
## IAM Least Privilege
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-bucket/uploads/*"
},
{
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query", "dynamodb:UpdateItem"],
"Resource": [
"arn:aws:dynamodb:us-east-1:123456789012:table/MyApp",
"arn:aws:dynamodb:us-east-1:123456789012:table/MyApp/index/*"
]
},
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:/myapp/prod/*"
}
]
}
```
```
# BAD: wildcard on resource
"Action": "s3:*", "Resource": "*"
# BAD: admin permissions for app role
"Action": "*", "Resource": "*"
# GOOD: specific actions, specific ARNs with path constraints
```
## Cost Controls
| Lever | Impact | How |
|---|---|---|
| DynamoDB on-demand vs provisioned | High | On-demand for unpredictable traffic; provisioned + auto-scaling for steady workloads |
| S3 storage classes | Medium | Lifecycle policy: `Standard` → `Standard-IA` after 30d → `Glacier` after 90d |
| Lambda memory sizing | Medium | Profile with Lambda Power Tuning; more memory often runs faster and costs less |
| DynamoDB DAX cache | Medium | Cache read-heavy tables; reduces read capacity units |
| S3 request costs | Low-medium | Use CloudFront in front of S3 for high-volume GET patterns |
| Secrets Manager calls | Low | Cache secrets in Lambda warm instance; don't call on every invocation |
| CloudWatch Logs retention | Low | Set retention (7–30d) — default is forever |
> See also: `event-driven`, `caching`, `observability`
## Red Flags
- **Hardcoded `aws_access_key_id` in code or config files** — long-term credentials in code are a top AWS compromise vector; use the credential chain (IAM role, instance profile, environment variable)
- **`*` in IAM policy actions or resources** — wildcard policies grant far more than needed; scope every policy to the minimum set of actions and specific resource ARNs
- **DynamoDB `Scan` in production code paths** — `Scan` reads every item in the table and consumes all provisioned capacity; design access patterns around `Query` using primary keys and GSIs
- **Lambda handler that creates DB or SDK connections on every invocation** — connections initialized inside the handler are destroyed and recreated per call; initialize SDK clients outside the handler in module scope
- **SQS visibility timeout shorter than Lambda timeout** — if visibility timeout < Lambda timeout, the message becomes visible before processing finishes, causing duplicate delivery; set visibility timeout to 6× Lambda timeout
- **S3 presigned URLs without a short expiry** — presigned URLs with a far-future expiry can be bookmarked and reused long after the intended access window; always set the shortest practical expiry
- **CloudWatch logs without structured JSON** — unstructured log lines can't be queried with CloudWatch Insights; emit JSON with consistent fields (`level`, `message`, `correlation_id`) from every Lambda
## Checklist
- [ ] All SDK clients use the credential chain — no hardcoded `aws_access_key_id` or `aws_secret_access_key`
- [ ] Lambda, ECS tasks, and EC2 instances use IAM roles/profiles — no long-lived keys in env vars
- [ ] IAM policies follow least privilege: specific actions, specific resource ARNs
- [ ] GitHub Actions uses OIDC role assumption — no IAM user keys stored as secrets
- [ ] boto3/SDK clients initialized outside Lambda handler (reused on warm invocations)
- [ ] botocore retry config set to `adaptive` mode with `max_attempts=5`
- [ ] `ClientError` caught and branched by error code — not caught and swallowed
- [ ] SQS Lambda consumer returns `batchItemFailures` for partial batch failure
- [ ] DynamoDB access patterns defined before schema — table designed around queries
- [ ] S3 lifecycle policy configured for infrequently accessed data
- [ ] Secrets retrieved from Secrets Manager or Parameter Store — not environment variables
- [ ] CloudWatch Logs retention period set on all log groups
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!