Last active
January 24, 2025 14:39
-
-
Save rclark/057059bfbd869743d1742a95b456bcff to your computer and use it in GitHub Desktop.
Revisions
-
rclark revised this gist
Feb 18, 2023 . No changes.There are no files selected for viewing
-
rclark revised this gist
Feb 18, 2023 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -6,7 +6,7 @@ Thanks to https://github.com/wolveix/satisfactory-server for the Docker image! ## Runs on AWS ECS The dedicated server application runs on ECS Fargate, so you get a more-or-less "serverless" setup. It uses Fargate Spot, which allows you to get the cheapest possible setup, though AWS may choose to stop and restart your server. FWIW I've never actually observed that happening. ## Files and backups -
rclark revised this gist
Feb 18, 2023 . 1 changed file with 2 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,5 +1,7 @@ # Satisfactory dedicated server on AWS ECS A CloudFormation stack that you can run in your AWS account to host up a dedicated Satisfactory server. Thanks to https://github.com/wolveix/satisfactory-server for the Docker image! ## Runs on AWS ECS -
rclark revised this gist
Feb 18, 2023 . 2 changed files with 82 additions and 13 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,25 +1,94 @@ # Satisfactory dedicated server on AWS ECS Thanks to https://github.com/wolveix/satisfactory-server for the Docker image! ## Runs on AWS ECS The dedicated server application runs on ECS Fargate, so you get a more-or-less "serverless" setup. It uses Fargate Spot, which allows you to get the cheapest possible setup, at the cost that sometimes AWS may choose to stop and restart your server. FWIW I've never actually observed that happening. ## Files and backups The game files and saves are stored on EFS, a network-attached storage system that allows these files to persist when/if ECS tasks stop and restart. On a daily basis, the save files are copied up from EFS to an S3 bucket in your account, named `satistfactory-backups-{aws account id number}`. This makes for a cheap daily backup + easier access to those files. ## Networking and connecting to the dedicated server When an ECS task launches, it gets a public IP address, with the exposed ports required to access the dedicated server application from the Satisfactory game client. However, if the task/container ever stops, a new one will launch to replace it, and it will have a new IP address. Because of this, we need to work with DNS records that we can update. You must bring your own domain name that you own, provided as a stack parameter. For example, I might own a domain `rclark.life`. The stack builds a Route53 hosted zone for a subdomain of your domain, for example `satisfactory.rclark.life`. That hosted zone's name servers are a stack output. After launching the stack, you are responsible for making an NS record under the owned domain that references these name servers. That (in a sense) forwards traffic through your domain registrar to AWS Route53. The stack also creates a Lambda function. Every time a new ECS task starts, the Lambda function runs. It finds out the new container's IP address, and updates an A record in the Route53 hosted zone, for example `www.satisfactory.rclark.life`. **That means in the Satisfactory game client, you connect to the server at a domain name like, for example, `www.satisfactory.rclark.life`.** If the server application crashes (and it will), or if AWS stops your task (I haven't noticed), you will have to exit your Satisfactory game client all the way to your desktop. Wait a few minutes before launching it again. In that time a new ECS task launches, and the DNS A record gets updated by the Lambda function. It appears that the game client will only do the DNS lookup when the client launches, so you do have to exit the client and start it again after the record has been updated. ## Costs Roughly, it seems to cost about $50-60 USD per month to run this setup 24/7. Almost all of that cost is from running the ECS Fargate Spot task constantly. My AWS bill last month was $60.08, and $46.03 of that was ECS. You can turn the dedicated server off and back on again by making adjustments to the ECS service's desired task count. The ECS service can be found in the ECS console by browsing to the `games` cluster. That cluster should host just 1 service called `satisfactory-server`. Set the number of desired tasks to `0` to tell ECS to run nothing. When you want to play again, set it back to `1`. If you do this, you'll reduces the monthly cost dramatically... unless you actually play for most of the day on most days, in which case you're just gonna have to pay up. **Note:** Never set the desired task count `> 1`. There'd be 2 dedicated servers trying to access the same gamefiles and save files at that point, and things would definitely get weird. ## Some troubleshooting Here are some aws-cli commands you can use to try and troubleshoot anything going wrong. Make sure you set the region properly for whatever AWS region you launched the stack into. Either add `--region` flags, or setup a default region in your `~/.aws/config` file. ### Turn the dedicated server off and on ```sh # OFF aws ecs update-service \ --cluster games \ --service satisfactory-server \ --desired-count 0 ## ON aws ecs update-service \ --cluster games \ --service satisfactory-server \ --desired-count 1 ``` ### Make an SSH connection to the running container ```sh aws ecs execute-command \ --cluster games \ --task $(aws ecs list-tasks \ --service-name satisfactory-server \ --cluster games \ --query "taskArns[0]" \ --output text) \ --container satisfactory-server \ --command "/bin/bash" \ --interactive ``` ### Find the IP address of the currently running container ```sh aws ec2 describe-network-interfaces \ --network-interface-ids $(aws ecs describe-tasks \ --cluster games \ --tasks $(aws ecs list-tasks \ --service-name satisfactory-server \ --cluster games \ --query "taskArns[0]" \ --output text) \ --query "tasks[0].attachments[0].details[1].value" \ --output text) \ --query "NetworkInterfaces[0].Association.PublicIp" \ --output text ``` ### Tell Lambda to update the DNS record ```sh aws lambda invoke \ --function-name satisfactory-dns-refresher \ --invocation-type EVENT \ --payload '{}' ``` This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -58,7 +58,7 @@ Resources: Properties: VpcId: !Ref VPC CidrBlock: 10.0.128.0/20 AvailabilityZone: !Sub "${AWS::Region}a" Disk: Type: AWS::EFS::FileSystem -
rclark revised this gist
Feb 18, 2023 . 1 changed file with 25 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,25 @@ # Satisfactory dedicated server on AWS ECS - Thanks to https://github.com/wolveix/satisfactory-server for the Docker image! - The dedicated server application runs on ECS Fargate, so you get a more-or-less "serverless" setup. It uses Fargate Spot, which allows you to get the cheapest possible setup, at the cost that sometimes AWS may choose to stop and restart your server. FWIW I've never actually observed that happening. - The game files and saves are stored on EFS, a network-attached storage system that allows these files to persist when/if ECS tasks stop and restart. - On a daily basis, the save files are backed up to an S3 bucket in your account, named `satistfactory-backups-{aws account id number}`. - When an ECS task launches, it gets a public IP address, with the exposed ports required to access the dedicated server application from the Satisfactory game client. However, if the task/container ever stops, a new one will launch to replace it, and it will have a new IP address. Because of this, we need to work with DNS records that we can update. - You must bring your own domain name that you own, provided as a stack parameter. For example, I might own a domain `rclark.life`. - The stack builds a Route53 hosted zone for a subdomain of your domain, for example `satisfactory.rclark.life`. That hosted zone's name servers are a stack output. After launching the stack, you are responsible for making an NS record under the owned domain that references these name servers. - The stack creates a Lambda function. Every time a new ECS task starts, the Lambda function is invoked. It finds out the new container's IP address, and updates an A record in the hosted zone, for example `www.satisfactory.rclark.life`. - That means in the Satisfactory game client, you connect to the server at that domain name, e.g. `www.satisfactory.rclark.life`. - If the server application crashes (and it will), or if AWS stops your task (I haven't noticed), you will have to exit your Satisfactory game client all the way to your desktop. Wait a few minutes before launching it again. It appears that the game client will only do the DNS lookup when the client launches. - Roughly, it seems to cost about 50-60 USD per month to run this setup. Almost all of that cost is from running the ECS Fargate Spot task 24 hours a day, 7 days a week. - You can turn the dedicated server off and back on again by making adjustments to the ECS service's "desired task count". -
rclark created this gist
Feb 18, 2023 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,460 @@ Parameters: TopLevelDomainName: Type: String Resources: VPC: Type: AWS::EC2::VPC Properties: CidrBlock: 10.0.0.0/16 EnableDnsHostnames: true EnableDnsSupport: true Tags: - Key: Name Value: !Ref AWS::StackName DHCP: Type: AWS::EC2::DHCPOptions Properties: DomainName: !Sub ${AWS::Region}.compute.internal DomainNameServers: - AmazonProvidedDNS DHCPAssociation: Type: AWS::EC2::VPCDHCPOptionsAssociation Properties: VpcId: !Ref VPC DhcpOptionsId: !Ref DHCP Gateway: Type: AWS::EC2::InternetGateway GatewayAttachment: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref VPC InternetGatewayId: !Ref Gateway RouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref Subnet RouteTableId: !Ref RouteTable InternetRoute: Type: AWS::EC2::Route Properties: RouteTableId: !Ref RouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref Gateway Subnet: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC CidrBlock: 10.0.128.0/20 AvailabilityZone: us-west-2a Disk: Type: AWS::EFS::FileSystem Properties: Encrypted: true LifecyclePolicies: - TransitionToIA: AFTER_14_DAYS PerformanceMode: generalPurpose ThroughputMode: bursting Mount: Type: AWS::EFS::MountTarget Properties: FileSystemId: !Ref Disk SecurityGroups: - !GetAtt DiskAccess.GroupId SubnetId: !Ref Subnet AccessPoint: Type: AWS::EFS::AccessPoint Properties: FileSystemId: !Ref Disk PosixUser: Uid: "1000" Gid: "1000" RootDirectory: Path: /home/satisfactory-server CreationInfo: OwnerUid: "1000" OwnerGid: "1000" Permissions: "755" DiskAccess: Type: AWS::EC2::SecurityGroup Properties: VpcId: !Ref VPC GroupDescription: Access to satisfactory EFS disk SecurityGroupIngress: - IpProtocol: tcp FromPort: 2049 ToPort: 2049 CidrIp: 0.0.0.0/0 Task: Type: AWS::ECS::TaskDefinition Properties: Cpu: "4096" Memory: "12288" ExecutionRoleArn: !GetAtt ExecutionRole.Arn TaskRoleArn: !GetAtt TaskRole.Arn NetworkMode: awsvpc RequiresCompatibilities: - FARGATE ContainerDefinitions: - Name: satisfactory-server Image: wolveix/satisfactory-server PortMappings: - ContainerPort: 7777 Protocol: udp - ContainerPort: 15000 Protocol: udp - ContainerPort: 15777 Protocol: udp MountPoints: - ContainerPath: /config SourceVolume: disk LogConfiguration: LogDriver: awslogs Options: awslogs-group: !Ref Logs awslogs-region: !Ref AWS::Region awslogs-stream-prefix: !Ref AWS::StackName Command: - --disable-telemetry Environment: - Name: PGID Value: "1000" - Name: PUID Value: "1000" - Name: DISABLESEASONALEVENTS Value: "true" - Name: AUTOPAUSE Value: "false" Volumes: - Name: disk EFSVolumeConfiguration: FilesystemId: !Ref Disk TransitEncryption: ENABLED TransitEncryptionPort: 2050 AuthorizationConfig: AccessPointId: !Ref AccessPoint IAM: ENABLED TaskRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Action: sts:AssumeRole Principal: Service: ecs-tasks.amazonaws.com Policies: - PolicyName: main PolicyDocument: Statement: - Effect: Allow Action: - elasticfilesystem:ClientMount - elasticfilesystem:ClientWrite Resource: !GetAtt Disk.Arn Condition: StringEquals: elasticfilesystem:AccessPointArn: !GetAtt AccessPoint.Arn - PolicyName: exec PolicyDocument: Statement: - Effect: Allow Action: - ssmmessages:CreateControlChannel - ssmmessages:CreateDataChannel - ssmmessages:OpenControlChannel - ssmmessages:OpenDataChannel Resource: "*" - Effect: Allow Action: kms:Decrypt Resource: !GetAtt ExecKey.Arn ExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Action: sts:AssumeRole Principal: Service: ecs-tasks.amazonaws.com Policies: - PolicyName: main PolicyDocument: Statement: - Effect: Allow Action: logs:* Resource: !GetAtt Logs.Arn TaskAccess: Type: AWS::EC2::SecurityGroup Properties: VpcId: !Ref VPC GroupDescription: Ingress for satisfactory-server SecurityGroupIngress: - IpProtocol: udp FromPort: 7777 ToPort: 7777 CidrIp: 0.0.0.0/0 - IpProtocol: udp FromPort: 15000 ToPort: 15000 CidrIp: 0.0.0.0/0 - IpProtocol: udp FromPort: 15777 ToPort: 15777 CidrIp: 0.0.0.0/0 Logs: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Ref AWS::StackName RetentionInDays: 14 Cluster: Type: AWS::ECS::Cluster Properties: ClusterName: games Service: Type: AWS::ECS::Service Properties: CapacityProviderStrategy: - CapacityProvider: FARGATE_SPOT Weight: 1 Cluster: !Ref Cluster DesiredCount: 1 EnableECSManagedTags: true EnableExecuteCommand: true NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: ENABLED SecurityGroups: - !GetAtt TaskAccess.GroupId Subnets: - !Ref Subnet ServiceName: satisfactory-server TaskDefinition: !Ref Task ExecKey: Type: AWS::KMS::Key Properties: KeyPolicy: Version: 2012-10-17 Id: key-default-1 Statement: - Sid: Default Effect: Allow Principal: AWS: !Sub arn:aws:iam::${AWS::AccountId}:root Action: kms:* Resource: "*" Domain: Type: AWS::Route53::HostedZone Properties: Name: !Sub "satisfactory.${TopLevelDomainName}" Refresher: Type: AWS::Lambda::Function Properties: FunctionName: satisfactory-dns-refresher Role: !GetAtt RefresherRole.Arn Handler: index.handler Runtime: nodejs14.x Code: ZipFile: !Sub | const AWS = require('aws-sdk'); exports.handler = async () => { const ecs = new AWS.ECS(); const ec2 = new AWS.EC2(); const r53 = new AWS.Route53(); const tasks = await ecs.listTasks({ cluster: 'games', serviceName: 'satisfactory-server' }).promise(); if (tasks.taskArns.length === 0) return; const desc = await ecs.describeTasks({ cluster: 'games', tasks: [tasks.taskArns[0]] }).promise(); if (desc.tasks.length === 0) return; const eni = desc.tasks[0].attachments[0].details.find((a) => a.name === 'networkInterfaceId').value; const interface = await ec2.describeNetworkInterfaces({ NetworkInterfaceIds: [eni] }).promise(); if (interface.NetworkInterfaces.length === 0) return; await r53.changeResourceRecordSets({ HostedZoneId: '${Domain.Id}', ChangeBatch: { Changes: [{ Action: 'UPSERT', ResourceRecordSet: { Name: 'www.satisfactory.${TopLevelDomainName}', Type: 'A', TTL: 60, ResourceRecords: [{ Value: interface.NetworkInterfaces[0].Association.PublicIp }] } }] } }).promise(); }; RefresherLogs: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/lambda/satisfactory-dns-refresher RetentionInDays: 14 RefresherRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Action: sts:AssumeRole Principal: Service: lambda.amazonaws.com Policies: - PolicyName: main PolicyDocument: Statement: - Effect: Allow Action: logs:* Resource: !GetAtt Logs.Arn - Effect: Allow Action: - ecs:DescribeTasks - ecs:ListTasks - ec2:DescribeNetworkInterfaces - route53:ChangeResourceRecordSets Resource: "*" RefresherEvents: Type: AWS::Events::Rule Properties: Name: satisfactory-server-dns-refresh-termination EventPattern: source: - aws.ecs detail-type: - ECS Task State Change detail: lastStatus: - RUNNING clusterArn: - !GetAtt Cluster.Arn Targets: - Id: satisfactory-server-dns-refresher Arn: !GetAtt Refresher.Arn EventsPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !Ref Refresher Principal: events.amazonaws.com SourceArn: !GetAtt RefresherEvents.Arn BackupBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub satisfactory-backups-${AWS::AccountId} PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true LifecycleConfiguration: Rules: - Status: Enabled AbortIncompleteMultipartUpload: DaysAfterInitiation: 1 BackupSource: Type: AWS::DataSync::LocationEFS Properties: EfsFilesystemArn: !GetAtt Disk.Arn Subdirectory: /home/satisfactory-server/saved/server # on ECS task, saves in /config/saved/server Ec2Config: SecurityGroupArns: - !Sub arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:security-group/${DiskAccess.GroupId} SubnetArn: !Sub arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:subnet/${Subnet} BackupDestination: Type: AWS::DataSync::LocationS3 Properties: S3BucketArn: !GetAtt BackupBucket.Arn S3Config: BucketAccessRoleArn: !GetAtt BackupRole.Arn S3StorageClass: STANDARD_IA Subdirectory: saves # in S3, everything ends up under saves/ key BackupRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Action: sts:AssumeRole Principal: Service: datasync.amazonaws.com Policies: - PolicyName: main PolicyDocument: Statement: - Effect: Allow Action: - s3:GetBucketLocation - s3:ListBucket - s3:ListBucketMultipartUploads Resource: !GetAtt BackupBucket.Arn - Effect: Allow Action: - s3:AbortMultipartUpload - s3:DeleteObject - s3:GetObject - s3:ListMultipartUploadParts - s3:PutObjectTagging - s3:GetObjectTagging - s3:PutObject Resource: !Sub ${BackupBucket.Arn}/* BackupTask: Type: AWS::DataSync::Task Properties: DestinationLocationArn: !Ref BackupDestination SourceLocationArn: !Ref BackupSource Name: satisfactory-backups Options: Atime: BEST_EFFORT Mtime: PRESERVE Schedule: ScheduleExpression: rate(1 days) Outputs: NameServers: Value: !Join - "," - !GetAtt Domain.NameServers