Sending custom logs and metrics to CloudWatch

I’m trying to send some logs and metrics from a game server running in GameLift to CloudWatch but I’ve been having some permissions problem.

Here’s the policy I’m using:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "cloudwatch:PutMetricData",
                "logs:DescribeLogGroups",
                "logs:CreateLogGroup",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}

I have a role with that policy attached, and here’s the trust relationship from that role.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "gamelift.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Here’s some ruby code I use for deploying the code:

puts "[#{env}][#{aws_gamelift_region}] creating #{fleet_type_to_name(fleet_type)} fleet"
fleet = client.create_fleet({
name: aws_fleet_name(fleet_type),
build_id: build.build_id,
ec2_instance_type: "c4.large",
ec2_inbound_permissions: ec2_inbound_permissions,
new_game_session_protection_policy: "FullProtection", # accepts NoProtection, FullProtection
runtime_configuration: {
  server_processes: [
    {
      launch_path: "/local/game/run.sh",
      concurrent_executions: 30,
    },
  ],
},
fleet_type: fleet_type, # accepts ON_DEMAND, SPOT
instance_role_arn: "arn:aws:iam::378349243592:role/gameLiftInstanceRole",
}).fleet_attributes

I’m getting the following error:

Unhandled Exception: System.AggregateException: One or more errors occurred. (User: arn:aws:sts::028872612690:assumed-role/DevAppStack-883c0077-56cc-43ef-bc5-AppInstanceRole-1XSRSEC11X8EW/i-0e1b1cee88757c557 is not authorized to perform: logs:CreateLogStream on resource: arn:aws:logs:sa-east-1:028872612690:log-group:Machinal:log-stream:i-0e1b1cee88757c557_5308_2019-08-12T23.28.13Z) ---> Amazon.CloudWatchLogs.AmazonCloudWatchLogsException: User: arn:aws:sts::028872612690:assumed-role/DevAppStack-883c0077-56cc-43ef-bc5-AppInstanceRole-1XSRSEC11X8EW/i-0e1b1cee88757c557 is not authorized to perform: logs:CreateLogStream on resource: arn:aws:logs:sa-east-1:028872612690:log-group:Machinal:log-stream:i-0e1b1cee88757c557_5308_2019-08-12T23.28.13Z ---> Amazon.Runtime.Internal.HttpErrorResponseException: Exception of type 'Amazon.Runtime.Internal.HttpErrorResponseException' was thrown.

Here’s the offending code:

public Log(string _logGroupName)
{
    logGroupName = _logGroupName;
    acwlc = new Amazon.CloudWatchLogs.AmazonCloudWatchLogsClient(Amazon.RegionEndpoint.USEast1);
    logEvents = new List<Amazon.CloudWatchLogs.Model.InputLogEvent>();
    logStreamName = GetInstanceID() + "_" + Process.GetCurrentProcess().Id + "_" + DateTime.UtcNow.ToString("yyyy-MM-ddTHH.mm.ssZ"); // time is not ISO8601, colon not allowed in stream name
    Console.WriteLine(":) LogStreamName: " + logStreamName);
    CreateLogStream();
}

private void CreateLogStream()
{
    var clsreq = new Amazon.CloudWatchLogs.Model.CreateLogStreamRequest(logGroupName, logStreamName);
    Amazon.CloudWatchLogs.Model.CreateLogStreamResponse clsres = acwlc.CreateLogStream(clsreq);
    if (clsres.HttpStatusCode != System.Net.HttpStatusCode.OK)
    {
        Console.WriteLine(":( LOGGING FAILED: CreateLogStream() returned " + clsres.HttpStatusCode.ToString());
        return;
    }
}

This is a workaround where I explicitly assume the role:

private static Amazon.SecurityToken.Model.Credentials GetTemporaryCredentials () {
    var amazonSecurityTokenServiceClient = new Amazon.SecurityToken.AmazonSecurityTokenServiceClient ();
    var assumeRoleRequest = new AssumeRoleRequest ();
    assumeRoleRequest.DurationSeconds = 1600;
    assumeRoleRequest.RoleSessionName = "Session_" + DateTime.UtcNow.ToString ("yyyy-MM-ddTHH.mm.ssZ");
    assumeRoleRequest.RoleArn = "arn:aws:iam::378349243592:role/gameLiftInstanceRole";
    var assumeRoleResponse = amazonSecurityTokenServiceClient.AssumeRoleAsync (assumeRoleRequest);
    assumeRoleResponse.Wait ();
    if (assumeRoleResponse.Status != TaskStatus.RanToCompletion) {
        Console.WriteLine (":( LOGGING FAILED: AssumeRoleAsync() returned " + assumeRoleResponse.Status.ToString ());
    }
    return assumeRoleResponse.Result.Credentials;
}

public Log(string _logGroupName)
{
    logGroupName = _logGroupName;
    acwlc = new Amazon.CloudWatchLogs.AmazonCloudWatchLogsClient(GetTemporaryCredentials ());
    logEvents = new List<Amazon.CloudWatchLogs.Model.InputLogEvent>();
    logStreamName = GetInstanceID() + "_" + Process.GetCurrentProcess().Id + "_" + DateTime.UtcNow.ToString("yyyy-MM-ddTHH.mm.ssZ"); // time is not ISO8601, colon not allowed in stream name
    Console.WriteLine(":) LogStreamName: " + logStreamName);
    CreateLogStream();
}

Is this the correct way of doing this?
Do I need to explicitly assume the role?
Do I need to get some new temporary credentials every time I call CreateLogStreamAsync, PutLogEventsAsync or PutMetricDataAsync?

Your ‘workaround’ is the correct way. You need to:

  • Create the Instance Role
  • Provide it during fleet creation
  • Your server then needs to assume that role when running on GameLift
  • Your server gets to address the AWS resources in your account as required

See https://docs.aws.amazon.com/gamelift/latest/developerguide/gamelift-sdk-server-resources.html

You do not need to get credentials everytime as STS credentials are valid for as long as you defined (see duration-second option in AssumeRole calls, by default its 1hr) .

A typically pattern is to have a credentials provider that can talk to STS and know how long the credentials are good for. It should refresh them as needed via STS

Most of the AWS SDKs have some form of provider like this ie https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/STSAssumeRoleSessionCredentialsProvider.Builder.html

Yes, that’s the link I used for my workaround. The doubt came because the original code was provided by a gamelift tech guy. Also the code I wrote didn’t feel right, but the typical pattern you mentioned there addresses this.

Thanks for answering!