Most developers use boto3 to interact with AWS.
But have you ever wonderedβ¦
π What actually happens behind the scenes?
π How does AWS authenticate your API requests?
I always used boto3 without thinking much about it β until I tried calling AWS APIs directly.
In this blog, weβll go one level deeper and:
π₯ Create an EC2 instance using raw HTTP requests with AWS Signature Version 4 (SigV4)
π§ boto3 is Just a Wrapper
When you run:
ec2.run_instances(...)
Behind the scenes, boto3:
- Builds an HTTP request
- Signs it using AWS Signature Version 4
- Sends it to AWS APIs
In this blog, weβll do all of that manually.
βοΈ What Weβre Building
Weβll create a Python script that:
- Uses
requests - Implements AWS SigV4 authentication
- Launches a real EC2 instance
No SDK. No shortcuts.
π High-Level Flow
Client β Canonical Request β String to Sign β Signature β AWS API β Response
π Understanding AWS Signature Version 4 (SigV4)
AWS secures every API request using SigV4. It ensures:
- Authentication (who you are)
- Integrity (request not tampered)
π» Full Working Code
import requests
import datetime
import hashlib
import hmac
import urllib.parse
# π AWS credentials
ACCESS_KEY = "YOUR_ACCESS_KEY"
SECRET_KEY = "YOUR_SECRET_KEY"
REGION = "ap-south-1"
SERVICE = "ec2"
HOST = f"ec2.{REGION}.amazonaws.com"
ENDPOINT = f"https://{HOST}/"
# π¦ EC2 parameters
params = {
"Action": "RunInstances",
"ImageId": "ami-0f5ee92e2d63afc18",
"InstanceType": "t2.micro",
"MinCount": "1",
"MaxCount": "1",
"Version": "2016-11-15"
}
# π Time
t = datetime.datetime.utcnow()
amz_date = t.strftime('%Y%m%dT%H%M%SZ')
date_stamp = t.strftime('%Y%m%d')
# πΉ Step 1: Canonical Query String
canonical_querystring = '&'.join(
f"{urllib.parse.quote(k)}={urllib.parse.quote(v)}"
for k, v in sorted(params.items())
)
# πΉ Step 2: Canonical Request
canonical_headers = f"host:{HOST}\nx-amz-date:{amz_date}\n"
signed_headers = "host;x-amz-date"
payload_hash = hashlib.sha256(b"").hexdigest()
canonical_request = (
"GET\n"
"/\n"
f"{canonical_querystring}\n"
f"{canonical_headers}\n"
f"{signed_headers}\n"
f"{payload_hash}"
)
# πΉ Step 3: String to Sign
algorithm = "AWS4-HMAC-SHA256"
credential_scope = f"{date_stamp}/{REGION}/{SERVICE}/aws4_request"
string_to_sign = (
f"{algorithm}\n"
f"{amz_date}\n"
f"{credential_scope}\n"
f"{hashlib.sha256(canonical_request.encode()).hexdigest()}"
)
# πΉ Step 4: Signing Key
def sign(key, msg):
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
k_date = sign(("AWS4" + SECRET_KEY).encode(), date_stamp)
k_region = sign(k_date, REGION)
k_service = sign(k_region, SERVICE)
k_signing = sign(k_service, "aws4_request")
signature = hmac.new(
k_signing,
string_to_sign.encode(),
hashlib.sha256
).hexdigest()
# πΉ Step 5: Headers
authorization_header = (
f"{algorithm} Credential={ACCESS_KEY}/{credential_scope}, "
f"SignedHeaders={signed_headers}, Signature={signature}"
)
headers = {
"x-amz-date": amz_date,
"Authorization": authorization_header
}
# πΉ Final request
request_url = ENDPOINT + "?" + canonical_querystring
print("π Calling:", request_url)
response = requests.get(request_url, headers=headers)
print("\nStatus Code:", response.status_code)
print("\nResponse:\n", response.text)
π Breaking Down the Code (with Snippets)
Letβs understand whatβs happening step by step.
π 1. AWS Credentials & Config
ACCESS_KEY = "YOUR_ACCESS_KEY"
SECRET_KEY = "YOUR_SECRET_KEY"
REGION = "ap-south-1"
SERVICE = "ec2"
HOST = f"ec2.{REGION}.amazonaws.com"
π Defines credentials + region + service endpoint.
π¦ 2. EC2 Parameters
params = {
"Action": "RunInstances",
"ImageId": "ami-0f5ee92e2d63afc18",
"InstanceType": "t2.micro",
"MinCount": "1",
"MaxCount": "1",
"Version": "2016-11-15"
}
π This is the actual API request payload.
π 3. Timestamp
t = datetime.datetime.utcnow()
amz_date = t.strftime('%Y%m%dT%H%M%SZ')
π Required for AWS request validation.
πΉ 4. Canonical Query String
canonical_querystring = '&'.join(
f"{urllib.parse.quote(k)}={urllib.parse.quote(v)}"
for k, v in sorted(params.items())
)
π Sort + encode parameters
π Critical step (most common failure point)
πΉ 5. Canonical Request
canonical_request = (
"GET\n"
"/\n"
f"{canonical_querystring}\n"
f"{canonical_headers}\n"
f"{signed_headers}\n"
f"{payload_hash}"
)
π Exact request AWS validates internally.
πΉ 6. String to Sign
string_to_sign = (
f"{algorithm}\n"
f"{amz_date}\n"
f"{credential_scope}\n"
f"{hashlib.sha256(canonical_request.encode()).hexdigest()}"
)
π This is what gets signed.
π 7. Signing Key
k_date = sign(("AWS4" + SECRET_KEY).encode(), date_stamp)
k_region = sign(k_date, REGION)
k_service = sign(k_region, SERVICE)
k_signing = sign(k_service, "aws4_request")
π Multi-step key derivation:
Secret β Date β Region β Service β aws4_request
π 8. Signature
signature = hmac.new(
k_signing,
string_to_sign.encode(),
hashlib.sha256
).hexdigest()
π Final cryptographic signature.
π¬ 9. Authorization Header
headers = {
"x-amz-date": amz_date,
"Authorization": authorization_header
}
π This header authenticates your request.
π 10. Sending Request
response = requests.get(request_url, headers=headers)
π Sends request β AWS validates β returns response.
π§ Simple Analogy
Think of SigV4 like a sealed envelope:
- Canonical request β message
- Signing key β secret stamp
- Signature β seal
AWS checks the seal before accepting it.
β Real Output
- Status Code:
200 - Instance created successfully
- Instance ID:
i-069a545b1814c6cec
β οΈ Common Errors
β SignatureDoesNotMatch
- Wrong encoding
- Params not sorted
I got SignatureDoesNotMatch for almost 30 minutes before realizing I wasnβt sorting parameters correctly.
β UnauthorizedOperation
- Missing IAM permissions
β InvalidAMIID.NotFound
- Wrong AMI
βοΈ boto3 vs requests
| Feature | boto3 | requests + SigV4 |
|---|---|---|
| Ease | β Easy | β Complex |
| Control | β Limited | β Full |
| Use Case | Production | Learning |
π§ Key Takeaway
After doing this, boto3 didnβt feel like magic anymore β just automation over HTTP.
π¨ Important Note
Use this approach for:
β
Learning
β
Debugging
β
Deep understanding
β Not production
βοΈ Final Thought
The hardest part wasnβt writing the code β it was getting the signature exactly right.
Top comments (0)