Skip to content

Allure Hosting Architecture on AWS

This document describes a secure, reusable way to host Allure reports on AWS without public S3 website hosting. It also outlines a pytest plugin flow that generates the static Allure site and publishes it to a private S3 bucket behind CloudFront.

High level

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  subgraph UserLayer["User Layer"]
    User[User]
  end

  subgraph CDNLayer["CDN Layer - optional"]
    CF[CloudFront\nHTTPS and caching]
    WAF[WAF - allow list or auth]
  end

  subgraph StorageLayer["Storage Layer"]
    S3[S3 Bucket\nPrivate objects]
  end

  User -->|view report URL| CF
  CF -->|signed request via OAC| S3
  WAF -. policy .- CF

Why not S3 website hosting: website endpoints are public only and do not use IAM. Privacy is achieved by keeping S3 private and letting CloudFront read via Origin Access Control. Viewer access is controlled at the CDN edge using IP allow list, signed cookies, or SSO.

Components

  • S3 bucket: private, versioning on, lifecycle rules. Folder layout:
  • s3://<BUCKET>/<PREFIX>/<project>/<branch>/<run_id>/
  • s3://<BUCKET>/<PREFIX>/<project>/<branch>/latest/
  • CloudFront: origin is the S3 bucket (not the website endpoint). Default root object index.html. Error responses map 403 and 404 to /index.html so Allure widget routes resolve.
  • Origin Access Control: grants CloudFront read access to the bucket. Bucket policy restricts s3:GetObject to the CloudFront distribution ARN.
  • Auth options at the edge:
  • WAF IP allow list for company egress ranges
  • Signed URLs or cookies for link scoped access
  • SSO via Cognito or federated IdP with Lambda at Edge
  • CI runner: Jenkins, GitHub Actions, or similar invokes tests and uploads.
  • pytest plugin: builds the static site and publishes it with proper cache headers.

Request and publish flow with pytest plugin

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
  autonumber
  participant Dev as Developer
  participant CI as CI runner
  participant PL as Pytest plugin
  participant AL as Allure CLI
  participant S3 as S3 private bucket
  participant CF as CloudFront
  participant U as User

  Dev->>CI: push code and tests
  CI->>PL: run pytest with allure results
  PL->>AL: generate static site into allure-report
  Note right of AL: assets css js icons\nindex html and widgets
  PL->>S3: upload <PREFIX>/<project>/<branch>/<run_id>/ with immutable cache
  PL->>S3: upload index.html with no cache
  PL->>S3: copy to <PREFIX>/<project>/<branch>/latest/
  U->>CF: open report URL
  CF->>S3: fetch objects via OAC
  CF-->>U: serve cached content

The pytest plugin:

  • Reads options such as bucket, base prefix, project, branch, run id source, CloudFront domain.
  • Downloads latest/history to enrich the next allure-results.
  • Calls allure generate ... --clean -o allure-report.
  • Uploads to <PREFIX>/<project>/<branch>/<run_id>/ and to latest/ with cache headers.
  • Writes a small manifest runs/index.json for navigation.
  • Prints final URLs in the test summary.

Caching strategy

  • All files upload with Cache-Control: public, max-age=31536000, immutable.
  • Override index.html with Cache-Control: no-cache to allow quick refresh of the landing page.
  • Optional: set no-cache for the widgets folder to refresh summary data faster.

URL scheme

  • Latest per branch: https://<cloudfront-domain>/<PREFIX>/<project>/<branch>/latest/
  • Immutable run: https://<cloudfront-domain>/<PREFIX>/<project>/<branch>/<run_id>/

Security model

  • S3 stays private, block public access on.
  • CloudFront reads with Origin Access Control (OAC). Bucket policy restricts s3:GetObject to CloudFront distribution ARN.
  • Viewer controls at the CDN:
  • WAF allow list for corporate IPs
  • Signed cookies for time-bound sharing
  • SSO at the edge

Example bucket policy snippet:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudFrontReadOnly",
      "Effect": "Allow",
      "Principal": { "Service": "cloudfront.amazonaws.com" },
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::<BUCKET>/<PREFIX>/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::<ACCOUNT_ID>:distribution/<DISTRIBUTION_ID>"
        }
      }
    }
  ]
}

Quickstart (secondary AWS profile)

aws configure --profile allure-test
export AWS_PROFILE=allure-test
export AWS_REGION=eu-west-1

# Produce results (example)
pytest -q --alluredir=allure-results

# Dry run with validation
publish-allure \
  --bucket <BUCKET> \
  --prefix <PREFIX> \
  --project demo \
  --branch main \
  --cloudfront https://<cf-domain> \
  --check --dry-run

# Real publish
publish-allure \
  --bucket <BUCKET> \
  --prefix <PREFIX> \
  --project demo \
  --branch main \
  --cloudfront https://<cf-domain> \
  --ttl-days 30 \
  --max-keep-runs 5

Notes on CI usage

Detailed CI pipeline examples such as Jenkins or GitHub Actions belong in docs/ci.md.

See CI examples for Jenkins and GitHub Actions usage.

CloudFront configuration (SPA routing)

Set the distribution with:

  • Origin: S3 bucket (NOT the website endpoint)
  • Default root object: index.html
  • Custom error responses (to support Allure's client-side routes):
  • 403 -> Response page path /index.html, HTTP response code 200
  • 404 -> Response page path /index.html, HTTP response code 200
  • Set Error Caching Minimum TTL to 0 to avoid stale SPA shell during rollouts

Recommended distribution tunings:

  • Allowed methods: GET, HEAD (others unnecessary)
  • Enable compression: GZIP + Brotli (saves 60–80% on JS/CSS)
  • Cache policy: long-lived for immutable assets, but index.html should have no caching (already controlled by upload headers—no invalidations typically required)
  • Invalidation generally NOT required because new runs use unique prefixes; only invalidate if you change shared root assets unexpectedly. Example (rare):
aws cloudfront create-invalidation --distribution-id DXXXXXXXX --paths "/<PREFIX>/demo/main/latest/index.html"

S3 security checklist

Item State
Block Public Access (all 4 settings) ENABLED
Bucket ACLs Not used (bucket-owner enforced)
Public bucket policy None (no Principal: *)
S3 Static website hosting DISABLED
Access path CloudFront OAC only

Least privilege IAM (writer role)

Attach to the CI role that publishes reports. Replace placeholders.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListBranchPrefix",
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::<BUCKET>",
      "Condition": {
        "StringLike": { "s3:prefix": ["<PREFIX>/<PROJECT>/<BRANCH>/*"] }
      }
    },
    {
      "Sid": "RWReports",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:PutObjectTagging",
        "s3:CopyObject"
      ],
      "Resource": "arn:aws:s3:::<BUCKET>/<PREFIX>/<PROJECT>/<BRANCH>/*"
    }
  ]
}

Optional additions if you enable encryption with a KMS key: add kms:Encrypt, kms:Decrypt, kms:GenerateDataKey* on that key.

Two-phase latest promotion (race-safe)

When multiple pipelines might publish concurrently, avoid partially updated latest/ contents.

  1. Upload run to immutable prefix: <project>/<branch>/<run_id>/...
  2. Sync report to a temporary prefix: <project>/<branch>/latest_tmp/
  3. Write readiness marker: latest_tmp/LATEST_READY
  4. Delete existing latest/ (best effort)
  5. Copy / sync latest_tmp/ -> latest/
  6. Delete latest_tmp/

Consumers (if they need strict consistency) can optionally poll for the absence of latest_tmp/ or presence of latest/LATEST_READY (if you copy the marker) before reading.

Implementation note The two-phase latest_tmp → latest synchronization is fully implemented in the publish-allure CLI. It performs copy, readiness marker creation, index verification, and cleanup to ensure atomic promotion even under concurrent runs. Trend and history assets are revalidated with Cache-Control: no-cache to ensure freshness across CDN and browsers.

Cache headers summary

Path / Pattern Cache-Control
**/* (default) public, max-age=31536000, immutable
index.html no-cache
widgets/** (optional) no-cache

Alternative without CDN

If CloudFront is not allowed, serve the static site behind an internal ALB:

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  subgraph VPC[Private network]
    ALB[Internal ALB]
    ECS[ECS Fargate task\nNginx read only]
    S3[S3 Bucket\nVPC endpoint]
  end

  User[User over VPN] --> ALB
  ALB --> ECS
  ECS --> S3
  • Nginx container serves files from a mounted sync dir. A small sidecar pulls from S3 on demand or on a schedule. Access is VPN only.

Gotchas

  • S3 website endpoints are public only. Use CloudFront or an internal server.
  • SPA routing for Allure needs 403 and 404 to map to index.html at the CDN.
  • Make sure Content-Type is set by the client or inferred correctly by S3.
  • Large reports benefit from gzip or brotli at the CDN.
  • Region mismatch (HTTP 301 redirect from S3) can break history pull — verify bucket region with aws s3api get-bucket-location.

Future ideas

  • Index page listing latest runs per project and branch from the manifest files.
  • Slack or Teams notification with direct links after CI publish.
  • Signed link generator for time bound external sharing.