AWSTemplateFormatVersion: '2010-09-09'
Description: 'PostHog Hobby: Ubuntu 24.04, Automated AMI, Spot/On-Demand ASG'

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Network Configuration
        Parameters:
          - VpcId
          - SubnetId
      - Label:
          default: Instance Configuration
        Parameters:
          - InstanceType
          - PurchaseModel
          - KeyName
          - LatestAmiId
          - PostHogDataVolumeSize
      - Label:
          default: PostHog Settings
        Parameters:
          - PostHogDomain
          - PostHogTag

Parameters:
  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: The ID of the VPC where you want to deploy the PostHog instance.
  SubnetId:
    Type: AWS::EC2::Subnet::Id
    Description: The ID of the specific Subnet within your VPC.
  InstanceType:
    Type: String
    Default: t3.xlarge
    Description: 'The EC2 instance type. Note: PostHog requires a minimum of 16GB of
      RAM (e.g., t3.xlarge, m5.xlarge, or r5.large).'
  KeyName:
    Type: AWS::EC2::KeyPair::KeyName
    Description: The name of an existing EC2 Key Pair to allow SSH access to the instance.
  PurchaseModel:
    Type: String
    Default: spot
    AllowedValues:
      - spot
      - on-demand
    Description: The purchasing model for the instance. 'spot' is significantly
      cheaper but can be interrupted; 'on-demand' is more stable.
  LatestAmiId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id
    Description: Automatically retrieves the latest Ubuntu 24.04 LTS AMI ID for your
      current region via SSM.
  PostHogDataVolumeSize:
    Type: Number
    Default: 100
    MinValue: 30
    Description: Size (in GB) of the persistent EBS volume for PostHog data
  PostHogTag:
    Type: String
    Default: latest
    Description: The specific Docker image tag of PostHog you wish to deploy (e.g.,
      'latest' or a specific version number).
  PostHogDomain:
    Type: String
    Description: The domain name you will use to access PostHog. This is used to
      configure SSL and application routing.

Conditions:
  IsSpot: !Equals
    - !Ref PurchaseModel
    - spot

Resources:
  # Security Group for PostHog traffic
  PostHogSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow HTTP (80), HTTPS (443), and SSH (22) traffic.
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0

  # Elastic IP to maintain a consistent address across instance replacements
  PostHogEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  # IAM Role for Instance Management
  PostHogInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
      Policies:
        - PolicyName: PostHogUtilityPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ec2:AssociateAddress
                  - ec2:DescribeAddresses
                  - autoscaling:CompleteLifecycleAction
                Resource: '*'

  PostHogInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref PostHogInstanceRole

  # Launch Template defining the Instance configuration
  PostHogLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        InstanceType: !Ref InstanceType
        KeyName: !Ref KeyName
        IamInstanceProfile:
          Name: !Ref PostHogInstanceProfile
        ImageId: !Ref LatestAmiId
        SecurityGroupIds:
          - !Ref PostHogSecurityGroup
        InstanceMarketOptions: !If
          - IsSpot
          - MarketType: spot
          - !Ref AWS::NoValue
        BlockDeviceMappings:
          - DeviceName: /dev/sda1
            Ebs:
              VolumeSize: !Ref PostHogDataVolumeSize
              VolumeType: gp3
              DeleteOnTermination: true
        UserData: !Base64
          Fn::Sub: |
            #!/bin/bash
            set -e
            export DEBIAN_FRONTEND=noninteractive
            apt-get update -y
            apt-get -y upgrade
            apt-get install -y docker.io docker-compose-v2 zip
            usermod -aG docker ubuntu
            # Install AWS CLI
            curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "/tmp/awscliv2.zip"
            unzip /tmp/awscliv2.zip -d /tmp
            /tmp/aws/install
            rm -rf /tmp/aws /tmp/awscliv2.zip

            ########################################
            # IMDSv2 Metadata
            ########################################
            TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
            INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)
            REGION=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/region)

            ########################################
            # Associate Elastic IP
            ########################################
            aws ec2 associate-address --instance-id "$INSTANCE_ID" --allocation-id ${PostHogEIP.AllocationId} --region "$REGION" --allow-reassociation

            ########################################
            # posthog script
            ########################################
            mkdir -p /mnt/posthog-data/posthog
            cd /mnt/posthog-data/posthog
            TAG=$PostHogTag
            DOMAIN=$PostHogDomain
            curl -fsSL https://raw.githubusercontent.com/posthog/posthog/HEAD/bin/deploy-hobby | sudo bash -s -- "$TAG" "$DOMAIN"
            ########################################
            # Spot Termination Handler Script
            ########################################
            cat <<'EOF' > /usr/local/bin/spot-termination-handler.sh
            #!/bin/bash
            set -e

            while true; do
            TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
            HTTP_CODE=$(curl -s -o /tmp/spot_action.json -w "%{http_code}" -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/spot/instance-action)

            if [ "$HTTP_CODE" = "200" ]; then
                echo "Spot termination detected"
                cd /mnt/posthog-data/posthog
                docker compose down --timeout 90 || true
                sync
                sleep 10
                exit 0
            fi

            sleep 5
            done

            chmod +x /usr/local/bin/spot-termination-handler.sh

            ########################################
            # systemd Service for Spot Handler
            ########################################
            cat <<'EOF' > /etc/systemd/system/spot-termination-handler.service
            [Unit]
            Description=Spot Instance Termination Handler
            After=docker.service
            Requires=docker.service

            [Service]
            ExecStart=/usr/local/bin/spot-termination-handler.sh
            Restart=always

            [Install]
            WantedBy=multi-user.target
            EOF

            systemctl daemon-reexec
            systemctl daemon-reload
            systemctl enable spot-termination-handler
            systemctl start spot-termination-handler
            ########################################
            # Done
            ########################################
            echo "PostHog deployment complete"

  # Auto Scaling Group to manage the instance lifecycle
  PostHogASG:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      AutoScalingGroupName: !Sub ${AWS::StackName}-ASG
      VPCZoneIdentifier:
        - !Ref SubnetId
      LaunchTemplate:
        LaunchTemplateId: !Ref PostHogLaunchTemplate
        Version: !GetAtt PostHogLaunchTemplate.LatestVersionNumber
      MinSize: '1'
      MaxSize: '1'
      DesiredCapacity: '1'

  # Lifecycle Hook to allow time for graceful shutdown during termination
  PostHogTerminationHook:
    Type: AWS::AutoScaling::LifecycleHook
    Properties:
      AutoScalingGroupName: !Ref PostHogASG
      LifecycleTransition: autoscaling:EC2_INSTANCE_TERMINATING
      HeartbeatTimeout: 300

Outputs:
  PostHogElasticIP:
    Description: Elastic IP address attached to the PostHog instance
    Value: !Ref PostHogEIP
    Export:
      Name: !Sub "${AWS::StackName}-ElasticIP"

  PostHogElasticIPAllocationId:
    Description: Allocation ID of the PostHog Elastic IP
    Value: !GetAtt PostHogEIP.AllocationId
    Export:
      Name: !Sub "${AWS::StackName}-ElasticIPAllocationId"

  PostHogLaunchTemplateId:
    Description: EC2 Launch Template ID used by PostHog
    Value: !Ref PostHogLaunchTemplate
    Export:
      Name: !Sub "${AWS::StackName}-LaunchTemplateId"

  PostHogLaunchTemplateLatestVersion:
    Description: Latest version number of the PostHog Launch Template
    Value: !GetAtt PostHogLaunchTemplate.LatestVersionNumber
    Export:
      Name: !Sub "${AWS::StackName}-LaunchTemplateVersion"

  PostHogAutoScalingGroupName:
    Description: Auto Scaling Group name managing the PostHog instance
    Value: !Ref PostHogASG
    Export:
      Name: !Sub "${AWS::StackName}-ASGName"

  PostHogInstanceRoleName:
    Description: IAM Role name attached to the PostHog EC2 instance
    Value: !Ref PostHogInstanceRole
    Export:
      Name: !Sub "${AWS::StackName}-InstanceRoleName"

  PostHogInstanceProfileName:
    Description: IAM Instance Profile name used by the PostHog instance
    Value: !Ref PostHogInstanceProfile
    Export:
      Name: !Sub "${AWS::StackName}-InstanceProfileName"

  PostHogSecurityGroupId:
    Description: Security Group ID used by the PostHog instance
    Value: !Ref PostHogSecurityGroup
    Export:
      Name: !Sub "${AWS::StackName}-SecurityGroupId"