AWSTemplateFormatVersion: '2010-09-09'

# $Id: cedardb-x86-existing-vpc-cloudformation.yaml,v 1.2 2025/11/11 20:20:41 mgoddard Exp mgoddard $

Description: >
  Deploy CedarDB in Docker on an x86-based Ubuntu EC2 instance, with EBS storage.
  Supports either creating a new VPC (default) or launching into an existing VPC.

Parameters:
  KeyName:
    Type: AWS::EC2::KeyPair::KeyName
    Description: Name of an existing EC2 KeyPair to enable SSH access

  InstanceType:
    Type: String
    Default: m7a.xlarge
    AllowedValues:
      - m7a.large
      - m7a.xlarge
      - m7a.2xlarge
      - m7a.4xlarge
      - m7a.8xlarge
      - r7i.8xlarge
      - r7i.4xlarge
      - c6in.2xlarge
      - m7a.12xlarge
    Description: x86-based EC2 instance type

  DataVolumeSize:
    Type: Number
    Default: 128
    Description: Size (GiB) of the EBS volume for CedarDB data

  CedarDbImage:
    Type: String
    Default: cedardb/cedardb:latest
    Description: Docker image URI for CedarDB

  CedarDbUser:
    Type: String
    Default: postgres
    Description: User / role for accessing CedarDB via SQL client

  CedarDbPasswd:
    Type: String
    Default: mi3Sch3rian3chR0mit3
    Description: Password for accessing CedarDB via SQL client

  CedarDbDb:
    Type: String
    Default: postgres
    Description: Default database

  # ---------- Existing VPC mode (optional) ----------
  ExistingVpcId:
    Type: String
    Default: ''
    Description: >
      OPTIONAL: ID of an existing VPC (e.g. vpc-0123456789abcdef0).
      When set (non-empty), the stack will NOT create networking resources.

  ExistingSubnetId:
    Type: String
    Default: ''
    Description: >
      REQUIRED when ExistingVpcId is set: the SubnetId to launch the instance into (e.g. subnet-0123...).

  ExistingSecurityGroupId:
    Type: String
    Default: ''
    Description: >
      REQUIRED when ExistingVpcId is set: a Security Group ID to attach to the instance (e.g. sg-0123...).

  AmiId:
    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

Conditions:
  UseExistingVpc: !Not [ !Equals [ !Ref ExistingVpcId, '' ] ]
  UseManagedVpc:  !Equals [ !Ref ExistingVpcId, '' ]

Resources:

  # ---------------- Managed VPC mode only ----------------
  VPC:
    Condition: UseManagedVpc
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.20.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-vpc

  IGW:
    Condition: UseManagedVpc
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-igw

  AttachIgw:
    Condition: UseManagedVpc
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref IGW
      VpcId: !Ref VPC

  PublicSubnet:
    Condition: UseManagedVpc
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      CidrBlock: 10.20.0.0/20
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-public-subnet

  Rt:
    Condition: UseManagedVpc
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC

  RtDefault:
    Condition: UseManagedVpc
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref Rt
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref IGW

  RtAssoc1:
    Condition: UseManagedVpc
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref Rt

  CedarDbSecurityGroup:
    Condition: UseManagedVpc
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: CedarDB SG
      VpcId: !Ref VPC
      SecurityGroupIngress:
        # SSH for bootstrap/debug (lock this down as needed)
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0
        # PostgreSQL
        - IpProtocol: tcp
          FromPort: 5432
          ToPort: 5432
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-db-sg

  # ---------------- Shared resources ----------------
  CedarDbEBSVolume:
    Type: AWS::EC2::Volume
    Properties:
      Size: !Ref DataVolumeSize
      AvailabilityZone: !GetAtt CedarDbInstance.AvailabilityZone
      VolumeType: gp3
      Encrypted: true
      Tags:
        - Key: Name
          Value: CedarDBDataVolume

  CedarDbInstance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      KeyName: !Ref KeyName
      # Subnet: existing or created
      SubnetId: !If
        - UseExistingVpc
        - !Ref ExistingSubnetId
        - !Ref PublicSubnet
      # SGs: existing or created
      SecurityGroupIds: !If
        - UseExistingVpc
        - [ !Ref ExistingSecurityGroupId ]
        - [ !Ref CedarDbSecurityGroup ]
      ImageId: !Ref AmiId
      Tags:
        - Key: Name
          Value: CedarDB-Server
      UserData:
        Fn::Base64:
          Fn::Sub: |
            #!/bin/bash -xe

            # Prepare data directory and mount EBS volume
            docker_dir="/var/lib/docker"
            data_dir="$docker_dir/cedardb/data"
            dev="/dev/nvme1n1"
            until lsblk $dev > /dev/null 2>&1 ; do echo "Waiting for $dev ..." ; sleep 1 ; done
            mkfs -t xfs $dev || true
            mkdir -p $docker_dir
            mount $dev $docker_dir
            mkdir -p $data_dir
            chmod 700 $data_dir
            uuid=$( blkid -s UUID -o value $dev )
            echo "UUID=$uuid  $docker_dir  xfs  defaults,nofail  0  2" >> /etc/fstab

            # Install Docker on Ubuntu
            apt-get update -y
            apt-get install -y docker.io
            systemctl enable docker
            systemctl start docker

            # Run CedarDB container
            docker pull ${CedarDbImage}
            docker run -d \
              --privileged --ulimit memlock=1073741824 \
              --name cedardb \
              -v $data_dir:/var/lib/cedardb/data \
              -p 5432:5432 \
              -e CEDAR_PASSWORD=${CedarDbPasswd} \
              -e CEDAR_USER=${CedarDbUser} \
              -e CEDAR_DB=${CedarDbDb} \
              ${CedarDbImage}

            # Write a restart script
            echo "docker start \$( docker ps -a | egrep '\bcedardb\b' | awk '{print \$1}' )" > ~ubuntu/docker_restart_cedardb.sh
            log_file="cloud-init-output.log"
            cp /var/log/$log_file ~ubuntu/
            chown ubuntu: ~ubuntu/*

  CedarDbVolumeAttachment:
    Type: AWS::EC2::VolumeAttachment
    Properties:
      InstanceId: !Ref CedarDbInstance
      VolumeId: !Ref CedarDbEBSVolume
      Device: /dev/xvdb

Outputs:

  # Public IP is meaningful only in the managed VPC (public subnet)
  CedarDbPublicIP:
    Condition: UseManagedVpc
    Description: Public IPv4 address of the CedarDB instance
    Value: !GetAtt CedarDbInstance.PublicIp

  # Private IP is especially useful when launching into an existing VPC
  CedarDbPrivateIP:
    Condition: UseExistingVpc
    Description: Private IPv4 address of the CedarDB instance
    Value: !GetAtt CedarDbInstance.PrivateIp

  CedarDbEndpoint:
    Description: Connection string for CedarDB clients
    Value: !If
      - UseExistingVpc
      - !Sub "postgresql://${CedarDbUser}:${CedarDbPasswd}@${CedarDbInstance.PrivateIp}:5432/${CedarDbDb}?sslmode=require"
      - !Sub "postgresql://${CedarDbUser}:${CedarDbPasswd}@${CedarDbInstance.PublicDnsName}:5432/${CedarDbDb}?sslmode=require"

