import os from aws_cdk import CfnOutput, Duration, RemovalPolicy, Stack from aws_cdk import aws_cognito as cognito from aws_cdk import aws_ec2 as ec2 from aws_cdk import aws_iam as iam from aws_cdk import aws_rds as rds from aws_cdk import aws_ssm as ssm from constructs import Construct class IptvManagerStack(Stack): def __init__( self, scope: Construct, construct_id: str, freedns_user: str, freedns_password: str, domain_name: str, ssh_public_key: str, repo_url: str, letsencrypt_email: str, **kwargs, ) -> None: super().__init__(scope, construct_id, **kwargs) # Create VPC vpc = ec2.Vpc( self, "IptvManagerVPC", max_azs=2, # Need at least 2 AZs for RDS subnet group nat_gateways=0, # No NAT Gateway to stay in free tier subnet_configuration=[ ec2.SubnetConfiguration( name="public", subnet_type=ec2.SubnetType.PUBLIC, cidr_mask=24 ), ec2.SubnetConfiguration( name="private", subnet_type=ec2.SubnetType.PRIVATE_ISOLATED, cidr_mask=24, ), ], ) # Security Group security_group = ec2.SecurityGroup( self, "IptvManagerSG", vpc=vpc, allow_all_outbound=True ) security_group.add_ingress_rule( ec2.Peer.any_ipv4(), ec2.Port.tcp(443), "Allow HTTPS traffic" ) security_group.add_ingress_rule( ec2.Peer.any_ipv4(), ec2.Port.tcp(80), "Allow HTTP traffic" ) security_group.add_ingress_rule( ec2.Peer.any_ipv4(), ec2.Port.tcp(22), "Allow SSH traffic" ) # Allow PostgreSQL port for tunneling restricted to developer IP security_group.add_ingress_rule( ec2.Peer.ipv4("47.189.88.48/32"), # Developer IP ec2.Port.tcp(5432), "Allow PostgreSQL traffic for tunneling", ) # Key pair for IPTV Manager instance key_pair = ec2.KeyPair( self, "IptvManagerKeyPair", key_pair_name="iptv-manager-key", public_key_material=ssh_public_key, ) # Create IAM role for EC2 role = iam.Role( self, "IptvManagerRole", assumed_by=iam.ServicePrincipal("ec2.amazonaws.com"), ) # Add SSM managed policy role.add_managed_policy( iam.ManagedPolicy.from_aws_managed_policy_name( "AmazonSSMManagedInstanceCore" ) ) # Add EC2 describe permissions role.add_to_policy( iam.PolicyStatement(actions=["ec2:DescribeInstances"], resources=["*"]) ) # Add SSM SendCommand permissions role.add_to_policy( iam.PolicyStatement( actions=["ssm:SendCommand"], resources=[ # Allow on all EC2 instances f"arn:aws:ec2:{self.region}:{self.account}:instance/*", # Required for the RunShellScript document f"arn:aws:ssm:{self.region}:{self.account}:document/AWS-RunShellScript", ], ) ) # Add Cognito permissions to instance role role.add_managed_policy( iam.ManagedPolicy.from_aws_managed_policy_name("AmazonCognitoReadOnly") ) # Add Cognito User Pool user_pool = cognito.UserPool( self, "IptvManagerUserPool", user_pool_name="iptv-manager-users", self_sign_up_enabled=False, # Only admins can create users password_policy=cognito.PasswordPolicy( min_length=8, require_lowercase=True, require_digits=True, require_symbols=True, require_uppercase=True, ), account_recovery=cognito.AccountRecovery.EMAIL_ONLY, removal_policy=RemovalPolicy.DESTROY, ) # Add App Client with the correct callback URL client = user_pool.add_client( "IptvManagerClient", access_token_validity=Duration.minutes(60), id_token_validity=Duration.minutes(60), refresh_token_validity=Duration.days(1), auth_flows=cognito.AuthFlow(user_password=True), o_auth=cognito.OAuthSettings( flows=cognito.OAuthFlows(implicit_code_grant=True) ), prevent_user_existence_errors=True, generate_secret=True, enable_token_revocation=True, ) # Add domain for hosted UI domain = user_pool.add_domain( "IptvManagerDomain", cognito_domain=cognito.CognitoDomainOptions(domain_prefix="iptv-manager"), ) # Read the userdata script with proper path resolution script_dir = os.path.dirname(os.path.abspath(__file__)) userdata_path = os.path.join(script_dir, "userdata.sh") userdata_file = open(userdata_path, "rb").read() # Creates a userdata object for Linux hosts userdata = ec2.UserData.for_linux() # Add environment variables for acme.sh from parameters userdata.add_commands( f'export FREEDNS_User="{freedns_user}"', f'export FREEDNS_Password="{freedns_password}"', f'export DOMAIN_NAME="{domain_name}"', f'export REPO_URL="{repo_url}"', f'export LETSENCRYPT_EMAIL="{letsencrypt_email}"', ) # Adds one or more commands to the userdata object. userdata.add_commands( ( f'echo "COGNITO_USER_POOL_ID=' f'{user_pool.user_pool_id}" >> /etc/environment' ), ( f'echo "COGNITO_CLIENT_ID=' f'{client.user_pool_client_id}" >> /etc/environment' ), ( f'echo "COGNITO_CLIENT_SECRET=' f'{client.user_pool_client_secret.to_string()}" >> /etc/environment' ), f'echo "DOMAIN_NAME={domain_name}" >> /etc/environment', ) userdata.add_commands(str(userdata_file, "utf-8")) # Create RDS Security Group rds_sg = ec2.SecurityGroup( self, "RdsSecurityGroup", vpc=vpc, description="Security group for RDS PostgreSQL", ) rds_sg.add_ingress_rule( security_group, ec2.Port.tcp(5432), "Allow PostgreSQL access from EC2 instance", ) # Create RDS PostgreSQL instance (free tier compatible - db.t3.micro) db = rds.DatabaseInstance( self, "IptvManagerDB", engine=rds.DatabaseInstanceEngine.postgres( version=rds.PostgresEngineVersion.VER_13 ), instance_type=ec2.InstanceType.of( ec2.InstanceClass.T3, ec2.InstanceSize.MICRO ), vpc=vpc, vpc_subnets=ec2.SubnetSelection( subnet_type=ec2.SubnetType.PRIVATE_ISOLATED ), security_groups=[rds_sg], allocated_storage=10, max_allocated_storage=10, database_name="iptv_manager", removal_policy=RemovalPolicy.DESTROY, deletion_protection=False, publicly_accessible=False, # Avoid public IPv4 charges ) # Add RDS permissions to instance role role.add_managed_policy( iam.ManagedPolicy.from_aws_managed_policy_name("AmazonRDSFullAccess") ) # Store DB connection info in SSM Parameter Store ssm.StringParameter( self, "DBHostParam", parameter_name="/iptv-manager/DB_HOST", string_value=db.db_instance_endpoint_address, ) ssm.StringParameter( self, "DBNameParam", parameter_name="/iptv-manager/DB_NAME", string_value="iptv_manager", ) ssm.StringParameter( self, "DBUserParam", parameter_name="/iptv-manager/DB_USER", string_value=db.secret.secret_value_from_json("username").to_string(), ) ssm.StringParameter( self, "DBPassParam", parameter_name="/iptv-manager/DB_PASSWORD", string_value=db.secret.secret_value_from_json("password").to_string(), ) # Add SSM read permissions to instance role role.add_managed_policy( iam.ManagedPolicy.from_aws_managed_policy_name("AmazonSSMReadOnlyAccess") ) # EC2 Instance (created after all dependencies are ready) instance = ec2.Instance( self, "IptvManagerInstance", vpc=vpc, vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC), instance_type=ec2.InstanceType.of( ec2.InstanceClass.T2, ec2.InstanceSize.MICRO ), machine_image=ec2.AmazonLinuxImage( generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023 ), security_group=security_group, key_pair=key_pair, role=role, # Option: 1: Enable auto-assign public IP (free tier compatible) associate_public_ip_address=True, ) # Ensure instance depends on SSM parameters being created instance.node.add_dependency(db) ssm_params = [ ssm.StringParameter.from_string_parameter_name( self, "DBHostParamRef", "/iptv-manager/DB_HOST" ), ssm.StringParameter.from_string_parameter_name( self, "DBNameParamRef", "/iptv-manager/DB_NAME" ), ssm.StringParameter.from_string_parameter_name( self, "DBUserParamRef", "/iptv-manager/DB_USER" ), ssm.StringParameter.from_string_parameter_name( self, "DBPassParamRef", "/iptv-manager/DB_PASSWORD" ), ] for param in ssm_params: instance.node.add_dependency(param) # Option: 2: Create Elastic IP (not free tier compatible) # eip = ec2.CfnEIP( # self, "IptvManagerEIP", # domain="vpc", # instance_id=instance.instance_id # ) # Update instance with userdata instance.add_user_data(userdata.render()) # Outputs CfnOutput(self, "DBEndpoint", value=db.db_instance_endpoint_address) # Option: 1: Use EC2 instance public IP (free tier compatible) CfnOutput(self, "InstancePublicIP", value=instance.instance_public_ip) # Option: 2: Use EIP (not free tier compatible) # CfnOutput(self, "InstancePublicIP", value=eip.attr_public_ip) CfnOutput(self, "UserPoolId", value=user_pool.user_pool_id) CfnOutput(self, "UserPoolClientId", value=client.user_pool_client_id) CfnOutput( self, "CognitoDomainUrl", value=f"https://{domain.domain_name}.auth.{self.region}.amazoncognito.com", )