Files
Stefano 34c446bcfa
All checks were successful
AWS Deploy on Push / build (push) Successful in 10m26s
Make sure DB credentials are available when running userdata (fix-2)
2025-05-29 17:52:53 -05:00

308 lines
11 KiB
Python

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
db_host_param = ssm.StringParameter(
self,
"DBHostParam",
parameter_name="/iptv-manager/DB_HOST",
string_value=db.db_instance_endpoint_address,
)
db_name_param = ssm.StringParameter(
self,
"DBNameParam",
parameter_name="/iptv-manager/DB_NAME",
string_value="iptv_manager",
)
db_user_param = ssm.StringParameter(
self,
"DBUserParam",
parameter_name="/iptv-manager/DB_USER",
string_value=db.secret.secret_value_from_json("username").to_string(),
)
db_pass_param = 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)
instance.node.add_dependency(db_host_param)
instance.node.add_dependency(db_name_param)
instance.node.add_dependency(db_user_param)
instance.node.add_dependency(db_pass_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",
)