DEV Community

POTHURAJU JAYAKRISHNA YADAV
POTHURAJU JAYAKRISHNA YADAV

Posted on

πŸš€ Creating an EC2 Instance Using Python requests (Without boto3)

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.

Top comments (0)