diff --git a/backend/pdm.lock b/backend/pdm.lock index 6f27b27526..0f1f1a4314 100644 --- a/backend/pdm.lock +++ b/backend/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev", "lambda"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.1" -content_hash = "sha256:a43f3913e5037fc729c09aab1c53641eb64bac9448840344dd11e679f0646450" +content_hash = "sha256:c6953195b1f84d74e7eedd81ac75f1f378a0c9b2a49d7e8c188d438d212993f2" [[package]] name = "amqp" @@ -1098,6 +1098,20 @@ files = [ {file = "graphql_core-3.2.3-py3-none-any.whl", hash = "sha256:5766780452bd5ec8ba133f8bf287dc92713e3868ddd83aee4faab9fc3e303dc3"}, ] +[[package]] +name = "gunicorn" +version = "23.0.0" +requires_python = ">=3.7" +summary = "WSGI HTTP Server for UNIX" +groups = ["default"] +dependencies = [ + "packaging", +] +files = [ + {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, + {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, +] + [[package]] name = "h11" version = "0.14.0" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index a6c9bc1044..0b1e1bcaf6 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -94,6 +94,7 @@ dependencies = [ "cryptography>=43.0.0", "openai>=1.52.0", "pydantic>=2.9.2", + "gunicorn>=23.0.0", ] name = "backend" version = "0.1.0" diff --git a/infrastructure/applications/.tool-versions b/infrastructure/applications/.tool-versions new file mode 100644 index 0000000000..2c087aba58 --- /dev/null +++ b/infrastructure/applications/.tool-versions @@ -0,0 +1 @@ +terraform 1.9.8 diff --git a/infrastructure/applications/applications.tf b/infrastructure/applications/applications.tf index 5fd88fdcb5..981c35e9a0 100644 --- a/infrastructure/applications/applications.tf +++ b/infrastructure/applications/applications.tf @@ -12,13 +12,20 @@ locals { module "pretix" { source = "./pretix" - count = local.deploy_pretix ? 1 : 0 + count = 1 ecs_arm_ami = local.ecs_arm_ami + server_ip = module.cluster.server_ip + cluster_id = module.cluster.cluster_id + logs_group_name = module.cluster.logs_group_name } module "pycon_backend" { source = "./pycon_backend" ecs_arm_ami = local.ecs_arm_ami + cluster_id = module.cluster.cluster_id + security_group_id = module.cluster.security_group_id + server_ip = module.cluster.server_ip + logs_group_name = module.cluster.logs_group_name providers = { aws = aws @@ -40,3 +47,17 @@ module "emails" { aws.us = aws.us } } + +module "cluster" { + source = "./cluster" + ecs_arm_ami = local.ecs_arm_ami + + providers = { + aws = aws + aws.us = aws.us + } +} + +output "server_public_ip" { + value = module.cluster.server_public_ip +} diff --git a/infrastructure/components/cloudfront/main.tf b/infrastructure/applications/cluster/cloudfront.tf similarity index 60% rename from infrastructure/components/cloudfront/main.tf rename to infrastructure/applications/cluster/cloudfront.tf index 1aaf319877..c32210118d 100644 --- a/infrastructure/components/cloudfront/main.tf +++ b/infrastructure/applications/cluster/cloudfront.tf @@ -1,24 +1,38 @@ +locals { + pycon_web_domain = local.is_prod ? "admin.pycon.it" : "${terraform.workspace}-admin.pycon.it" + pretix_web_domain = local.is_prod ? "tickets.pycon.it" : "${terraform.workspace}-tickets.pycon.it" +} + +data "aws_cloudfront_origin_request_policy" "all_viewer" { + name = "Managed-AllViewer" +} + data "aws_cloudfront_cache_policy" "caching_disabled" { name = "Managed-CachingDisabled" } -data "aws_cloudfront_origin_request_policy" "all_viewer_except_host_header" { - name = "Managed-AllViewerExceptHostHeader" +data "aws_acm_certificate" "cert" { + domain = "*.pycon.it" + statuses = ["ISSUED"] + provider = aws.us } resource "aws_cloudfront_distribution" "application" { enabled = true is_ipv6_enabled = true - comment = "${terraform.workspace}-${var.application}" + comment = "${terraform.workspace} server" wait_for_deployment = false - aliases = [var.domain] + aliases = [ + local.pycon_web_domain, + local.pretix_web_domain + ] origin { - domain_name = var.origin_url + domain_name = aws_eip.server.public_dns origin_id = "default" custom_origin_config { - origin_protocol_policy = "https-only" + origin_protocol_policy = "http-only" http_port = "80" https_port = "443" origin_ssl_protocols = ["TLSv1"] @@ -29,7 +43,7 @@ resource "aws_cloudfront_distribution" "application" { cloudfront_default_certificate = false minimum_protocol_version = "TLSv1" ssl_support_method = "sni-only" - acm_certificate_arn = var.certificate_arn + acm_certificate_arn = data.aws_acm_certificate.cert.arn } default_cache_behavior { @@ -38,16 +52,10 @@ resource "aws_cloudfront_distribution" "application" { target_origin_id = "default" cache_policy_id = data.aws_cloudfront_cache_policy.caching_disabled.id - origin_request_policy_id = data.aws_cloudfront_origin_request_policy.all_viewer_except_host_header.id + origin_request_policy_id = data.aws_cloudfront_origin_request_policy.all_viewer.id viewer_protocol_policy = "redirect-to-https" compress = true - - lambda_function_association { - event_type = "viewer-request" - lambda_arn = var.forward_host_header_lambda_arn - include_body = false - } } restrictions { diff --git a/infrastructure/applications/cluster/domains.tf b/infrastructure/applications/cluster/domains.tf new file mode 100644 index 0000000000..99d6ee2cea --- /dev/null +++ b/infrastructure/applications/cluster/domains.tf @@ -0,0 +1,27 @@ +data "aws_route53_zone" "zone" { + name = "pycon.it" +} + +resource "aws_route53_record" "web_pycon" { + zone_id = data.aws_route53_zone.zone.zone_id + name = local.pycon_web_domain + type = "A" + + alias { + name = aws_cloudfront_distribution.application.domain_name + zone_id = aws_cloudfront_distribution.application.hosted_zone_id + evaluate_target_health = false + } +} + +resource "aws_route53_record" "web_tickets" { + zone_id = data.aws_route53_zone.zone.zone_id + name = local.pretix_web_domain + type = "A" + + alias { + name = aws_cloudfront_distribution.application.domain_name + zone_id = aws_cloudfront_distribution.application.hosted_zone_id + evaluate_target_health = false + } +} diff --git a/infrastructure/applications/cluster/iam.tf b/infrastructure/applications/cluster/iam.tf new file mode 100644 index 0000000000..e1a98f3e2a --- /dev/null +++ b/infrastructure/applications/cluster/iam.tf @@ -0,0 +1,81 @@ +resource "aws_iam_instance_profile" "server" { + name = "pythonit-${terraform.workspace}-server" + role = aws_iam_role.server.name +} + +resource "aws_iam_role" "server" { + name = "pythonit-${terraform.workspace}-server-role" + assume_role_policy = data.aws_iam_policy_document.server_assume_role.json +} + +resource "aws_iam_role_policy" "server" { + name = "pythonit-${terraform.workspace}-server-policy" + role = aws_iam_role.server.id + policy = data.aws_iam_policy_document.server_role_policy.json +} + +data "aws_iam_policy_document" "server_assume_role" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com", "ecs-tasks.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +data "aws_iam_policy_document" "server_role_policy" { + statement { + effect = "Allow" + actions = [ + "iam:PassRole", + "ses:*", + "ecs:*", + "ecr:*", + "ec2:DescribeInstances", + ] + resources = [ + "*" + ] + } + + statement { + effect = "Allow" + actions = ["cloudwatch:PutMetricData", "logs:*"] + resources = ["*"] + } + + statement { + effect = "Allow" + actions = ["s3:*"] + resources = [ + "arn:aws:s3:::${terraform.workspace}-pycon-backend-media", + "arn:aws:s3:::${terraform.workspace}-pycon-backend-media/*", + "arn:aws:s3:::${terraform.workspace}-pretix-media", + "arn:aws:s3:::${terraform.workspace}-pretix-media/*", + ] + } + + statement { + actions = [ + "sns:CreatePlatformEndpoint", + "sns:Publish" + ] + resources = ["*"] + effect = "Allow" + } + + statement { + actions = [ + "sqs:SendMessage", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes", + "sqs:ReceiveMessage", + ] + resources = ["*"] + effect = "Allow" + } +} diff --git a/infrastructure/applications/cluster/load_balancer_task.tf b/infrastructure/applications/cluster/load_balancer_task.tf new file mode 100644 index 0000000000..9bdc0baed4 --- /dev/null +++ b/infrastructure/applications/cluster/load_balancer_task.tf @@ -0,0 +1,83 @@ +resource "aws_ecs_task_definition" "traefik" { + family = "pythonit-${terraform.workspace}-traefik" + + container_definitions = jsonencode([ + { + name = "traefik" + image = "traefik:v3.1.2" + memoryReservation = 200 + essential = true + + environment = [ + { + name = "TRAEFIK_PROVIDERS_ECS_CLUSTERS" + value = aws_ecs_cluster.cluster.name + }, + { + name = "TRAEFIK_PROVIDERS_ECS_AUTODISCOVERCLUSTERS" + value = "false", + }, + { + name = "TRAEFIK_PROVIDERS_ECS_EXPOSEDBYDEFAULT", + value = "false", + }, + { + name = "TRAEFIK_ENTRYPOINTS_WEB_ADDRESS", + value = ":80" + }, + { + name = "TRAEFIK_LOG_LEVEL", + value = "DEBUG" + } + ] + + portMappings = [ + { + containerPort = 80 + hostPort = 80 + }, + ] + + mountPoints = [] + systemControls = [ + { + "namespace" : "net.core.somaxconn", + "value" : "4096" + } + ] + + logConfiguration = { + logDriver = "awslogs" + options = { + "awslogs-group" = aws_cloudwatch_log_group.cluster.name + "awslogs-region" = "eu-central-1" + "awslogs-stream-prefix" = "traefik" + } + } + + healthCheck = { + retries = 3 + command = [ + "CMD-SHELL", + "echo 4" + ] + timeout = 3 + interval = 10 + } + + stopTimeout = 300 + } + ]) + + requires_compatibilities = [] + tags = {} +} + +resource "aws_ecs_service" "traefik" { + name = "traefik" + cluster = aws_ecs_cluster.cluster.id + task_definition = aws_ecs_task_definition.traefik.arn + desired_count = 1 + deployment_minimum_healthy_percent = 0 + deployment_maximum_percent = 100 +} diff --git a/infrastructure/applications/cluster/logs.tf b/infrastructure/applications/cluster/logs.tf new file mode 100644 index 0000000000..275acf79f0 --- /dev/null +++ b/infrastructure/applications/cluster/logs.tf @@ -0,0 +1,9 @@ +resource "aws_cloudwatch_log_group" "cluster" { + name = "/ecs/pythonit-${terraform.workspace}-cluster" + retention_in_days = 3 +} + + +output "logs_group_name" { + value = aws_cloudwatch_log_group.cluster.name +} diff --git a/infrastructure/applications/cluster/main.tf b/infrastructure/applications/cluster/main.tf new file mode 100644 index 0000000000..117505d16b --- /dev/null +++ b/infrastructure/applications/cluster/main.tf @@ -0,0 +1,16 @@ +locals { + is_prod = terraform.workspace == "production" +} + +resource "aws_ecs_cluster" "cluster" { + name = "pythonit-${terraform.workspace}" +} + +output "cluster_id" { + value = aws_ecs_cluster.cluster.id +} + +resource "aws_ecs_account_setting_default" "trunking" { + name = "awsvpcTrunking" + value = "enabled" +} diff --git a/infrastructure/applications/cluster/redis_task.tf b/infrastructure/applications/cluster/redis_task.tf new file mode 100644 index 0000000000..9fd616d90c --- /dev/null +++ b/infrastructure/applications/cluster/redis_task.tf @@ -0,0 +1,67 @@ +resource "aws_ecs_task_definition" "redis" { + family = "pythonit-${terraform.workspace}-redis" + + container_definitions = jsonencode([ + { + name = "redis" + image = "redis:6.2.6" + memoryReservation = 400 + essential = true + portMappings = [ + { + containerPort = 6379 + hostPort = 6379 + name = "redis" + } + ] + + mountPoints = [ + { + sourceVolume = "redis-data" + containerPath = "/data" + readOnly = false + } + ] + systemControls = [] + + logConfiguration = { + logDriver = "awslogs" + options = { + "awslogs-group" = aws_cloudwatch_log_group.cluster.name + "awslogs-region" = "eu-central-1" + "awslogs-stream-prefix" = "redis" + } + } + + healthCheck = { + retries = 3 + command = [ + "CMD-SHELL", + "redis-cli ping" + ] + timeout = 3 + interval = 10 + } + + stopTimeout = 300 + } + ]) + + volume { + name = "redis-data" + host_path = "/redis-data" + } + + requires_compatibilities = [] + tags = {} +} + + +resource "aws_ecs_service" "redis" { + name = "redis" + cluster = aws_ecs_cluster.cluster.id + task_definition = aws_ecs_task_definition.redis.arn + desired_count = 1 + deployment_minimum_healthy_percent = 0 + deployment_maximum_percent = 100 +} diff --git a/infrastructure/applications/cluster/security.tf b/infrastructure/applications/cluster/security.tf new file mode 100644 index 0000000000..cdceb0c581 --- /dev/null +++ b/infrastructure/applications/cluster/security.tf @@ -0,0 +1,45 @@ +resource "aws_security_group" "server" { + name = "${terraform.workspace}-server" + description = "${terraform.workspace} server" + vpc_id = data.aws_vpc.default.id +} + +resource "aws_security_group_rule" "out_all" { + type = "egress" + from_port = 0 + to_port = 0 + protocol = "all" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.server.id +} + +resource "aws_security_group_rule" "server_rds" { + type = "egress" + from_port = 5432 + to_port = 5432 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.server.id +} + +resource "aws_security_group_rule" "web_http" { + type = "ingress" + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.server.id +} + +resource "aws_security_group_rule" "server_ssh" { + type = "ingress" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.server.id +} + +output "security_group_id" { + value = aws_security_group.server.id +} diff --git a/infrastructure/applications/cluster/server.tf b/infrastructure/applications/cluster/server.tf new file mode 100644 index 0000000000..2f5ba894ed --- /dev/null +++ b/infrastructure/applications/cluster/server.tf @@ -0,0 +1,59 @@ +locals { + server_user_data = templatefile("${path.module}/userdata.sh", { + ecs_cluster = aws_ecs_cluster.cluster.name + swap_size = "4G" + }) +} + +resource "aws_eip" "server" { + instance = aws_instance.server.id + domain = "vpc" +} + +resource "aws_instance" "server" { + ami = "ami-0d683ccb0045afce1" + instance_type = "t4g.large" + subnet_id = data.aws_subnet.public_1a.id + availability_zone = "eu-central-1a" + vpc_security_group_ids = [ + aws_security_group.server.id, + ] + source_dest_check = false + user_data = local.server_user_data + iam_instance_profile = aws_iam_instance_profile.server.name + key_name = "pretix" + user_data_replace_on_change = true + + root_block_device { + volume_size = 30 + } + + tags = { + Name = "pythonit-${terraform.workspace}-server" + Role = "server" + } +} + +resource "aws_ebs_volume" "redis_data" { + availability_zone = "eu-central-1a" + size = 10 + type = "gp3" + + tags = { + Name = "redis-data" + } +} + +resource "aws_volume_attachment" "redis_data_attachment" { + device_name = "/dev/sdf" + volume_id = aws_ebs_volume.redis_data.id + instance_id = aws_instance.server.id +} + +output "server_ip" { + value = aws_instance.server.private_ip +} + +output "server_public_ip" { + value = aws_eip.server.public_ip +} diff --git a/infrastructure/applications/pretix/user_data.sh b/infrastructure/applications/cluster/userdata.sh similarity index 92% rename from infrastructure/applications/pretix/user_data.sh rename to infrastructure/applications/cluster/userdata.sh index e4362b9d47..94d1443b33 100644 --- a/infrastructure/applications/pretix/user_data.sh +++ b/infrastructure/applications/cluster/userdata.sh @@ -1,9 +1,18 @@ #!/bin/bash set -x -# Config ECS agent echo "ECS_CLUSTER=${ecs_cluster}" > /etc/ecs/ecs.config +fallocate -l ${swap_size} /swapfile +chmod 600 /swapfile +mkswap /swapfile +swapon /swapfile +echo "/swapfile swap swap defaults 0 0" >> /etc/fstab + +mkdir /redis-data -p +echo '/dev/nvme1n1 /redis-data xfs defaults,nofail 0 2' >> /etc/fstab +mount -a + # Reclaim unused Docker disk space cat << "EOF" > /usr/local/bin/claimspace.sh #!/bin/bash @@ -74,9 +83,3 @@ systemctl daemon-reload systemctl enable --now claimspace.timer systemctl enable --now pretixcron.timer - -dd if=/dev/zero of=/swapfile bs=128M count=32 -chmod 600 /swapfile -mkswap /swapfile -swapon /swapfile -echo "/swapfile swap swap defaults 0 0" >> /etc/fstab diff --git a/infrastructure/applications/cluster/variables.tf b/infrastructure/applications/cluster/variables.tf new file mode 100644 index 0000000000..15b58209f2 --- /dev/null +++ b/infrastructure/applications/cluster/variables.tf @@ -0,0 +1 @@ +variable "ecs_arm_ami" {} diff --git a/infrastructure/applications/cluster/vpc.tf b/infrastructure/applications/cluster/vpc.tf new file mode 100644 index 0000000000..7162daf1c0 --- /dev/null +++ b/infrastructure/applications/cluster/vpc.tf @@ -0,0 +1,20 @@ +data "aws_vpc" "default" { + filter { + name = "tag:Name" + values = ["pythonit-vpc"] + } +} + +data "aws_subnet" "public_1a" { + vpc_id = data.aws_vpc.default.id + + filter { + name = "tag:Type" + values = ["public"] + } + + filter { + name = "tag:AZ" + values = ["eu-central-1a"] + } +} diff --git a/infrastructure/applications/database/db.tf b/infrastructure/applications/database/db.tf index 28ebc5ce47..71ac4858df 100644 --- a/infrastructure/applications/database/db.tf +++ b/infrastructure/applications/database/db.tf @@ -19,7 +19,7 @@ resource "aws_db_instance" "database" { allow_major_version_upgrade = true engine_version = "14.12" instance_class = "db.t4g.micro" - db_name = "${local.normalized_workspace}backend" + db_name = local.is_prod ? "${local.normalized_workspace}backend" : "pycon" username = "root" password = module.common_secrets.value.database_password multi_az = "false" diff --git a/infrastructure/applications/pretix/cache_ecs.tf b/infrastructure/applications/pretix/cache_ecs.tf deleted file mode 100644 index 0d2a310a7b..0000000000 --- a/infrastructure/applications/pretix/cache_ecs.tf +++ /dev/null @@ -1,118 +0,0 @@ -resource "aws_ecs_cluster" "redis" { - name = "pythonit-${terraform.workspace}-redis" -} - -data "template_file" "redis_data" { - template = file("${path.module}/redis_userdata.sh") - vars = { - ecs_cluster = aws_ecs_cluster.redis.name - } -} - -resource "aws_ebs_volume" "redis_data" { - availability_zone = "eu-central-1a" - size = 10 - type = "gp3" - - tags = { - Name = "redis-data" - } -} - -resource "aws_volume_attachment" "redis_data_attachment" { - device_name = "/dev/sdf" - volume_id = aws_ebs_volume.redis_data.id - instance_id = aws_instance.redis.id -} - -resource "aws_instance" "redis" { - ami = var.ecs_arm_ami - instance_type = "t4g.nano" - subnet_id = data.aws_subnet.private.id - availability_zone = "eu-central-1a" - vpc_security_group_ids = [ - aws_security_group.instance.id - ] - source_dest_check = true - user_data = data.template_file.redis_data.rendered - iam_instance_profile = aws_iam_instance_profile.instance.name - key_name = "pretix" - - root_block_device { - volume_size = 8 - } - - tags = { - Name = "pythonit-${terraform.workspace}-redis" - } -} - -resource "aws_cloudwatch_log_group" "redis_logs" { - name = "/ecs/pythonit-${terraform.workspace}-redis" - retention_in_days = 3 -} - -resource "aws_ecs_task_definition" "redis" { - family = "pythonit-${terraform.workspace}-redis" - container_definitions = jsonencode([ - { - name = "redis" - image = "redis:6.2.6" - memoryReservation = 400 - essential = true - portMappings = [ - { - containerPort = 6379 - hostPort = 6379 - } - ] - - mountPoints = [ - { - sourceVolume = "redis-data" - containerPath = "/data" - readOnly = false - } - ] - systemControls = [] - - logConfiguration = { - logDriver = "awslogs" - options = { - "awslogs-group" = aws_cloudwatch_log_group.redis_logs.name - "awslogs-region" = "eu-central-1" - "awslogs-stream-prefix" = "ecs" - } - } - - healthCheck = { - retries = 3 - command = [ - "CMD-SHELL", - "redis-cli ping" - ] - timeout = 3 - interval = 10 - } - - stopTimeout = 300 - } - ]) - - volume { - name = "redis-data" - host_path = "/redis-data" - } - - requires_compatibilities = [] - tags = {} -} - -resource "aws_ecs_service" "redis" { - name = "pythonit-${terraform.workspace}-redis" - cluster = aws_ecs_cluster.redis.id - task_definition = aws_ecs_task_definition.redis.arn - desired_count = 1 - deployment_minimum_healthy_percent = 0 - deployment_maximum_percent = 100 -} diff --git a/infrastructure/applications/pretix/cloudfront.tf b/infrastructure/applications/pretix/cloudfront.tf deleted file mode 100644 index 557dc7553c..0000000000 --- a/infrastructure/applications/pretix/cloudfront.tf +++ /dev/null @@ -1,59 +0,0 @@ -locals { - is_prod = terraform.workspace == "production" - alias = local.is_prod ? "tickets.pycon.it" : "${terraform.workspace}-tickets.pycon.it" -} - -resource "aws_cloudfront_distribution" "distribution" { - aliases = [local.alias] - - origin { - domain_name = aws_eip.ip.public_dns - origin_id = "pretix" - - custom_origin_config { - origin_protocol_policy = "http-only" - http_port = "80" - https_port = "443" - origin_ssl_protocols = ["TLSv1"] - } - } - - enabled = true - is_ipv6_enabled = true - comment = "Pretix ${terraform.workspace}" - wait_for_deployment = false - - viewer_certificate { - cloudfront_default_certificate = false - acm_certificate_arn = module.common_secrets.value.ssl_certificate_arn - minimum_protocol_version = "TLSv1" - ssl_support_method = "sni-only" - } - - default_cache_behavior { - allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] - cached_methods = ["GET", "HEAD"] - target_origin_id = "pretix" - - forwarded_values { - query_string = true - headers = ["*"] - - cookies { - forward = "all" - } - } - - viewer_protocol_policy = "redirect-to-https" - compress = true - min_ttl = 0 - default_ttl = 604800 - max_ttl = 31536000 - } - - restrictions { - geo_restriction { - restriction_type = "none" - } - } -} diff --git a/infrastructure/applications/pretix/domain.tf b/infrastructure/applications/pretix/domain.tf deleted file mode 100644 index 55b2f47cd7..0000000000 --- a/infrastructure/applications/pretix/domain.tf +++ /dev/null @@ -1,15 +0,0 @@ -data "aws_route53_zone" "zone" { - name = "pycon.it" -} - -resource "aws_route53_record" "record" { - zone_id = data.aws_route53_zone.zone.zone_id - name = local.alias - type = "A" - - alias { - name = aws_cloudfront_distribution.distribution.domain_name - zone_id = aws_cloudfront_distribution.distribution.hosted_zone_id - evaluate_target_health = false - } -} diff --git a/infrastructure/applications/pretix/main.tf b/infrastructure/applications/pretix/main.tf index 9398fc50c3..5473db13fd 100644 --- a/infrastructure/applications/pretix/main.tf +++ b/infrastructure/applications/pretix/main.tf @@ -1,55 +1,13 @@ -data "aws_db_instance" "database" { - db_instance_identifier = "pythonit-${terraform.workspace}" +locals { + is_prod = terraform.workspace == "production" + alias = local.is_prod ? "tickets.pycon.it" : "${terraform.workspace}-tickets.pycon.it" } -resource "aws_ecs_cluster" "pretix" { - name = "${terraform.workspace}-pretix" -} - -data "template_file" "user_data" { - template = file("${path.module}/user_data.sh") - vars = { - ecs_cluster = aws_ecs_cluster.pretix.name - } -} - -resource "aws_instance" "pretix" { - ami = var.ecs_arm_ami - instance_type = "t4g.small" - subnet_id = data.aws_subnet.public.id - availability_zone = "eu-central-1a" - vpc_security_group_ids = [ - aws_security_group.instance.id, - data.aws_security_group.rds.id - ] - source_dest_check = false - user_data = data.template_file.user_data.rendered - iam_instance_profile = aws_iam_instance_profile.instance.name - key_name = "pretix" - - root_block_device { - volume_size = 15 - } - - tags = { - Name = "${terraform.workspace}-pretix-instance" - } -} - -resource "aws_cloudwatch_log_group" "pretix_logs" { - name = "/ecs/pythonit-${terraform.workspace}-pretix" - retention_in_days = 7 -} - -resource "aws_eip" "ip" { - instance = aws_instance.pretix.id - domain = "vpc" - tags = { - Name = "${terraform.workspace}-pretix" - } +data "aws_db_instance" "database" { + db_instance_identifier = "pythonit-${terraform.workspace}" } -resource "aws_ecs_task_definition" "pretix_service" { +resource "aws_ecs_task_definition" "pretix" { family = "${terraform.workspace}-pretix" container_definitions = jsonencode([ { @@ -57,6 +15,12 @@ resource "aws_ecs_task_definition" "pretix_service" { image = "${data.aws_ecr_repository.repo.repository_url}@${data.aws_ecr_image.image.image_digest}" memoryReservation = 1840 essential = true + + dockerLabels = { + "traefik.enable" = "true" + "traefik.http.routers.pretix-web.rule" = "Host(`${local.alias}`)" + } + environment = [ { name = "PRETIX_SENTRY_DSN" @@ -68,7 +32,7 @@ resource "aws_ecs_task_definition" "pretix_service" { }, { name = "PRETIX_REDIS_LOCATION", - value = "redis://${aws_instance.redis.private_ip}/0" + value = "redis://${var.server_ip}/0" }, { name = "PRETIX_REDIS_SESSIONS", @@ -76,15 +40,15 @@ resource "aws_ecs_task_definition" "pretix_service" { }, { name = "PRETIX_CELERY_BROKER", - value = "redis://${aws_instance.redis.private_ip}/1" + value = "redis://${var.server_ip}/1" }, { name = "PRETIX_CELERY_BACKEND", - value = "redis://${aws_instance.redis.private_ip}/2" + value = "redis://${var.server_ip}/2" }, { name = "PRETIX_PRETIX_URL", - value = "https://tickets.pycon.it/" + value = "https://${local.alias}/" }, { name = "PRETIX_PRETIX_TRUST_X_FORWARDED_PROTO", @@ -184,7 +148,7 @@ resource "aws_ecs_task_definition" "pretix_service" { portMappings = [ { containerPort = 80 - hostPort = 80 + hostPort = 0 } ] systemControls = [ @@ -196,9 +160,9 @@ resource "aws_ecs_task_definition" "pretix_service" { logConfiguration = { logDriver = "awslogs" options = { - "awslogs-group" = aws_cloudwatch_log_group.pretix_logs.name + "awslogs-group" = var.logs_group_name "awslogs-region" = "eu-central-1" - "awslogs-stream-prefix" = "ecs" + "awslogs-stream-prefix" = "pretix" } } }, @@ -209,9 +173,9 @@ resource "aws_ecs_task_definition" "pretix_service" { } resource "aws_ecs_service" "pretix" { - name = "${terraform.workspace}-pretix-service" - cluster = aws_ecs_cluster.pretix.id - task_definition = aws_ecs_task_definition.pretix_service.arn + name = "pretix" + cluster = var.cluster_id + task_definition = aws_ecs_task_definition.pretix.arn desired_count = 1 deployment_minimum_healthy_percent = 0 deployment_maximum_percent = 100 diff --git a/infrastructure/applications/pretix/redis_userdata.sh b/infrastructure/applications/pretix/redis_userdata.sh deleted file mode 100644 index bd9cd2d487..0000000000 --- a/infrastructure/applications/pretix/redis_userdata.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -set -x - -# Config ECS agent -echo "ECS_CLUSTER=${ecs_cluster}" > /etc/ecs/ecs.config - -# Reclaim unused Docker disk space -cat << "EOF" > /usr/local/bin/claimspace.sh -#!/bin/bash -# Run fstrim on the host OS periodically to reclaim the unused container data blocks -docker ps -q | xargs docker inspect --format='{{ .State.Pid }}' | xargs -IZ sudo fstrim /proc/Z/root/ -exit $? -EOF - -chmod +x /usr/local/bin/claimspace.sh -echo "0 0 * * * root /usr/local/bin/claimspace.sh" > /etc/cron.d/claimspace - -mkdir /redis-data - -sudo echo "UUID=6dbc6ff5-7b78-47fe-85af-4ab6fa4473cc /redis-data xfs defaults,nofail 0 2" >> /etc/fstab -sudo mount -a - -sudo su -sudo dd if=/dev/zero of=/swapfile bs=128M count=32 -sudo chmod 600 /swapfile -sudo mkswap /swapfile -sudo swapon /swapfile -sudo echo "/swapfile swap swap defaults 0 0" >> /etc/fstab diff --git a/infrastructure/applications/pretix/role.tf b/infrastructure/applications/pretix/role.tf deleted file mode 100644 index 63aa35d9d7..0000000000 --- a/infrastructure/applications/pretix/role.tf +++ /dev/null @@ -1,75 +0,0 @@ -resource "aws_iam_role" "instance" { - name = "${terraform.workspace}-pretix-role" - - assume_role_policy = < /etc/ecs/ecs.config - -# Reclaim unused Docker disk space -cat << "EOF" > /usr/local/bin/claimspace.sh -#!/bin/bash -# Run fstrim on the host OS periodically to reclaim the unused container data blocks -docker ps -q | xargs docker inspect --format='{{ .State.Pid }}' | xargs -IZ sudo fstrim /proc/Z/root/ -exit $? -EOF - -chmod +x /usr/local/bin/claimspace.sh -echo "0 0 * * * root /usr/local/bin/claimspace.sh" > /etc/cron.d/claimspace - -sudo su -sudo dd if=/dev/zero of=/swapfile bs=128M count=32 -sudo chmod 600 /swapfile -sudo mkswap /swapfile -sudo swapon /swapfile -sudo echo "/swapfile swap swap defaults 0 0" >> /etc/fstab diff --git a/infrastructure/applications/pycon_backend/variables.tf b/infrastructure/applications/pycon_backend/variables.tf index b0ee208d5b..582eecfad8 100644 --- a/infrastructure/applications/pycon_backend/variables.tf +++ b/infrastructure/applications/pycon_backend/variables.tf @@ -5,3 +5,7 @@ locals { } variable "ecs_arm_ami" {} +variable "cluster_id" {} +variable "security_group_id" {} +variable "server_ip" {} +variable "logs_group_name" {} diff --git a/infrastructure/applications/pycon_backend/web_task.tf b/infrastructure/applications/pycon_backend/web_task.tf new file mode 100644 index 0000000000..242cf2cf90 --- /dev/null +++ b/infrastructure/applications/pycon_backend/web_task.tf @@ -0,0 +1,73 @@ +resource "aws_ecs_task_definition" "web" { + family = "pythonit-${terraform.workspace}-web" + + container_definitions = jsonencode([ + { + name = "web" + image = "${data.aws_ecr_repository.be_repo.repository_url}@${data.aws_ecr_image.be_arm_image.image_digest}" + memoryReservation = 400 + essential = true + entrypoint = [ + "/home/app/.venv/bin/gunicorn", + ] + + command = [ + "-w", "5", "-b", "0.0.0.0:8000", "pycon.wsgi" + ] + + dockerLabels = { + "traefik.enable" = "true" + "traefik.http.routers.backend-web.rule" = "PathPrefix(`/`)" + } + environment = local.env_vars + + portMappings = [ + { + containerPort = 8000 + hostPort = 0 + }, + ] + + mountPoints = [] + systemControls = [ + { + "namespace" : "net.core.somaxconn", + "value" : "4096" + } + ] + + logConfiguration = { + logDriver = "awslogs" + options = { + "awslogs-group" = var.logs_group_name + "awslogs-region" = "eu-central-1" + "awslogs-stream-prefix" = "backend-web" + } + } + + healthCheck = { + retries = 3 + command = [ + "CMD-SHELL", + "echo 1" + ] + timeout = 3 + interval = 10 + } + + stopTimeout = 300 + } + ]) + + requires_compatibilities = [] + tags = {} +} + +resource "aws_ecs_service" "backend" { + name = "backend-web" + cluster = var.cluster_id + task_definition = aws_ecs_task_definition.web.arn + desired_count = 1 + deployment_minimum_healthy_percent = 100 + deployment_maximum_percent = 200 +} diff --git a/infrastructure/applications/pycon_backend/worker.tf b/infrastructure/applications/pycon_backend/worker.tf index dab5f804f5..731ad262cd 100644 --- a/infrastructure/applications/pycon_backend/worker.tf +++ b/infrastructure/applications/pycon_backend/worker.tf @@ -102,7 +102,7 @@ locals { }, { name = "CACHE_URL", - value = local.is_prod ? "redis://${data.aws_instance.redis.private_ip}/8" : "redis://${data.aws_instance.redis.private_ip}/13" + value = local.is_prod ? "redis://${var.server_ip}/8" : "redis://${var.server_ip}/13" }, { name = "STRIPE_WEBHOOK_SIGNATURE_SECRET", @@ -134,11 +134,11 @@ locals { }, { name = "CELERY_BROKER_URL", - value = local.is_prod ? "redis://${data.aws_instance.redis.private_ip}/5" : "redis://${data.aws_instance.redis.private_ip}/14" + value = local.is_prod ? "redis://${var.server_ip}/5" : "redis://${var.server_ip}/14" }, { name = "CELERY_RESULT_BACKEND", - value = local.is_prod ? "redis://${data.aws_instance.redis.private_ip}/6" : "redis://${data.aws_instance.redis.private_ip}/15" + value = local.is_prod ? "redis://${var.server_ip}/6" : "redis://${var.server_ip}/15" }, { name = "ENV", @@ -182,6 +182,10 @@ locals { { name = "AWS_SES_CONFIGURATION_SET" value = data.aws_sesv2_configuration_set.main.configuration_set_name + }, + { + name = "SNS_WEBHOOK_SECRET", + value = "module.common_secrets.value.sns_webhook_secret" } ] } @@ -203,15 +207,6 @@ resource "aws_iam_role" "ecs_service" { }) } -resource "aws_iam_role_policy_attachment" "volume_policy" { - policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSInfrastructureRolePolicyForVolumes" - role = aws_iam_role.ecs_service.name -} - -resource "aws_ecs_cluster" "worker" { - name = "pythonit-${terraform.workspace}-worker" -} - data "aws_subnet" "private_1a" { vpc_id = data.aws_vpc.default.id @@ -240,61 +235,9 @@ data "aws_subnet" "public_1a" { } } - -data "template_file" "user_data" { - template = file("${path.module}/user_data.sh") - vars = { - ecs_cluster = aws_ecs_cluster.worker.name - } -} - - -resource "aws_instance" "instance_1" { - ami = var.ecs_arm_ami - instance_type = local.is_prod ? "t4g.micro" : "t4g.nano" - subnet_id = data.aws_subnet.private_1a.id - availability_zone = "eu-central-1a" - vpc_security_group_ids = [ - data.aws_security_group.rds.id, - data.aws_security_group.lambda.id, - aws_security_group.instance.id - ] - source_dest_check = false - user_data = data.template_file.user_data.rendered - iam_instance_profile = aws_iam_instance_profile.worker.name - key_name = "pretix" - - dynamic "instance_market_options" { - for_each = terraform.workspace == "production" ? [] : [1] - - content { - market_type = "spot" - - spot_options { - max_price = 0.0031 - spot_instance_type = "persistent" - instance_interruption_behavior = "stop" - } - } - } - - root_block_device { - volume_size = 20 - } - - tags = { - Name = "pythonit-${terraform.workspace}-worker" - } -} - -resource "aws_cloudwatch_log_group" "worker_logs" { - name = "/ecs/pythonit-${terraform.workspace}-worker" - retention_in_days = 7 -} - - resource "aws_ecs_task_definition" "worker" { family = "pythonit-${terraform.workspace}-worker" + container_definitions = jsonencode([ { name = "worker" @@ -322,9 +265,9 @@ resource "aws_ecs_task_definition" "worker" { logConfiguration = { logDriver = "awslogs" options = { - "awslogs-group" = aws_cloudwatch_log_group.worker_logs.name + "awslogs-group" = var.logs_group_name "awslogs-region" = "eu-central-1" - "awslogs-stream-prefix" = "ecs" + "awslogs-stream-prefix" = "backend-worker" } } @@ -348,6 +291,7 @@ resource "aws_ecs_task_definition" "worker" { resource "aws_ecs_task_definition" "beat" { family = "pythonit-${terraform.workspace}-beat" + container_definitions = jsonencode([ { name = "beat" @@ -375,9 +319,9 @@ resource "aws_ecs_task_definition" "beat" { logConfiguration = { logDriver = "awslogs" options = { - "awslogs-group" = aws_cloudwatch_log_group.worker_logs.name + "awslogs-group" = var.logs_group_name "awslogs-region" = "eu-central-1" - "awslogs-stream-prefix" = "ecs" + "awslogs-stream-prefix" = "beat" } } @@ -400,8 +344,8 @@ resource "aws_ecs_task_definition" "beat" { } resource "aws_ecs_service" "worker" { - name = "pythonit-${terraform.workspace}-worker" - cluster = aws_ecs_cluster.worker.id + name = "backend-worker" + cluster = var.cluster_id task_definition = aws_ecs_task_definition.worker.arn desired_count = 1 deployment_minimum_healthy_percent = 0 @@ -409,8 +353,8 @@ resource "aws_ecs_service" "worker" { } resource "aws_ecs_service" "beat" { - name = "pythonit-${terraform.workspace}-beat" - cluster = aws_ecs_cluster.worker.id + name = "backend-beat" + cluster = var.cluster_id task_definition = aws_ecs_task_definition.beat.arn desired_count = 1 deployment_minimum_healthy_percent = 0 diff --git a/infrastructure/components/cloudfront/records.tf b/infrastructure/components/cloudfront/records.tf deleted file mode 100644 index 96410fa383..0000000000 --- a/infrastructure/components/cloudfront/records.tf +++ /dev/null @@ -1,15 +0,0 @@ -data "aws_route53_zone" "zone" { - name = var.zone_name -} - -resource "aws_route53_record" "record" { - zone_id = data.aws_route53_zone.zone.zone_id - name = var.domain - type = "A" - - alias { - name = aws_cloudfront_distribution.application.domain_name - zone_id = aws_cloudfront_distribution.application.hosted_zone_id - evaluate_target_health = false - } -} diff --git a/infrastructure/components/cloudfront/variables.tf b/infrastructure/components/cloudfront/variables.tf deleted file mode 100644 index 2713c97095..0000000000 --- a/infrastructure/components/cloudfront/variables.tf +++ /dev/null @@ -1,6 +0,0 @@ -variable "application" {} -variable "origin_url" {} -variable "domain" {} -variable "certificate_arn" {} -variable "zone_name" {} -variable "forward_host_header_lambda_arn" {}