422 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			422 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
#!/usr/bin/env python3
 | 
						|
 | 
						|
import json
 | 
						|
 | 
						|
from aws_cdk import (
 | 
						|
    App,
 | 
						|
    CfnOutput,
 | 
						|
    CfnParameter,
 | 
						|
    Duration,
 | 
						|
    RemovalPolicy,
 | 
						|
    Stack,
 | 
						|
)
 | 
						|
from aws_cdk import (
 | 
						|
    aws_ec2 as ec2,
 | 
						|
)
 | 
						|
from aws_cdk import (
 | 
						|
    aws_ecs as ecs,
 | 
						|
)
 | 
						|
from aws_cdk import (
 | 
						|
    aws_efs as efs,
 | 
						|
)
 | 
						|
from aws_cdk import (
 | 
						|
    aws_elasticache as elasticache,
 | 
						|
)
 | 
						|
from aws_cdk import (
 | 
						|
    aws_elasticloadbalancingv2 as elbv2,
 | 
						|
)
 | 
						|
from aws_cdk import (
 | 
						|
    aws_rds as rds,
 | 
						|
)
 | 
						|
from aws_cdk import (
 | 
						|
    aws_secretsmanager as secretsmanager,
 | 
						|
)
 | 
						|
from constructs import Construct
 | 
						|
 | 
						|
from authentik import __version__
 | 
						|
 | 
						|
 | 
						|
class AuthentikStack(Stack):
 | 
						|
    def __init__(self, scope: Construct, id: str, **kwargs):
 | 
						|
        super().__init__(scope, id, *kwargs)
 | 
						|
 | 
						|
        ### Inputs
 | 
						|
 | 
						|
        db_instance_type = CfnParameter(
 | 
						|
            self,
 | 
						|
            "DBInstanceType",
 | 
						|
            type="String",
 | 
						|
            default="m5.large",
 | 
						|
            description="RDS PostgreSQL instance type (without the leading db.)",
 | 
						|
        )
 | 
						|
        db_version = CfnParameter(
 | 
						|
            self, "DBVersion", type="String", default="17.1", description="RDS PostgreSQL version"
 | 
						|
        )
 | 
						|
        db_storage = CfnParameter(
 | 
						|
            self,
 | 
						|
            "DBStorage",
 | 
						|
            type="Number",
 | 
						|
            default=10,
 | 
						|
            min_value=10,
 | 
						|
            description="RDS PostgreSQL storage size in GB",
 | 
						|
        )
 | 
						|
 | 
						|
        redis_instance_type = CfnParameter(
 | 
						|
            self,
 | 
						|
            "RedisInstanceType",
 | 
						|
            type="String",
 | 
						|
            default="cache.t4g.medium",
 | 
						|
            description="ElastiCache Redis instance type (with the leading cache.)",
 | 
						|
        )
 | 
						|
        redis_version = CfnParameter(
 | 
						|
            self,
 | 
						|
            "RedisVersion",
 | 
						|
            type="String",
 | 
						|
            default="7.1",
 | 
						|
            description="ElastiCache Redis version",
 | 
						|
        )
 | 
						|
 | 
						|
        authentik_image = CfnParameter(
 | 
						|
            self,
 | 
						|
            "AuthentikImage",
 | 
						|
            type="String",
 | 
						|
            default="ghcr.io/goauthentik/server",
 | 
						|
            description="authentik Docker image",
 | 
						|
        )
 | 
						|
        authentik_version = CfnParameter(
 | 
						|
            self,
 | 
						|
            "AuthentikVersion",
 | 
						|
            type="String",
 | 
						|
            default=__version__,
 | 
						|
            description="authentik Docker image tag",
 | 
						|
        )
 | 
						|
 | 
						|
        server_cpu = CfnParameter(
 | 
						|
            self,
 | 
						|
            "AuthentikServerCPU",
 | 
						|
            type="Number",
 | 
						|
            default=512,
 | 
						|
            description="authentik server CPU units (1024 = 1 vCPU)",
 | 
						|
        )
 | 
						|
        server_memory = CfnParameter(
 | 
						|
            self,
 | 
						|
            "AuthentikServerMemory",
 | 
						|
            type="Number",
 | 
						|
            default=1024,
 | 
						|
            description="authentik server memory in MiB",
 | 
						|
        )
 | 
						|
        server_desired_count = CfnParameter(
 | 
						|
            self,
 | 
						|
            "AuthentikServerDesiredCount",
 | 
						|
            type="Number",
 | 
						|
            default=2,
 | 
						|
            min_value=1,
 | 
						|
            description="Desired number of authentik server tasks",
 | 
						|
        )
 | 
						|
 | 
						|
        worker_cpu = CfnParameter(
 | 
						|
            self,
 | 
						|
            "AuthentikWorkerCPU",
 | 
						|
            type="Number",
 | 
						|
            default=512,
 | 
						|
            description="authentik worker CPU units (1024 = 1 vCPU)",
 | 
						|
        )
 | 
						|
        worker_memory = CfnParameter(
 | 
						|
            self,
 | 
						|
            "AuthentikWorkerMemory",
 | 
						|
            type="Number",
 | 
						|
            default=1024,
 | 
						|
            description="authentik worker memory in MiB",
 | 
						|
        )
 | 
						|
        worker_desired_count = CfnParameter(
 | 
						|
            self,
 | 
						|
            "AuthentikWorkerDesiredCount",
 | 
						|
            type="Number",
 | 
						|
            default=2,
 | 
						|
            min_value=1,
 | 
						|
            description="Desired number of authentik worker tasks",
 | 
						|
        )
 | 
						|
 | 
						|
        certificate_arn = CfnParameter(
 | 
						|
            self,
 | 
						|
            "CertificateARN",
 | 
						|
            type="String",
 | 
						|
            description="ACM certificate ARN for HTTPS access",
 | 
						|
        )
 | 
						|
 | 
						|
        ### Resources
 | 
						|
 | 
						|
        # VPC
 | 
						|
 | 
						|
        vpc = ec2.Vpc(self, "AuthentikVpc", max_azs=2, nat_gateways=1)
 | 
						|
 | 
						|
        # Security Groups
 | 
						|
 | 
						|
        db_security_group = ec2.SecurityGroup(
 | 
						|
            self, "DatabaseSG", vpc=vpc, description="Security Group for authentik RDS PostgreSQL"
 | 
						|
        )
 | 
						|
        redis_security_group = ec2.SecurityGroup(
 | 
						|
            self, "RedisSG", vpc=vpc, description="Security Group for authentik ElastiCache Redis"
 | 
						|
        )
 | 
						|
        authentik_security_group = ec2.SecurityGroup(
 | 
						|
            self, "AuthentikSG", vpc=vpc, description="Security Group for authentik services"
 | 
						|
        )
 | 
						|
        db_security_group.add_ingress_rule(
 | 
						|
            peer=authentik_security_group,
 | 
						|
            connection=ec2.Port.tcp(5432),
 | 
						|
            description="Allow authentik to connect to RDS PostgreSQL",
 | 
						|
        )
 | 
						|
        redis_security_group.add_ingress_rule(
 | 
						|
            peer=authentik_security_group,
 | 
						|
            connection=ec2.Port.tcp(6379),
 | 
						|
            description="Allow authentik to connect to ElastiCache Redis",
 | 
						|
        )
 | 
						|
 | 
						|
        # Generated secrets
 | 
						|
 | 
						|
        db_password = secretsmanager.Secret(
 | 
						|
            self,
 | 
						|
            "DBPassword",
 | 
						|
            generate_secret_string=secretsmanager.SecretStringGenerator(
 | 
						|
                secret_string_template=json.dumps({"username": "authentik"}),
 | 
						|
                generate_string_key="password",
 | 
						|
                password_length=64,
 | 
						|
                exclude_characters='"@/\\',
 | 
						|
            ),
 | 
						|
        )
 | 
						|
        secret_key = secretsmanager.Secret(
 | 
						|
            self,
 | 
						|
            "AuthentikSecretKey",
 | 
						|
            generate_secret_string=secretsmanager.SecretStringGenerator(
 | 
						|
                password_length=64, exclude_characters='"@/\\'
 | 
						|
            ),
 | 
						|
        )
 | 
						|
 | 
						|
        # Database
 | 
						|
 | 
						|
        database = rds.DatabaseInstance(
 | 
						|
            self,
 | 
						|
            "AuthentikDB",
 | 
						|
            engine=rds.DatabaseInstanceEngine.postgres(
 | 
						|
                version=rds.PostgresEngineVersion.of(db_version.value_as_string, ""),
 | 
						|
            ),
 | 
						|
            instance_type=ec2.InstanceType(db_instance_type.value_as_string),
 | 
						|
            vpc=vpc,
 | 
						|
            vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS),
 | 
						|
            allocated_storage=db_storage.value_as_number,
 | 
						|
            security_groups=[db_security_group],
 | 
						|
            database_name="authentik",
 | 
						|
            credentials=rds.Credentials.from_secret(db_password),
 | 
						|
            multi_az=True,
 | 
						|
            removal_policy=RemovalPolicy.SNAPSHOT,
 | 
						|
        )
 | 
						|
 | 
						|
        # Redis
 | 
						|
 | 
						|
        redis_subnet_group = elasticache.CfnSubnetGroup(
 | 
						|
            self,
 | 
						|
            "AuthentikRedisSubnetGroup",
 | 
						|
            subnet_ids=vpc.select_subnets(
 | 
						|
                subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS
 | 
						|
            ).subnet_ids,
 | 
						|
            description="Subnet group for authentik ElastiCache Redis",
 | 
						|
        )
 | 
						|
 | 
						|
        redis = elasticache.CfnReplicationGroup(
 | 
						|
            self,
 | 
						|
            "AuthentikRedis",
 | 
						|
            replication_group_description="Redis cluster for authentik",
 | 
						|
            engine="redis",
 | 
						|
            engine_version=redis_version.value_as_string,
 | 
						|
            cache_node_type=redis_instance_type.value_as_string,
 | 
						|
            num_cache_clusters=2,
 | 
						|
            automatic_failover_enabled=True,
 | 
						|
            security_group_ids=[redis_security_group.security_group_id],
 | 
						|
            cache_subnet_group_name=redis_subnet_group.ref,
 | 
						|
        )
 | 
						|
 | 
						|
        # Storage
 | 
						|
 | 
						|
        media_fs = efs.FileSystem(
 | 
						|
            self,
 | 
						|
            "AuthentikMediaEFS",
 | 
						|
            vpc=vpc,
 | 
						|
            removal_policy=RemovalPolicy.RETAIN,
 | 
						|
            security_group=ec2.SecurityGroup(
 | 
						|
                self,
 | 
						|
                "AuthentikMediaEFSSecurityGroup",
 | 
						|
                vpc=vpc,
 | 
						|
                description="Security group for authentik media EFS",
 | 
						|
                allow_all_outbound=True,
 | 
						|
            ),
 | 
						|
            encrypted=True,
 | 
						|
            performance_mode=efs.PerformanceMode.GENERAL_PURPOSE,
 | 
						|
            throughput_mode=efs.ThroughputMode.BURSTING,
 | 
						|
        )
 | 
						|
        media_fs.connections.allow_default_port_from(authentik_security_group)
 | 
						|
 | 
						|
        media_access_point = media_fs.add_access_point(
 | 
						|
            "AuthentikMediaAccessPoint",
 | 
						|
            path="/media",
 | 
						|
            create_acl=efs.Acl(owner_uid="1000", owner_gid="1000", permissions="755"),
 | 
						|
            posix_user=efs.PosixUser(uid="1000", gid="1000"),
 | 
						|
        )
 | 
						|
 | 
						|
        # ECS Cluster
 | 
						|
 | 
						|
        cluster = ecs.Cluster(self, "AuthentikCluster", vpc=vpc)
 | 
						|
 | 
						|
        environment = {
 | 
						|
            "AUTHENTIK_POSTGRESQL__HOST": database.instance_endpoint.hostname,
 | 
						|
            "AUTHENTIK_POSTGRESQL__USER": "authentik",
 | 
						|
            "AUTHENTIK_REDIS__HOST": redis.attr_primary_end_point_address,
 | 
						|
        }
 | 
						|
 | 
						|
        secrets = {
 | 
						|
            "AUTHENTIK_POSTGRESQL__PASSWORD": ecs.Secret.from_secrets_manager(
 | 
						|
                db_password, field="password"
 | 
						|
            ),
 | 
						|
            "AUTHENTIK_SECRET_KEY": ecs.Secret.from_secrets_manager(secret_key),
 | 
						|
        }
 | 
						|
 | 
						|
        server_task = ecs.FargateTaskDefinition(
 | 
						|
            self,
 | 
						|
            "AuthentikServerTask",
 | 
						|
            cpu=server_cpu.value_as_number,
 | 
						|
            memory_limit_mib=server_memory.value_as_number,
 | 
						|
        )
 | 
						|
        server_task.add_volume(
 | 
						|
            name="media",
 | 
						|
            efs_volume_configuration=ecs.EfsVolumeConfiguration(
 | 
						|
                file_system_id=media_fs.file_system_id,
 | 
						|
                transit_encryption="ENABLED",
 | 
						|
                authorization_config=ecs.AuthorizationConfig(
 | 
						|
                    access_point_id=media_access_point.access_point_id,
 | 
						|
                    iam="ENABLED",
 | 
						|
                ),
 | 
						|
            ),
 | 
						|
        )
 | 
						|
        server_container = server_task.add_container(
 | 
						|
            "AuthentikServerContainer",
 | 
						|
            image=ecs.ContainerImage.from_registry(
 | 
						|
                f"{authentik_image.value_as_string}:{authentik_version.value_as_string}"
 | 
						|
            ),
 | 
						|
            command=["server"],
 | 
						|
            environment=environment,
 | 
						|
            secrets=secrets,
 | 
						|
            logging=ecs.LogDriver.aws_logs(stream_prefix="authentik-server"),
 | 
						|
            enable_restart_policy=True,
 | 
						|
            health_check=ecs.HealthCheck(
 | 
						|
                command=["CMD", "ak", "healthcheck"],
 | 
						|
                interval=Duration.seconds(30),
 | 
						|
                retries=3,
 | 
						|
                start_period=Duration.seconds(60),
 | 
						|
                timeout=Duration.seconds(30),
 | 
						|
            ),
 | 
						|
        )
 | 
						|
        server_container.add_port_mappings(ecs.PortMapping(container_port=9000))
 | 
						|
        server_container.add_mount_points(
 | 
						|
            ecs.MountPoint(container_path="/media", source_volume="media", read_only=False)
 | 
						|
        )
 | 
						|
        server_service = ecs.FargateService(
 | 
						|
            self,
 | 
						|
            "AuthentikServerService",
 | 
						|
            cluster=cluster,
 | 
						|
            task_definition=server_task,
 | 
						|
            desired_count=server_desired_count.value_as_number,
 | 
						|
            security_groups=[authentik_security_group],
 | 
						|
            vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS),
 | 
						|
            enable_execute_command=True,
 | 
						|
        )
 | 
						|
 | 
						|
        worker_task = ecs.FargateTaskDefinition(
 | 
						|
            self,
 | 
						|
            "AuthentikWorkerTask",
 | 
						|
            cpu=worker_cpu.value_as_number,
 | 
						|
            memory_limit_mib=worker_memory.value_as_number,
 | 
						|
        )
 | 
						|
        worker_task.add_volume(
 | 
						|
            name="media",
 | 
						|
            efs_volume_configuration=ecs.EfsVolumeConfiguration(
 | 
						|
                file_system_id=media_fs.file_system_id,
 | 
						|
                transit_encryption="ENABLED",
 | 
						|
                authorization_config=ecs.AuthorizationConfig(
 | 
						|
                    access_point_id=media_access_point.access_point_id,
 | 
						|
                    iam="ENABLED",
 | 
						|
                ),
 | 
						|
            ),
 | 
						|
        )
 | 
						|
        worker_container = worker_task.add_container(
 | 
						|
            "AuthentikWorkerContainer",
 | 
						|
            image=ecs.ContainerImage.from_registry(
 | 
						|
                f"{authentik_image.value_as_string}:{authentik_version.value_as_string}"
 | 
						|
            ),
 | 
						|
            command=["worker"],
 | 
						|
            environment=environment,
 | 
						|
            secrets=secrets,
 | 
						|
            logging=ecs.LogDriver.aws_logs(stream_prefix="authentik-worker"),
 | 
						|
            enable_restart_policy=True,
 | 
						|
            health_check=ecs.HealthCheck(
 | 
						|
                command=["CMD", "ak", "healthcheck"],
 | 
						|
                interval=Duration.seconds(30),
 | 
						|
                retries=3,
 | 
						|
                start_period=Duration.seconds(60),
 | 
						|
                timeout=Duration.seconds(30),
 | 
						|
            ),
 | 
						|
        )
 | 
						|
        worker_container.add_mount_points(
 | 
						|
            ecs.MountPoint(container_path="/media", source_volume="media", read_only=False)
 | 
						|
        )
 | 
						|
        worker_service = ecs.FargateService(  # noqa: F841
 | 
						|
            self,
 | 
						|
            "AuthentikWorkerService",
 | 
						|
            cluster=cluster,
 | 
						|
            task_definition=worker_task,
 | 
						|
            desired_count=worker_desired_count.value_as_number,
 | 
						|
            security_groups=[authentik_security_group],
 | 
						|
            vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS),
 | 
						|
            enable_execute_command=True,
 | 
						|
        )
 | 
						|
 | 
						|
        # Load balancer
 | 
						|
 | 
						|
        lb = elbv2.ApplicationLoadBalancer(
 | 
						|
            self,
 | 
						|
            "AuthentikALB",
 | 
						|
            vpc=vpc,
 | 
						|
            internet_facing=True,
 | 
						|
        )
 | 
						|
        https_redirect = lb.add_listener(  # noqa: F841
 | 
						|
            "AuthentikHttpListener",
 | 
						|
            port=80,
 | 
						|
            default_action=elbv2.ListenerAction.redirect(permanent=True, protocol="HTTPS"),
 | 
						|
        )
 | 
						|
        listener = lb.add_listener(
 | 
						|
            "AuthentikHttpsListener",
 | 
						|
            port=443,
 | 
						|
            certificates=[
 | 
						|
                elbv2.ListenerCertificate(certificate_arn=certificate_arn.value_as_string)
 | 
						|
            ],
 | 
						|
        )
 | 
						|
        target_group = listener.add_targets(  # noqa: F841
 | 
						|
            "AuthentikServerTarget",
 | 
						|
            protocol=elbv2.ApplicationProtocol.HTTP,
 | 
						|
            port=9000,
 | 
						|
            targets=[server_service],
 | 
						|
            health_check=elbv2.HealthCheck(
 | 
						|
                path="/-/health/live/",
 | 
						|
                healthy_http_codes="200",
 | 
						|
            ),
 | 
						|
        )
 | 
						|
 | 
						|
        CfnOutput(
 | 
						|
            self,
 | 
						|
            "LoadBalancerDNS",
 | 
						|
            value=lb.load_balancer_dns_name,
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
app = App()
 | 
						|
AuthentikStack(app, "AuthentikStack")
 | 
						|
app.synth()
 |