AWSTemplateFormatVersion: "2010-09-09" Description: Factorio Spot Price Server via Docker / ECS Parameters: ECSAMI: Description: AWS ECS AMI ID Type: AWS::SSM::Parameter::Value Default: /aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id FactorioImageTag: Type: String Description: "(Examples include latest, stable, 0.17, 0.17.33) Refer to tag descriptions available here: https://hub.docker.com/r/factoriotools/factorio/)" Default: latest ServerState: Type: String Description: "Running: A spot instance will launch shortly after setting this parameter. Stopped: Your spot instance will be terminated shortly after setting this parameter." Default: Running AllowedValues: - Running - Stopped InstanceType: Type: String Description: "m3.medium is a good cost effective (albeit older) instance, 1 VCPU and 3.75 GB of RAM with moderate network performance. Change at your discretion. https://aws.amazon.com/ec2/instance-types/." Default: m3.medium SpotPrice: Type: String Description: "An m3.medium shouldn't cost more than a cent per hour. Note: Leave this blank to use on-demand pricing." Default: "0.05" KeyPairName: Type: AWS::EC2::KeyPair::KeyName Description: (Optional - An empty value disables this feature) Default: '' YourIp: Type: String Description: (Optional - An empty value disables this feature) Default: '' HostedZoneId: Type: String Description: (Optional - An empty value disables this feature) If you have a hosted zone in Route 53 and wish to set a DNS record whenever your Factorio instance starts, supply the hosted zone ID here. Default: '' RecordName: Type: String Description: (Optional - An empty value disables this feature) If you have a hosted zone in Route 53 and wish to set a DNS record whenever your Factorio instance starts, supply the name of the record here (e.g. factorio.mydomain.com). Default: '' EnableRcon: Type: String Description: Refer to https://hub.docker.com/r/factoriotools/factorio/ for further RCON configuration details. This parameter simply opens / closes the port on the security group. Default: false AllowedValues: - true - false Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Essential Configuration Parameters: - FactorioImageTag - ServerState - InstanceType - SpotPrice - EnableRcon - Label: default: Remote Access (SSH) Configuration (Optional) Parameters: - KeyPairName - YourIp - Label: default: DNS Configuration (Optional) Parameters: - HostedZoneId - RecordName ParameterLabels: FactorioImageTag: default: "Which version of Factorio do you want to launch?" ServerState: default: "Update this parameter to shut down / start up your Factorio server as required to save on cost. Takes a few minutes to take effect." InstanceType: default: "Which instance type?" SpotPrice: default: "Maximum spot price per hour? Leave blank to disable spot pricing." KeyPairName: default: "If you wish to access the instance via SSH, select a Key Pair to use." YourIp: default: "If you wish to access the instance via SSH, provide your public IP address." HostedZoneId: default: "If you have a hosted zone in Route 53 and wish to update a DNS record whenever your Factorio instance starts, supply the hosted zone ID here." RecordName: default: "If you have a hosted zone in Route 53 and wish to set a DNS record whenever your Factorio instance starts, supply a record name here (e.g. factorio.mydomain.com)." EnableRcon: default: "Do you wish to enable RCON?" Conditions: KeyPairNameProvided: !Not [ !Equals [ !Ref KeyPairName, '' ] ] IpAddressProvided: !Not [ !Equals [ !Ref YourIp, '' ] ] DnsConfigEnabled: !And [ !Not [ !Equals [ !Ref HostedZoneId, '' ] ], !Not [ !Equals [ !Ref RecordName, '' ] ] ] SpotPriceProvided: !Not [ !Equals [ !Ref SpotPrice, '' ] ] DoEnableRcon: !Equals [ !Ref EnableRcon, 'true' ] Mappings: ServerState: Running: AutoScalingCapacity: 1 Stopped: AutoScalingCapacity: 0 Resources: # ==================================================== # BASIC VPC # ==================================================== Vpc: Type: AWS::EC2::VPC Properties: CidrBlock: 10.100.0.0/26 EnableDnsSupport: true EnableDnsHostnames: true SubnetA: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Select - 0 - !GetAZs Ref: 'AWS::Region' CidrBlock: !Select [ 0, !Cidr [ 10.100.0.0/26, 4, 4 ] ] VpcId: !Ref Vpc SubnetARoute: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref RouteTable SubnetId: !Ref SubnetA SubnetBRoute: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref RouteTable SubnetId: !Ref SubnetB SubnetB: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Select - 1 - !GetAZs Ref: 'AWS::Region' CidrBlock: !Select [ 1, !Cidr [ 10.100.0.0/26, 4, 4 ] ] VpcId: !Ref Vpc InternetGateway: Type: AWS::EC2::InternetGateway Properties: {} InternetGatewayAttachment: Type: AWS::EC2::VPCGatewayAttachment Properties: InternetGatewayId: !Ref InternetGateway VpcId: !Ref Vpc RouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref Vpc Route: Type: AWS::EC2::Route Properties: DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway RouteTableId: !Ref RouteTable # ==================================================== # EFS FOR PERSISTENT DATA # ==================================================== Efs: Type: AWS::EFS::FileSystem Properties: {} MountA: Type: AWS::EFS::MountTarget Properties: FileSystemId: !Ref Efs SecurityGroups: - !Ref EfsSg SubnetId: !Ref SubnetA MountB: Type: AWS::EFS::MountTarget Properties: FileSystemId: !Ref Efs SecurityGroups: - !Ref EfsSg SubnetId: !Ref SubnetB EfsSg: Type: AWS::EC2::SecurityGroup Properties: GroupName: factorio-efs GroupDescription: factorio-efs SecurityGroupIngress: - FromPort: 2049 ToPort: 2049 IpProtocol: tcp SourceSecurityGroupId: !Ref Ec2Sg VpcId: !Ref Vpc # ==================================================== # INSTANCE CONFIG # ==================================================== Ec2Sg: Type: AWS::EC2::SecurityGroup Properties: GroupName: factorio-ec2 GroupDescription: factorio-ec2 SecurityGroupIngress: - !If - IpAddressProvided - FromPort: 22 ToPort: 22 IpProtocol: tcp CidrIp: !Sub "${YourIp}/32" - !Ref 'AWS::NoValue' - FromPort: 34197 ToPort: 34197 IpProtocol: udp CidrIp: 0.0.0.0/0 - !If - DoEnableRcon - FromPort: 27015 ToPort: 27015 IpProtocol: tcp CidrIp: 0.0.0.0/0 - !Ref 'AWS::NoValue' VpcId: !Ref Vpc LaunchConfiguration: Type: AWS::AutoScaling::LaunchConfiguration Properties: AssociatePublicIpAddress: true IamInstanceProfile: !Ref InstanceProfile ImageId: !Ref ECSAMI InstanceType: !Ref InstanceType KeyName: !If [ KeyPairNameProvided, !Ref KeyPairName, !Ref 'AWS::NoValue' ] SecurityGroups: - !Ref Ec2Sg SpotPrice: !If [ SpotPriceProvided, !Ref SpotPrice, !Ref 'AWS::NoValue' ] UserData: Fn::Base64: !Sub | #!/bin/bash -xe yum install -y amazon-efs-utils mkdir /opt/factorio mount -t efs ${Efs}:/ /opt/factorio chown 845:845 /opt/factorio echo ECS_CLUSTER=${EcsCluster} >> /etc/ecs/ecs.config AutoScalingGroup: Type: AWS::AutoScaling::AutoScalingGroup DependsOn: - MountA - MountB Properties: AutoScalingGroupName: factorio DesiredCapacity: !FindInMap [ ServerState, !Ref ServerState, AutoScalingCapacity ] LaunchConfigurationName: !Ref LaunchConfiguration MaxSize: !FindInMap [ ServerState, !Ref ServerState, AutoScalingCapacity ] MinSize: !FindInMap [ ServerState, !Ref ServerState, AutoScalingCapacity ] VPCZoneIdentifier: - !Ref SubnetA - !Ref SubnetB InstanceRole: 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/service-role/AmazonEC2ContainerServiceforEC2Role Policies: - PolicyName: root PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: "route53:*" Resource: "*" InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Roles: - !Ref InstanceRole EcsCluster: Type: AWS::ECS::Cluster Properties: ClusterName: factorio EcsService: Type: AWS::ECS::Service Properties: Cluster: !Ref EcsCluster DesiredCount: 1 ServiceName: factorio TaskDefinition: !Ref EcsTask DeploymentConfiguration: MaximumPercent: 100 MinimumHealthyPercent: 0 EcsTask: Type: AWS::ECS::TaskDefinition Properties: Volumes: - Host: SourcePath: /opt/factorio Name: factorio ContainerDefinitions: - Name: factorio MemoryReservation: 1024 Image: !Sub "factoriotools/factorio:${FactorioImageTag}" PortMappings: - ContainerPort: 34197 HostPort: 34197 Protocol: udp - ContainerPort: 27015 HostPort: 27015 Protocol: tcp MountPoints: - ContainerPath: /factorio SourceVolume: factorio ReadOnly: false # ==================================================== # SET DNS RECORD # ==================================================== SetDNSRecordLambdaRole: Type: AWS::IAM::Role Condition: DnsConfigEnabled Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: root PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: "route53:*" Resource: "*" - Effect: "Allow" Action: "ec2:DescribeInstance*" Resource: "*" SetDNSRecordLambda: Type: "AWS::Lambda::Function" Condition: DnsConfigEnabled Properties: Environment: Variables: HostedZoneId: !Ref HostedZoneId RecordName: !Ref RecordName Code: ZipFile: | import boto3 import os def handler(event, context): new_instance = boto3.resource('ec2').Instance(event['detail']['EC2InstanceId']) boto3.client('route53').change_resource_record_sets( HostedZoneId= os.environ['HostedZoneId'], ChangeBatch={ 'Comment': 'updating', 'Changes': [ { 'Action': 'UPSERT', 'ResourceRecordSet': { 'Name': os.environ['RecordName'], 'Type': 'A', 'TTL': 60, 'ResourceRecords': [ { 'Value': new_instance.public_ip_address }, ] } }, ] }) Description: Sets Route 53 DNS Record for Factorio FunctionName: factorio-set-dns Handler: index.handler MemorySize: 128 Role: !GetAtt SetDNSRecordLambdaRole.Arn Runtime: python3.7 Timeout: 20 LaunchEvent: Type: AWS::Events::Rule Condition: DnsConfigEnabled Properties: EventPattern: source: - aws.autoscaling detail-type: - EC2 Instance Launch Successful detail: AutoScalingGroupName: - !Ref AutoScalingGroup Name: Factorio-Instance-Launch State: ENABLED Targets: - Arn: !GetAtt SetDNSRecordLambda.Arn Id: factorio-set-dns LaunchEventLambdaPermission: Type: AWS::Lambda::Permission Condition: DnsConfigEnabled Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt SetDNSRecordLambda.Arn Principal: events.amazonaws.com SourceArn: !GetAtt LaunchEvent.Arn Outputs: CheckInstanceIp: Description: To find your Factorio instance IP address, visit the following link. Click on the instance to find its Public IP address. Value: !Sub "https://${AWS::Region}.console.aws.amazon.com/ec2/v2/home?region=${AWS::Region}#Instances:tag:aws:autoscaling:groupName=${AutoScalingGroup};sort=tag:Name"