ava-labs/avalanche-ops

Move dev-machine Cloudformation template to the same repo

gyuho opened this issue · 3 comments

gyuho commented

Currently it's in my private repo...

gyuho commented

This is the current template

---
# ref. https://github.com/gyuho/dev-machine/blob/main/aws-dev-machine/src/cfn-templates/asg_ubuntu.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Development machine"

# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html
Parameters:
  Id:
    Type: String
    Description: Unique identifier, prefix for all resources created below.

  KmsKeyArn:
    Type: String
    Description: KMS CMK ARN that de/encrypts resources.

  AadTag:
    Type: String
    Description: AAD tag for envelope encryption with KMS.

  S3BucketName:
    Type: String
    Description: S3 bucket name.

  Ec2KeyPairName:
    Type: AWS::EC2::KeyPair::KeyName
    Description: EC2 SSH key name

  InstanceProfileArn:
    Type: String
    Description: Instance profile ARN

  PublicSubnetIds:
    Type: List<AWS::EC2::Subnet::Id>
    Description: The public subnet IDs where node instances are to be created.

  SecurityGroupId:
    Type: AWS::EC2::SecurityGroup::Id
    Description: EC2 security group ID

  ArchType:
    Type: String
    Default: "amd64"
    Description: The name of the CPU/GPU architecture. Used for names only.

  OsType:
    Type: String
    AllowedValues: ["ubuntu20.04", "ubuntu22.04"]
    Default: "ubuntu20.04"
    Description: The name of the OS distribution and kind. Used for Rust binary download links.

  ImageId:
    Type: String
    Default: ""
    Description: (Optional) Custom image ID. This value overrides any AWS Systems Manager Parameter Store value specified above.

  # Make sure to use the same OS version as binary builder host
  # otherwise, it can fail with:
  # error while loading shared libraries: libssl.so.3: cannot open shared object file: No such file or directory
  # https://ubuntu.com/server/docs/cloud-images/amazon-ec2
  # https://aws.amazon.com/blogs/compute/query-for-the-latest-amazon-linux-ami-ids-using-aws-systems-manager-parameter-store/
  ImageIdSsmParameter:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/canonical/ubuntu/server/20.04/stable/current/amd64/hvm/ebs-gp2/ami-id
    Description: AWS Systems Manager Parameter Store parameter of the AMI ID.

  SshKeyEmail:
    Type: String
    Default: "none"
    Description: The email to link to an SSH key. If empty, SSH key is not created.

  # use https://github.com/gyuho/aws-manager/blob/main/src/ec2/mod.rs for better defaults
  InstanceTypes:
    Type: CommaDelimitedList
    Default: c6a.4xlarge,m6a.4xlarge,m5.4xlarge,c5.4xlarge
    # Default: c6g.4xlarge,m6g.4xlarge,r6g.4xlarge,t4g.2xlarge
    Description: EC2 instance types

  InstanceTypesCount:
    Type: Number
    Default: 4
    MinValue: 1
    MaxValue: 10
    Description: The number of instance types

  # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-blockdevicemapping-ebs.html#cfn-ec2-launchtemplate-blockdevicemapping-ebs-volumetype
  VolumeType:
    Type: String
    Default: gp3
    Description: Volume type.

  # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-blockdevicemapping-ebs.html#cfn-ec2-launchtemplate-blockdevicemapping-ebs-volumesize
  VolumeSize:
    Type: Number
    Default: 1024
    MinValue: 40
    MaxValue: 1024
    Description: Size of the root disk for the EC2 instances, in GiB.

  # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-blockdevicemapping-ebs.html#cfn-ec2-launchtemplate-blockdevicemapping-ebs-iops
  VolumeIops:
    Type: Number
    Default: 3000
    Description: The number of I/O operations per second (IOPS).

  # only for gp3
  # https://aws.amazon.com/ebs/volume-types/
  # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-blockdevicemapping-ebs.html#cfn-ec2-launchtemplate-blockdevicemapping-ebs-throughput
  # "1000" does not work -- "InvalidParameterValue - Throughput (MiBps) to iops ratio of 0.333333 is too high; maximum is 0.250000 MiBps per iops."
  VolumeThroughput:
    Type: Number
    Default: 500
    Description: The throughput to provision for a gp3 volume, with a maximum of 1,000 MiB/s.

  # wait longer by default, because EC2 tag population takes awhile...
  ProvisionerInitialWaitRandomSeconds:
    Type: Number
    Default: 30
    MinValue: 0
    MaxValue: 500
    Description: Only set non-zero if multiple instances may compete for the same EBS volume in the same zone.

  IpMode:
    Type: String
    AllowedValues: ["ephemeral", "elastic"]
    Default: "elastic"
    Description: Set "elastic" to allocate Elastic IP.

  InstanceMode:
    Type: String
    AllowedValues: ["spot", "on-demand"]
    Default: "spot"
    Description: Set to "spot" to run spot instance.

  AsgLaunchTemplateId:
    Type: String
    Default: ""
    Description: (Optional) Non-empty to reuse.

  AsgLaunchTemplateVersion:
    Type: String
    Default: ""
    Description: (Optional) Non-empty to reuse.

  AsgName:
    Type: String
    Description: Unique identifier for this Asg.

  AsgMinInstancesInService:
    Type: Number
    Description: Minimum instances in service for update.
    Default: 1
    MinValue: 1
    MaxValue: 1000

  AsgMinSize:
    Type: Number
    Description: Minimum size auto scaling group
    Default: 0
    MinValue: 0
    MaxValue: 3

  AsgMaxSize:
    Type: Number
    Description: Maximum size auto scaling group
    Default: 2
    MinValue: 1
    MaxValue: 3

  AsgDesiredCapacity:
    Type: Number
    Description: Desired size auto scaling group
    Default: 1
    MinValue: 0
    MaxValue: 3

  OnDemandPercentageAboveBaseCapacity:
    Type: Number
    Default: 100
    MinValue: 0
    MaxValue: 100
    Description: 0 for Spot only. 100 for On-Demand only.

Conditions:
  HasImageId:
    Fn::Not:
      - Fn::Equals:
          - Ref: ImageId
          - ""

  Has2InstanceTypes:
    Fn::Or:
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 2
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 3
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 4
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 5
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 6
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 7
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 8
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 9
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 10

  Has3InstanceTypes:
    Fn::Or:
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 3
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 4
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 5
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 6
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 7
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 8
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 9
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 10

  Has4InstanceTypes:
    Fn::Or:
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 4
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 5
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 6
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 7
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 8
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 9
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 10

  Has5InstanceTypes:
    Fn::Or:
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 5
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 6
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 7
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 8
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 9
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 10

  Has6InstanceTypes:
    Fn::Or:
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 6
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 7
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 8
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 9
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 10

  Has7InstanceTypes:
    Fn::Or:
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 7
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 8
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 9
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 10

  Has8InstanceTypes:
    Fn::Or:
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 8
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 9
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 10

  Has9InstanceTypes:
    Fn::Or:
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 9
      - Fn::Equals:
          - Ref: InstanceTypesCount
          - 10

  Has10InstanceTypes:
    Fn::Equals:
      - Ref: InstanceTypesCount
      - 10

Resources:
  # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata.html
  AsgLaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: !Join ["-", [!Ref Id, !Ref ArchType]]
      LaunchTemplateData:
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-iaminstanceprofile.html
        IamInstanceProfile:
          Arn: !Ref InstanceProfileArn
        ImageId:
          Fn::If:
            - HasImageId
            - !Ref ImageId
            - !Ref ImageIdSsmParameter
        KeyName: !Ref Ec2KeyPairName

        # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/device_naming.html
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-blockdevicemapping.html
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-blockdevicemapping-ebs.html#cfn-ec2-launchtemplate-blockdevicemapping-ebs-volumesize
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-blockdevicemapping-ebs.html
        # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/RootDeviceStorage.html
        BlockDeviceMappings:
          # mounted to "/dev/root"
          - DeviceName: "/dev/sda1"
            Ebs:
              VolumeType: gp3
              VolumeSize: 200

        Monitoring:
          Enabled: true

        # need this for public DNS + SSH access
        NetworkInterfaces:
          - AssociatePublicIpAddress: true
            DeleteOnTermination: true
            DeviceIndex: 0
            Groups:
              - !Ref SecurityGroupId
        TagSpecifications:
          - ResourceType: instance
            Tags:
              - { Key: Name, Value: !Sub "${Id}-${ArchType}" }

        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata.html#cfn-ec2-launchtemplate-launchtemplatedata-userdata
        # https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/QuickStartEC2Instance.html
        # /var/log/cloud-init-output.log
        # takes about 3-minute
        UserData:
          Fn::Base64:
            Fn::Sub: |
              #!/bin/bash
              set -xeu
              export DEBIAN_FRONTEND=noninteractive

              # install aws cli
              while [ 1 ]; do
                sudo rm -f /tmp/awscli-exe-linux-$(uname -m).zip || true;
                sudo apt-get install -yq wget unzip && wget --quiet --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 --tries=70 --directory-prefix=/tmp/ --continue https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip
                if [ $? = 0 ]; then break; fi; # check return value, break if successful (0)
                sleep 2s;
              done;
              unzip /tmp/awscli-exe-linux-$(uname -m).zip
              sudo ./aws/install
              /usr/local/bin/aws --version

              # TODO: move these to bash script
              pwd
              # running as "root"
              whoami
              sudo lscpu

              while [ 1 ]; do
                sudo apt-get update -y && sudo apt-get upgrade -y \
                && sudo apt-get install -yq \
                build-essential tmux zsh git \
                jq curl wget \
                unzip zip gzip tar \
                libssl-dev \
                python3-pip \
                pkg-config \
                protobuf-compiler \
                linux-headers-$(uname -r)
                if [ $? = 0 ]; then break; fi; # check return value, break if successful (0)
                sleep 2s;
              done;

              gcc --version

              cat<<EOF >> /home/ubuntu/.profile

              export GOPATH=/home/ubuntu/go
              export PATH=/usr/local/go/bin:/home/ubuntu/go/bin:/home/ubuntu/.cargo/bin:$PATH
              . /opt/rust/env

              EOF
              cat<<EOF >> /home/ubuntu/.bashrc

              export GOPATH=/home/ubuntu/go
              export PATH=/usr/local/go/bin:/home/ubuntu/go/bin:/home/ubuntu/.cargo/bin:$PATH
              . /opt/rust/env

              EOF

              # install go
              sudo rm -rf /usr/local/go
              GO_VERSION=1.20.4
              sudo curl -s https://storage.googleapis.com/golang/go$GO_VERSION.linux-$(dpkg --print-architecture).tar.gz | sudo tar -v -C /usr/local/ -xz
              /usr/local/go/bin/go version

              # install rust
              export RUSTUP_HOME=/opt/rust
              export CARGO_HOME=/opt/rust
              curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | bash -s -- -y --no-modify-path --default-toolchain stable --profile default
              sudo -H -u ubuntu bash -c 'source /opt/rust/env && rustup default stable'

              # install docker
              while [ 1 ]; do
                sudo apt-get install -yq \
                ca-certificates gnupg lsb-release
                if [ $? = 0 ]; then break; fi; # check return value, break if successful (0)
                sleep 2s;
              done;
              while [ 1 ]; do
                sudo rm -f /usr/share/keyrings/docker-archive-keyring.gpg && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
                if [ $? = 0 ]; then break; fi; # check return value, break if successful (0)
                sleep 2s;
              done;
              echo \
                "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
                $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
              while [ 1 ]; do
                sudo apt-get update -y && sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
                if [ $? = 0 ]; then break; fi; # check return value, break if successful (0)
                sleep 2s;
              done;
              sudo systemctl enable docker
              sudo usermod -aG docker ubuntu
              sudo newgrp docker
              sudo systemctl start docker.service
              sudo systemctl enable --now docker
              sudo docker ps
              sudo docker version

              # http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html
              # "x86_64" (mac, linux x86), "arm64" (M1), "aarch64" (graviton)
              # https://aws.amazon.com/blogs/developer/aws-cli-v2-now-available-for-linux-arm/
              export LINUX_ARCH_TYPE=$(uname -m)
              echo LINUX_ARCH_TYPE: ${!LINUX_ARCH_TYPE}

              # install ssm agent
              # https://docs.aws.amazon.com/systems-manager/latest/userguide/agent-install-ubuntu.html
              sudo snap install amazon-ssm-agent --classic
              sudo systemctl enable snap.amazon-ssm-agent.amazon-ssm-agent.service
              sudo systemctl restart snap.amazon-ssm-agent.amazon-ssm-agent.service
              mkdir -p /etc/systemd/system/snap.amazon-ssm-agent.amazon-ssm-agent.service.d
              cat > /tmp/amazon-ssm-agent-10-restart-always.conf <<EOF
              [Service]
              Restart=always
              RestartSec=60s
              EOF
              sudo mv /tmp/amazon-ssm-agent-10-restart-always.conf /etc/systemd/system/snap.amazon-ssm-agent.amazon-ssm-agent.service.d/10-restart-always.conf
              sudo systemctl start --no-block snap.amazon-ssm-agent.amazon-ssm-agent.service

              # install cw agent
              # https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/QuickStartEC2Instance.html
              while [ 1 ]; do
                sudo rm -f /tmp/amazon-cloudwatch-agent.deb || true;
                wget --quiet --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 --tries=70 --directory-prefix=/tmp/ --continue https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/$(dpkg --print-architecture)/latest/amazon-cloudwatch-agent.deb
                if [ $? = 0 ]; then break; fi; # check return value, break if successful (0)
                sleep 2s;
              done;
              while [ 1 ]; do
                echo "installing amazon-cloudwatch-agent"
                sudo dpkg -i -E /tmp/amazon-cloudwatch-agent.deb
                if [ $? = 0 ]; then break; fi; # check return value, break if successful (0)
                sleep 2s;
              done;

              # install aws-volume-manager
              # https://github.com/ava-labs/volume-manager/releases
              while [ 1 ]; do
                sudo rm -f /tmp/aws-volume-provisioner.${!LINUX_ARCH_TYPE}-${OsType}-linux-gnu || true;
                wget --quiet --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 --tries=70 --directory-prefix=/tmp/ --continue https://github.com/ava-labs/volume-manager/releases/download/latest/aws-volume-provisioner.${!LINUX_ARCH_TYPE}-${OsType}-linux-gnu
                if [ $? = 0 ]; then break; fi; # check return value, break if successful (0)
                sleep 2s;
              done;
              chmod +x /tmp/aws-volume-provisioner.${!LINUX_ARCH_TYPE}-${OsType}-linux-gnu
              /tmp/aws-volume-provisioner.${!LINUX_ARCH_TYPE}-${OsType}-linux-gnu --version
              /tmp/aws-volume-provisioner.${!LINUX_ARCH_TYPE}-${OsType}-linux-gnu \
              --log-level=info \
              --region ${AWS::Region} \
              --initial-wait-random-seconds=${ProvisionerInitialWaitRandomSeconds} \
              --id-tag-key=Id \
              --id-tag-value=${Id} \
              --kind-tag-key=Kind \
              --kind-tag-value=aws-volume-provisioner \
              --ec2-tag-asg-name-key=ASG_NAME \
              --asg-tag-key=autoscaling:groupName \
              --volume-type=${VolumeType} \
              --volume-size=${VolumeSize} \
              --volume-iops=${VolumeIops} \
              --volume-throughput=${VolumeThroughput} \
              --ebs-device-name=/dev/xvdb \
              --block-device-name=/dev/nvme1n1 \
              --filesystem-name=ext4 \
              --mount-directory-path=/data

              # set permissions
              sudo chown -R $(whoami) /data || true
              sudo chown -R ubuntu /data || true

              # install aws-ip-manager
              # https://github.com/ava-labs/ip-manager/releases
              if [[ ${IpMode} == "elastic" ]]; then
                sudo rm -f /tmp/aws-ip-provisioner.${!LINUX_ARCH_TYPE}-${OsType}-linux-gnu
                while [ 1 ]; do
                  sudo rm -f /tmp/aws-ip-provisioner.${!LINUX_ARCH_TYPE}-${OsType}-linux-gnu || true;
                  wget --quiet --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 --tries=70 --directory-prefix=/tmp/ --continue https://github.com/ava-labs/ip-manager/releases/download/latest/aws-ip-provisioner.${!LINUX_ARCH_TYPE}-${OsType}-linux-gnu
                  if [ $? = 0 ]; then break; fi; # check return value, break if successful (0)
                  sleep 2s;
                done;
                chmod +x /tmp/aws-ip-provisioner.${!LINUX_ARCH_TYPE}-${OsType}-linux-gnu
                /tmp/aws-ip-provisioner.${!LINUX_ARCH_TYPE}-${OsType}-linux-gnu --version
                /tmp/aws-ip-provisioner.${!LINUX_ARCH_TYPE}-${OsType}-linux-gnu \
                --log-level=info \
                --region ${AWS::Region} \
                --id-tag-key=Id \
                --id-tag-value=${Id} \
                --kind-tag-key=Kind \
                --kind-tag-value=aws-ip-provisioner \
                --ec2-tag-asg-name-key=ASG_NAME \
                --asg-tag-key=autoscaling:groupName \
                --initial-wait-random-seconds=${ProvisionerInitialWaitRandomSeconds} \
                --mounted-eip-file-path=/data/eip.yaml
              else
                echo "skipping allocating elastic IP address..."
              fi;

              # install saml2aws
              # https://api.github.com/repos/Versent/saml2aws/releases/latest
              while [ 1 ]; do
                export SAML2AWS_CURRENT_VERSION=$(curl -Ls https://api.github.com/repos/Versent/saml2aws/releases/latest | grep 'tag_name' | cut -d'v' -f2 | cut -d'"' -f1)
                echo SAML2AWS_CURRENT_VERSION: ${!SAML2AWS_CURRENT_VERSION}

                sudo rm -f /tmp/saml2aws_${!SAML2AWS_CURRENT_VERSION}_linux_$(dpkg --print-architecture).tar.gz || true;
                wget --quiet --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 --tries=70 --directory-prefix=/tmp/ --continue https://github.com/Versent/saml2aws/releases/download/v${!SAML2AWS_CURRENT_VERSION}/saml2aws_${!SAML2AWS_CURRENT_VERSION}_linux_$(dpkg --print-architecture).tar.gz -O - | tar -xzv -C /tmp

                if [ $? = 0 ]; then break; fi; # check return value, break if successful (0)
                sleep 2s;
              done;
              chmod u+x /tmp/saml2aws
              sudo cp /tmp/saml2aws /usr/local/bin/saml2aws
              saml2aws --version

              # create an SSH key
              if [[ "${SshKeyEmail}" != "none" ]]; then
                echo "generating an SSH key with ${SshKeyEmail}"
                ssh-keygen -q -t rsa -b 4096 -C "${SshKeyEmail}" -N '' -f /home/ubuntu/.ssh/id_rsa <<<y >/dev/null 2>&1
                eval "$(ssh-agent -s)"
                ssh-add /home/ubuntu/.ssh/id_rsa
                echo "generated the following SSH public key"
                cat /home/ubuntu/.ssh/id_rsa.pub

                # set permissions
                sudo chown -R $(whoami) /home/ubuntu/.ssh || true
                sudo chown -R ubuntu /home/ubuntu/.ssh || true
              else
                echo "skipping generating an SSH key..."
              fi;

              # sync time
              sudo timedatectl set-ntp on

              # e.g.,
              # "Accept error: accept tcp [::]:9650: accept4: too many open files; retrying in 1s"
              sudo echo "* hard nofile 1000000" >> /etc/security/limits.conf
              sudo echo "* soft nofile 1000000" >> /etc/security/limits.conf
              sudo sysctl -w fs.file-max=1000000
              sudo sysctl -p

              # TODO: more with bash script file from S3

  # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-group.html
  ASG:
    Type: AWS::AutoScaling::AutoScalingGroup
    UpdatePolicy:
      AutoScalingRollingUpdate:
        MinInstancesInService: !Ref AsgMinInstancesInService
        MaxBatchSize: 1
        SuspendProcesses:
          - HealthCheck
          - ReplaceUnhealthy
          - AZRebalance
          - AlarmNotification
          - ScheduledActions
    Properties:
      AutoScalingGroupName: !Ref AsgName
      MinSize: !Ref AsgMinSize
      MaxSize: !Ref AsgMaxSize
      DesiredCapacity: !Ref AsgDesiredCapacity
      VPCZoneIdentifier: !Ref PublicSubnetIds
      HealthCheckType: EC2
      HealthCheckGracePeriod: 120
      MetricsCollection:
        - Granularity: "1Minute"
      Tags:
        - Key: Name
          Value: !Ref AsgName
          PropagateAtLaunch: true
        - Key: ASG_NAME
          Value: !Ref AsgName
          PropagateAtLaunch: true
        - Key: ID
          Value: !Ref Id
          PropagateAtLaunch: true
        - Key: ARCH_TYPE
          Value: !Ref ArchType
          PropagateAtLaunch: true
        - Key: OS_TYPE
          Value: !Ref OsType
          PropagateAtLaunch: true
        - Key: INSTANCE_MODE
          Value: !Ref InstanceMode
          PropagateAtLaunch: true
        - Key: KMS_KEY_ARN
          Value: !Ref KmsKeyArn
          PropagateAtLaunch: true
        - Key: AAD_TAG
          Value: !Ref AadTag
          PropagateAtLaunch: true
        - Key: S3_BUCKET_NAME
          Value: !Ref S3BucketName
          PropagateAtLaunch: true
      # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-autoscalinggroup-mixedinstancespolicy.html
      # https://aws.amazon.com/getting-started/hands-on/ec2-auto-scaling-spot-instances/
      MixedInstancesPolicy:
        # define balance between spot vs. on-demand
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-autoscalinggroup-instancesdistribution.html
        # https://ec2spotworkshops.com/launching_ec2_spot_instances/asg.html
        InstancesDistribution:
          OnDemandAllocationStrategy: "lowest-price"
          # minimum amount of the Auto Scaling group's capacity that must be fulfilled by On-Demand Instances
          OnDemandBaseCapacity: 0
          # percentages of On-Demand Instances and Spot Instances for your additional capacity beyond OnDemandBaseCapacity
          # 20 specifies 20% On-Demand Instances, 80% Spot Instances
          # If set to 0, only Spot Instances are used.
          # If set to 100, only On-Demand Instances are used.
          OnDemandPercentageAboveBaseCapacity: !Ref OnDemandPercentageAboveBaseCapacity
          SpotAllocationStrategy: "lowest-price"
          # number of Spot Instance pools across which to allocate your Spot Instances
          SpotInstancePools: 3
        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-autoscalinggroup-launchtemplate.html
        LaunchTemplate:
          LaunchTemplateSpecification:
            LaunchTemplateId: !Ref AsgLaunchTemplate
            Version: !GetAtt AsgLaunchTemplate.LatestVersionNumber
          # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-autoscaling-autoscalinggroup-launchtemplateoverrides.html
          Overrides:
            - InstanceType: !Select [0, !Ref InstanceTypes]
            - Fn::If:
                - Has2InstanceTypes
                - InstanceType: !Select [1, !Ref InstanceTypes]
                - !Ref AWS::NoValue
            - Fn::If:
                - Has3InstanceTypes
                - InstanceType: !Select [2, !Ref InstanceTypes]
                - !Ref AWS::NoValue
            - Fn::If:
                - Has4InstanceTypes
                - InstanceType: !Select [3, !Ref InstanceTypes]
                - !Ref AWS::NoValue
            - Fn::If:
                - Has5InstanceTypes
                - InstanceType: !Select [4, !Ref InstanceTypes]
                - !Ref AWS::NoValue
            - Fn::If:
                - Has6InstanceTypes
                - InstanceType: !Select [5, !Ref InstanceTypes]
                - !Ref AWS::NoValue
            - Fn::If:
                - Has7InstanceTypes
                - InstanceType: !Select [6, !Ref InstanceTypes]
                - !Ref AWS::NoValue
            - Fn::If:
                - Has8InstanceTypes
                - InstanceType: !Select [7, !Ref InstanceTypes]
                - !Ref AWS::NoValue
            - Fn::If:
                - Has9InstanceTypes
                - InstanceType: !Select [8, !Ref InstanceTypes]
                - !Ref AWS::NoValue
            - Fn::If:
                - Has10InstanceTypes
                - InstanceType: !Select [9, !Ref InstanceTypes]
                - !Ref AWS::NoValue

Outputs:
  # same as "AutoScalingGroupName"
  AsgLogicalId:
    Value: !Ref ASG

Exported by these functions

use std::io::{self, Error, ErrorKind};

use rust_embed::RustEmbed;

pub fn asg_ubuntu_yaml() -> io::Result<String> {
    #[derive(RustEmbed)]
    #[folder = "src/cfn-templates/"]
    #[prefix = "src/cfn-templates/"]
    struct Asset;
    let f = Asset::get("src/cfn-templates/asg_ubuntu.yaml").unwrap();
    let s = std::str::from_utf8(f.data.as_ref()).map_err(|e| {
        Error::new(
            ErrorKind::InvalidInput,
            format!("failed to convert embed file to str {}", e),
        )
    })?;
    Ok(s.to_string())
}

pub fn ec2_instance_role_yaml() -> io::Result<String> {
    #[derive(RustEmbed)]
    #[folder = "src/cfn-templates/"]
    #[prefix = "src/cfn-templates/"]
    struct Asset;
    let f = Asset::get("src/cfn-templates/ec2_instance_role.yaml").unwrap();
    let s = std::str::from_utf8(f.data.as_ref()).map_err(|e| {
        Error::new(
            ErrorKind::InvalidInput,
            format!("failed to convert embed file to str {}", e),
        )
    })?;
    Ok(s.to_string())
}

pub fn vpc_yaml() -> io::Result<String> {
    #[derive(RustEmbed)]
    #[folder = "src/cfn-templates/"]
    #[prefix = "src/cfn-templates/"]
    struct Asset;
    let f = Asset::get("src/cfn-templates/vpc.yaml").unwrap();
    let s = std::str::from_utf8(f.data.as_ref()).map_err(|e| {
        Error::new(
            ErrorKind::InvalidInput,
            format!("failed to convert embed file to str {}", e),
        )
    })?;
    Ok(s.to_string())
}
exdx commented

Closing as this was completed in #376