From 2a8340f5361034d4dac689ad3ad935c39c2d3568 Mon Sep 17 00:00:00 2001 From: tomfrew Date: Fri, 9 Feb 2024 21:41:24 +0000 Subject: [PATCH] Base setup --- __main__.py | 131 +++++++------ backend.py | 530 +++++++++++++++++++++++++++++----------------------- bucket.py | 59 ++++++ frontend.py | 230 +++++++++++++---------- network.py | 439 ++++++++++++++++++++++++------------------- 5 files changed, 812 insertions(+), 577 deletions(-) create mode 100644 bucket.py diff --git a/__main__.py b/__main__.py index 1b32e7b..02211fe 100644 --- a/__main__.py +++ b/__main__.py @@ -11,81 +11,100 @@ import cluster import frontend import backend +import bucket config = pulumi.Config() -service_name = config.get('service_name') or 'lago' -lago_version = config.get('lago_version') -db_name = config.get('db_name') or 'lago' -db_user = config.get('db_user') or 'lago' +service_name = config.get("service_name") or "lago" +lago_version = config.get("lago_version") +db_name = config.get("db_name") or "lago" +db_user = config.get("db_user") or "lago" -db_password = config.get_secret('db_password') +db_password = config.get_secret("db_password") if not db_password: - password = random.RandomPassword('db_password', - length=16, - special=True, - override_special='_%', - ) - db_password = password.result + password = random.RandomPassword( + "db_password", + length=16, + ) + db_password = password.result # Create an AWS VPC with Subnets and Security Groups -network = network.Vpc(f'{service_name}-net', network.VpcArgs()) +network = network.Vpc(f"{service_name}-net", network.VpcArgs()) subnet_ids = [] for subnet in network.subnets: - subnet_ids.append(subnet.id) + subnet_ids.append(subnet.id) # Create an RDS PostgreSQL instance -db = database.Db(f'{service_name}-db', - database.DbArgs( - db_name=db_name, - db_user=db_user, - db_password=db_password, - subnet_ids=subnet_ids, - security_group_ids=[network.rds_security_group.id], - ), +db = database.Db( + f"{service_name}-db", + database.DbArgs( + db_name=db_name, + db_user=db_user, + db_password=db_password, + subnet_ids=subnet_ids, + security_group_ids=[network.rds_security_group.id], + ), ) # Create a Elasticache Redis instance -redis = database.Redis(f'{service_name}-redis', - database.RedisArgs( - redis_name=db_name, - subnet_ids=subnet_ids, - security_group_ids=[network.redis_security_group.id], - ), +redis = database.Redis( + f"{service_name}-redis", + database.RedisArgs( + redis_name=db_name, + subnet_ids=subnet_ids, + security_group_ids=[network.redis_security_group.id], + ), ) # Create ECS Cluster -cluster = cluster.Cluster(f'{service_name}-ecs') +cluster = cluster.Cluster(f"{service_name}-ecs") + +bucket = bucket.Bucket( + f"keel-{service_name}-storage", bucket.BucketArgs(role_name=cluster.role.name) +) # Create Backend -backend = backend.Backend(f'{service_name}-be', backend.BackendArgs( - cluster_arn=cluster.cluster.arn, - role_arn=cluster.role.arn, - lago_version=lago_version, - vpc_id=network.vpc.id, - subnet_ids=subnet_ids, - security_group_ids=[network.app_security_group.id], - db_host=db.db.address, - db_name=db_name, - db_user=db_user, - db_password=db_password, - redis_host=redis.redis.cache_nodes[0].address, -)) +backend = backend.Backend( + f"{service_name}-be", + backend.BackendArgs( + cluster_arn=cluster.cluster.arn, + role_arn=cluster.role.arn, + lago_version=lago_version, + vpc_id=network.vpc.id, + subnet_ids=subnet_ids, + app_security_group=network.app_security_group, + container_security_group=network.be_security_group, + db_host=db.db.address, + db_name=db_name, + db_user=db_user, + db_password=db_password, + redis_host=redis.redis.cache_nodes[0].address, + bucket_name=bucket.bucket.bucket, + front_url=network.front_lb.dns_name, + api_url=network.back_lb.dns_name, + alb_arn=network.back_lb.arn, + ), +) # Create Frontend -front = frontend.Frontend(f'{service_name}-front', frontend.FrontendArgs( - cluster_arn=cluster.cluster.arn, - role_arn=cluster.role.arn, - lago_version=lago_version, - vpc_id=network.vpc.id, - subnet_ids=subnet_ids, - security_group_ids=[network.app_security_group.id], -)) +front = frontend.Frontend( + f"{service_name}-front", + frontend.FrontendArgs( + cluster_arn=cluster.cluster.arn, + role_arn=cluster.role.arn, + lago_version=lago_version, + vpc_id=network.vpc.id, + subnet_ids=subnet_ids, + security_group_ids=[network.app_security_group.id], + api_url=network.back_lb.dns_name, + alb_arn=network.front_lb.arn, + ), +) -front_url = pulumi.Output.concat('http://', front.alb.dns_name) -api_url = pulumi.Output.concat('http://', backend.alb.dns_name) +front_url = pulumi.Output.concat("http://", network.front_lb.dns_name) +api_url = pulumi.Output.concat("http://", network.back_lb.dns_name) -pulumi.export('Lago Front URL', front_url) -pulumi.export('Lago API URL', api_url) -pulumi.export('ECS Cluster Name', cluster.cluster.name) -pulumi.export('Lago Version', lago_version) -pulumi.export('Service Name', service_name) +pulumi.export("Lago Front URL", front_url) +pulumi.export("Lago API URL", api_url) +pulumi.export("ECS Cluster Name", cluster.cluster.name) +pulumi.export("Lago Version", lago_version) +pulumi.export("Service Name", service_name) diff --git a/backend.py b/backend.py index 444c9bc..4ca00bf 100644 --- a/backend.py +++ b/backend.py @@ -1,245 +1,309 @@ import base64 import json -import pulumi import pulumi_random as random import pulumi_tls as tls from pulumi import ComponentResource, Output, ResourceOptions from pulumi_aws import lb, ecs + class BackendArgs: - - def __init__(self, - lago_version=None, - cluster_arn=None, - role_arn=None, - vpc_id=None, - subnet_ids=None, - security_group_ids=None, - db_host=None, - db_port='5432', - db_name=None, - db_user=None, - db_password=None, - redis_host=None, - redis_port='6379', - ): - self.lago_version = lago_version - self.cluster_arn = cluster_arn - self.role_arn = role_arn - self.vpc_id = vpc_id - self.subnet_ids = subnet_ids - self.security_group_ids = security_group_ids - self.db_host = db_host - self.db_port = db_port - self.db_name = db_name - self.db_user = db_user - self.db_password = db_password - self.redis_host = redis_host - self.redis_port = redis_port + + def __init__( + self, + lago_version=None, + cluster_arn=None, + role_arn=None, + vpc_id=None, + subnet_ids=None, + app_security_group=None, + container_security_group=None, + db_host=None, + db_port="5432", + db_name=None, + db_user=None, + db_password=None, + redis_host=None, + redis_port="6379", + bucket_name=None, + front_url=None, + alb_arn=None, + api_url=None, + ): + self.lago_version = lago_version + self.cluster_arn = cluster_arn + self.role_arn = role_arn + self.vpc_id = vpc_id + self.subnet_ids = subnet_ids + self.app_security_group_id = app_security_group + self.container_security_group = container_security_group + self.db_host = db_host + self.db_port = db_port + self.db_name = db_name + self.db_user = db_user + self.db_password = db_password + self.redis_host = redis_host + self.redis_port = redis_port + self.bucket_name = bucket_name + self.front_url = front_url + self.alb_arn = alb_arn + self.api_url = api_url + class Backend(ComponentResource): - def __init__(self, - name: str, - args: BackendArgs, - opts: ResourceOptions = None, - ): - super().__init__('custom:resource:Backend', name, {}, opts) - - # Create a Load Balancer to listen to HTTP traffic on port 80 - self.alb = lb.LoadBalancer(f'{name}-alb', - security_groups=args.security_group_ids, - subnets=args.subnet_ids, - opts=ResourceOptions(parent=self), - ) - - # Create a Target Group for API - api_target_group = lb.TargetGroup(f'{name}-api-tg', - port=3000, - protocol='HTTP', - target_type='ip', - vpc_id=args.vpc_id, - health_check=lb.TargetGroupHealthCheckArgs( - path='/health', - port=3000, - healthy_threshold=2, - interval=5, - timeout=4, - protocol='HTTP', - matcher='200-399', - ), - opts=ResourceOptions(parent=self), - ) - - # Create a listener for API - api_listener = lb.Listener(f'{name}-api-listener', - load_balancer_arn=self.alb.arn, - port=80, - default_actions=[lb.ListenerDefaultActionArgs( - type='forward', - target_group_arn=api_target_group.arn, - )], - opts=ResourceOptions(parent=self), - ) - - database_url = Output.concat( - 'postgres://', - args.db_user, - ':', - args.db_password, - '@', - args.db_host, - ':', - args.db_port, - '/', - args.db_name, - ) - - redis_url = Output.concat( - 'redis://', - args.redis_host, - ':', - args.redis_port, - ) - - rsa_private_key = tls.PrivateKey(f'{name}-private-key', - algorithm='RSA', - ).private_key_pem.apply(lambda key: base64.b64encode(key.encode()).decode()) - secret_key_base = random.RandomString(f'{name}-secret-key-base', - length=64, - special=False, - ).result.apply(lambda key: base64.b64encode(key.encode()).decode()) - encryption_deterministic_key = random.RandomString(f'{name}-encryption-deterministic-key', - length=32, - special=False, - ).result.apply(lambda key: base64.b64encode(key.encode()).decode()) - encryption_key_derivation_salt = random.RandomString(f'{name}-encryption-key-derivation-salt', - length=32, - special=False, - ).result.apply(lambda key: base64.b64encode(key.encode()).decode()) - encryption_primary_key = random.RandomString(f'{name}-encryption-primary-key', - length=32, - special=False, - ).result.apply(lambda key: base64.b64encode(key.encode()).decode()) - - # Create the API ECS Task Definition - api_task_name = f'{name}-api-task' - api_container_name = f'{name}-api-container' - self.api_task_definition = ecs.TaskDefinition(api_task_name, - family=api_task_name, - cpu='1024', - memory='2048', - network_mode='awsvpc', - requires_compatibilities=['FARGATE'], - execution_role_arn=args.role_arn, - container_definitions=Output.json_dumps([{ - 'name': api_container_name, - 'image': f'getlago/api:v{args.lago_version}', - 'portMappings': [{ - 'containerPort': 3000, - 'hostPort': 3000, - 'protocol': 'tcp', - }], - 'environment': [ - { - 'name': 'RAILS_ENV', - 'value': 'production', - }, - { - 'name': 'DATABASE_URL', - 'value': database_url, - }, - { - 'name': 'REDIS_URL', - 'value': redis_url, - }, - { - 'name': 'REDIS_CACHE_URL', - 'value': redis_url, - }, - { - 'name': 'LAGO_SIDEKIQ_WEB', - 'value': 'true', - }, - { - 'name': 'RAILS_LOG_TO_STDOUT', - 'value': 'true', - }, - { - 'name': 'LAGO_RSA_PRIVATE_KEY', - 'value': rsa_private_key, - }, - { - 'name': 'SECRET_KEY_BASE', - 'value': secret_key_base, - }, - { - 'name': 'ENCRYPTION_DETERMINISTIC_KEY', - 'value': encryption_deterministic_key, - }, - { - 'name': 'ENCRYPTION_KEY_DERIVATION_SALT', - 'value': encryption_key_derivation_salt, - }, - { - 'name': 'ENCRYPTION_PRIMARY_KEY', - 'value': encryption_primary_key, - }, - { - 'name': 'LAGO_DISABLE_SEGMENT', - 'value': 'true', - }, - { - 'name': 'LAGO_DISABLE_SIGNUP', - 'value': 'false', - }, - { - 'name': 'DATABASE_POOL', - 'value': '10', - }, - { - 'name': 'RAILS_MAX_THREADS', - 'value': '5', - }, - { - 'name': 'RAILS_MIN_THREADS', - 'value': '0', - }, - { - 'name': 'SIDEKIQ_EVENTS', - 'value': 'true', - }, - { - 'name': 'WEB_CONCURRENCY', - 'value': '2', - }, - { - 'name': 'LAGO_USE_AWS_S3', - 'value': 'true', - }, - ], - }]), - opts=ResourceOptions(parent=self), - ) - - # Create the API ECS Service - self.api_service = ecs.Service(f'{name}-api-svc', - cluster=args.cluster_arn, - desired_count=1, - launch_type='FARGATE', - task_definition=self.api_task_definition.arn, - network_configuration=ecs.ServiceNetworkConfigurationArgs( - assign_public_ip=True, - subnets=args.subnet_ids, - security_groups=args.security_group_ids, - ), - load_balancers=[ecs.ServiceLoadBalancerArgs( - target_group_arn=api_target_group.arn, - container_name=api_container_name, - container_port=3000, - )], - opts=ResourceOptions(depends_on=[api_listener], parent=self), - ) - - self.register_outputs({}) \ No newline at end of file + def __init__( + self, + name: str, + args: BackendArgs, + opts: ResourceOptions = None, + ): + super().__init__("custom:resource:Backend", name, {}, opts) + + # Create a Load Balancer to listen to HTTP traffic on port 80 + self.alb = lb.LoadBalancer( + f"{name}-alb", + security_groups=[args.app_security_group_id], + subnets=args.subnet_ids, + opts=ResourceOptions(parent=self), + ) + + # Create a Target Group for API + api_target_group = lb.TargetGroup( + f"{name}-api-tg", + port=3000, + protocol="HTTP", + target_type="ip", + vpc_id=args.vpc_id, + health_check=lb.TargetGroupHealthCheckArgs( + path="/health", + port=3000, + healthy_threshold=2, + interval=5, + timeout=4, + protocol="HTTP", + matcher="200-399", + ), + opts=ResourceOptions(parent=self), + ) + + # Create a listener for API + api_listener = lb.Listener( + f"{name}-api-listener", + load_balancer_arn=args.alb_arn, + port=80, + default_actions=[ + lb.ListenerDefaultActionArgs( + type="forward", + target_group_arn=api_target_group.arn, + ) + ], + opts=ResourceOptions(parent=self), + ) + + database_url = Output.concat( + "postgres://", + args.db_user, + ":", + args.db_password, + "@", + args.db_host, + ":", + args.db_port, + "/", + args.db_name, + ) + + redis_url = Output.concat( + "redis://", + args.redis_host, + ":", + args.redis_port, + ) + + rsa_private_key = tls.PrivateKey( + f"{name}-private-key", + algorithm="RSA", + ).private_key_pem.apply(lambda key: base64.b64encode(key.encode()).decode()) + secret_key_base = random.RandomString( + f"{name}-secret-key-base", + length=64, + special=False, + ).result.apply(lambda key: base64.b64encode(key.encode()).decode()) + encryption_deterministic_key = random.RandomString( + f"{name}-encryption-deterministic-key", + length=32, + special=False, + ).result.apply(lambda key: base64.b64encode(key.encode()).decode()) + encryption_key_derivation_salt = random.RandomString( + f"{name}-encryption-key-derivation-salt", + length=32, + special=False, + ).result.apply(lambda key: base64.b64encode(key.encode()).decode()) + encryption_primary_key = random.RandomString( + f"{name}-encryption-primary-key", + length=32, + special=False, + ).result.apply(lambda key: base64.b64encode(key.encode()).decode()) + + environment = [ + { + "name": "RAILS_ENV", + "value": "production", + }, + { + "name": "DATABASE_URL", + "value": database_url, + }, + { + "name": "REDIS_URL", + "value": redis_url, + }, + { + "name": "REDIS_CACHE_URL", + "value": redis_url, + }, + { + "name": "LAGO_SIDEKIQ_WEB", + "value": "true", + }, + { + "name": "RAILS_LOG_TO_STDOUT", + "value": "true", + }, + { + "name": "LAGO_RSA_PRIVATE_KEY", + "value": rsa_private_key, + }, + { + "name": "SECRET_KEY_BASE", + "value": secret_key_base, + }, + { + "name": "ENCRYPTION_DETERMINISTIC_KEY", + "value": encryption_deterministic_key, + }, + { + "name": "ENCRYPTION_KEY_DERIVATION_SALT", + "value": encryption_key_derivation_salt, + }, + { + "name": "ENCRYPTION_PRIMARY_KEY", + "value": encryption_primary_key, + }, + { + "name": "LAGO_DISABLE_SEGMENT", + "value": "true", + }, + { + "name": "LAGO_DISABLE_SIGNUP", + "value": "false", + }, + { + "name": "DATABASE_POOL", + "value": "10", + }, + { + "name": "RAILS_MAX_THREADS", + "value": "5", + }, + { + "name": "RAILS_MIN_THREADS", + "value": "0", + }, + { + "name": "SIDEKIQ_EVENTS", + "value": "true", + }, + { + "name": "WEB_CONCURRENCY", + "value": "2", + }, + { + "name": "LAGO_USE_AWS_S3", + "value": "true", + }, + { + "name": "AWS_REGION", + "value": "eu-west-2", + }, + { + "name": "LAGO_AWS_S3_REGION", + "value": "eu-west-2", + }, + { + "name": "LAGO_AWS_S3_BUCKET", + "value": args.bucket_name, + }, + { + "name": "LAGO_FRONT_URL", + "value": Output.concat("http://", args.front_url), + }, + { + "name": "LAGO_API_URL", + "value": Output.concat("http://", args.api_url), + }, + ] + + # Create the API ECS Task Definition + api_task_name = f"{name}-api-task" + api_container_name = f"{name}-api-container" + self.api_task_definition = ecs.TaskDefinition( + api_task_name, + family=api_task_name, + cpu="1024", + memory="2048", + network_mode="awsvpc", + requires_compatibilities=["FARGATE"], + execution_role_arn=args.role_arn, + task_role_arn=args.role_arn, + container_definitions=Output.json_dumps( + [ + { + "name": api_container_name, + "image": f"getlago/api:v{args.lago_version}", + "portMappings": [ + { + "containerPort": 3000, + "hostPort": 3000, + "protocol": "tcp", + } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-create-group": "true", + "awslogs-group": f"/ecs/{name}-api-task", + "awslogs-region": "eu-west-2", + "awslogs-stream-prefix": "ecs", + }, + }, + "environment": environment, + } + ] + ), + opts=ResourceOptions(parent=self), + ) + + # Create the API ECS Service + self.api_service = ecs.Service( + f"{name}-api-svc", + cluster=args.cluster_arn, + desired_count=1, + launch_type="FARGATE", + task_definition=self.api_task_definition.arn, + network_configuration=ecs.ServiceNetworkConfigurationArgs( + subnets=args.subnet_ids, + security_groups=[args.container_security_group], + ), + load_balancers=[ + ecs.ServiceLoadBalancerArgs( + target_group_arn=api_target_group.arn, + container_name=api_container_name, + container_port=3000, + ) + ], + opts=ResourceOptions(depends_on=[api_listener], parent=self), + ) + + self.register_outputs({}) diff --git a/bucket.py b/bucket.py new file mode 100644 index 0000000..3c66176 --- /dev/null +++ b/bucket.py @@ -0,0 +1,59 @@ +import json + +from pulumi import ComponentResource, ResourceOptions +from pulumi_aws import ecs, iam, s3 + + +class BucketArgs: + + def __init__( + self, + role_name=None, + ): + self.role_name = role_name + + +class Bucket(ComponentResource): + + def __init__( + self, + name: str, + args: BucketArgs, + opts: ResourceOptions = None, + ): + super().__init__("custom:resource:Bucket", name, {}, opts) + + # Create an S3 bucket + self.bucket = s3.Bucket(name, acl="private", opts=ResourceOptions(parent=self)) + + # Grant the ECS task role permission to access the S3 bucket + self.bucket_policy = iam.Policy( + f"{name}-access-policy", + policy=self.bucket.arn.apply( + lambda arn: json.dumps( + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "s3:*", + "Resource": [ + f"{arn}/*", # Grant access to objects in the bucket + arn, # Grant access to the bucket itself + ], + } + ], + } + ) + ), + opts=ResourceOptions(parent=self), + ) + + self.rpa = iam.RolePolicyAttachment( + f"{name}-s3-role-policy-attachment", + policy_arn=self.bucket_policy.arn, + role=args.role_name, + opts=ResourceOptions(parent=self), + ) + + self.register_outputs({}) diff --git a/frontend.py b/frontend.py index 5c0a863..05188cf 100644 --- a/frontend.py +++ b/frontend.py @@ -1,113 +1,145 @@ import json from pulumi import ComponentResource, Output, ResourceOptions -from pulumi_aws import lb, ecs +from pulumi_aws import lb, ecs, cloudwatch + class FrontendArgs: - def __init__(self, - lago_version=None, - cluster_arn=None, - role_arn=None, - vpc_id=None, - subnet_ids=None, - security_group_ids=None, - ): - self.lago_version = lago_version - self.cluster_arn = cluster_arn - self.role_arn = role_arn - self.vpc_id = vpc_id - self.subnet_ids = subnet_ids - self.security_group_ids = security_group_ids + def __init__( + self, + lago_version=None, + cluster_arn=None, + role_arn=None, + vpc_id=None, + subnet_ids=None, + security_group_ids=None, + api_url=None, + alb_arn=None, + ): + self.lago_version = lago_version + self.cluster_arn = cluster_arn + self.role_arn = role_arn + self.vpc_id = vpc_id + self.subnet_ids = subnet_ids + self.security_group_ids = security_group_ids + self.api_url = api_url + self.alb_arn = alb_arn + class Frontend(ComponentResource): - - def __init__(self, - name: str, - args: FrontendArgs, - opts: ResourceOptions = None, - ): - super().__init__('custom:resource:Frontend', name, {}, opts) - # Create a Load Balancer - self.alb = lb.LoadBalancer(f'{name}-alb', - security_groups=args.security_group_ids, - subnets=args.subnet_ids, - opts=ResourceOptions(parent=self), - ) + def __init__( + self, + name: str, + args: FrontendArgs, + opts: ResourceOptions = None, + ): + super().__init__("custom:resource:Frontend", name, {}, opts) + + # Create a Target Group + target_group = lb.TargetGroup( + f"{name}-tg", + port=80, + protocol="HTTP", + target_type="ip", + vpc_id=args.vpc_id, + health_check=lb.TargetGroupHealthCheckArgs( + healthy_threshold=2, + interval=5, + timeout=4, + protocol="HTTP", + matcher="200-399", + ), + opts=ResourceOptions(parent=self), + ) - # Create a Target Group - target_group = lb.TargetGroup(f'{name}-tg', - port=80, - protocol='HTTP', - target_type='ip', - vpc_id=args.vpc_id, - health_check=lb.TargetGroupHealthCheckArgs( - healthy_threshold=2, - interval=5, - timeout=4, - protocol='HTTP', - matcher='200-399', - ), - opts=ResourceOptions(parent=self), - ) + # Create a Listener + listener = lb.Listener( + f"{name}-listener", + load_balancer_arn=args.alb_arn, + port=80, + default_actions=[ + lb.ListenerDefaultActionArgs( + type="forward", + target_group_arn=target_group.arn, + ) + ], + opts=ResourceOptions(parent=self), + ) - # Create a Listener - listener = lb.Listener(f'{name}-listener', - load_balancer_arn=self.alb.arn, - port=80, - default_actions=[lb.ListenerDefaultActionArgs( - type='forward', - target_group_arn=target_group.arn, - )], - opts=ResourceOptions(parent=self), - ) + log_group = cloudwatch.LogGroup(f"/ecs/{name}-api-task", retention_in_days=14) - # Create the Frontend ECS Task Definition - task_name = f'{name}-task' - container_name = f'{name}-container' - self.task_definition = ecs.TaskDefinition(task_name, - family=task_name, - cpu='256', - memory='512', - network_mode='awsvpc', - requires_compatibilities=['FARGATE'], - execution_role_arn=args.role_arn, - container_definitions=Output.json_dumps([{ - 'name': container_name, - 'image': f'getlago/front:v{args.lago_version}', - 'portMappings': [{ - 'containerPort': 80, - 'hostPort': 80, - 'protocol': 'tcp', - }], - 'environment': [ - { - 'name': 'APP_ENV', - 'value': 'production', - }, - ], - }]), - opts=ResourceOptions(parent=self), - ) + # Create the Frontend ECS Task Definition + task_name = f"{name}-task" + container_name = f"{name}-container" + self.task_definition = ecs.TaskDefinition( + task_name, + family=task_name, + cpu="256", + memory="512", + network_mode="awsvpc", + requires_compatibilities=["FARGATE"], + execution_role_arn=args.role_arn, + container_definitions=Output.json_dumps( + [ + { + "name": container_name, + "image": f"getlago/front:v{args.lago_version}", + "portMappings": [ + { + "containerPort": 80, + "hostPort": 80, + "protocol": "tcp", + } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": log_group.name, + "awslogs-region": "eu-west-2", + "awslogs-stream-prefix": "ecs", + }, + }, + "environment": [ + { + "name": "APP_ENV", + "value": "production", + }, + { + "name": "API_URL", + "value": Output.concat("http://", args.api_url), + }, + { + "name": "CODEGEN_API", + "value": Output.concat("http://", args.api_url), + }, + ], + } + ] + ), + opts=ResourceOptions(parent=self), + ) - # Create the ECS Frontend Service - self.service = ecs.Service(f'{name}-svc', - cluster=args.cluster_arn, - desired_count=1, - launch_type='FARGATE', - task_definition=self.task_definition.arn, - network_configuration=ecs.ServiceNetworkConfigurationArgs( - assign_public_ip=True, - subnets=args.subnet_ids, - security_groups=args.security_group_ids, - ), - load_balancers=[ecs.ServiceLoadBalancerArgs( - target_group_arn=target_group.arn, - container_name=container_name, - container_port=80, - )], - opts=ResourceOptions(depends_on=[listener], parent=self), - ) + # Create the ECS Frontend Service + self.service = ecs.Service( + f"{name}-svc", + cluster=args.cluster_arn, + desired_count=1, + launch_type="FARGATE", + task_definition=self.task_definition.arn, + network_configuration=ecs.ServiceNetworkConfigurationArgs( + subnets=args.subnet_ids, + security_groups=args.security_group_ids, + ), + load_balancers=[ + ecs.ServiceLoadBalancerArgs( + target_group_arn=target_group.arn, + container_name=container_name, + container_port=80, + ) + ], + opts=ResourceOptions(depends_on=[listener], parent=self), + ) - self.register_outputs({}) \ No newline at end of file + self.register_outputs({}) diff --git a/network.py b/network.py index 187094a..1544c3f 100644 --- a/network.py +++ b/network.py @@ -1,197 +1,258 @@ from pulumi import ComponentResource, ResourceOptions -from pulumi_aws import ec2, get_availability_zones +from pulumi_aws import ec2, get_availability_zones, lb # VPC + class VpcArgs: - def __init__(self, - cidr_block='172.42.0.0/16', - instance_tenancy='default', - enable_dns_hostnames=True, - enable_dns_support=True, - ): - self.cidr_block = cidr_block - self.instance_tenancy = instance_tenancy - self.enable_dns_hostnames = enable_dns_hostnames - self.enable_dns_support = enable_dns_support - + def __init__( + self, + cidr_block="172.42.0.0/16", + instance_tenancy="default", + enable_dns_hostnames=True, + enable_dns_support=True, + ): + self.cidr_block = cidr_block + self.instance_tenancy = instance_tenancy + self.enable_dns_hostnames = enable_dns_hostnames + self.enable_dns_support = enable_dns_support + + class Vpc(ComponentResource): - def __init__(self, - name: str, - args: VpcArgs, - opts: ResourceOptions = None - ): - super().__init__('custom:resource:VPC', name, {}, opts) - - vpc_name = name+'-vpc' - self.vpc = ec2.Vpc(vpc_name, - cidr_block=args.cidr_block, - instance_tenancy=args.instance_tenancy, - enable_dns_hostnames=args.enable_dns_hostnames, - enable_dns_support=args.enable_dns_support, - tags={ - 'Name': vpc_name, - }, - opts=ResourceOptions(parent=self), - ) - - # Internet Gateway - - igw_name = name+'-igw' - self.igw = ec2.InternetGateway(igw_name, - vpc_id = self.vpc.id, - tags={ - 'Name': igw_name, - }, - opts=ResourceOptions(parent=self), - ) - - # Route Table - - rt_name = name+'-rt' - self.route_table = ec2.RouteTable(rt_name, - vpc_id=self.vpc.id, - routes=[ec2.RouteTableRouteArgs( - cidr_block='0.0.0.0/0', - gateway_id=self.igw.id, - )], - tags={ - 'Name': rt_name, - }, - opts=ResourceOptions(parent=self), - ) - - # Subnets - - all_zones = get_availability_zones() - zone_names = [all_zones.names[0], all_zones.names[1], all_zones.names[2]] - self.subnets = [] - subnet_base_name = f'{name}-subnet' - - for zone in zone_names: - vpc_subnet = ec2.Subnet(f'{subnet_base_name}-{zone}', - assign_ipv6_address_on_creation=False, - vpc_id=self.vpc.id, - map_public_ip_on_launch=True, - cidr_block=f'172.42.{len(self.subnets)}.0/24', - availability_zone=zone, - tags={ - 'Name': f'{subnet_base_name}-{zone}', - }, - opts=ResourceOptions(parent=self), - ) - ec2.RouteTableAssociation( - f'vpc-route-table-assoc-{zone}', - route_table_id=self.route_table.id, - subnet_id=vpc_subnet.id, - opts=ResourceOptions(parent=self), - ) - self.subnets.append(vpc_subnet) - - # Security Groups - - rds_sg_name = f'{name}-rds-sg' - self.rds_security_group = ec2.SecurityGroup(rds_sg_name, - vpc_id=self.vpc.id, - description='Allow client access.', - tags={ - 'Name': rds_sg_name, - }, - ingress=[ - ec2.SecurityGroupIngressArgs( - cidr_blocks=[ - '172.42.0.0/16', - ], - from_port=5432, - to_port=5432, - protocol='tcp', - description='Allow rds access.', - ), - ], - egress=[ - ec2.SecurityGroupEgressArgs( - protocol='-1', - from_port=0, - to_port=0, - cidr_blocks=[ - '0.0.0.0/0', - ], - ), - ], - opts=ResourceOptions(parent=self), - ) - - redis_sg_name = f'{name}-redis-sg' - self.redis_security_group = ec2.SecurityGroup(redis_sg_name, - vpc_id=self.vpc.id, - description='Allow client access.', - tags={ - 'Name': redis_sg_name, - }, - ingress=[ - ec2.SecurityGroupIngressArgs( - cidr_blocks=[ - '172.42.0.0/16', - ], - from_port=6379, - to_port=6379, - protocol='tcp', - description='Allow redis access.', - ), - ], - egress=[ - ec2.SecurityGroupEgressArgs( - protocol='-1', - from_port=0, - to_port=0, - cidr_blocks=[ - '0.0.0.0/0', - ], - ), - ], - opts=ResourceOptions(parent=self), - ) - - app_sg_name = f'{name}-fe-sg' - self.app_security_group = ec2.SecurityGroup(app_sg_name, - vpc_id=self.vpc.id, - description='Allow all HTTP(s) traffic.', - tags={ - 'Name': app_sg_name, - }, - ingress=[ - ec2.SecurityGroupIngressArgs( - cidr_blocks=[ - '0.0.0.0/0', - ], - from_port=443, - to_port=443, - protocol='tcp', - description='Allow https access.', - ), - ec2.SecurityGroupIngressArgs( - cidr_blocks=[ - '0.0.0.0/0', - ], - from_port=80, - to_port=80, - protocol='tcp', - description='Allow http access.' - ), - ], - egress=[ - ec2.SecurityGroupEgressArgs( - protocol='-1', - from_port=0, - to_port=0, - cidr_blocks=[ - '0.0.0.0/0', - ], - ), - ], - opts=ResourceOptions(parent=self), - ) - - self.register_outputs({}) + def __init__(self, name: str, args: VpcArgs, opts: ResourceOptions = None): + super().__init__("custom:resource:VPC", name, {}, opts) + + vpc_name = name + "-vpc" + self.vpc = ec2.Vpc( + vpc_name, + cidr_block=args.cidr_block, + instance_tenancy=args.instance_tenancy, + enable_dns_hostnames=args.enable_dns_hostnames, + enable_dns_support=args.enable_dns_support, + tags={ + "Name": vpc_name, + }, + opts=ResourceOptions(parent=self), + ) + + # Internet Gateway + + igw_name = name + "-igw" + self.igw = ec2.InternetGateway( + igw_name, + vpc_id=self.vpc.id, + tags={ + "Name": igw_name, + }, + opts=ResourceOptions(parent=self), + ) + + # Route Table + + rt_name = name + "-rt" + self.route_table = ec2.RouteTable( + rt_name, + vpc_id=self.vpc.id, + routes=[ + ec2.RouteTableRouteArgs( + cidr_block="0.0.0.0/0", + gateway_id=self.igw.id, + ) + ], + tags={ + "Name": rt_name, + }, + opts=ResourceOptions(parent=self), + ) + + # Subnets + + all_zones = get_availability_zones() + zone_names = [all_zones.names[0], all_zones.names[1], all_zones.names[2]] + self.subnets = [] + subnet_base_name = f"{name}-subnet" + + for zone in zone_names: + vpc_subnet = ec2.Subnet( + f"{subnet_base_name}-{zone}", + assign_ipv6_address_on_creation=False, + vpc_id=self.vpc.id, + map_public_ip_on_launch=True, + cidr_block=f"172.42.{len(self.subnets)}.0/24", + availability_zone=zone, + tags={ + "Name": f"{subnet_base_name}-{zone}", + }, + opts=ResourceOptions(parent=self), + ) + ec2.RouteTableAssociation( + f"vpc-route-table-assoc-{zone}", + route_table_id=self.route_table.id, + subnet_id=vpc_subnet.id, + opts=ResourceOptions(parent=self), + ) + self.subnets.append(vpc_subnet) + + # Security Groups + + rds_sg_name = f"{name}-rds-sg" + self.rds_security_group = ec2.SecurityGroup( + rds_sg_name, + vpc_id=self.vpc.id, + description="Allow client access.", + tags={ + "Name": rds_sg_name, + }, + ingress=[ + ec2.SecurityGroupIngressArgs( + cidr_blocks=[ + "172.42.0.0/16", + ], + from_port=5432, + to_port=5432, + protocol="tcp", + description="Allow rds access.", + ), + ], + egress=[ + ec2.SecurityGroupEgressArgs( + protocol="-1", + from_port=0, + to_port=0, + cidr_blocks=[ + "0.0.0.0/0", + ], + ), + ], + opts=ResourceOptions(parent=self), + ) + + redis_sg_name = f"{name}-redis-sg" + self.redis_security_group = ec2.SecurityGroup( + redis_sg_name, + vpc_id=self.vpc.id, + description="Allow client access.", + tags={ + "Name": redis_sg_name, + }, + ingress=[ + ec2.SecurityGroupIngressArgs( + cidr_blocks=[ + "172.42.0.0/16", + ], + from_port=6379, + to_port=6379, + protocol="tcp", + description="Allow redis access.", + ), + ], + egress=[ + ec2.SecurityGroupEgressArgs( + protocol="-1", + from_port=0, + to_port=0, + cidr_blocks=[ + "0.0.0.0/0", + ], + ), + ], + opts=ResourceOptions(parent=self), + ) + + app_sg_name = f"{name}-fe-sg" + self.app_security_group = ec2.SecurityGroup( + app_sg_name, + vpc_id=self.vpc.id, + description="Allow all HTTP(s) traffic.", + tags={ + "Name": app_sg_name, + }, + ingress=[ + ec2.SecurityGroupIngressArgs( + cidr_blocks=[ + "0.0.0.0/0", + ], + from_port=443, + to_port=443, + protocol="tcp", + description="Allow https access.", + ), + ec2.SecurityGroupIngressArgs( + cidr_blocks=[ + "0.0.0.0/0", + ], + from_port=80, + to_port=80, + protocol="tcp", + description="Allow http access.", + ), + ], + egress=[ + ec2.SecurityGroupEgressArgs( + protocol="-1", + from_port=0, + to_port=0, + cidr_blocks=[ + "0.0.0.0/0", + ], + ), + ], + opts=ResourceOptions(parent=self), + ) + + # Security group for internal container traffic + be_sg_name = f"{name}-be-sg" + self.be_security_group = ec2.SecurityGroup( + be_sg_name, + vpc_id=self.vpc.id, + description="Allow container traffic.", + tags={ + "Name": be_sg_name, + }, + ingress=[ + ec2.SecurityGroupIngressArgs( + cidr_blocks=[ + "0.0.0.0/0", + ], + from_port=3000, + to_port=3000, + protocol="tcp", + description="Allow access.", + ), + ], + egress=[ + ec2.SecurityGroupEgressArgs( + protocol="-1", + from_port=0, + to_port=0, + cidr_blocks=[ + "0.0.0.0/0", + ], + ), + ], + opts=ResourceOptions(parent=self), + ) + + subnet_ids = [] + for subnet in self.subnets: + subnet_ids.append(subnet.id) + + # Create Load Balancers + + self.front_lb = lb.LoadBalancer( + f"{name}-front-alb", + security_groups=[self.app_security_group.id], + subnets=subnet_ids, + opts=ResourceOptions(parent=self), + ) + + self.back_lb = lb.LoadBalancer( + f"{name}-back-alb", + security_groups=[self.app_security_group.id], + subnets=subnet_ids, + opts=ResourceOptions(parent=self), + ) + + self.register_outputs({})