How to Build a Screenshot Capture API on AWS: Step by Step Guide
By Braincuber Team
Published on April 7, 2026
This complete tutorial walks you through building a production-ready screenshot capture API from scratch. Whether you are analyzing phishing URLs, generating website previews, or archiving web pages, this step by step guide covers every detail you need. You will use Terraform for infrastructure as code, AWS API Gateway for the REST endpoint, and AWS Lambda running Selenium with headless Chrome to capture screenshots and store them in S3.
What You'll Learn:
- How to configure Terraform with AWS provider and S3 backend
- How to create Lambda layers for ChromeDriver and headless Chromium
- How to write a Python Lambda function using Selenium for screenshot capture
- How to set up API Gateway with GET and POST methods
- How to secure your API with API keys and usage plans
- How to deploy the entire infrastructure with a single Terraform apply
Infrastructure as Code
Use Terraform to define and version all AWS resources including Lambda, API Gateway, S3, and IAM roles.
Serverless Architecture
AWS Lambda handles screenshot capture with no servers to manage, scaling automatically with demand.
Headless Chrome
Selenium drives headless Chromium in Lambda to render JavaScript-heavy pages and capture full screenshots.
API Key Security
Protect your API with API Gateway usage plans and keys so only authorized clients can trigger screenshots.
Prerequisites
You need an AWS account, the Terraform binary installed, an existing S3 bucket for Terraform state, and an AWS IAM user with programmatic access and administrative permissions.
How to Set Up the Project Structure
The first step in this beginner guide is creating your project directory and initializing Terraform. This establishes the foundation for all infrastructure resources you will define.
Create the Project Directory
Create a new directory called screenshot-service and navigate into it. This will be the root of your Terraform project.
Initialize Terraform
Run terraform init to initialize the working directory. This downloads the AWS provider plugin and prepares the backend.
mkdir .\screenshot-service
cd .\screenshot-service
.\terraform init
How to Configure the AWS Provider
The AWS provider tells Terraform which cloud to manage and what credentials to use. Create a file called provider.tf in the root of your project directory.
provider "aws" {
region = "us-east-1"
access_key = "ACCESSKEY"
secret_key = "SECRETKEY"
}
terraform {
backend "s3" {
bucket = "EXISTING_BUCKET"
region = "us-east-1"
key = "KEYFORSTATE"
access_key = "ACCESSKEY"
secret_key = "SECRETKEY"
encrypt = "true"
}
}
Replace ACCESSKEY, SECRETKEY, EXISTING_BUCKET, and KEYFORSTATE with your actual AWS credentials and S3 bucket name. The S3 backend stores your Terraform state file remotely, enabling team collaboration and state locking.
Security Best Practice
Never hardcode credentials in production. Use environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) or AWS CLI profiles instead of embedding keys directly in Terraform files.
How to Configure the S3 Bucket for Screenshots
You need an S3 bucket to store all captured screenshots. Create a file called s3.tf in the root of your project directory.
resource "aws_s3_bucket" "screenshot_bucket" {
bucket = "STORAGE_BUCKET_NAME"
force_destroy = true
acl = "public-read"
versioning {
enabled = false
}
}
The force_destroy = true setting allows Terraform to delete the bucket even when it contains objects. The public-read ACL makes screenshots accessible via URL. Replace STORAGE_BUCKET_NAME with a globally unique bucket name.
How to Create the Lambda Layer for ChromeDriver
Lambda layers let you package binaries separately from your function code. This layer will contain ChromeDriver and headless Chromium, which are required for Selenium to take screenshots.
Create the Layer Directory
Create a folder called chromedriver_layer in the root of your project. This directory will hold the browser binaries.
Download ChromeDriver and Chromium
Download the Linux binaries for ChromeDriver and headless Chromium. These are compiled for Amazon Linux, the OS that Lambda runs on.
Compress the Layer
Zip the chromedriver_layer directory so Terraform can upload it as a Lambda layer. Terraform will handle the layer creation automatically.
cd .\chromedriver_layer
wget https://chromedriver.storage.googleapis.com/2.41/chromedriver_linux64.zip -OutFile .\chromedriver.zip
wget https://github.com/adieuadieu/serverless-chrome/releases/download/v1.0.0-54/stable-headless-chromium-amazonlinux-2017-03.zip -OutFile .\headless-chromium.zip
Expand-Archive .\headless-chromium.zip
rm *.zip
cd ..\
Compress-Archive .\chromedriver_layer -DestinationPath .\chromedriver_layer.zip
How to Configure the Lambda Function Infrastructure
Now you will define the Lambda function, its execution role, and IAM policies. Create a file called lambda.tf in the root of your project directory.
Creating the Execution Role
The execution role determines what AWS services the Lambda function can access. Start by creating the role that allows Lambda to assume it:
resource "aws_iam_role" "lambda_exec_role" {
name = "lambda_exec_role"
description = "Execution role for Lambda functions"
assume_role_policy = <<EOF
{
"Version" : "2012-10-17",
"Statement": [
{
"Action" : "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid" : ""
}
]
}
EOF
}
Adding IAM Policies for Logging and S3 Access
Add two policies to the execution role: one for CloudWatch logging and one for S3 access to upload screenshots.
resource "aws_iam_role_policy" "lambda_logging" {
name = "lambda_logging"
role = aws_iam_role.lambda_exec_role.id
policy = <<EOF
{
"Version" : "2012-10-17",
"Statement": [
{
"Effect" : "Allow",
"Resource": "*",
"Action" : [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:CreateLogGroup"
]
}
]
}
EOF
}
resource "aws_iam_role_policy" "lambda_s3_access" {
name = "lambda_s3_access"
role = aws_iam_role.lambda_exec_role.id
policy = <<EOF
{
"Version" : "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBuckets",
"s3:PutObject",
"s3:PutObjectAcl",
"s3:GetObjectAcl"
],
"Resource": ["*"]
}
]
}
EOF
}
Defining the Lambda Function Resource
Now define the actual Lambda function resource. This references the execution role, the Lambda layer, and sets the runtime, timeout, and memory configuration.
resource "aws_lambda_function" "take_screenshot" {
filename = "./screenshot-service.zip"
function_name = "take_screenshot"
role = aws_iam_role.lambda_exec_role.arn
handler = "screenshot-service.handler"
runtime = "python3.7"
source_code_hash = filebase64sha256("./screenshot-service.zip")
timeout = 600
memory_size = 512
layers = ["${aws_lambda_layer_version.chromedriver_layer.arn}"]
environment {
variables = {
s3_bucket = "${aws_s3_bucket.screenshot_bucket.bucket}"
}
}
}
| Parameter | Value | Description |
|---|---|---|
| runtime | python3.7 | Python runtime for the Lambda function |
| timeout | 600 seconds | Maximum execution time (10 minutes) |
| memory_size | 512 MB | Memory allocated to the function |
| handler | screenshot-service.handler | Module and function name to invoke |
| s3_bucket | env variable | Target S3 bucket for screenshot storage |
How to Write the Lambda Function Code
This is the core of the screenshot API. Create a folder called lambda in the root of your project directory and create a file called screenshot-service.py inside it.
Imports and Logging Configuration
#!/usr/bin/env python
# -*- coding utf-8 -*-
import json
import logging
from urllib.parse import urlparse, unquote
from selenium import webdriver
from datetime import datetime
import os
from shutil import copyfile
import boto3
import stat
import urllib.request
import tldextract
# Configure logging
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
Binary Configuration Function
Lambda layers mount at /opt, but the Lambda /tmp directory is the only writable location. This function copies the binaries and makes them executable.
def configure_binaries():
"""Copy the binary files from the lambda layer to /tmp and make them executable"""
copyfile("/opt/chromedriver", "/tmp/chromedriver")
copyfile("/opt/headless-chromium", "/tmp/headless-chromium")
os.chmod("/tmp/chromedriver", 755)
os.chmod("/tmp/headless-chromium", 755)
Screenshot Capture Function
The get_screenshot function is the heart of the service. It configures headless Chrome with all the necessary flags for Lambda, navigates to the URL, captures the screenshot, and uploads it to S3.
def get_screenshot(url, s3_bucket, screenshot_title = None):
configure_binaries()
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless')
chrome_options.add_argument("disable-infobars")
chrome_options.add_argument("enable-automation")
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument('--window-size=1280x1696')
chrome_options.add_argument('--user-data-dir=/tmp/user-data')
chrome_options.add_argument('--hide-scrollbars')
chrome_options.add_argument('--enable-logging')
chrome_options.add_argument('--log-level=0')
chrome_options.add_argument('--disable-dev-shm-usage')
chrome_options.add_argument('--v=99')
chrome_options.add_argument('--single-process')
chrome_options.add_argument('--data-path=/tmp/data-path')
chrome_options.add_argument('--ignore-certificate-errors')
chrome_options.add_argument('--homedir=/tmp')
chrome_options.add_argument('--disk-cache-dir=/tmp/cache-dir')
chrome_options.add_argument(
'user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36')
chrome_options.binary_location = "/tmp/headless-chromium"
if screenshot_title is None:
ext = tldextract.extract(url)
domain = f"{''.join(ext[:2])}:{urlparse(url).port}.{ext[2]}"
screenshot_title = f"{domain}_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}"
logger.debug(f"Screenshot title: {screenshot_title}")
with webdriver.Chrome(chrome_options=chrome_options, executable_path="/tmp/chromedriver", service_log_path="/tmp/selenium.log") as driver:
driver.set_window_size(1024, 768)
logger.info(f"Obtaining screenshot for {url}")
driver.get(url)
driver.save_screenshot(f"/tmp/{screenshot_title}.png")
logger.info(f"Uploading /tmp/{screenshot_title}.png to S3 bucket {s3_bucket}/{screenshot_title}.png")
s3 = boto3.client("s3")
s3.upload_file(f"/tmp/{screenshot_title}.png", s3_bucket, f"{screenshot_title}.png", ExtraArgs={'ContentType': 'image/png', 'ACL': 'public-read'})
return f"https://{s3_bucket}.s3.amazonaws.com/{screenshot_title}.png"
API Handler Function
The handler function processes incoming API Gateway requests, validates the URL, calls the screenshot function, and returns the result as JSON. It supports both GET and POST methods.
def handler(event, context):
logger.debug("## ENVIRONMENT VARIABLES ##")
logger.debug(os.environ)
logger.debug("## EVENT ##")
logger.debug(event)
bucket_name = os.environ["s3_bucket"]
if event["httpMethod"] == "GET":
if event["queryStringParameters"]:
try:
url = event["queryStringParameters"]["url"]
except Exception as e:
logger.error(e)
raise e
else:
return {
"statusCode": 400,
"body": json.dumps("No URL provided...")
}
elif event["httpMethod"] == "POST":
if event["body"]:
try:
body = json.loads(event["body"])
url = body["url"]
except Exception as e:
logger.error(e)
raise e
else:
return {
"statusCode": 400,
"body": json.dumps("No URL provided...")
}
else:
return {
"statusCode": 405,
"body": json.dumps(f"Invalid HTTP Method {event['httpMethod']} supplied")
}
url = unquote(url)
try:
parsed_url = urlparse(url)
if parsed_url.scheme != "http" and parsed_url.scheme != "https":
parsed_url = urlparse(f"http://{url}")
if parsed_url.port is None:
if parsed_url.scheme == "http":
parsed_url = urlparse(f"{parsed_url.geturl()}:80")
elif parsed_url.scheme == "https":
parsed_url = urlparse(f"{parsed_url.geturl()}:443")
except Exception as e:
logger.error(e)
raise e
try:
screenshot_url = get_screenshot(parsed_url.geturl(), bucket_name)
except Exception as e:
logger.error(e)
raise e
response_body = {
"message": f"Successfully captured screenshot of {parsed_url.geturl()}",
"screenshot_url": screenshot_url
}
return {
"statusCode": 200,
"body" : json.dumps(response_body)
}
Installing Dependencies and Packaging
Install the required Python packages into the lambda directory and create the zip archive that Terraform will upload.
cd .\lambda
pip install selenium tldextract -t .\
cd ..\
Compress-Archive .\lambda -DestinationPath .\screenshot-service.zip
How to Configure API Gateway
API Gateway exposes your Lambda function as a REST API endpoint. Create a file called apigw.tf in the root of your project directory.
REST API and Resource Configuration
resource "aws_api_gateway_rest_api" "screenshot_api" {
name = "screenshot_api"
description = "Lambda-powered screenshot API"
depends_on = [
aws_lambda_function.take_screenshot
]
}
resource "aws_api_gateway_resource" "screenshot_api_gateway" {
path_part = "screenshot"
parent_id = aws_api_gateway_rest_api.screenshot_api.root_resource_id
rest_api_id = aws_api_gateway_rest_api.screenshot_api.id
}
Stage, API Key, and Usage Plan
A stage represents a deployment of your API. The usage plan and API key restrict access to authorized clients only.
resource "aws_api_gateway_stage" "prod_stage" {
stage_name = "prod"
rest_api_id = aws_api_gateway_rest_api.screenshot_api.id
deployment_id = aws_api_gateway_deployment.api_gateway_deployment_get.id
}
resource "aws_api_gateway_usage_plan" "apigw_usage_plan" {
name = "apigw_usage_plan"
api_stages {
api_id = aws_api_gateway_rest_api.screenshot_api.id
stage = aws_api_gateway_stage.prod_stage.stage_name
}
}
resource "aws_api_gateway_usage_plan_key" "apigw_usage_plan_key" {
key_id = aws_api_gateway_api_key.apigw_prod_key.id
key_type = "API_KEY"
usage_plan_id = aws_api_gateway_usage_plan.apigw_usage_plan.id
}
resource "aws_api_gateway_api_key" "apigw_prod_key" {
name = "prod_key"
}
HTTP Methods and Lambda Integration
Configure GET and POST methods on the API Gateway, then wire them to the Lambda function using AWS_PROXY integration.
resource "aws_api_gateway_method" "take_screenshot_get" {
rest_api_id = aws_api_gateway_rest_api.screenshot_api.id
resource_id = aws_api_gateway_resource.screenshot_api_gateway.id
http_method = "GET"
authorization = "NONE"
api_key_required = true
}
resource "aws_api_gateway_method" "take_screenshot_post" {
rest_api_id = aws_api_gateway_rest_api.screenshot_api.id
resource_id = aws_api_gateway_resource.screenshot_api_gateway.id
http_method = "POST"
authorization = "NONE"
api_key_required = true
}
resource "aws_lambda_permission" "apigw" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.take_screenshot.arn
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.screenshot_api.execution_arn}/*/*/*"
}
resource "aws_api_gateway_integration" "lambda_integration_get" {
depends_on = [aws_lambda_permission.apigw]
rest_api_id = aws_api_gateway_rest_api.screenshot_api.id
resource_id = aws_api_gateway_method.take_screenshot_get.resource_id
http_method = aws_api_gateway_method.take_screenshot_get.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.take_screenshot.invoke_arn
}
resource "aws_api_gateway_integration" "lambda_integration_post" {
depends_on = [aws_lambda_permission.apigw]
rest_api_id = aws_api_gateway_rest_api.screenshot_api.id
resource_id = aws_api_gateway_method.take_screenshot_post.resource_id
http_method = aws_api_gateway_method.take_screenshot_post.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.take_screenshot.invoke_arn
}
CloudWatch Logging for API Gateway
Give API Gateway permission to write logs to CloudWatch for debugging and monitoring.
resource "aws_api_gateway_account" "apigw_account" {
cloudwatch_role_arn = aws_iam_role.apigw_cloudwatch.arn
}
resource "aws_iam_role" "apigw_cloudwatch" {
name = "api_gateway_cloudwatch_global"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "apigateway.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
resource "aws_iam_role_policy" "apigw_cloudwatch" {
name = "default"
role = aws_iam_role.apigw_cloudwatch.id
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams",
"logs:PutLogEvents",
"logs:GetLogEvents",
"logs:FilterLogEvents"
],
"Resource": "*"
}
]
}
EOF
}
API Deployment
The deployment resource ties everything together and ensures the API is deployed after all dependencies are created.
resource "aws_api_gateway_deployment" "api_gateway_deployment_get" {
depends_on = [
aws_api_gateway_integration.lambda_integration_get,
aws_api_gateway_method.take_screenshot_get,
aws_api_gateway_integration.lambda_integration_post,
aws_api_gateway_method.take_screenshot_post
]
rest_api_id = aws_api_gateway_rest_api.screenshot_api.id
}
How to Package Lambda Archives with Terraform
In main.tf, use Terraform's archive data source to automatically zip your Lambda code and layer files during the plan/apply phase.
data "archive_file" "screenshot_service_zip" {
type = "zip"
source_dir = "./lambda"
output_path = "./screenshot-service.zip"
}
data "archive_file" "screenshot_service_layer_zip" {
type = "zip"
source_dir = "./chromedriver_layer"
output_path = "./chromedriver_lambda_layer.zip"
}
How to Define Outputs and Deploy
Create a file called output.tf to output the API Gateway URL and API key after deployment. These values tell you where to send requests and what key to include.
output "api_gateway_url" {
value = "${aws_api_gateway_stage.prod_stage.invoke_url}/${aws_api_gateway_resource.screenshot_api_gateway.path_part}"
}
output "api_key" {
value = aws_api_gateway_api_key.apigw_prod_key.value
}
Now run terraform apply to create all resources. After deployment, you will receive the API Gateway URL and API key as output.
.\terraform apply
How to Test the Screenshot API
Once deployed, you can test the API using curl or any HTTP client. Include the API key in the x-api-key header.
curl -H "x-api-key: YOUR_API_KEY" \
"https://YOUR_API_ID.execute-api.us-east-1.amazonaws.com/prod/screenshot?url=https://example.com"
The response will contain a JSON object with the screenshot URL:
{
"message": "Successfully captured screenshot of https://example.com",
"screenshot_url": "https://STORAGE_BUCKET_NAME.s3.amazonaws.com/example.com_20260407_123456.png"
}
Important Note on Lambda Timeout
Screenshot capture can take time, especially for JavaScript-heavy pages. The 600-second timeout provides ample time, but you may want to adjust the memory size based on your workload. Higher memory also means more CPU allocation in Lambda.
Architecture Overview
Here is how all the components work together in this step by step guide to the complete architecture:
| Component | Role | Terraform Resource |
|---|---|---|
| API Gateway | REST API endpoint with API key auth | aws_api_gateway_rest_api |
| AWS Lambda | Runs Selenium + headless Chrome | aws_lambda_function |
| Lambda Layer | ChromeDriver + headless Chromium | aws_lambda_layer_version |
| S3 Bucket | Stores captured screenshots | aws_s3_bucket |
| IAM Roles | Permissions for Lambda and API Gateway | aws_iam_role + policies |
| CloudWatch | Logging and monitoring | aws_iam_role (apigw_cloudwatch) |
Frequently Asked Questions
How to build a screenshot API with Terraform and AWS Lambda?
Define your infrastructure with Terraform including API Gateway, Lambda, S3, and IAM roles. Package Selenium and headless Chrome as a Lambda layer, then deploy with terraform apply.
Can AWS Lambda run headless Chrome for screenshots?
Yes. By packaging headless Chromium and ChromeDriver as a Lambda layer and copying them to /tmp at runtime, Lambda can run Selenium to capture website screenshots.
What Lambda memory and timeout settings work best for screenshots?
Use 512 MB memory and 600 seconds timeout as a starting point. Higher memory provides more CPU, which speeds up rendering. Adjust based on page complexity.
How to secure a screenshot API on AWS API Gateway?
Enable API key required on the method, create a usage plan tied to your stage, and distribute the API key to authorized clients. Include the key in the x-api-key header.
Where are the captured screenshots stored?
Screenshots are uploaded to an S3 bucket with public-read ACL. The Lambda function returns the public S3 URL in the API response for immediate access.
Need Help with AWS Infrastructure?
Our experts can help you design, deploy, and optimize your cloud infrastructure on AWS, Azure, and GCP.
