Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/build_container.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
name: "Build container"
"on":
pull_request:
push:
branches:
- main
permissions:
packages: write
contents: read
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- # https://github.com/docker/login-action/#github-container-registry
name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v6
with:
push: ${{ github.ref == 'refs/heads/main' }}
platforms: linux/amd64,linux/arm64
# https://github.com/docker/build-push-action/issues/254
tags: ghcr.io/${{ github.repository }}:latest
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
83 changes: 83 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

thin-controller is a FastAPI-based web application that controls AWS EC2 instances via AWS Lambda. It allows starting/stopping EC2 instances tagged with `thin_controller_managed=true` through a web interface.

## Architecture

- **FastAPI Application** (`thin_controller/__init__.py`): Main application with REST API endpoints and static file serving
- **AWS Lambda Handler** (`thin_controller/handler.py`): Mangum wrapper that adapts FastAPI for Lambda execution
- **Models** (`thin_controller/models.py`): Pydantic models for AWS instances and configuration
- `AWSInstance`: Parses EC2 instance data from boto3 responses
- `Config`: Application configuration with environment variable support (prefix: `THIN_CONTROLLER_`)
- **CLI** (`thin_controller/__main__.py`): Click-based CLI for running uvicorn locally
- **Terraform** (`terraform/`): Infrastructure as Code for deploying to AWS Lambda with Lambda layers

## Development Commands

### Running the Application
```bash
# Start development server with auto-reload
uv run thin-controller --reload

# Start without reload
uv run thin-controller
```

### Testing and Quality Checks
```bash
# Run all checks (lint + types + test)
just check

# Run tests
just test
# or
uv run pytest

# Run linting
just lint
# or
uv run ruff check thin_controller tests

# Run type checking
just types
# or
uv run mypy --strict thin_controller tests

# Run coverage
just coverage
```

### Container
```bash
# Build Docker container
just build_container
# or
docker build -t ghcr.io/yaleman/thin-controller:latest .
```

## Key Configuration

- **Python Version**: Requires Python 3.12+
- **Package Manager**: Uses `uv` (not poetry)
- **AWS Configuration**: Set `THIN_CONTROLLER_REGIONS` environment variable to control which AWS regions to scan (defaults to all EC2 regions)
- **Managed Instances**: Only EC2 instances with tag `thin_controller_managed=true` are controllable

## AWS Lambda Deployment

The Terraform module creates:
- Lambda layer with dependencies (built using `pip install` into `thin_controller_layer/`)
- Lambda function using the `terraform_lambda` module (v1.0.9)
- Python 3.12 runtime with 30-second timeout

The layer building process uses `python3.13` locally but targets `python3.12` runtime in Lambda.

## State Management

Instance state changes follow strict rules in `STATE_CHANGES`:
- `running` → can only `stop`
- `stopped` → can only `start`
- Other states (`pending`, `shutting-down`, `terminated`, `stopping`) are not directly actionable
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM python:3.13-slim


ADD thin_controller /app/thin_controller
ADD README.md /app/README.md
ADD pyproject.toml /app/pyproject.toml


RUN adduser nonroot

USER nonroot

RUN pip install --user --no-cache /app

WORKDIR /home/nonroot

EXPOSE 8000

ENTRYPOINT ["/home/nonroot/.local/bin/thin-controller", "--host", "0.0.0.0"]
4 changes: 4 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ lint:

types:
uv run mypy --strict thin_controller tests


build_container:
docker build -t ghcr.io/yaleman/thin-controller:latest .
26 changes: 13 additions & 13 deletions terraform/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 69 additions & 0 deletions terraform/cloudfront.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# CloudFront distribution with WAF
resource "aws_cloudfront_distribution" "thin_controller" {
count = var.use_fargate ? 1 : 0
enabled = true
comment = "Thin Controller CloudFront distribution"

origin {
domain_name = aws_lb.thin_controller_alb[0].dns_name
origin_id = "alb-origin"

custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "http-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}

default_cache_behavior {
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "alb-origin"

forwarded_values {
query_string = true
headers = ["*"]

cookies {
forward = "all"
}
}

viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 0
max_ttl = 0
compress = true
}

restrictions {
geo_restriction {
restriction_type = "none"
}
}

viewer_certificate {
cloudfront_default_certificate = true
}

web_acl_id = aws_wafv2_web_acl.thin_controller[0].arn

tags = merge(
local.common_tags,
{
Name = "thin-controller"
}
)
}

# Output
output "cloudfront_domain_name" {
description = "CloudFront distribution domain name"
value = var.use_fargate ? aws_cloudfront_distribution.thin_controller[0].domain_name : null
}

output "cloudfront_url" {
description = "CloudFront URL for accessing the application"
value = var.use_fargate ? "https://${aws_cloudfront_distribution.thin_controller[0].domain_name}" : null
}
Loading