Skip to main content

AWS EventBridge Triggering SSM Automation IAM Role Error

I recently wanted to create an Amazon EventBridge rule that will schedule an SSM Automation document.

A rule watches for certain events (cron in my case) and then routes them to AWS targets that you choose. You can create a rule that performs an AWS action automatically when another AWS action happens, or a rule that performs an AWS action regularly on a set schedule.

EventBridge needs permission to call SSM Start Automation Execution with the supplied Automation document and parameters. The rule will offer the generation of a new IAM role for this task.

In my case I received an error like below:

Error Output

The Automation definition for an SSM Automation target must contain an AssumeRole that evaluates to an IAM role ARN.

If you recieving this error you can create the role manually using the following CloudFormation Template.

AWSTemplateFormatVersion: '2010-09-09'
Description: AWS CloudFormation template IAM Roles for Event Bridge | SSM Automation

Resources:
  AutomationServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - events.amazonaws.com
          Action: sts:AssumeRole
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AmazonSSMAutomationRole
      Path: "/"
      RoleName: EventBridgeAutomationServiceRole

AWS ECS CloudFormation Fails – Unable to assume the service linked role.

I ran into an interesting issue when building a new ECS Cluster using CloudFormation. The CloudFormation stack would fail on Type: AWS::ECS::Service with error:

Unable to assume the service linked role. Please verify that the ECS service linked role exists. (Service: AmazonECS; Status Code: 400; Error Code: InvalidParameterException; Request ID: beadf3d5-3406-11e9-828d-b16cd52796ef)

Okay google, what’s this service linked role thingy?

A service-linked role is a unique type of IAM role that is linked directly to Amazon ECS. Service-linked roles are predefined by Amazon ECS and include all the permissions that the service requires to call other AWS services on your behalf.

https://docs.aws.amazon.com/AmazonECS/latest/developerguide/using-service-linked-roles.html

The first few times I ran my stack I assumed that this was for an IAM role that I was needing to assign to the AWS::ECS::Service to perform tasks much like a IamInstanceProfile of Type: AWS::EC2::Instance. When reviewing the available properties for Type: AWS::ECS::Service there was a Role definition:

  • Cluster
  • DeploymentConfiguration
  • DesiredCount
  • HealthCheckGracePeriodSeconds
  • LaunchType
  • LoadBalancers
  • NetworkConfiguration
  • PlacementConstraints
  • PlacementStrategies
  • PlatformVersion
  • Role
  • SchedulingStrategy
  • ServiceName
  • ServiceRegistries
  • TaskDefinition
Role - The name or ARN of an AWS Identity and Access Management (IAM) role that allows your Amazon ECS container agent to make calls to your load balancer.

I had some well defined Type: AWS::IAM::Role objects in my YAML for ECS execution and task roles but none of them were helping me with service linked account issue no matter how far I took the IAM policies.

Solution

To cut a long story and much googling short, the issue was nothing to do with my IAM policies but rather that the very first ECS cluster you create in the console using the getting started wizard creates the linked account in the backend. If your unlike me and read the full article about service linked roles you would have read:

when you create a new cluster (for example, with the Amazon ECS first run, the cluster creation wizard, or the AWS CLI or SDKs), or create or update a service in the AWS Management Console, Amazon ECS creates the service-linked role for you, if it does not already exist.

No mention in the above statement about CloudFormation. As per usual I jumped straight into a CloudFormation template without a test drive of the service and this time my attempt at being clever had given me a few moments of madness.

The easiest fix is to open up AWS CLI and run the following against your account once, then jump back into CloudFormation for YAML fun:

aws iam create-service-linked-role --aws-service-name ecs.amazonaws.com  

Resulting output:

{
    "Role": {
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17", 
            "Statement": [
                {
                    "Action": [
                        "sts:AssumeRole"
                    ], 
                    "Effect": "Allow", 
                    "Principal": {
                        "Service": [
                            "ecs.amazonaws.com"
                        ]
                    }
                }
            ]
        }, 
        "RoleId": "AROAIXGB2WBYGCXSPXY4O", 
        "CreateDate": "2019-02-19T05:55:58Z", 
        "RoleName": "AWSServiceRoleForECS", 
        "Path": "/aws-service-role/ecs.amazonaws.com/", 
        "Arn": "arn:aws:iam::112233445566:role/aws-service-role/ecs.amazonaws.com/AWSServiceRoleForECS"
    }
}

Job done. It all seemed so simple in retrospect.

YAML it Rhymes with Camel

I’ve blogged before about my passion for automation and the use of ARM templating in the Azure world to eradicate the burden of dull and mundane tasks from the daily routine of system administrators for whom I do consulting for.

I loath repetitive tasks, its in this space where subtle differences and inconsistency love to live. Recently I was asked to help out with a simple task, provisioning a couple of EC2 Windows servers in AWS. So in the spirit of infrastructure as code, I thought, there is no better time to try out AWS CloudFormation to describe my EC2 instances . I’ve actually used CloudFormation before in the past, but always describing my stack in JSON. CloudFormation also supports YAML, so challenge accepted and away I went. . .

So what is YAML anyway. . .Yet Another Mark-up Language. Interestingly its described at the official YAML website (https://yaml.org) as a “YAML Ain’t Markup Language” rather,  “human friendly data serialisation standard for all programming languages”.

What attracted me to YAML is its simplicity, there are no curly braces {} just indenting. Its also super easy to read. So if JSON looks a bit to cody for your liking, YAML may be a more palatable alternative.

So how would you get started? As you’d expect AWS have extensive CloudFormation documentation. The AWS::EC2::Instance resource is described here: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance.html#cfn-ec2-instance-volumes. You’ll notice that there is a Syntax description for JSON and YAML. The YAML looks like this:

Type: AWS::EC2::Instance
Properties: 
  Affinity: String
  AvailabilityZone: String
  BlockDeviceMappings: 
    - EC2 Block Device Mapping
  CreditSpecification: CreditSpecification
  DisableApiTermination: Boolean
  EbsOptimized: Boolean
  ElasticGpuSpecifications: [ ElasticGpuSpecification, ... ]
  ElasticInferenceAccelerators: 
    - ElasticInferenceAccelerator
  HostId: String
  IamInstanceProfile: String
  ImageId: String
  InstanceInitiatedShutdownBehavior: String
  InstanceType: String
  Ipv6AddressCount: Integer
  Ipv6Addresses:
    - IPv6 Address Type
  KernelId: String
  KeyName: String
  LaunchTemplate: LaunchTemplateSpecification
  LicenseSpecifications: 
    - LicenseSpecification
  Monitoring: Boolean
  NetworkInterfaces: 
    - EC2 Network Interface
  PlacementGroupName: String
  PrivateIpAddress: String
  RamdiskId: String
  SecurityGroupIds: 
    - String
  SecurityGroups: 
    - String
  SourceDestCheck: Boolean
  SsmAssociations: 
    - SSMAssociation
  SubnetId: String
  Tags: 
    - Resource Tag
  Tenancy: String
  UserData: String
  Volumes: 
    - EC2 MountPoint
  AdditionalInfo: String

With this as a starting point I was quickly able to build a EC2 instance and customise my YAML so as to do some extra things.

If you’ve got this far and YAML is starting to look like it might be the ticket for you, its worth familiarising yourself with the CloudFormation built-in functions. You can use these to do things like assign values to properties that are not available until runtime.

Fn::Base64
Fn::Cidr
Condition Functions
Fn::FindInMap
Fn::GetAtt
Fn::GetAZs
Fn::Join
Fn::Select
Fn::Split
Fn::Sub
Fn::Transform
Ref

The link to the complete Intrinsic Function Reference can be found here: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html

With a learning curve of a couple of hours including a bit of googling and messing around I was able to achieve my goal. I built an EC2 instance, applied tagging, installed some Windows features post build via a PowerShell script (downloaded from S3 and launched with AWS::CloudFormation::Init cfn-init.exe), all without having to logon to the server or touch the console. Here is a copy of my YAML. . .

AWSTemplateFormatVersion: "2010-09-09"
Description: CloudFormation Template to deploy an EC2 instance
Parameters: 
  Hostname: 
    Type: String
    Description: Hostname - maximum 15 characters
    MaxLength: '15'    
  LatestAmiId :
    Type: 'AWS::SSM::Parameter::Value'
    Default: /aws/service/ami-windows-latest/Windows_Server-2019-English-Full-Base
  InstanceSize: 
    Type: String
    Description: Instance Size
    Default: t2.micro
    AllowedValues:
      - "t2.micro" 
      - "t2.small"
      - "t2.medium"
  AvailabilityZone:
    Type: String
    Description: Default AZ
    AllowedValues: 
      - ap-southeast-2a
      - ap-southeast-2b
      - ap-southeast-2c
    Default: ap-southeast-2a
  KeyPair: 
    Type: String
    Description: KeyPair Name
    Default: jtwo
  S3BucketName:
    Default: NotARealBucket
    Description: S3 bucket containing boot artefacts
    Type: String
  
  # tag values
  awPurpose: 
    Type: String
    Description: A plain English description of what the object is for.
    Default: WindowsServer2019 Domain Controller
  awChargeTo: 
    Type: String
    Description: Billing Code for charge back of resource.
    Default: IT-123
  awRegion: 
    Type: String
    Description: Accolade Wines Region not AWS. 
    Default: Australia
  awExpiry: 
    Type: String
    Description: The date when the resource(s) can be considered for decommissioning.
    Default: 01-01-2022
  awBusinessSegment: 
    Type: String
    Description: Agency code.
    Default: ICT
  awEnvironment: 
    Type: String
    Description: Specific environment for resource.
    AllowedValues: 
      - prod
      - prodServices
      - nonprod
      - uat
      - dev
      - test 
  awApplication: 
    Type: String
    Description: A single or multiple word with the name of the application that the infrastructure supports. "JDE", "AD", "Apache", "Utility", "INFOR", "PKI".
    Default: AD
Mappings:
  SubnetMap: 
    ap-southeast-2a:
      prodServices: "subnet-idGoesHere"
    ap-southeast-2b:
      prodServices: "subnet-idGoesHere"
    ap-southeast-2c:
      prodServices: "subnet-idGoesHere"
      
# Resources
Resources:
  # IAM Instance Profile
  Profile:
    Type: 'AWS::IAM::InstanceProfile'
    Properties:
      Roles:
        - !Ref HostRole
      Path: /
      InstanceProfileName: !Join
        - ''
        - - 'instance-profile-'
          - !Ref S3BucketName
  HostRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: !Join
        - ''
        - - 'role-s3-read-'
          - !Ref S3BucketName
      Policies:
        - PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                  - 's3:GetObject'
                Resource: !Join
                  - ''
                  - - 'arn:aws:s3:::'
                    - !Ref S3BucketName
                    - '/*'
                Effect: Allow
          PolicyName: s3-policy-read
      Path: /
      AssumeRolePolicyDocument:
        Statement:
          - Action:
              - 'sts:AssumeRole'
            Principal:
              Service:
                - ec2.amazonaws.com
            Effect: Allow
        Version: 2012-10-17  
  # ENI
  NIC1:
    Type: AWS::EC2::NetworkInterface
    Properties: 
      Description: !Sub 'ENI for EC2 instance: ${Hostname}-${awEnvironment}'
      GroupSet:
          - sg-050cadbf0e159b0ac
      SubnetId: !FindInMap [SubnetMap, !Ref AvailabilityZone, !Ref awEnvironment]
      Tags:
        - Key: Name
          Value: !Sub '${Hostname}-eni'
  
  # EC2 Instance
  Instance:
    Type: 'AWS::EC2::Instance'
    Metadata:
      'AWS::CloudFormation::Authentication':
        S3AccessCreds:
          type: S3
          buckets:
            - !Ref S3BucketName
          roleName: !Ref HostRole
      'AWS::CloudFormation::Init':
        configSets: 
          config:
            - get-files 
            - configure-instance
        get-files:
          files:
            'c:\s3-downloads\scripts\Add-WindowsFeature.ps1':
              source: https://NotARealBucket.s3.amazonaws.com/scripts/Add-WindowsFeature.ps1
              authentication: S3AccessCreds
        configure-instance:
          commands:
            1-set-powershell-execution-policy:
              command: >-
                powershell.exe -Command "Set-ExecutionPolicy UnRestricted -Force"
              waitAfterCompletion: '0'
            2-rename-computer:
              command: !Join
                - ''
                - - >-
                  -  powershell.exe -Command "Rename-Computer -Restart -NewName "
                  -  !Ref Hostname
              waitAfterCompletion: forever  
            3-install-windows-components:
              command: >-
                powershell.exe -Command "c:\s3-downloads\scripts\Add-WindowsFeature.ps1"
              waitAfterCompletion: '0'
    Properties:
      DisableApiTermination: 'false'
      AvailabilityZone: !Sub "${AvailabilityZone}"
      InstanceInitiatedShutdownBehavior: stop
      IamInstanceProfile: !Ref Profile
      ImageId: !Ref LatestAmiId
      InstanceType: !Sub "${InstanceSize}"
      KeyName: !Sub "${KeyPair}"
      UserData: !Base64
        'Fn::Join': 
          - ''
          - - "\n"
            - "cfn-init.exe "
            - " --stack "
            - "Ref": "AWS::StackId"
            - " --resource Instance"
            - " --region "
            - "Ref": "AWS::Region"
            - " --configsets config"
            - " -v \n"
            - "cfn-signal.exe  "
            - " ---exit-code 0"
            - " --region "
            - "Ref": "AWS::Region"
            - " --resource Instance" 
            - " --stack "
            - "Ref": "AWS::StackName"
            - "\n"           
            - "\n"
      Tags:
        - Key: Name
          Value: !Sub "${Hostname}"
        - Key: awPurpose
          Value: !Sub "${awPurpose}"
        - Key: awChargeTo
          Value: !Sub "${awChargeTo}"
        - Key: awRegion
          Value: !Sub "${awRegion}"
        - Key: awExpiry
          Value: !Sub "${awExpiry}"
        - Key: awBusinessSegment
          Value: !Sub "${awBusinessSegment}"
        - Key: awEnvironment
          Value: !Sub "${awEnvironment}"
        - Key: awApplication
          Value: !Sub "${awApplication}"
      NetworkInterfaces:
        - NetworkInterfaceId: !Ref NIC1
          DeviceIndex: 0
Outputs:
  InstanceId:
    Description: 'InstanceId'
    Value: !Ref Instance
    Export:
      Name: !Sub '${Hostname}-${awEnvironment}-InstanceId'
  InstancePrivateIP:
    Description: 'InstancePrivateIP'
    Value: !GetAtt Instance.PrivateIp
    Export:
      Name: !Sub '${Hostname}-${awEnvironment}-InstancePrivateIP'

So my question now is, why doesn’t Azure also support YAML?