Pharmacy Management System

start_ec2_instance.py


    # Run this script to
    # 1. Start an EC2 instance.
    # 2. Get the public IP from the EC2 API to return it in the Lambda response.
    # 3. Add the public IP to the Django settings for ALLOWED_HOSTS
    # 4. Start a Django Gunicorn server on the instance.
    # 5. Set a shutdown timer for the server to stop after 2 hours.

    import boto3 # type: ignore
    import time
    import datetime
    import json
    import os

    REGION = os.environ["REGION"]
    INSTANCE_ID = os.environ["INSTANCE_ID"]
    PORT = os.environ["PORT"]

    ec2 = boto3.client("ec2", region_name=REGION)
    ssm = boto3.client("ssm", region_name=REGION)
    s3 = boto3.client('s3')

    def initialize_shutdown_timer():
        shutdown_dt = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=2) # Will shut down in 2 hours
        shutdown_time = shutdown_dt.isoformat().replace('+00:00', 'Z')

        s3.put_object(
            Bucket='pharmanet-shutdown-bucket',
            Key='shutdown_timer.json',
            Body=json.dumps({'shutdown_at': shutdown_time}),
            ContentType='application/json'
        )

        readable_time = shutdown_dt.strftime('%A, %B %d, %Y at %I:%M %p UTC')
        print(f"Server will shut down on {readable_time}")
        return readable_time

    def get_public_ip(instance_id):
        try:
            reservations = ec2.describe_instances(InstanceIds=[instance_id])['Reservations']
            instance = reservations[0]['Instances'][0]
            return instance['PublicIpAddress']
        except (KeyError, IndexError) as e:
            raise RuntimeError("Failed to retrieve public IP address") from e

    def check_server_status(instance_id):
        # Check if Gunicorn is already running
        check_response = ssm.send_command(
            InstanceIds=[instance_id],
            DocumentName="AWS-RunShellScript",
            Parameters={"commands": ["pgrep -f gunicorn"]},
        )

        command_id = check_response["Command"]["CommandId"]

        # Wait for the command result
        for _ in range(10):  # Retry up to ~10 seconds
            time.sleep(1)
            output = ssm.get_command_invocation(
                CommandId=command_id,
                InstanceId=instance_id,
            )
            if output["Status"] in ("Success", "Failed", "Cancelled", "TimedOut"):
                break

        # Determine if Gunicorn is running
        if output["Status"] == "Success" and output["StandardOutputContent"].strip():
            print("Gunicorn is already running. Skipping restart.")
            return True
        else:
            print("Gunicorn is not running. Will start the server.")
            return False

    def lambda_handler(event, context):
        # Check current EC2 instance state
        response = ec2.describe_instances(InstanceIds=[INSTANCE_ID])
        current_state = response['Reservations'][0]['Instances'][0]['State']['Name']
        print(f"Current EC2 state: {current_state}")

        if current_state != 'running':
            # 1. Start an EC2 instance.
            ec2.start_instances(InstanceIds=[INSTANCE_ID])
            print("Starting EC2 instance...")

            # Wait until the instance is running
            waiter = ec2.get_waiter("instance_running")
            waiter.wait(InstanceIds=[INSTANCE_ID])
            print("EC2 instance is running.")
        else:
            print("EC2 instance already running.")

        # Wait until instance is online in SSM
        for i in range(30):
            response = ssm.describe_instance_information()
            instances = response.get("InstanceInformationList", [])
            if any(inst["InstanceId"] == INSTANCE_ID for inst in instances):
                print("Instance is ready in SSM.")
                break
            print("Waiting for SSM availability...")
            time.sleep(10)
        else:
            raise TimeoutError("SSM not ready after waiting.")

        # 2. Get the public IP from the EC2 API to return it in the Lambda response.
        ip = get_public_ip(INSTANCE_ID)
        print(f"EC2's Public IP: {ip}")

        print("Starting Django server...")
        duckdns_host = "pharmanet.duckdns.org"
        hosts = {duckdns_host, ip}
        allowed_hosts_value = ",".join(hosts)
        commands = [
            "pkill gunicorn || true",
            "cd /home/ubuntu/PharmaNet2 || exit 1",
            ". /home/ubuntu/PharmaNet2/venv/bin/activate",
            "export DJANGO_ENV=production",

            # 3. Add the public IP to the Django settings for ALLOWED_HOSTS
            f"sed -i -E \"s/^ALLOWED_HOSTS=.*/ALLOWED_HOSTS={allowed_hosts_value}/\" .env.production",

            # 4. Start a Django Gunicorn server on the instance.
            (
                "nohup /home/ubuntu/PharmaNet2/venv/bin/gunicorn "
                "--env DJANGO_SETTINGS_MODULE=pharmacy_management.settings.production "
                "--bind unix:/home/ubuntu/PharmaNet2/gunicorn.sock "
                "pharmacy_management.wsgi:application "
                "> /home/ubuntu/PharmaNet2/gunicorn.log 2>&1 &"
            )

        ]
        time.sleep(5)  # Small delay to ensure stability
        response = ssm.send_command(
            InstanceIds=[INSTANCE_ID],
            DocumentName="AWS-RunShellScript",
            Parameters={"commands": commands},
        )
        print("Django server is running.")
        print("Command sent:", response["Command"]["CommandId"])        

        print(f"Server is available at http://{ip}:{PORT}")

        # 5. Set a shutdown timer for the server to stop after 2 hours.    
        print("Shutdown timer updated.")
        shutdown_at_str = initialize_shutdown_timer()

        return {
            "statusCode": 200,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps({
                "status": "EC2 instance started and Django server launching.",
                "public_ip": ip,
                "url": f"http://{ip}:{PORT}",
                "shutdown_at": shutdown_at_str
            })
        }