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(...)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸ” 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)
Enter fullscreen mode Exit fullscreen mode

πŸ” 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"
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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"
}
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ This is the actual API request payload.


πŸ•’ 3. Timestamp

t = datetime.datetime.utcnow()
amz_date = t.strftime('%Y%m%dT%H%M%SZ')
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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())
)
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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}"
)
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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()}"
)
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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")
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Multi-step key derivation:

Secret β†’ Date β†’ Region β†’ Service β†’ aws4_request
Enter fullscreen mode Exit fullscreen mode

πŸ” 8. Signature

signature = hmac.new(
    k_signing,
    string_to_sign.encode(),
    hashlib.sha256
).hexdigest()
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Final cryptographic signature.


πŸ“¬ 9. Authorization Header

headers = {
    "x-amz-date": amz_date,
    "Authorization": authorization_header
}
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ This header authenticates your request.


🌐 10. Sending Request

response = requests.get(request_url, headers=headers)
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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.