Advertisement
sidjha57

AWS Infra

Apr 9th, 2025 (edited)
31
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 24.58 KB | None | 0 0
  1. from aws_cdk import (
  2.     Stack,
  3.     RemovalPolicy,
  4.     Duration,
  5.     CfnOutput,
  6.     Fn,
  7.     aws_ec2 as ec2,
  8.     aws_iam as iam,
  9.     aws_rds as rds,
  10.     aws_s3 as s3,
  11.     aws_ecr as ecr,
  12.     aws_autoscaling as autoscaling,
  13.     aws_elasticloadbalancingv2 as elbv2,
  14.     aws_secretsmanager as secretsmanager,
  15.     aws_logs as logs,
  16.     # aws_grafana as grafana, # Grafana not used in the final setup, commented out
  17.     aws_cloudfront as cloudfront,
  18.     aws_cloudfront_origins as origins,
  19.     aws_ssm as ssm
  20. )
  21. from constructs import Construct
  22. import os
  23. import json
  24.  
  25. class CalorieMitraStack(Stack):
  26.     def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
  27.         super().__init__(scope, construct_id, **kwargs)
  28.  
  29.         # --- Get context variables ---
  30.         app_name = self.node.try_get_context("app_name") or "CalorieMitra"
  31.         app_port = self.node.try_get_context("app_port") or 8000
  32.         container_port = 8000 # Set based on your Dockerfile EXPOSE
  33.         docker_image_tag_context = self.node.try_get_context("docker_image_tag") or "latest"
  34.         app_secret_arn = self.node.try_get_context("app_secret_arn")
  35.         acm_certificate_arn = self.node.try_get_context("acm_certificate_arn")
  36.  
  37.         if not app_secret_arn:
  38.              raise ValueError("Missing required secret ARN in CDK context (app_secret_arn)")
  39.  
  40.         ssm_image_tag_parameter_name = f"/app/{app_name.lower()}/image-tag"
  41.  
  42.         # --- Networking ---
  43.         vpc = ec2.Vpc(
  44.             self, f"{app_name}Vpc",
  45.             max_azs=2, cidr="10.10.0.0/16", nat_gateways=1,
  46.             subnet_configuration=[
  47.                 ec2.SubnetConfiguration(name="public", subnet_type=ec2.SubnetType.PUBLIC, cidr_mask=24),
  48.                 ec2.SubnetConfiguration(name="private", subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS, cidr_mask=24),
  49.             ]
  50.         )
  51.  
  52.         # --- Security Groups ---
  53.         alb_sg = ec2.SecurityGroup(self, f"{app_name}AlbSg", vpc=vpc, description="ALB SG", allow_all_outbound=True)
  54.         alb_sg.add_ingress_rule(ec2.Peer.any_ipv4(), ec2.Port.tcp(80), "Allow HTTP")
  55.         if acm_certificate_arn:
  56.              alb_sg.add_ingress_rule(ec2.Peer.any_ipv4(), ec2.Port.tcp(443), "Allow HTTPS")
  57.  
  58.         ec2_sg = ec2.SecurityGroup(self, f"{app_name}Ec2Sg", vpc=vpc, description="EC2 SG", allow_all_outbound=True)
  59.         ec2_sg.add_ingress_rule(alb_sg, ec2.Port.tcp(app_port), f"Allow traffic from ALB on port {app_port}")
  60.         ec2_sg.add_ingress_rule(ec2.Peer.any_ipv4(), ec2.Port.tcp(443), "Allow HTTPS for SSM") # For SSM Agent
  61.  
  62.         rds_sg = ec2.SecurityGroup(self, f"{app_name}RdsSg", vpc=vpc, description="RDS SG", allow_all_outbound=True)
  63.         rds_sg.add_ingress_rule(ec2_sg, ec2.Port.tcp(5432), "Allow traffic from EC2")
  64.  
  65.         # --- ECR Repository ---
  66.         ecr_repository = ecr.Repository(
  67.             self, f"{app_name}EcrRepo", repository_name=f"{app_name.lower()}-app-repo",
  68.             removal_policy=RemovalPolicy.RETAIN, image_tag_mutability=ecr.TagMutability.MUTABLE
  69.         )
  70.  
  71.         # --- S3 Bucket ---
  72.         meal_images_bucket = s3.Bucket(
  73.             self, f"{app_name}MealImagesBucket", block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
  74.             encryption=s3.BucketEncryption.S3_MANAGED, removal_policy=RemovalPolicy.RETAIN, auto_delete_objects=False,
  75.             lifecycle_rules=[s3.LifecycleRule(id="MoveToIA", enabled=True, transitions=[
  76.                 s3.Transition(storage_class=s3.StorageClass.INFREQUENT_ACCESS, transition_after=Duration.days(90))])]
  77.         )
  78.  
  79.         # --- Cloudfront Distribution with OAC ---
  80.         cfn_oac = cloudfront.CfnOriginAccessControl(self, f"{app_name}OAC",
  81.             origin_access_control_config=cloudfront.CfnOriginAccessControl.OriginAccessControlConfigProperty(
  82.                 name=f"{app_name}-OAC-{self.region}", origin_access_control_origin_type="s3",
  83.                 signing_behavior="always", signing_protocol="sigv4", description="OAC for S3 Bucket"))
  84.  
  85.         cloudfront_distribution = cloudfront.Distribution(
  86.             self, f"{app_name}CloudfrontDistribution",
  87.             default_behavior=cloudfront.BehaviorOptions(
  88.                 origin=origins.S3Origin(meal_images_bucket),
  89.                 viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
  90.                 cache_policy=cloudfront.CachePolicy.CACHING_OPTIMIZED),
  91.             comment=f"{app_name} CDN")
  92.  
  93.         cfn_distribution = cloudfront_distribution.node.default_child
  94.         cfn_distribution.add_property_override("DistributionConfig.Origins.0.OriginAccessControlId", cfn_oac.attr_id)
  95.         cfn_distribution.add_property_override("DistributionConfig.Origins.0.S3OriginConfig.OriginAccessIdentity", "")
  96.  
  97.         meal_images_bucket.add_to_resource_policy(
  98.             iam.PolicyStatement(actions=["s3:GetObject"], resources=[meal_images_bucket.arn_for_objects("*")],
  99.                 principals=[iam.ServicePrincipal("cloudfront.amazonaws.com")],
  100.                 conditions={"StringEquals": { "AWS:SourceArn": f"arn:aws:cloudfront::{self.account}:distribution/{cloudfront_distribution.distribution_id}" }}))
  101.  
  102.         # --- RDS PostgreSQL Instance ---
  103.         db_instance_name = "defaultdb"
  104.         db_instance = rds.DatabaseInstance(
  105.             self, f"{app_name}RdsInstance", engine=rds.DatabaseInstanceEngine.postgres(version=rds.PostgresEngineVersion.VER_15),
  106.             instance_type=ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE4_GRAVITON, ec2.InstanceSize.MEDIUM),
  107.             vpc=vpc, vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS),
  108.             security_groups=[rds_sg], allocated_storage=50, storage_type=rds.StorageType.GP3,
  109.             database_name=db_instance_name, multi_az=False, backup_retention=Duration.days(7), delete_automated_backups=False,
  110.             deletion_protection=True, copy_tags_to_snapshot=True, enable_performance_insights=True,
  111.             removal_policy=RemovalPolicy.RETAIN)
  112.  
  113.         # --- SSM Parameter for Docker Image Tag ---
  114.         ssm_image_tag_parameter = ssm.StringParameter(
  115.             self, f"{app_name}ImageTagParam", parameter_name=ssm_image_tag_parameter_name,
  116.             string_value=docker_image_tag_context, description=f"Docker image tag for {app_name}",
  117.             tier=ssm.ParameterTier.STANDARD)
  118.  
  119.         # --- IAM Role for EC2 Instances ---
  120.         ec2_role = iam.Role(
  121.             self, f"{app_name}Ec2Role",
  122.             assumed_by=iam.ServicePrincipal("ec2.amazonaws.com"),
  123.             managed_policies=[
  124.                 iam.ManagedPolicy.from_aws_managed_policy_name("AmazonSSMManagedInstanceCore"),
  125.                 iam.ManagedPolicy.from_aws_managed_policy_name("CloudWatchAgentServerPolicy"),
  126.                 iam.ManagedPolicy.from_aws_managed_policy_name("AmazonEC2ContainerRegistryReadOnly")
  127.             ]
  128.         )
  129.         ec2_role.add_to_policy(iam.PolicyStatement(
  130.             actions=[
  131.                 "logs:CreateLogGroup",
  132.                 "logs:CreateLogStream",
  133.                 "logs:PutLogEvents",
  134.                 "logs:DescribeLogStreams",
  135.                 "logs:DescribeLogGroups"
  136.             ],
  137.             resources=[
  138.                 f"arn:aws:logs:{self.region}:{self.account}:log-group:/app/{app_name.lower()}/*",
  139.                 f"arn:aws:logs:{self.region}:{self.account}:log-group:/app/{app_name.lower()}/*:*",
  140.                 f"arn:aws:logs:{self.region}:{self.account}:log-group:/var/log/user-data*",
  141.                 f"arn:aws:logs:{self.region}:{self.account}:log-group:/var/log/user-data*:*"
  142.             ]
  143.         ))
  144.         ec2_role.add_to_policy(iam.PolicyStatement(
  145.             actions=["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"],
  146.             resources=[f"arn:aws:ssm:{self.region}:{self.account}:parameter{ssm_image_tag_parameter_name}"]
  147.         ))
  148.         ec2_role.add_to_policy(iam.PolicyStatement(
  149.             actions=["autoscaling:SetInstanceHealth", "autoscaling:DescribeAutoScalingInstances"],
  150.             resources=["*"]
  151.         ))
  152.  
  153.         ecr_repository.grant_pull(ec2_role)
  154.         meal_images_bucket.grant_read_write(ec2_role)
  155.         app_secret = secretsmanager.Secret.from_secret_complete_arn(self, "AppSecret", app_secret_arn)
  156.         app_secret.grant_read(ec2_role)
  157.         ssm_image_tag_parameter.grant_read(ec2_role)
  158.  
  159.         # --- CloudWatch Log Group for Application Logs ---
  160.         app_log_group = logs.LogGroup(
  161.             self, f"{app_name}AppLogGroup",
  162.             log_group_name=f"/app/{app_name.lower()}/docker-logs",
  163.             retention=logs.RetentionDays.TWO_WEEKS,
  164.             removal_policy=RemovalPolicy.DESTROY
  165.         )
  166.         app_log_group.grant_write(ec2_role)
  167.  
  168.         # --- User Data Script (Streamlined for Custom AMI) ---
  169.         # Pass the log group name to the user data script
  170.         app_log_group_name = app_log_group.log_group_name
  171.         # Define your custom AMI ID here
  172.         custom_ami_id = "ami-04c5ae52f42a72334" # <-- YOUR CUSTOM AMI ID
  173.  
  174.         user_data_script = f"""#!/bin/bash -xeu
  175. # -x: print commands, -e: exit on error, -u: treat unset variables as error
  176. set -o pipefail
  177.  
  178. # Redirect stdout/stderr to a log file and to the system logger
  179. exec > >(tee /var/log/user-data.log | logger -t user-data -s 2>/dev/console) 2>&1
  180.  
  181. # --- Basic Logging Function (Output to stderr) ---
  182. log() {{
  183.    # Use >&2 to redirect echo output to standard error
  184.    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >&2
  185. }}
  186.  
  187. log "--- UserData Script Start (Using Custom AMI: {custom_ami_id}) ---"
  188.  
  189. # --- Essential Variables (Injected by CDK f-string) ---
  190. APP_PORT="{app_port}"
  191. CONTAINER_PORT="{container_port}"
  192. AWS_REGION="{self.region}"
  193. AWS_ACCOUNT_ID="{self.account}"
  194. APP_SECRET_ARN="{app_secret_arn}"
  195. SSM_PARAM_NAME="{ssm_image_tag_parameter_name}"
  196. ECR_REPOSITORY_NAME="{ecr_repository.repository_name}"
  197. DB_ENDPOINT_ADDRESS="{db_instance.db_instance_endpoint_address}"
  198. DB_ENDPOINT_PORT="{db_instance.db_instance_endpoint_port}"
  199. DB_NAME="{db_instance_name}"
  200. S3_BUCKET="{meal_images_bucket.bucket_name}"
  201. CLOUDFRONT_DOMAIN="{cloudfront_distribution.distribution_domain_name}"
  202. APP_LOG_GROUP_NAME="{app_log_group_name}" # Use the variable passed from CDK
  203. # -----------------------------------------------------
  204.  
  205. # --- Error Handling Setup ---
  206. handle_error() {{
  207.    local error_msg="$1"
  208.    local exit_code="${{2:-1}}"
  209.    log "ERROR: $error_msg (Exit Code: $exit_code)"
  210.    TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60" || true)
  211.    INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id || curl -s http://169.254.169.254/latest/meta-data/instance-id || echo "unknown")
  212.    if [[ "$INSTANCE_ID" != "unknown" ]] && command -v aws &> /dev/null; then
  213.        log "Attempting to signal ASG Unhealthy for instance $INSTANCE_ID in region $AWS_REGION..."
  214.        aws autoscaling set-instance-health --instance-id "$INSTANCE_ID" --health-status Unhealthy --region "$AWS_REGION" || \\
  215.            log "Warning: Failed to set instance health via AWS CLI. Check IAM permissions (autoscaling:SetInstanceHealth)."
  216.    else
  217.        log "Warning: Could not get Instance ID or AWS CLI not available. Cannot signal ASG."
  218.    fi
  219.    exit $exit_code
  220. }}
  221. trap 'handle_error "Script failed on line $LINENO" $?' ERR
  222.  
  223. # --- Verify Prerequisites (Should be installed in AMI) ---
  224. log "Verifying prerequisite tools (should be installed in AMI)..."
  225. command -v docker >/dev/null || handle_error "Docker command not found. AMI build might have failed."
  226. command -v aws >/dev/null || handle_error "AWS CLI command not found. AMI build might have failed."
  227. command -v jq >/dev/null || handle_error "jq command not found. AMI build might have failed."
  228. command -v git >/dev/null || handle_error "git command not found. AMI build might have failed."
  229. command -v nc >/dev/null || handle_error "netcat (nc) command not found. AMI build might have failed."
  230. log "Prerequisite tools verified."
  231.  
  232. # --- Configure and Start Docker ---
  233. log "Configuring Docker log driver (awslogs)..."
  234. sudo mkdir -p /etc/docker # Ensure directory exists
  235. sudo tee /etc/docker/daemon.json > /dev/null <<EOF
  236. {{
  237.  "log-driver": "awslogs",
  238.  "log-opts": {{
  239.    "awslogs-region": "$AWS_REGION",
  240.    "awslogs-group": "$APP_LOG_GROUP_NAME",
  241.    "awslogs-create-group": "true",
  242.    "tag": "{{{{.Name}}}}/{{{{.ID}}}}"
  243.  }}
  244. }}
  245. EOF
  246. log "Docker daemon.json configured for awslogs."
  247.  
  248. log "Ensuring Docker service is enabled and restarting it..."
  249. # Docker service should be enabled by the AMI build process
  250. # Restart is needed to apply the daemon.json configuration
  251. sudo systemctl restart docker
  252. sleep 5 # Give docker a moment to restart
  253. sudo systemctl is-active --quiet docker || handle_error "Docker service failed to restart after configuration. Check /etc/docker/daemon.json syntax and system logs (journalctl -u docker.service)."
  254. sudo usermod -a -G docker ubuntu || log "Warning: Failed to add 'ubuntu' user to docker group."
  255. log "Docker service configured and running."
  256.  
  257.  
  258. # --- Fetch Secrets Function (No internal success log before echo) ---
  259. function get_secret {{
  260.    local secret_arn="$1"
  261.    local retries=3
  262.    local count=0
  263.    local delay=5
  264.    local secret_json=""
  265.  
  266.    while [ $count -lt $retries ]; do
  267.        secret_json=$(aws secretsmanager get-secret-value --secret-id "$secret_arn" --region "$AWS_REGION" --query SecretString --output text 2>/var/log/awscli_secret_error.log)
  268.        local aws_exit_code=$?
  269.        if [ $aws_exit_code -eq 0 ] && [ -n "$secret_json" ] && [ "$secret_json" != "null" ]; then
  270.            if echo "$secret_json" | jq -e . > /dev/null; then
  271.                rm -f /var/log/awscli_secret_error.log
  272.                echo "$secret_json" # Echo ONLY the JSON to stdout
  273.                return 0
  274.            else
  275.                log "Warning: Fetched secret for $secret_arn is not valid JSON (attempt $((count+1))/$retries). Retrying..."
  276.            fi
  277.        else
  278.            log "Warning: Failed to fetch secret $secret_arn (AWS CLI exit code: $aws_exit_code, attempt $((count+1))/$retries). Check /var/log/awscli_secret_error.log. Retrying..."
  279.        fi
  280.        count=$((count+1))
  281.        sleep $delay
  282.    done
  283.    log "ERROR: Failed to fetch valid secret JSON for $secret_arn after $retries attempts."
  284.    return 1
  285. }}
  286.  
  287. log "Fetching App secrets from ARN: $APP_SECRET_ARN"
  288. APP_SECRET_JSON=$(get_secret "$APP_SECRET_ARN") || handle_error "Failed to fetch App secrets from $APP_SECRET_ARN after retries" $?
  289. log "Successfully fetched and validated secret JSON from: $APP_SECRET_ARN"
  290.  
  291. # Parse secrets using jq
  292. log "Parsing essential secrets..."
  293. GEMINI_API_KEY=$(echo "$APP_SECRET_JSON" | jq -r '.GEMINI_API_KEY // ""')
  294. ENDPOINT=$(echo "$APP_SECRET_JSON" | jq -r '.ENDPOINT // ""')
  295. FIREBASE_PROJECT_ID=$(echo "$APP_SECRET_JSON" | jq -r '.FIREBASE_PROJECT_ID // ""')
  296. FIREBASE_CLIENT_EMAIL=$(echo "$APP_SECRET_JSON" | jq -r '.FIREBASE_CLIENT_EMAIL // ""')
  297. FIREBASE_PRIVATE_KEY=$(echo "$APP_SECRET_JSON" | jq -r '.FIREBASE_PRIVATE_KEY // ""')
  298. DB_USERNAME=$(echo "$APP_SECRET_JSON" | jq -r '.DB_USERNAME // ""')
  299. DB_PASSWORD=$(echo "$APP_SECRET_JSON" | jq -r '.DB_PASSWORD // ""')
  300.  
  301. if [ -z "$DB_USERNAME" ] || [ -z "$DB_PASSWORD" ]; then
  302.    log "Warning: DB_USERNAME or DB_PASSWORD missing from secrets."
  303. fi
  304. log "Secrets parsed."
  305.  
  306. # --- Fetch Docker Image Tag from SSM ---
  307. log "Fetching image tag from SSM parameter: $SSM_PARAM_NAME"
  308. TARGET_IMAGE_TAG=$(aws ssm get-parameter --name "$SSM_PARAM_NAME" --query "Parameter.Value" --output text --region "$AWS_REGION" 2>/var/log/awscli_ssm_error.log)
  309. ssm_exit_code=$?
  310. if [ $ssm_exit_code -ne 0 ] || [ -z "$TARGET_IMAGE_TAG" ] || [ "$TARGET_IMAGE_TAG" == "None" ]; then
  311.    log "Warning: Failed to get tag from SSM $SSM_PARAM_NAME. Using 'latest' as fallback."
  312.    TARGET_IMAGE_TAG="latest"
  313.    rm -f /var/log/awscli_ssm_error.log
  314. fi
  315. log "Using image tag: $TARGET_IMAGE_TAG"
  316.  
  317. # --- ECR Login ---
  318. ECR_LOGIN_SERVER="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"
  319. log "Logging into ECR: $ECR_LOGIN_SERVER"
  320. aws ecr get-login-password --region "$AWS_REGION" | sudo docker login --username AWS --password-stdin "$ECR_LOGIN_SERVER" || \\
  321.    handle_error "Failed to log into ECR server $ECR_LOGIN_SERVER"
  322. log "ECR login successful."
  323.  
  324. # --- Prepare and Run Container ---
  325. ECR_REPO_URI="$ECR_LOGIN_SERVER/$ECR_REPOSITORY_NAME"
  326. IMAGE_URI="$ECR_REPO_URI:$TARGET_IMAGE_TAG"
  327.  
  328. log "Stopping and removing existing container named 'myapp' (if any)..."
  329. sudo docker stop myapp 2>/dev/null || true
  330. sudo docker rm -f myapp 2>/dev/null || true
  331.  
  332. log "Pulling image: $IMAGE_URI"
  333. PULL_SUCCESS="false"
  334. for i in {{1..3}}; do
  335.    if sudo docker pull "$IMAGE_URI"; then
  336.        log "Image pull successful (attempt $i)."
  337.        PULL_SUCCESS="true"
  338.        break
  339.    else
  340.        log "Warning: Pull attempt $i failed for $IMAGE_URI."
  341.        if [ $i -lt 3 ]; then
  342.           log "Retrying pull in 5 seconds..."
  343.           sleep 5;
  344.        else
  345.           log "Error: Final pull attempt failed.";
  346.        fi
  347.    fi
  348. done
  349. if [ "$PULL_SUCCESS" != "true" ]; then
  350.    handle_error "FATAL: Failed to pull image $IMAGE_URI after 3 attempts."
  351. fi
  352. log "Image pull successful."
  353.  
  354. log "Running container 'myapp' from image $IMAGE_URI..."
  355. # Build DATABASE_URL
  356. DB_URL_CREDENTIALS=""
  357. if [ -n "$DB_USERNAME" ]; then
  358.    DB_URL_CREDENTIALS="$DB_USERNAME:$DB_PASSWORD@"
  359. fi
  360. DATABASE_URL="postgresql://$DB_URL_CREDENTIALS$DB_ENDPOINT_ADDRESS:$DB_ENDPOINT_PORT/$DB_NAME"
  361.  
  362. # Build docker run options array
  363. declare -a docker_env_opts
  364. docker_env_opts+=( "-e" "DATABASE_HOST=$DB_ENDPOINT_ADDRESS" )
  365. docker_env_opts+=( "-e" "DATABASE_PORT=$DB_ENDPOINT_PORT" )
  366. docker_env_opts+=( "-e" "DATABASE_NAME=$DB_NAME" )
  367. docker_env_opts+=( "-e" "DATABASE_URL=$DATABASE_URL" )
  368. docker_env_opts+=( "-e" "S3_BUCKET_NAME=$S3_BUCKET" )
  369. docker_env_opts+=( "-e" "CLOUDFRONT_DOMAIN_NAME=$CLOUDFRONT_DOMAIN" )
  370. docker_env_opts+=( "-e" "AWS_REGION=$AWS_REGION" )
  371. docker_env_opts+=( "-e" "NODE_ENV=production" )
  372. docker_env_opts+=( "-e" "PORT=$CONTAINER_PORT" )
  373.  
  374. [ -n "$DB_USERNAME" ] && docker_env_opts+=( "-e" "DATABASE_USERNAME=$DB_USERNAME" )
  375. [ -n "$DB_PASSWORD" ] && docker_env_opts+=( "-e" "DATABASE_PASSWORD=$DB_PASSWORD" )
  376. [ -n "$GEMINI_API_KEY" ] && docker_env_opts+=( "-e" "GEMINI_API_KEY=$GEMINI_API_KEY" )
  377. [ -n "$ENDPOINT" ] && docker_env_opts+=( "-e" "ENDPOINT=$ENDPOINT" )
  378. [ -n "$FIREBASE_PROJECT_ID" ] && docker_env_opts+=( "-e" "FIREBASE_PROJECT_ID=$FIREBASE_PROJECT_ID" )
  379. [ -n "$FIREBASE_CLIENT_EMAIL" ] && docker_env_opts+=( "-e" "FIREBASE_CLIENT_EMAIL=$FIREBASE_CLIENT_EMAIL" )
  380. [ -n "$FIREBASE_PRIVATE_KEY" ] && docker_env_opts+=( "-e" "FIREBASE_PRIVATE_KEY=$FIREBASE_PRIVATE_KEY" )
  381.  
  382. # Execute docker run
  383. # No need to specify --log-driver here since it's set in daemon.json
  384. sudo docker run -d --name myapp \\
  385.    --restart always \\
  386.    -p "$APP_PORT":"$CONTAINER_PORT" \\
  387.    "${{docker_env_opts[@]}}" \\
  388.    "$IMAGE_URI" || handle_error "Failed to execute 'docker run' command."
  389.  
  390. # Brief pause and check if container is running
  391. log "Waiting 10 seconds for container to stabilize..."
  392. sleep 10
  393. if ! sudo docker ps -q --filter name=^/myapp$; then
  394.    log "--- Docker Status After Failed Start ---"
  395.    sudo docker ps -a || log "Warning: Failed to run 'docker ps -a'."
  396.    log "--- Last 100 lines of 'myapp' logs (if exists - may not show if driver failed) ---"
  397.    # Note: docker logs might not work correctly if the awslogs driver itself had issues initializing
  398.    sudo docker logs --tail 100 myapp || log "Could not get logs for 'myapp' via docker logs command."
  399.    handle_error "Container 'myapp' is not running shortly after start. Check CloudWatch Logs ($APP_LOG_GROUP_NAME) and system logs (/var/log/syslog or journalctl -u docker.service)."
  400. fi
  401.  
  402. log "Container 'myapp' started successfully. Logs should appear in CloudWatch Group: $APP_LOG_GROUP_NAME"
  403. log "--- UserData Script Finished Successfully ---"
  404. exit 0
  405. """
  406.  
  407.         # --- User Data Object ---
  408.         user_data = ec2.UserData.custom(user_data_script)
  409.  
  410.         # --- Use the Custom AMI ---
  411.         # Replace the Ubuntu AMI lookup with generic_linux using your specific AMI ID
  412.         custom_calorie_mitra_ami = ec2.MachineImage.generic_linux({
  413.             # Map the region where the stack is deployed to your AMI ID
  414.             self.region: custom_ami_id
  415.         })
  416.         # Note: Ensure your custom AMI (ami-04c5ae52f42a72334) exists in the region(s)
  417.         # where you deploy this stack. If deploying to multiple regions, you'll need
  418.         # the corresponding AMI ID for each region in this map.
  419.  
  420.         # --- EC2 Launch Template (Using Custom AMI) ---
  421.         launch_template = ec2.LaunchTemplate(
  422.             self, f"{app_name}LaunchTemplate",
  423.             instance_type=ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE4_GRAVITON, ec2.InstanceSize.MEDIUM), # Ensure AMI architecture matches instance type
  424.             machine_image=custom_calorie_mitra_ami, # <-- USE YOUR CUSTOM AMI HERE
  425.             role=ec2_role,
  426.             security_group=ec2_sg,
  427.             user_data=user_data, # Assign the updated UserData object here
  428.             block_devices=[ec2.BlockDevice(
  429.                 device_name="/dev/sda1", # Verify root device name for your custom AMI if needed
  430.                 volume=ec2.BlockDeviceVolume.ebs(
  431.                     volume_size=20,
  432.                     volume_type=ec2.EbsDeviceVolumeType.GP3,
  433.                     delete_on_termination=True
  434.                 )
  435.             )]
  436.         )
  437.  
  438.         # --- Define Auto Scaling Group ---
  439.         asg = autoscaling.AutoScalingGroup(
  440.             self, f"{app_name}Asg",
  441.             vpc=vpc,
  442.             launch_template=launch_template,
  443.             vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS),
  444.             min_capacity=1, max_capacity=4, desired_capacity=1,
  445.             health_check=autoscaling.HealthCheck.elb(grace=Duration.minutes(10))
  446.         )
  447.         asg.scale_on_cpu_utilization(f"{app_name}CpuScaling", target_utilization_percent=80)
  448.  
  449.         # --- Application Load Balancer ---
  450.         alb = elbv2.ApplicationLoadBalancer(self, f"{app_name}Alb", vpc=vpc, internet_facing=True, security_group=alb_sg)
  451.  
  452.         # --- Target Group ---
  453.         target_group = elbv2.ApplicationTargetGroup(
  454.             self, f"{app_name}TargetGroup",
  455.             vpc=vpc,
  456.             port=app_port,
  457.             protocol=elbv2.ApplicationProtocol.HTTP,
  458.             target_type=elbv2.TargetType.INSTANCE,
  459.             targets=[asg],
  460.             health_check=elbv2.HealthCheck(
  461.                 path="/healthz",
  462.                 port=str(app_port),
  463.                 protocol=elbv2.Protocol.HTTP,
  464.                 interval=Duration.seconds(60),
  465.                 timeout=Duration.seconds(30),
  466.                 healthy_threshold_count=2,
  467.                 unhealthy_threshold_count=10,
  468.                 healthy_http_codes="200-299"
  469.             ),
  470.             deregistration_delay=Duration.seconds(60)
  471.         )
  472.  
  473.         # --- ALB Listener ---
  474.         if acm_certificate_arn:
  475.             certificate = elbv2.ListenerCertificate.from_arn(acm_certificate_arn)
  476.             https_listener = alb.add_listener(f"{app_name}HttpsListener", port=443,
  477.                 protocol=elbv2.ApplicationProtocol.HTTPS, certificates=[certificate],
  478.                 default_target_groups=[target_group])
  479.             alb.add_redirect(source_protocol=elbv2.ApplicationProtocol.HTTP, source_port=80,
  480.                              target_protocol=elbv2.ApplicationProtocol.HTTPS, target_port=443)
  481.         else:
  482.             http_listener = alb.add_listener(f"{app_name}HttpListener", port=80,
  483.                 protocol=elbv2.ApplicationProtocol.HTTP, default_target_groups=[target_group])
  484.  
  485.         # --- Outputs ---
  486.         CfnOutput(self, "Region", value=self.region)
  487.         CfnOutput(self, "VpcId", value=vpc.vpc_id)
  488.         CfnOutput(self, "EcrRepositoryUri", value=ecr_repository.repository_uri)
  489.         CfnOutput(self, "MealImagesBucketName", value=meal_images_bucket.bucket_name)
  490.         CfnOutput(self, "CloudfrontDomainName", value=cloudfront_distribution.distribution_domain_name)
  491.         CfnOutput(self, "LoadBalancerDns", value=alb.load_balancer_dns_name)
  492.         CfnOutput(self, "RdsEndpointAddress", value=db_instance.db_instance_endpoint_address)
  493.         CfnOutput(self, "RdsEndpointPort", value=db_instance.db_instance_endpoint_port)
  494.         CfnOutput(self, "EC2RoleArn", value=ec2_role.role_arn)
  495.         CfnOutput(self, "AppLogGroupName", value=app_log_group.log_group_name)
  496.         CfnOutput(self, "SsmImageTagParameterName", value=ssm_image_tag_parameter.parameter_name)
  497.         # Updated output to show the hardcoded AMI ID being used
  498.         CfnOutput(self, "SelectedAmiId", value=custom_ami_id)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement