Remove decorative icons and update CORS configuration
This commit is contained in:
162
terraform/alb.tf
Normal file
162
terraform/alb.tf
Normal 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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
18
terraform/cloudflare-api.tf
Normal file
18
terraform/cloudflare-api.tf
Normal 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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
136
terraform/ec2-api.tf
Normal 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
34
terraform/ecr.tf
Normal 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"
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
98
terraform/rds.tf
Normal 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
68
terraform/user-data.sh
Normal 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
|
||||
@@ -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
58
terraform/vpc.tf
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user