An interactive Mandelbrot explorer built as a cloud/platform demo project. The backend renders fractals on demand with Go Lambda functions, the frontend is a React/Vite app delivered through CloudFront and private S3, and the infrastructure is managed with Terraform.
Live demo: mandelbrot.i-dmytro.org
Deployment-specific values such as domain names, AWS regions, AWS account IDs, bucket names, and GitHub repository owners are configured through .env, terraform.tfvars, Terraform outputs, and GitHub Actions variables.
graph TD
Browser["Browser\ncustom domain"]
subgraph CDN["CDN"]
CF["CloudFront\nACM viewer certificate"]
end
subgraph Frontend["Frontend"]
S3["Private S3 bucket\nReact/Vite static assets"]
end
subgraph API["API"]
APIGW["API Gateway HTTP API\nGET /render\nGET /render-distributed"]
end
subgraph Compute["Lambda · arm64"]
Renderer["Renderer Lambda\nSingle-image render\nPNG response"]
Orchestrator["Orchestrator Lambda\nFan-out coordinator\nPNG response"]
Worker["Worker Lambda\nTile renderer\nRGBA bytes"]
end
subgraph Observability["Observability"]
CW["CloudWatch Logs\nStructured JSON + EMF metrics"]
end
subgraph State["Terraform State"]
TFS3["S3 bucket\nRemote state + S3 lockfile"]
end
subgraph CICD["CI/CD"]
GHA["GitHub Actions OIDC deploy role\nCI: test · lint · tf validate\nCD: deploy frontend + backend"]
end
Browser -->|HTTPS| CF
CF -->|OAC| S3
Browser -->|HTTPS API calls| APIGW
APIGW -->|GET /render| Renderer
APIGW -->|GET /render-distributed| Orchestrator
Orchestrator -->|lambda:InvokeFunction| Worker
Renderer -->|logs + EMF metrics| CW
Orchestrator -->|logs + EMF metrics| CW
Worker -->|logs + EMF metrics| CW
GHA -->|s3 sync + CloudFront invalidation| S3
GHA -->|lambda update-function-code| Renderer
GHA -->|lambda update-function-code| Orchestrator
GHA -->|lambda update-function-code| Worker
| Path | Endpoint | Description |
|---|---|---|
| Single | GET /render |
One Lambda renders the full image using goroutine work blocks |
| Distributed | GET /render-distributed |
Orchestrator fans out to worker Lambdas by tile and assembles the PNG |
Both public paths return PNG. Raw RGBA is used internally between the orchestrator and worker.
- arm64 Lambdas for better price/performance on numeric workloads.
- Single render path as a baseline so distributed rendering has something measurable to compare against.
- Direct Lambda fan-out for the distributed path to minimize orchestration latency.
- PNG encoding at the Lambda boundary to reduce API Gateway payload size and browser decode work.
- CloudFront + private S3 for secure static frontend hosting.
- Terraform-managed infrastructure with an S3 remote state backend.
- GitHub Actions OIDC for deployment without long-lived AWS credentials.
backend/ Go Lambda renderer, orchestrator, and worker
frontend/fractal-app/ React/Vite + Chakra UI 3 Mandelbrot explorer
infra/terraform/ AWS app infrastructure
infra/terraform-state/ Terraform remote state bootstrap
scripts/ Build, package, deploy, and inventory helpers
docs/ Architecture, observability, and CI/CD notes
.github/workflows/ CI plus frontend/backend deployment workflows
Generated AWS inventory files under docs/aws-*.md are ignored because they can contain account IDs, ARNs, resource names, and topology details.
- Go 1.26.4
- Node.js 24 + npm
- Terraform >= 1.9
- AWS CLI with an authenticated profile
- A custom domain for the frontend
- An issued ACM certificate matching that domain
CloudFront viewer certificates must be in us-east-1, even when the rest of the stack runs in another AWS region.
Create local config from examples:
cp .env.example .env
cp infra/terraform/terraform.tfvars.example infra/terraform/terraform.tfvarsSet real values in those local files:
| Local value | Where | Purpose |
|---|---|---|
AWS_PROFILE |
.env, terraform.tfvars |
AWS CLI profile for local commands |
AWS_REGION / aws_region |
.env, terraform.tfvars |
Region for Lambda, API Gateway, S3, and logs |
ACM_REGION / acm_region |
.env, terraform.tfvars |
Region for the CloudFront certificate, usually us-east-1 |
FRONTEND_DOMAIN / frontend_domain |
.env, terraform.tfvars |
Public frontend hostname, for example mandelbrot.example.com |
CERTIFICATE_DOMAIN / certificate_domain |
.env, terraform.tfvars |
ACM certificate lookup name, for example *.example.com |
GITHUB_ORG / github_org |
.env, terraform.tfvars |
GitHub user or organization allowed to assume the deploy role |
GITHUB_REPO / github_repo |
.env, terraform.tfvars |
Repository allowed to assume the deploy role |
Frontend-only local values live in frontend/fractal-app/.env.example.
make help
make backend-test
make backend-package
make frontend-install
make frontend-lint
make frontend-build
make tf-init
make tf-plan
make tf-apply
make tf-outputThe root Makefile reads values from environment variables or command-line overrides. If you keep local values in .env, export them before running Make targets:
set -a
source .env
set +aRun the local backend. It exposes both GET /render and GET /render-distributed:
make backend-localSmoke-test the local backend:
make local-api-smokeRun the frontend against the local backend:
make frontend-local
# open http://127.0.0.1:5173See docs/cicd.md for the full CI/CD setup. Short local bootstrap:
# Build the Lambda package used by Terraform on first apply.
make backend-package
# Bootstrap remote Terraform state once per AWS account/environment.
make tf-state-init
make tf-state-apply
make tf-write-backend-config
# Initialize and apply the application stack.
make tf-init
make tf-plan
make tf-apply
# Build and upload frontend assets manually if needed.
make frontend-build
make deploy-frontend \
FRONTEND_BUCKET=$(terraform -chdir=infra/terraform output -raw frontend_bucket_name) \
DISTRIBUTION_ID=$(terraform -chdir=infra/terraform output -raw cloudfront_distribution_id)After GitHub Actions is configured, pushes to main can deploy frontend and backend application code automatically.
Create these repository or environment variables in GitHub Actions:
| Variable | Value |
|---|---|
AWS_REGION |
Same value as Terraform aws_region |
FRONTEND_BUCKET |
terraform output frontend_bucket_name |
CLOUDFRONT_DISTRIBUTION_ID |
terraform output cloudfront_distribution_id |
VITE_API_URL |
terraform output api_url |
VITE_DISTRIBUTED_API_URL |
terraform output distributed_api_url |
RENDERER_FUNCTION_NAME |
terraform output lambda_function_name |
WORKER_FUNCTION_NAME |
terraform output worker_lambda_function_name |
ORCHESTRATOR_FUNCTION_NAME |
terraform output orchestrator_lambda_function_name |
Create this GitHub Actions secret:
| Secret | Value |
|---|---|
AWS_DEPLOY_ROLE_ARN |
terraform output github_actions_deploy_role_arn |
No GitHub secret should contain AWS access keys. The deploy workflows assume the Terraform-managed role through GitHub OIDC.
curl -o /tmp/fractal.png \
"$(terraform -chdir=infra/terraform output -raw api_url)?width=64&height_px=64&samples=1&maxIter=100"
file /tmp/fractal.png
curl -o /tmp/fractal-dist.png \
"$(terraform -chdir=infra/terraform output -raw distributed_api_url)?width=64&height_px=64&samples=1&maxIter=100&tileSize=400"
file /tmp/fractal-dist.pngThe Go Lambda emits structured JSON logs and CloudWatch metrics via AWS Embedded Metric Format. Metrics emitted per request include:
RenderDurationMsRenderSuccessRenderFailureRenderValidationFailure
Log retention is controlled by var.log_retention_days and defaults to 1 day for cost control. See docs/observability.md.
make tf-destroyThe ACM certificate is a data source and is not destroyed by this stack.
Original Go Mandelbrot idea adapted from parallel-mandelbrot-go by Daniela Ferreira, then rebuilt as a serverless AWS Lambda application.