Remove decorative icons and update CORS configuration

This commit is contained in:
DaX
2025-06-20 13:05:36 +02:00
parent 18110ab159
commit 62a4891112
51 changed files with 4284 additions and 2385 deletions

162
terraform/alb.tf Normal file
View File

@@ -0,0 +1,162 @@
# Application Load Balancer
resource "aws_lb" "api" {
name = "${var.app_name}-api-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = aws_subnet.public[*].id
enable_deletion_protection = false
enable_http2 = true
tags = {
Name = "${var.app_name}-api-alb"
}
}
# ALB Security Group
resource "aws_security_group" "alb" {
name = "${var.app_name}-alb-sg"
description = "Security group for ALB"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTP from anywhere"
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS from anywhere"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound"
}
tags = {
Name = "${var.app_name}-alb-sg"
}
}
# Target Group
resource "aws_lb_target_group" "api" {
name = "${var.app_name}-api-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
health_check {
enabled = true
healthy_threshold = 2
interval = 30
matcher = "200"
path = "/"
port = "traffic-port"
protocol = "HTTP"
timeout = 5
unhealthy_threshold = 2
}
tags = {
Name = "${var.app_name}-api-tg"
}
}
# Attach EC2 instance to target group
resource "aws_lb_target_group_attachment" "api" {
target_group_arn = aws_lb_target_group.api.arn
target_id = aws_instance.api.id
port = 80
}
# HTTP Listener (redirects to HTTPS)
resource "aws_lb_listener" "api_http" {
load_balancer_arn = aws_lb.api.arn
port = "80"
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
# Request ACM certificate
resource "aws_acm_certificate" "api" {
domain_name = "api.${var.domain_name}"
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
tags = {
Name = "${var.app_name}-api-cert"
}
}
# Create DNS validation record in Cloudflare
resource "cloudflare_record" "api_cert_validation" {
for_each = {
for dvo in aws_acm_certificate.api.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = data.cloudflare_zone.domain[0].id
name = replace(each.value.name, ".${var.domain_name}.", "")
value = trimsuffix(each.value.record, ".")
type = each.value.type
ttl = 60
proxied = false
}
# Certificate validation
resource "aws_acm_certificate_validation" "api" {
certificate_arn = aws_acm_certificate.api.arn
validation_record_fqdns = [for record in cloudflare_record.api_cert_validation : record.hostname]
}
# HTTPS Listener
resource "aws_lb_listener" "api_https" {
load_balancer_arn = aws_lb.api.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate_validation.api.certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.api.arn
}
}
# Output ALB DNS
output "alb_dns_name" {
value = aws_lb.api.dns_name
description = "ALB DNS name"
}
# Output ALB Zone ID (needed for Route53 alias)
output "alb_zone_id" {
value = aws_lb.api.zone_id
description = "ALB Zone ID"
}

View File

@@ -1,281 +0,0 @@
# API Gateway REST API
resource "aws_api_gateway_rest_api" "api" {
name = "${var.app_name}-api"
description = "API for ${var.app_name}"
}
# API Gateway Resources
resource "aws_api_gateway_resource" "filaments" {
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_rest_api.api.root_resource_id
path_part = "filaments"
}
resource "aws_api_gateway_resource" "filament" {
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_resource.filaments.id
path_part = "{id}"
}
resource "aws_api_gateway_resource" "auth" {
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_rest_api.api.root_resource_id
path_part = "auth"
}
resource "aws_api_gateway_resource" "login" {
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_resource.auth.id
path_part = "login"
}
# Lambda Authorizer
resource "aws_api_gateway_authorizer" "jwt_authorizer" {
name = "${var.app_name}-jwt-authorizer"
rest_api_id = aws_api_gateway_rest_api.api.id
type = "TOKEN"
authorizer_uri = aws_lambda_function.auth_api.invoke_arn
authorizer_credentials = aws_iam_role.api_gateway_auth_invocation.arn
identity_source = "method.request.header.Authorization"
}
# IAM role for API Gateway to invoke Lambda authorizer
resource "aws_iam_role" "api_gateway_auth_invocation" {
name = "${var.app_name}-api-gateway-auth-invocation"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "apigateway.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy" "api_gateway_auth_invocation" {
name = "${var.app_name}-api-gateway-auth-invocation"
role = aws_iam_role.api_gateway_auth_invocation.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "lambda:InvokeFunction"
Resource = aws_lambda_function.auth_api.arn
}
]
})
}
# Methods for /filaments
resource "aws_api_gateway_method" "get_filaments" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.filaments.id
http_method = "GET"
authorization = "NONE"
}
resource "aws_api_gateway_method" "post_filament" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.filaments.id
http_method = "POST"
authorization = "CUSTOM"
authorizer_id = aws_api_gateway_authorizer.jwt_authorizer.id
}
# Methods for /filaments/{id}
resource "aws_api_gateway_method" "get_filament" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.filament.id
http_method = "GET"
authorization = "NONE"
}
resource "aws_api_gateway_method" "put_filament" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.filament.id
http_method = "PUT"
authorization = "CUSTOM"
authorizer_id = aws_api_gateway_authorizer.jwt_authorizer.id
}
resource "aws_api_gateway_method" "delete_filament" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.filament.id
http_method = "DELETE"
authorization = "CUSTOM"
authorizer_id = aws_api_gateway_authorizer.jwt_authorizer.id
}
# Method for /auth/login
resource "aws_api_gateway_method" "login" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.login.id
http_method = "POST"
authorization = "NONE"
}
# OPTIONS methods for CORS
resource "aws_api_gateway_method" "options_filaments" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.filaments.id
http_method = "OPTIONS"
authorization = "NONE"
}
resource "aws_api_gateway_method" "options_filament" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.filament.id
http_method = "OPTIONS"
authorization = "NONE"
}
resource "aws_api_gateway_method" "options_login" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.login.id
http_method = "OPTIONS"
authorization = "NONE"
}
# Lambda integrations
resource "aws_api_gateway_integration" "filaments_get" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.filaments.id
http_method = aws_api_gateway_method.get_filaments.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.filaments_api.invoke_arn
}
resource "aws_api_gateway_integration" "filaments_post" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.filaments.id
http_method = aws_api_gateway_method.post_filament.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.filaments_api.invoke_arn
}
resource "aws_api_gateway_integration" "filament_get" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.filament.id
http_method = aws_api_gateway_method.get_filament.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.filaments_api.invoke_arn
}
resource "aws_api_gateway_integration" "filament_put" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.filament.id
http_method = aws_api_gateway_method.put_filament.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.filaments_api.invoke_arn
}
resource "aws_api_gateway_integration" "filament_delete" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.filament.id
http_method = aws_api_gateway_method.delete_filament.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.filaments_api.invoke_arn
}
resource "aws_api_gateway_integration" "login" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.login.id
http_method = aws_api_gateway_method.login.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.auth_api.invoke_arn
}
# OPTIONS integrations for CORS
resource "aws_api_gateway_integration" "options_filaments" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.filaments.id
http_method = aws_api_gateway_method.options_filaments.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.filaments_api.invoke_arn
}
resource "aws_api_gateway_integration" "options_filament" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.filament.id
http_method = aws_api_gateway_method.options_filament.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.filaments_api.invoke_arn
}
resource "aws_api_gateway_integration" "options_login" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.login.id
http_method = aws_api_gateway_method.options_login.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.auth_api.invoke_arn
}
# Lambda permissions for API Gateway
resource "aws_lambda_permission" "api_gateway_filaments" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.filaments_api.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*"
}
resource "aws_lambda_permission" "api_gateway_auth" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.auth_api.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*"
}
# API Gateway Deployment
resource "aws_api_gateway_deployment" "api" {
rest_api_id = aws_api_gateway_rest_api.api.id
depends_on = [
aws_api_gateway_integration.filaments_get,
aws_api_gateway_integration.filaments_post,
aws_api_gateway_integration.filament_get,
aws_api_gateway_integration.filament_put,
aws_api_gateway_integration.filament_delete,
aws_api_gateway_integration.login,
aws_api_gateway_integration.options_filaments,
aws_api_gateway_integration.options_filament,
aws_api_gateway_integration.options_login
]
lifecycle {
create_before_destroy = true
}
}
# API Gateway Stage
resource "aws_api_gateway_stage" "api" {
deployment_id = aws_api_gateway_deployment.api.id
rest_api_id = aws_api_gateway_rest_api.api.id
stage_name = var.environment
}

View File

@@ -0,0 +1,18 @@
# Cloudflare DNS for API subdomain
data "cloudflare_zone" "domain" {
count = var.domain_name != "" && var.cloudflare_api_token != "" ? 1 : 0
name = var.domain_name
}
# CNAME record for api.filamenteka.rs pointing to ALB
resource "cloudflare_record" "api" {
count = var.domain_name != "" && var.cloudflare_api_token != "" ? 1 : 0
zone_id = data.cloudflare_zone.domain[0].id
name = "api"
type = "CNAME"
value = aws_lb.api.dns_name
ttl = 1
proxied = false
comment = "ALB API endpoint with HTTPS"
}

View File

@@ -1,22 +0,0 @@
# Cloudflare DNS configuration
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
# Data source to find the zone
data "cloudflare_zone" "main" {
count = var.domain_name != "" && var.cloudflare_api_token != "" ? 1 : 0
name = var.domain_name
}
# Create CNAME record for API subdomain
resource "cloudflare_record" "api" {
count = var.domain_name != "" && var.cloudflare_api_token != "" ? 1 : 0
zone_id = data.cloudflare_zone.main[0].id
name = "api"
content = replace(replace(aws_api_gateway_stage.api.invoke_url, "https://", ""), "/production", "")
type = "CNAME"
ttl = 120
proxied = false
comment = "API Gateway endpoint"
}

View File

@@ -1,52 +0,0 @@
# DynamoDB table for storing filament data
resource "aws_dynamodb_table" "filaments" {
name = "${var.app_name}-filaments"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
attribute {
name = "brand"
type = "S"
}
attribute {
name = "tip"
type = "S"
}
attribute {
name = "status"
type = "S"
}
# Global secondary index for querying by brand
global_secondary_index {
name = "brand-index"
hash_key = "brand"
projection_type = "ALL"
}
# Global secondary index for querying by type
global_secondary_index {
name = "tip-index"
hash_key = "tip"
projection_type = "ALL"
}
# Global secondary index for querying by status
global_secondary_index {
name = "status-index"
hash_key = "status"
projection_type = "ALL"
}
tags = {
Name = "${var.app_name}-filaments"
Environment = var.environment
}
}

136
terraform/ec2-api.tf Normal file
View File

@@ -0,0 +1,136 @@
# Security group for API instance
resource "aws_security_group" "api_instance" {
name = "${var.app_name}-api-instance-sg"
description = "Security group for API EC2 instance"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
description = "HTTP from ALB only"
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.app_name}-api-instance-sg"
}
}
# IAM role for EC2 instance
resource "aws_iam_role" "api_instance" {
name = "${var.app_name}-api-instance-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
}
# IAM policy for ECR access
resource "aws_iam_role_policy" "ecr_access" {
name = "${var.app_name}-ecr-access"
role = aws_iam_role.api_instance.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
]
Resource = "*"
}
]
})
}
# Instance profile
resource "aws_iam_instance_profile" "api" {
name = "${var.app_name}-api-instance-profile"
role = aws_iam_role.api_instance.name
}
# Get latest Amazon Linux 2 AMI
data "aws_ami" "amazon_linux_2" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# EC2 instance for API
resource "aws_instance" "api" {
ami = data.aws_ami.amazon_linux_2.id
instance_type = "t3.micro"
subnet_id = aws_subnet.public[0].id
vpc_security_group_ids = [aws_security_group.api_instance.id]
associate_public_ip_address = true
iam_instance_profile = aws_iam_instance_profile.api.name
user_data = base64encode(templatefile("${path.module}/user-data.sh", {
database_url = "postgresql://${aws_db_instance.filamenteka.username}:${random_password.db_password.result}@${aws_db_instance.filamenteka.endpoint}/${aws_db_instance.filamenteka.db_name}?sslmode=require"
jwt_secret = random_password.jwt_secret.result
admin_password = var.admin_password
ecr_url = aws_ecr_repository.api.repository_url
aws_region = var.aws_region
}))
tags = {
Name = "${var.app_name}-api-instance"
}
depends_on = [aws_db_instance.filamenteka]
}
# Elastic IP for API instance
resource "aws_eip" "api" {
instance = aws_instance.api.id
domain = "vpc"
tags = {
Name = "${var.app_name}-api-eip"
}
}
# Output the API URL
output "api_instance_url" {
value = "http://${aws_eip.api.public_ip}"
description = "API instance URL"
}

34
terraform/ecr.tf Normal file
View File

@@ -0,0 +1,34 @@
# ECR Repository for API
resource "aws_ecr_repository" "api" {
name = "${var.app_name}-api"
image_tag_mutability = "MUTABLE"
image_scanning_configuration {
scan_on_push = true
}
tags = {
Name = "${var.app_name}-api"
}
}
# ECR Lifecycle Policy
resource "aws_ecr_lifecycle_policy" "api" {
repository = aws_ecr_repository.api.name
policy = jsonencode({
rules = [{
rulePriority = 1
description = "Keep last 10 images"
selection = {
tagStatus = "tagged"
tagPrefixList = ["v"]
countType = "imageCountMoreThan"
countNumber = 10
}
action = {
type = "expire"
}
}]
})
}

View File

@@ -1,110 +0,0 @@
# IAM role for Lambda functions
resource "aws_iam_role" "lambda_role" {
name = "${var.app_name}-lambda-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}
# IAM policy for Lambda to access DynamoDB
resource "aws_iam_role_policy" "lambda_dynamodb_policy" {
name = "${var.app_name}-lambda-dynamodb-policy"
role = aws_iam_role.lambda_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:Scan",
"dynamodb:Query"
]
Resource = [
aws_dynamodb_table.filaments.arn,
"${aws_dynamodb_table.filaments.arn}/index/*"
]
},
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "arn:aws:logs:*:*:*"
}
]
})
}
# Lambda function for filaments CRUD
resource "aws_lambda_function" "filaments_api" {
filename = data.archive_file.filaments_lambda_zip.output_path
function_name = "${var.app_name}-filaments-api"
role = aws_iam_role.lambda_role.arn
handler = "index.handler"
runtime = "nodejs18.x"
timeout = 30
memory_size = 256
source_code_hash = data.archive_file.filaments_lambda_zip.output_base64sha256
environment {
variables = {
TABLE_NAME = aws_dynamodb_table.filaments.name
CORS_ORIGIN = "*"
}
}
depends_on = [aws_iam_role_policy.lambda_dynamodb_policy]
}
# Lambda function for authentication
resource "aws_lambda_function" "auth_api" {
filename = data.archive_file.auth_lambda_zip.output_path
function_name = "${var.app_name}-auth-api"
role = aws_iam_role.lambda_role.arn
handler = "index.handler"
runtime = "nodejs18.x"
timeout = 10
memory_size = 128
source_code_hash = data.archive_file.auth_lambda_zip.output_base64sha256
environment {
variables = {
JWT_SECRET = var.jwt_secret
ADMIN_USERNAME = var.admin_username
ADMIN_PASSWORD_HASH = var.admin_password_hash
CORS_ORIGIN = "*"
}
}
depends_on = [aws_iam_role_policy.lambda_dynamodb_policy]
}
# Archive files for Lambda deployment
data "archive_file" "filaments_lambda_zip" {
type = "zip"
source_dir = "${path.module}/../lambda/filaments"
output_path = "${path.module}/../lambda/filaments.zip"
}
data "archive_file" "auth_lambda_zip" {
type = "zip"
source_dir = "${path.module}/../lambda/auth"
output_path = "${path.module}/../lambda/auth.zip"
}

View File

@@ -16,6 +16,10 @@ provider "aws" {
region = "eu-central-1" # Frankfurt
}
provider "cloudflare" {
api_token = var.cloudflare_api_token != "" ? var.cloudflare_api_token : "dummy" # Dummy token if not provided
}
resource "aws_amplify_app" "filamenteka" {
name = "filamenteka"
repository = var.github_repository
@@ -49,7 +53,7 @@ resource "aws_amplify_app" "filamenteka" {
# Environment variables
environment_variables = {
NEXT_PUBLIC_API_URL = aws_api_gateway_stage.api.invoke_url
NEXT_PUBLIC_API_URL = "https://api.filamenteka.rs/api" # Using Cloudflare proxied subdomain
}
# Custom rules for single-page app

View File

@@ -18,17 +18,28 @@ output "custom_domain_url" {
value = var.domain_name != "" ? "https://${var.domain_name}" : "Not configured"
}
output "api_url" {
description = "API Gateway URL"
value = aws_api_gateway_stage.api.invoke_url
output "rds_endpoint" {
value = aws_db_instance.filamenteka.endpoint
description = "RDS instance endpoint"
}
output "dynamodb_table_name" {
description = "DynamoDB table name"
value = aws_dynamodb_table.filaments.name
output "rds_database_name" {
value = aws_db_instance.filamenteka.db_name
description = "Database name"
}
output "api_custom_url" {
description = "Custom API URL via Cloudflare"
value = var.domain_name != "" && var.cloudflare_api_token != "" ? "https://api.${var.domain_name}" : "Use api_url output instead"
output "rds_username" {
value = aws_db_instance.filamenteka.username
description = "Database username"
}
output "rds_password_secret_arn" {
value = aws_secretsmanager_secret.db_credentials.arn
description = "ARN of the secret containing the database password"
}
output "database_url" {
value = "postgresql://${aws_db_instance.filamenteka.username}:[PASSWORD]@${aws_db_instance.filamenteka.endpoint}/${aws_db_instance.filamenteka.db_name}"
description = "Database connection URL (replace [PASSWORD] with actual password from Secrets Manager)"
sensitive = true
}

98
terraform/rds.tf Normal file
View File

@@ -0,0 +1,98 @@
# RDS PostgreSQL Database
resource "aws_db_subnet_group" "filamenteka" {
name = "${var.app_name}-db-subnet-group"
subnet_ids = aws_subnet.public[*].id
tags = {
Name = "${var.app_name}-db-subnet-group"
}
}
resource "aws_security_group" "rds" {
name = "${var.app_name}-rds-sg"
description = "Security group for RDS database"
vpc_id = aws_vpc.main.id
# Allow access from your local IP for development
# IMPORTANT: Replace with your actual IP address
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # WARNING: This allows access from anywhere. Replace with your IP!
description = "Development access - RESTRICT THIS IN PRODUCTION"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.app_name}-rds-sg"
}
}
resource "aws_db_instance" "filamenteka" {
identifier = var.app_name
engine = "postgres"
engine_version = "15"
instance_class = "db.t3.micro"
allocated_storage = 20
max_allocated_storage = 100
storage_type = "gp3"
storage_encrypted = true
db_name = "filamenteka"
username = "filamenteka_admin"
password = random_password.db_password.result
# Make it publicly accessible for development
publicly_accessible = true
vpc_security_group_ids = [aws_security_group.rds.id]
db_subnet_group_name = aws_db_subnet_group.filamenteka.name
backup_retention_period = 7
backup_window = "03:00-04:00"
maintenance_window = "sun:04:00-sun:05:00"
deletion_protection = false # Set to true in production
skip_final_snapshot = true # Set to false in production
enabled_cloudwatch_logs_exports = ["postgresql"]
tags = {
Name = "${var.app_name}-db"
}
}
resource "random_password" "db_password" {
length = 32
special = false # RDS doesn't allow certain special characters
}
resource "aws_secretsmanager_secret" "db_credentials" {
name = "${var.app_name}-db-credentials"
}
resource "aws_secretsmanager_secret_version" "db_credentials" {
secret_id = aws_secretsmanager_secret.db_credentials.id
secret_string = jsonencode({
username = aws_db_instance.filamenteka.username
password = random_password.db_password.result
host = aws_db_instance.filamenteka.endpoint
port = aws_db_instance.filamenteka.port
database = aws_db_instance.filamenteka.db_name
})
}
# Random password for JWT
resource "random_password" "jwt_secret" {
length = 64
special = false
}

68
terraform/user-data.sh Normal file
View File

@@ -0,0 +1,68 @@
#!/bin/bash
# Update system
yum update -y
# Install Docker
amazon-linux-extras install docker -y
service docker start
usermod -a -G docker ec2-user
chkconfig docker on
# Install docker-compose
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
# Configure AWS CLI
aws configure set region ${aws_region}
# Login to ECR
aws ecr get-login-password --region ${aws_region} | docker login --username AWS --password-stdin ${ecr_url}
# Create environment file
cat > /home/ec2-user/.env <<EOF
DATABASE_URL=${database_url}
JWT_SECRET=${jwt_secret}
ADMIN_PASSWORD=${admin_password}
NODE_ENV=production
PORT=80
NODE_TLS_REJECT_UNAUTHORIZED=0
EOF
# Create docker-compose file
cat > /home/ec2-user/docker-compose.yml <<EOF
version: '3.8'
services:
api:
image: ${ecr_url}:latest
ports:
- "80:80"
env_file: .env
restart: always
EOF
# Start the API
cd /home/ec2-user
docker-compose pull
docker-compose up -d
# Setup auto-restart on reboot
cat > /etc/systemd/system/api.service <<EOF
[Unit]
Description=Filamenteka API
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/home/ec2-user
ExecStart=/usr/local/bin/docker-compose up -d
ExecStop=/usr/local/bin/docker-compose down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target
EOF
systemctl enable api.service

View File

@@ -27,21 +27,10 @@ variable "app_name" {
default = "filamenteka"
}
variable "jwt_secret" {
description = "JWT secret for authentication"
type = string
sensitive = true
}
variable "admin_username" {
description = "Admin username"
type = string
default = "admin"
}
variable "admin_password_hash" {
description = "BCrypt hash of admin password"
variable "admin_password" {
description = "Admin password for the application"
type = string
default = "admin123"
sensitive = true
}
@@ -50,4 +39,10 @@ variable "cloudflare_api_token" {
type = string
default = ""
sensitive = true
}
variable "aws_region" {
description = "AWS region"
type = string
default = "eu-central-1"
}

58
terraform/vpc.tf Normal file
View File

@@ -0,0 +1,58 @@
# VPC Configuration
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.app_name}-vpc"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.app_name}-igw"
}
}
# Public Subnets for RDS (needs at least 2 for subnet group)
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.app_name}-public-subnet-${count.index + 1}"
}
}
# Route Table for Public Subnets
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.app_name}-public-rt"
}
}
# Associate Public Subnets with Route Table
resource "aws_route_table_association" "public" {
count = 2
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# Data source for availability zones
data "aws_availability_zones" "available" {
state = "available"
}