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
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Environment
ENVIRONMENT=development
DEBUG=true

# Application
APP_NAME=FastAPI Starter
APP_VERSION=1.0.0

# Celery
CELERY_BROKER_URL=amqp://guest:guest@localhost:5672//
CELERY_RESULT_BACKEND=
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ jobs:
code-quality:
name: Code Quality Checks
runs-on: ubuntu-latest
env:
CELERY_BROKER_URL: memory://
CELERY_RESULT_BACKEND: cache+memory://

steps:
- name: Checkout code
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ fastapi==0.104.1
uvicorn[standard]==0.24.0
python-multipart==0.0.6
celery==5.3.4
kombu==5.3.4
kombu==5.3.4
pydantic-settings==2.1.0
13 changes: 5 additions & 8 deletions src/celery_app.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import os
from celery import Celery # type: ignore[import-untyped]

from celery import Celery
from src.config import get_settings

# RabbitMQ connection URL
broker_url = os.getenv("CELERY_BROKER_URL")
# RPC backend for results (uses RabbitMQ RPC, no additional service needed)
result_backend = os.getenv("CELERY_RESULT_BACKEND")
settings = get_settings()

# Create Celery instance
celery_app = Celery(
"fastapi_app",
broker=broker_url,
backend=result_backend,
broker=settings.CELERY_BROKER_URL,
backend=settings.CELERY_RESULT_BACKEND,
include=["src.tasks"],
)

Expand Down
57 changes: 57 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from functools import lru_cache
from typing import Literal

from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
"""Centralna konfiguracja aplikacji z walidacją."""

model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)

# Environment
ENVIRONMENT: Literal["development", "staging", "production"] = Field(
default="development", description="Środowisko uruchomieniowe"
)
DEBUG: bool = Field(default=False, description="Tryb debugowania")

# Application
APP_NAME: str = Field(default="FastAPI Starter", description="Nazwa aplikacji")
APP_VERSION: str = Field(default="1.0.0", description="Wersja aplikacji")

# Celery
CELERY_BROKER_URL: str = Field(..., description="URL brokera Celery (RabbitMQ/Redis)")
CELERY_RESULT_BACKEND: str = Field(
default="", description="Backend dla wyników Celery (opcjonalne)"
)

@field_validator("CELERY_BROKER_URL")
@classmethod
def validate_celery_broker(cls, v: str) -> str:
if not v:
raise ValueError("CELERY_BROKER_URL jest wymagane")
return v

@property
def is_production(self) -> bool:
"""Sprawdza czy aplikacja działa w produkcji."""
return self.ENVIRONMENT == "production"

@property
def is_development(self) -> bool:
"""Sprawdza czy aplikacja działa w trybie deweloperskim."""
return self.ENVIRONMENT == "development"


@lru_cache
def get_settings() -> Settings:
"""Singleton dla ustawień.
Używa lru_cache aby załadować konfigurację tylko raz.
"""
return Settings() # type: ignore[call-arg]
11 changes: 9 additions & 2 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

from src.config import get_settings
from src.tasks import process_message

app = FastAPI(title="FastAPI Starter", description="A starter FastAPI application", version="1.0.0")
settings = get_settings()

app = FastAPI(
title=settings.APP_NAME,
description="A starter FastAPI application",
version=settings.APP_VERSION,
debug=settings.DEBUG,
)

# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
Expand Down
19 changes: 19 additions & 0 deletions terraform/cloudwatch.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "app" {
name = "/ecs/${var.project_name}"
retention_in_days = 7

tags = {
Name = "${var.project_name}-logs"
}
}

# CloudWatch Log Group dla RabbitMQ
resource "aws_cloudwatch_log_group" "rabbitmq" {
name = "/ecs/${var.project_name}-rabbitmq"
retention_in_days = 7

tags = {
Name = "${var.project_name}-rabbitmq-logs"
}
}
22 changes: 22 additions & 0 deletions terraform/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Data source for availability zones
data "aws_availability_zones" "available" {
state = "available"
}

# Data source do pobrania account ID
data "aws_caller_identity" "current" {}

# ECR Repository - using existing repository (data source)
data "aws_ecr_repository" "app" {
name = var.ecr_repository_name
}

# Data source do odczytu wszystkich parametrów z Parameter Store
data "aws_ssm_parameters_by_path" "app_secrets" {
path = "/${var.project_name}"
recursive = true

depends_on = [
aws_ssm_parameter.celery_broker_url
]
}
172 changes: 172 additions & 0 deletions terraform/ecs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# ECS Cluster
resource "aws_ecs_cluster" "main" {
name = "${var.project_name}-cluster"

tags = {
Name = "${var.project_name}-cluster"
}
}

# ECS Task Definition dla RabbitMQ
resource "aws_ecs_task_definition" "rabbitmq" {
family = "${var.project_name}-rabbitmq"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = 256
memory = 512
execution_role_arn = aws_iam_role.ecs_task_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn

container_definitions = jsonencode([
{
name = "rabbitmq"
image = "rabbitmq:3.12-management-alpine"

portMappings = [
{
containerPort = 5672
protocol = "tcp"
},
{
containerPort = 15672
protocol = "tcp"
}
]

environment = [
{
name = "RABBITMQ_DEFAULT_USER"
value = var.rabbitmq_username
},
{
name = "RABBITMQ_DEFAULT_PASS"
value = var.rabbitmq_password
}
]

logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.rabbitmq.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "ecs"
}
}

healthCheck = {
command = ["CMD-SHELL", "rabbitmq-diagnostics ping"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
}
])

tags = {
Name = "${var.project_name}-rabbitmq-task"
}
}

# ECS Service dla RabbitMQ
resource "aws_ecs_service" "rabbitmq" {
name = "${var.project_name}-rabbitmq-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.rabbitmq.arn
desired_count = 1
launch_type = "FARGATE"

network_configuration {
subnets = [aws_subnet.public.id]
security_groups = [aws_security_group.rabbitmq.id]
assign_public_ip = true
}

# Service Discovery
service_registries {
registry_arn = aws_service_discovery_service.rabbitmq.arn
}

depends_on = [
aws_iam_role_policy_attachment.ecs_task_execution
]

tags = {
Name = "${var.project_name}-rabbitmq-service"
}
}

# ECS Task Definition
resource "aws_ecs_task_definition" "app" {
family = var.project_name
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = 256
memory = 512
execution_role_arn = aws_iam_role.ecs_task_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn

container_definitions = jsonencode([
{
name = var.project_name
image = "${data.aws_ecr_repository.app.repository_url}:latest"

portMappings = [
{
containerPort = 8000
protocol = "tcp"
}
]

environment = local.environment_vars
secrets = local.ssm_secrets

logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "ecs"
}
}

healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
}
])

tags = {
Name = "${var.project_name}-task-definition"
}
}

# ECS Service
resource "aws_ecs_service" "app" {
name = "${var.project_name}-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = 1
launch_type = "FARGATE"

network_configuration {
subnets = [aws_subnet.public.id]
security_groups = [aws_security_group.ecs_tasks.id]
assign_public_ip = true
}

depends_on = [
aws_iam_role_policy_attachment.ecs_task_execution,
aws_ecs_service.rabbitmq,
aws_ssm_parameter.celery_broker_url,
aws_iam_role_policy.ecs_task_execution_ssm
]

tags = {
Name = "${var.project_name}-service"
}
}
Loading