using Amazon; using Amazon.GameLift; using Amazon.GameLift.Model; using Amazon.IdentityManagement; using Amazon.IdentityManagement.Model; using Amazon.S3; using Amazon.S3.Model; using Amazon.S3.Transfer; using Amazon.S3.Util; using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; namespace DeployTool { class Program { static RegionEndpoint region = Amazon.RegionEndpoint.USEast1; // default static string bucket = null; static void Main(string[] args) { MainAsync(args).GetAwaiter().GetResult(); } static async Task MainAsync(string[] args) { string name = null; string version = null; string rootpath = null; bool alias = false; // default string aliasUpdate = null; bool aliasop = false; try { for (int i = 0; i < args.Length; i++) { if (String.Equals(args[i], @"--name", StringComparison.OrdinalIgnoreCase)) { if (name != null) throw new ArgumentException("Error: --name may only be specified once"); if (++i > args.Length) throw new ArgumentException("Error: --name should be followed by a <name>"); name = args[i]; if (name.StartsWith("--")) throw new ArgumentException("Error: --name should be followed by a name"); if (name.Length > 40) throw new ArgumentException("Error: name is too long"); continue; } if (String.Equals(args[i], @"--version", StringComparison.OrdinalIgnoreCase)) { if (version != null) throw new ArgumentException("Error: --version may only be specified once"); if (++i > args.Length) throw new ArgumentException("Error: --version should be followed by a <version>"); version = args[i]; if (version.StartsWith("--")) throw new ArgumentException("Error: --version should be followed by a <version>"); if (version.Length > 15) throw new ArgumentException("Error: version is too long"); continue; } if (String.Equals(args[i], @"--root-path", StringComparison.OrdinalIgnoreCase)) { if (rootpath != null) throw new ArgumentException("Error: --root-path may only be specified once"); if (++i > args.Length) throw new ArgumentException("Error: --root-path should be followed by a <rootpath>"); rootpath = args[i]; if (rootpath.StartsWith("--")) throw new ArgumentException("Error: --root-path should be followed by a <rootpath>"); if (rootpath.Length > 1024) throw new ArgumentException("Error: root-path is too long"); continue; } if (String.Equals(args[i], @"--region", StringComparison.OrdinalIgnoreCase)) { bool valid = false; if (++i > args.Length) throw new ArgumentException("Error: --region should be followed by a <region>"); foreach (RegionEndpoint r in RegionEndpoint.EnumerableAllRegions) { if (String.Equals(args[i], r.SystemName, StringComparison.OrdinalIgnoreCase)) { region = r; valid = true; } } if (!valid) throw new ArgumentException("Error: --region should be followed by a valid <region> system name, e.g. us-east-1"); continue; } if (String.Equals(args[i], @"--alias", StringComparison.OrdinalIgnoreCase)) { if (aliasop) throw new ArgumentException("Error: only one alias operation allowed, either --alias, --no-alias, or --update-alias"); aliasop = true; alias = true; continue; } if (String.Equals(args[i], @"--noalias", StringComparison.OrdinalIgnoreCase)) { if (aliasop) throw new ArgumentException("Error: only one alias operation allowed, either --alias, --no-alias, or --update-alias"); aliasop = true; alias = false; continue; } if (String.Equals(args[i], @"--no-alias", StringComparison.OrdinalIgnoreCase)) { if (aliasop) throw new ArgumentException("Error: only one alias operation allowed, either --alias, --no-alias, or --update-alias"); aliasop = true; alias = false; continue; } if (String.Equals(args[i], @"--update-alias", StringComparison.OrdinalIgnoreCase)) { if (aliasop) throw new ArgumentException("Error: only one alias operation allowed, either --alias, --no-alias, or --update-alias"); aliasop = true; if (++i > args.Length) throw new ArgumentException("Error: --update-alias should be followed by an <aliasid>"); if (Regex.Match(args[i], @"^alias-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", RegexOptions.IgnoreCase).Success) { aliasUpdate = args[i].ToLowerInvariant(); } else { throw new ArgumentException("Error: --update-alias should be followed by a valid alias id, e.g. alias-3edb1432-14c8-4650-b350-aaf7090fd5a2"); } continue; } if (String.Equals(args[i], @"--help", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("Help:"); } if (args[i].StartsWith("--")) throw new ArgumentException("Error: Unknown option \"" + args[i] + "\""); } if (name == null) throw new ArgumentException("Error: --name <name> is mandatory"); if (version == null) throw new ArgumentException("Error: --version <version> is mandatory"); if (rootpath == null) throw new ArgumentException("Error: --root-path <rootpath> is mandatory"); await DeployBuild(name, version, rootpath, alias, aliasUpdate); } catch (ArgumentException e) { Console.WriteLine(e.Message); Console.WriteLine(); Console.WriteLine(@"Usage Examples:"); Console.WriteLine(@"DeployTool --help"); Console.WriteLine(@"DeployTool --name myfleet --version 1.0.1 --root-path C:\proj"); Console.WriteLine(@"DeployTool --name FleetName --version 1.24x --root-path C:\proj --alias"); Console.WriteLine(@"DeployTool --region us-west-2 --name FleetName --version six --root-path C:\proj"); Console.WriteLine(); Console.WriteLine(@"Grammar:"); Console.WriteLine(@"DeployTool <help>|<options>"); Console.WriteLine(@"<options> ::= <name>|<ver>|<root>|<region>|<aliasopt> [<options>]"); Console.WriteLine(@"<help> ::= --help show this message"); Console.WriteLine(@"<name> ::= --name <string> fleet name *"); Console.WriteLine(@"<ver> ::= --version <string> fleet version *"); Console.WriteLine(@"<root> ::= --root-path <path> image root *"); Console.WriteLine(@"<region> ::= --region <sys-name> deploy to region (default us-east-1)"); Console.WriteLine(@"<aliasopt> ::= <alias>|<update>"); Console.WriteLine(@"<alias> ::= --alias|--no-alias also create an alias to fleet (or not)"); Console.WriteLine(@"<update> ::= --update-alias <aliasid>"); Console.WriteLine(@"<aliasid> ::= alias-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); Console.WriteLine(@"<sys-name> ::= us-east-1|us-east-2|us-west-1|us-west-2|ap-south-1|"); Console.WriteLine(@" ap-northeast-2|ap-southeast-1|ap-southeast-2|ap-northeast-1|"); Console.WriteLine(@" ca-central-1|cn-north-1|eu-central-1|eu-west-1|eu-west-2|"); Console.WriteLine(@" sa-east-1"); Console.WriteLine(); Console.WriteLine(@"* name, ver and root options are mandatory for fleet creation"); Console.WriteLine(@" option order is not important; no spaces in arguments"); Console.WriteLine(@" case insensitive except for rootpath value on linux fleets"); Console.WriteLine(); } } static private async Task DeployBuild(string name, string version, string rootpath, bool alias, string aliasUpdate) { // Get AWS account number to make our bucket name unique var awsAccountId = GetAWSNum(); // Bucket bucket = "buildbucket-" + region.SystemName + "-" + awsAccountId; // Create the Role for GameLift to use S3 (if it doesn't already exist) string roleName = "GameLiftS3Access-"+region.SystemName; // the role for GameLift to use S3 string roleArn = await GetRoleArn(roleName); string policy = @"{""Version"": ""2012-10-17"",""Statement"": [{""Action"": [""s3:GetObject"",""s3:GetObjectVersion""],""Resource"": ""arn:aws:s3:::" + bucket + @"/*"",""Effect"": ""Allow""}]}"; if (roleArn == null) roleArn = await CreateGameLiftRole(roleName, policy); // Zip and upload the build to a unique bucket DeleteZip(); string zipfile = ZipBuild(rootpath); await CreateBucket(); var s3Location = UploadBuild(zipfile, roleArn); // Create the build from the S3 bucket and the fleet string build = await CreateBuild(name, version, s3Location); string fleetId = await CreateFleet(name, version, build); if (alias) { string aliasId = await CreateAlias(name, fleetId); Console.WriteLine(aliasId); } else if (aliasUpdate != null) { string aliasId = await UpdateAlias(aliasUpdate, fleetId); Console.WriteLine(aliasId); } else { Console.WriteLine(fleetId); } } private static string GetAWSNum() { try { var config = new AmazonIdentityManagementServiceConfig(); config.RegionEndpoint = region; using (var aimsc = new AmazonIdentityManagementServiceClient(config)) { var response = aimsc.GetUser(); string arn = response.User.Arn; return arn.Split(':')[4]; } } catch (NoSuchEntityException) // role was not present { return null; } catch (AmazonIdentityManagementServiceException imsException) { Console.WriteLine(imsException.Message, imsException.InnerException); throw; } } static string ZipBuild(string rootpath) { var zipfile = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), "build.zip"); ZipFile.CreateFromDirectory(rootpath, zipfile); return zipfile; } static void DeleteZip() { var zipfile = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), "build.zip"); File.Delete(zipfile); } static async Task<string> GetRoleArn(string roleName) { try { var config = new AmazonIdentityManagementServiceConfig(); config.RegionEndpoint = region; using (var aimsc = new AmazonIdentityManagementServiceClient(config)) { var response = await aimsc.GetRoleAsync(new GetRoleRequest { RoleName = roleName }); Role role = response.Role; return role.Arn; } } catch (NoSuchEntityException) // role was not present { return null; } catch (AmazonIdentityManagementServiceException imsException) { Console.WriteLine(imsException.Message, imsException.InnerException); throw; } } static async Task<string> CreateGameLiftRole(string roleName, string policy) { try { var config = new AmazonIdentityManagementServiceConfig(); config.RegionEndpoint = region; using (var aimsc = new AmazonIdentityManagementServiceClient(config)) { string assumeRole = @"{""Version"":""2012-10-17"",""Statement"":[{""Effect"":""Allow"",""Principal"":{""Service"": ""gamelift.amazonaws.com""},""Action"":""sts:AssumeRole""}]}"; var crres = await aimsc.CreateRoleAsync(new CreateRoleRequest { AssumeRolePolicyDocument = assumeRole, Path = "/", RoleName = roleName }); Role role = crres.Role; var cpres = await aimsc.CreatePolicyAsync(new CreatePolicyRequest { PolicyName = roleName + "Policy", Description = "This allows GameLift to access services", PolicyDocument = policy, Path = "/" }); var policyArn = cpres.Policy.Arn; var response = await aimsc.AttachRolePolicyAsync(new AttachRolePolicyRequest { PolicyArn = policyArn, RoleName = roleName }); return role.Arn; } } catch (AmazonIdentityManagementServiceException imsException) { Console.WriteLine(imsException.Message, imsException.InnerException); throw; } } static Amazon.GameLift.Model.S3Location UploadBuild(string zipfile, string roleArn) { try { TransferUtility fileTransferUtility = new TransferUtility(new AmazonS3Client(region)); fileTransferUtility.Upload(zipfile, bucket, "build.zip"); } catch (AmazonS3Exception s3Exception) { Console.WriteLine(s3Exception.Message, s3Exception.InnerException); throw; } var s3Location = new Amazon.GameLift.Model.S3Location(); s3Location.Bucket = bucket; s3Location.Key = "build.zip"; s3Location.RoleArn = roleArn; return s3Location; } public static async Task CreateBucket() { using (var client = new AmazonS3Client(region)) { if (!(await AmazonS3Util.DoesS3BucketExistAsync(client, bucket))) { await CreateBucket(client); } for (int x = 0; !(await AmazonS3Util.DoesS3BucketExistAsync(client, bucket)); x++) { Thread.Sleep(1); if (x == 30000) throw new Exception("Bucket " + bucket + " was successfully created but still could not be found after 30 seconds. Wait a few minutes and try again or check S3"); } } } static async Task<string> FindBucketLocation(IAmazonS3 client) { string bucketLocation; GetBucketLocationRequest request = new GetBucketLocationRequest() { BucketName = bucket }; GetBucketLocationResponse response = await client.GetBucketLocationAsync(request); bucketLocation = response.Location.ToString(); return bucketLocation; } static async Task CreateBucket(IAmazonS3 client) { try { PutBucketRequest putRequest1 = new PutBucketRequest { BucketName = bucket }; PutBucketResponse response1 = await client.PutBucketAsync(putRequest1); } catch (AmazonS3Exception amazonS3Exception) { if (amazonS3Exception.ErrorCode != null && (amazonS3Exception.ErrorCode.Equals("InvalidAccessKeyId") || amazonS3Exception.ErrorCode.Equals("InvalidSecurity"))) { Console.WriteLine("Check the provided AWS Credentials."); Console.WriteLine("For service sign up go to http://aws.amazon.com/s3"); } else { Console.WriteLine( "Error occurred. Message:'{0}' when writing an object" , amazonS3Exception.Message); } } } static async Task<string> CreateBuild(string name, string version, Amazon.GameLift.Model.S3Location s3Location) { using (var aglc = new AmazonGameLiftClient(new AmazonGameLiftConfig { RegionEndpoint = region })) { try { CreateBuildResponse cbres = await aglc.CreateBuildAsync(new CreateBuildRequest { Name = name, Version = version, StorageLocation = s3Location }); return cbres.Build.BuildId; } catch (AmazonGameLiftException glException) { Console.WriteLine(glException.Message, glException.InnerException); throw; } } } static async Task<string> CreateFleet(string name, string version, string buildId) { var config = new AmazonGameLiftConfig(); config.RegionEndpoint = region; using (AmazonGameLiftClient aglc = new AmazonGameLiftClient(config)) { // create launch configuration var serverProcess = new ServerProcess(); serverProcess.ConcurrentExecutions = 1; serverProcess.LaunchPath = @"C:\game\GameLiftUnity.exe"; // @"/local/game/ReproGLLinux.x86_64"; serverProcess.Parameters = "-batchmode -nographics"; // create inbound IP permissions var permission1 = new IpPermission(); permission1.FromPort = 1935; permission1.ToPort = 1935; permission1.Protocol = IpProtocol.TCP; permission1.IpRange = "0.0.0.0/0"; // create inbound IP permissions var permission2 = new IpPermission(); permission2.FromPort = 3389; permission2.ToPort = 3389; permission2.Protocol = IpProtocol.TCP; permission2.IpRange = "0.0.0.0/0"; // create fleet var cfreq = new CreateFleetRequest(); cfreq.Name = name; cfreq.Description = version; cfreq.BuildId = buildId; cfreq.EC2InstanceType = EC2InstanceType.C4Large; cfreq.EC2InboundPermissions.Add(permission1); cfreq.EC2InboundPermissions.Add(permission2); cfreq.RuntimeConfiguration = new RuntimeConfiguration(); cfreq.RuntimeConfiguration.ServerProcesses = new List<ServerProcess>(); cfreq.RuntimeConfiguration.ServerProcesses.Add(serverProcess); cfreq.NewGameSessionProtectionPolicy = ProtectionPolicy.NoProtection; CreateFleetResponse cfres = await aglc.CreateFleetAsync(cfreq); // set fleet capacity var ufcreq = new UpdateFleetCapacityRequest(); ufcreq.MinSize = 0; ufcreq.DesiredInstances = 1; ufcreq.MaxSize = 1; ufcreq.FleetId = cfres.FleetAttributes.FleetId; UpdateFleetCapacityResponse ufcres = await aglc.UpdateFleetCapacityAsync(ufcreq); // set scaling rule (switch fleet off after 1 day of inactivity) // If [MetricName] is [ComparisonOperator] [Threshold] for [EvaluationPeriods] minutes, then [ScalingAdjustmentType] to/by [ScalingAdjustment]. var pspreq = new PutScalingPolicyRequest(); pspreq.Name = "Switch fleet off after 1 day of inactivity"; pspreq.MetricName = MetricName.ActiveGameSessions; pspreq.ComparisonOperator = ComparisonOperatorType.LessThanOrEqualToThreshold; pspreq.Threshold = 0.0; // double (don't use int) pspreq.EvaluationPeriods = 1435; // just under 1 day, 1435 appears to be the maximum permitted value now. pspreq.ScalingAdjustmentType = ScalingAdjustmentType.ExactCapacity; pspreq.ScalingAdjustment = 0; pspreq.FleetId = cfres.FleetAttributes.FleetId; PutScalingPolicyResponse pspres = await aglc.PutScalingPolicyAsync(pspreq); return cfres.FleetAttributes.FleetId; } } static async Task<string> CreateAlias(string name, string fleetId) { var config = new AmazonGameLiftConfig(); config.RegionEndpoint = region; using (AmazonGameLiftClient aglc = new AmazonGameLiftClient(config)) { // create a new alias var careq = new Amazon.GameLift.Model.CreateAliasRequest(); careq.Name = name; careq.Description = "Alias to direct traffic to " + fleetId; careq.RoutingStrategy = new RoutingStrategy { Type = RoutingStrategyType.SIMPLE, FleetId = fleetId, Message = "" }; Amazon.GameLift.Model.CreateAliasResponse cares = await aglc.CreateAliasAsync(careq); return cares.Alias.AliasId; } } private static async Task<string> UpdateAlias(string aliasUpdate, string fleetId) { var config = new AmazonGameLiftConfig(); config.RegionEndpoint = region; using (AmazonGameLiftClient aglc = new AmazonGameLiftClient(config)) { // create a new alias var uareq = new Amazon.GameLift.Model.UpdateAliasRequest(); uareq.AliasId = aliasUpdate; uareq.RoutingStrategy = new RoutingStrategy { Type = RoutingStrategyType.SIMPLE, FleetId = fleetId, Message = "" }; Amazon.GameLift.Model.UpdateAliasResponse cares = await aglc.UpdateAliasAsync(uareq); return cares.Alias.AliasId; } } } }