Search code examples
amazon-web-servicesaws-cloudformationamazon-iamamazon-ecs

AWS region considerations for creating ECS ecsTaskExecutionRole via CloudFormation


If I manually run an ECS Fargate task via the AWS console, AWS is nice and automatically creates an ECS task execution IAM role for me, which I can then even reference in CloudFormation templates, e.g.:

ExecutionRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole"

This role uses the AWS-managed AmazonECSTaskExecutionRolePolicy. The above documentation explains how I can create this role via the console or via the CLI. Basically you create this role (or get AWS to create it for you) a single time, and then you can reference it forever in your CloudFormation templates. The ecsTaskExecutionRole AWS creates automatically has no reference to any AWS region, nor does the documentation mention any thing about region considerations for the role if creating via the console or via the CLI.

But I don't want to create the ecsTaskExecutionRole manually. If I create a new AWS account, I want to simply deploy a new CloudFormation stack without worrying that I have to first go in and manually configure something. (That is, after all, the central raison d'être of CloudFormation in the first place!) AWS doesn't tell me how, but after reading the documentation for AWS::IAM::Role I believe it's as simple as the following. (The documentation notes that creating the role via CloudFormation requires me to specify CAPABILITY_IAM somewhere, such as using the CLI, but that doesn't seem difficult.)

  EcsTaskExecutionRole:
    Type: AWS::IAM::Role
    DeletionPolicy: Retain
    Properties:
      Description: AWS managed policy role for executing tasks on ECS.
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - ecs-tasks.amazonaws.com
            Action: 
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
      RoleName: ecsTaskExecutionRole

Note that I set the deletion policy to Retain. This way all my CloudFormation templates can create the role once as needed—just as AWS itself does when I run an ECS Fargate task via the console—and just leave it there.

However in the documentation I read this foreboding warning:

Naming an IAM resource can cause an unrecoverable error if you reuse the same template in multiple Regions. To prevent this, we recommend using Fn::Join and AWS::Region to create a Region-specific name, as in the following example: {"Fn::Join": ["", [{"Ref": "AWS::Region"}, {"Ref": "MyResourceName"}]]}.

The documentation for CAPABILITY_NAMED_IAM similarly says:

If your template contains custom named IAM resources, don't create multiple stacks reusing the same template. IAM resources must be globally unique within your account. If you use the same template to create multiple stacks in different Regions, your stacks might share the same IAM resources, rather than each having a unique one. Shared resources among stacks can have unintended consequences from which you can't recover. For example, if you delete or update shared IAM resources in one stack, you will unintentionally modify the resources of other stacks.

But the ecsTaskExecutionRole that AWS creates doesn't indicate any region, and I presume that this role I would create via CloudFormation would be identical to that role. What's the problem?

It is my interpretation that the scary language is simply saying (albeit not so clearly) that I should be careful not to have one CloudFormation template delete the role if it's being used in another region (although I wouldn't want to delete the role if another CloudFormation template in the same region is using it either, so I'm not sure what regions have to do with it). So as long as I use DeletionPolicy: Retain when creating the role via CloudFormation, everything should be fine—or as least no worse than using the ecsTaskExecutionRole role that AWS creates automatically when I manually use ECS in the console.

Am I missing something? Is there any way creating an ecsTaskExecutionRole via CloudFormation using DeletionPolicy: Retain would be any different than coaxing AWS to create the same role by running an ECS task via the console? Is there any downside I haven't considered?


Solution

  • From the comments to my question (thanks to everyone who responded), I understand that managing a single account-wide ECS task execution role via CloudFormation may bring about extra weird behavior than creating it via other means. Moreover the AWS approach of creating a single account-wide role for executing ECS tasks isn't the best approach for multiple CloudFormation stacks anyway.

    But recently I've discovered that there are other considerations that sometimes make it preferable to create a task execution role for each service, not just for each CloudFormation template. For initially after posting this question I was creating a single task-execution role for an entire testing environment and then using that role for various services. But if a service injects passwords from Secrets Manager, you'll need an even more fine-grained definitions.

    Here is how you could set up a single ECS task execution role in CloudFormation to be used by all your services:

    Resources:
    
      EcsTaskExecutionRole:
        Type: AWS::IAM::Role
        Properties:
          RoleName: !Sub "${AWS::StackName}-${AWS::Region}-ecsTaskExecutionRole"
          Description: "Role for executing tasks on ECS in region ${AWS::Region}."
          AssumeRolePolicyDocument:
            Version: 2012-10-17
            Statement: 
              - Effect: Allow
                Principal: 
                  Service: 
                    - ecs-tasks.amazonaws.com
                Action: 
                  - sts:AssumeRole
          ManagedPolicyArns:
            - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
    

    You could even export that for use in related (e.g. per-service) CloudFormation templates:

    Outputs:
    
      EcsTaskExecutionRoleArn:
        Description: The ARN of the IAM role for executing ECS tasks.
        Value: !Ref EcsTaskExecutionRole
        Export:
          Name: !Sub "${AWS::StackName}-${AWS::Region}:ecsTaskExecutionRoleArn"
    

    That works fine until you need to inject secrets into a service. The AWS managed AmazonECSTaskExecutionRolePolicy allows these actions:

    • ecr:GetAuthorizationToken
    • ecr:BatchCheckLayerAvailability
    • ecr:GetDownloadUrlForLayer
    • ecr:BatchGetImage
    • logs:CreateLogStream
    • logs:PutLogEvents

    Notice that the secretsmanager:GetSecretValue action is not among them. So let's say that you want to want to inject passwords from Secrets Manager for a Spring Boot application to talk to DocumentDB, as explained in Using Secrets Manager and illustrated in Disable Spring Boot Data MongoDB retryable writes in AWS ECS Fargate with CloudFormation :

              Secrets:
                - Name: SPRING_DATA_MONGODB_USERNAME
                  ValueFrom: !Sub "${DbCredentials}:username::"
                - Name: SPRING_DATA_MONGODB_PASSWORD
                  ValueFrom: !Sub "${DbCredentials}:password::"
    

    This won't work using the EcsTaskExecutionRole defined above. You'll need to define a separate role that includes secretsmanager:GetSecretValue as explained at IAM policy examples for secrets in AWS Secrets Manager. You could go ahead and add secretsmanager:GetSecretValue to your custom role, but you probably don't want to let all services have access to the secrets meant for only the other services (i.e. PoLP). In that case it may be best to define a separate role for each service needing secrets injected. The following example assumes that you're going to be injecting the AWS::SecretsManager::Secret DbCredentials referenced above (and which is illustrated at https://stackoverflow.com/a/75999386)

      ServiceTaskExecutionRole:
        Type: AWS::IAM::Role
        Properties:
          RoleName: !Sub "my-service-${AWS::Region}-taskExecRole"
          AssumeRolePolicyDocument:
            Version: 2012-10-17
            Statement: 
              - Effect: Allow
                Principal: 
                  Service: 
                    - ecs-tasks.amazonaws.com
                Action: 
                  - sts:AssumeRole
          Policies:
            - PolicyName: !Sub "my-service-${AWS::Region}-secret-access-policy"
              PolicyDocument:
                Version: 2012-10-17
                Statement: 
                - Effect: Allow
                  Action:
                    - secretsmanager:GetSecretValue
                  Resource:
                    - !Ref DbCredentials
          ManagedPolicyArns:
            - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
    

    This sets up the same ECS task execution role shown at the beginning of this answer, with permissions to access only the DbCredentials secret.

    If someone knows how to use a shared EcsTaskExecutionRole and just add the permission to retrieve DbCredentials on a case-by-case basis without creating a new role, please let me know.