Skip to content

Infrastructure

Overview

Jester runs as a serverless architecture on AWS. API Gateway and Lambda sit at the centre; the Lambdas and DocumentDB live in private subnets inside a VPC, with S3, SQS and Bedrock forming an asynchronous processing pipeline.

Generation is split into AI generation and HTML build / persistence, separated by SQS, with independent pipelines for articles and blogs. WAF in front, VPC endpoints for private connectivity, and Session Manager for operational access keep the setup secure.


Architecture

flowchart LR
    Client([Admin UI / client]) -->|HTTPS| WAF[AWS WAF]
    WAF --> APIGW[API Gateway]

    subgraph AWSCloud[AWS Cloud]
        subgraph VPC[VPC]
            subgraph AppSubnet[Private Subnet: App]
                AppLambda[App Lambda<br/>odds_poc_app]
            end
            subgraph GenSubnet[Private Subnet: generation]
                ArticleGen[article_generation]
                BlogGen[blog_generation]
                BlogRewriter[blog_rewriter]
                ThumbLambda[thumbnail_generation]
            end
            subgraph BuildSubnet[Private Subnet: builders]
                ArticleBuilder[article_builder]
                BlogBuilder[blog_builder]
            end
            subgraph DBSubnet[Private Subnet: DB]
                DocumentDB[(Amazon DocumentDB)]
                EC2[Amazon EC2<br/>bastion]
                EC2 --> DocumentDB
            end
            VPCE_Int[VPC Endpoint<br/>Interface]
            VPCE_S3[VPC Endpoint<br/>Gateway: S3]
        end

        SQS_ArticleGen[SQS<br/>article-generation]
        SQS_ArticleBuild[SQS<br/>article-builder]
        SQS_BlogGen[SQS<br/>blog-generation]
        SQS_BlogRewrite[SQS<br/>blog-rewriter]
        SQS_BlogBuild[SQS<br/>blog-builder]
        SQS_Thumb[SQS<br/>thumbnail-generation]

        S3[(Amazon S3)]
        CloudFront[Amazon CloudFront]
        Bedrock[(Amazon Bedrock)]

        VPCE_Op[VPC Endpoint<br/>Session Manager]
    end

    APIGW --> AppLambda

    AppLambda --> VPCE_Int
    VPCE_Int --> SQS_ArticleGen
    VPCE_Int --> SQS_BlogGen
    VPCE_Int --> SQS_BlogRewrite
    VPCE_Int --> SQS_Thumb

    SQS_ArticleGen --> ArticleGen
    SQS_BlogGen --> BlogGen
    SQS_BlogRewrite --> BlogRewriter
    SQS_Thumb --> ThumbLambda

    ArticleGen --> SQS_ArticleBuild
    BlogGen --> SQS_BlogBuild
    BlogRewriter --> SQS_BlogBuild
    SQS_ArticleBuild --> ArticleBuilder
    SQS_BlogBuild --> BlogBuilder

    AppLambda --> VPCE_S3
    VPCE_S3 --> S3
    S3 --> ArticleGen
    S3 --> BlogGen
    S3 --> BlogRewriter
    S3 --> ThumbLambda
    S3 --> CloudFront
    CloudFront --> Client

    ArticleGen --> Bedrock
    BlogGen --> Bedrock
    BlogRewriter --> Bedrock
    ThumbLambda --> Bedrock
    AppLambda --> Bedrock

    AppLambda --> DocumentDB
    ArticleGen --> DocumentDB
    BlogGen --> DocumentDB
    BlogRewriter --> DocumentDB
    ArticleBuilder --> DocumentDB
    BlogBuilder --> DocumentDB
    ThumbLambda --> DocumentDB

    Compass([MongoDB Compass]) --> SessionMgr[Session Manager]
    SessionMgr --> VPCE_Op
    VPCE_Op --> EC2

AWS services

Service Use
AWS WAF Web application firewall in front of API Gateway
Amazon API Gateway REST API endpoint management
AWS Lambda Serverless compute (seven functions, all deployed as container images)
Amazon DocumentDB Main database (MongoDB-compatible), in a private subnet
Amazon EC2 Bastion host for DocumentDB access, reached through Session Manager
Amazon S3 Race videos, course images, uploaded media and thumbnails
Amazon CloudFront Public delivery of thumbnails from the private S3 bucket
Amazon SQS Asynchronous message queues between Lambdas (with DLQs)
Amazon Bedrock Generative AI inference (Nova Pro / Claude Sonnet / Titan Text Embeddings v2)
Amazon ECR Container image registry for the Lambdas
VPC Endpoint Private connectivity from the VPC to S3, SQS and others (interface and gateway types)
AWS Systems Manager Session Manager Operational access path to EC2

Lambdas

Lambda Role Language Trigger
App (odds_poc_app) REST API: CRUD, enqueuing async jobs, generating related information Python API Gateway
article_generation Writes the 10-second race result summary and sends it to article-builder Python SQS (article-generation)
article_builder Renders the result to HTML with React and stores article.body_html TypeScript SQS (article-builder)
blog_generation Writes race recap / prediction blogs and sends them to blog-builder Python SQS (blog-generation)
blog_rewriter Rewrites past blogs and sends them to blog-builder Python SQS (blog-rewrite / blog-rewriter)
blog_builder Renders the blog body with an article template and stores blog.body_html TypeScript SQS (blog-builder)
thumbnail_generation Extracts the frame at an AI-selected moment of the race video and stores it in S3 Python SQS (thumbnail-generation)

Every Lambda sits in a private subnet; traffic to S3, SQS and Bedrock goes through VPC endpoints.

Inference profiles and the Bedrock control plane

Lambdas inside the VPC cannot reach the Bedrock control plane (bedrock.*.amazonaws.com), so when an inference profile ARN is used, base_model_id is supplied explicitly to skip the GetInferenceProfile call. Without it, the call times out.


SQS queues

Queue Producer Consumer Notes
article-generation App Lambda article_generation Sent on article generation / regeneration requests
article-builder article_generation article_builder Sent when article generation completes
blog-generation App Lambda blog_generation Sent on race recap / prediction blog requests
blog-rewrite App Lambda blog_rewriter Sent by POST /v1/blogs/generation (the legacy path that creates a blog from a past blog). The payload carries only blog_id / old_blog_id and no race
blog-rewriter App Lambda blog_rewriter Sent by POST /v1/old_blogs/{old_blog_id}/rewrite. The payload also carries race_id / racing_type
blog-builder blog_generation / blog_rewriter blog_builder Sent when the blog body is ready
thumbnail-generation App Lambda thumbnail_generation Sent on thumbnail generation requests

Each queue has a dead letter queue for messages that exceed the maximum receive count.

There are two rewrite queue URLs

The App Lambda has two environment variables for dispatching rewrites: BLOG_REWRITER_QUEUE_URL (POST /v1/old_blogs/{id}/rewrite) and BLOG_REWRITE_QUEUE_URL (POST /v1/blogs/generation). Both are consumed by the blog_rewriter Lambda, which handles the same message shape either way.

Final-attempt handling

Each Lambda decides whether a delivery is the final attempt from the SQS record's ApproximateReceiveCount and the queue's redrive_policy.maxReceiveCount (environment variable SQS_MAX_RECEIVE_COUNT, default 2).

Situation Behaviour
Failure on a non-final attempt Re-raise and let SQS redelivery recover
Final-attempt failure (articles) Mark the article as status=failed with the reason; nothing is left in the DLQ
Final-attempt failure (blogs) Hard-delete the status=generating blog; nothing is left in the DLQ

Why nothing is left in the DLQ

Reprocessing the same input fails for the same reason, and the target record either no longer exists or is already recorded as failed โ€” so a DLQ entry has no value. The policy is to keep the database and the queues in the same state.


S3 layout

Use Object key
Race videos auto_racing/video/{race_id}.mp4 / bicycle_racing/video/{race_id}.mp4 / horse_racing/video/{race_id}.mp4
Course images (horse racing only) horse_racing/course_image/{race_id}.gif
Thumbnails (articles) {racing_type}/thumbnail/{race_id}.jpg
Thumbnails (blogs) {racing_type}/thumbnail/blog_{blog_id}.jpg
Uploaded media {media.name}

S3 is reached through a gateway VPC endpoint. The bucket is private (Block Public Access), so images that browsers need are served in one of two ways.

Use Delivery
Media API responses A freshly generated S3 presigned URL valid for one hour
Thumbnails embedded in blog bodies A public CloudFront URL

VPC layout

  • Private subnets by purpose
    • App Lambda subnet
    • Generation Lambda subnet (article_generation / blog_generation / blog_rewriter / thumbnail_generation)
    • Builder Lambda subnet (article_builder / blog_builder)
    • DocumentDB / EC2 bastion subnet
  • VPC endpoints
    • Interface: Lambda to SQS and other AWS APIs
    • Gateway: Lambdas to S3
    • Operational: Session Manager to EC2

Deployment

Every Lambda is deployed as a container image. GitHub Actions builds the Docker image, pushes it to ECR and updates the Lambda function image.

Lambda ECR repository / function name (dev) Workflow
App dev-jester-odds-poc-app / dev-jester-backend-odds-poc-app dev-odds-poc-app-deploy.yml
article_generation dev-jester-article-generation dev-article-generation-deploy.yml
article_builder dev-jester-article-builder dev-article-builder-deploy.yml
blog_generation dev-jester-blog-generation dev-blog-generation-deploy.yml
blog_rewriter dev-jester-blog-rewriter dev-blog-rewriter-deploy.yml
blog_builder dev-jester-blog-builder dev-blog-builder-deploy.yml
thumbnail_generation dev-jester-thumbnail-generation dev-thumbnail-generation-deploy.yml

The TypeScript Lambdas (article_builder / blog_builder) run npm run typecheck and npm run lint before the image is built.

The admin UI (jester-user-web) is built to static files and synced to S3 + CloudFront.


Operational access

  • Lambda-to-Lambda and service-to-service traffic is authenticated with IAM roles
  • Operational DocumentDB access goes MongoDB Compass โ†’ Session Manager โ†’ VPC Endpoint โ†’ EC2 โ†’ DocumentDB
  • DocumentDB connections use TLS (global-bundle.pem) with retryWrites=False (DocumentDB does not support retryable writes)