Cloudformation-coverage-roadmap: AWS::IoT::RoleAlias

Created on 1 Aug 2019  路  7Comments  路  Source: aws-cloudformation/cloudformation-coverage-roadmap

1. Title

AWS::IoT::RoleAlias is currently not supported

2. Scope of request

b) new resource type for an existing service is desired

IoT Core allows for the creation of RoleAlias entities which map to a given IAM Role:

https://docs.aws.amazon.com/iot/latest/apireference/API_CreateRoleAlias.html

This feature is not currently supported within CloudFormation AWS::IoT, but would be very helpful to have for environments where IoT enabled devices are using IoT to obtain temporary AWS credentials.

3. Expected behavior

Samples:
In Create, it should create a Role Alias object in IoT, with given roleArn, and credentialDurationSeconds attributes.

In Update, change the altered attribute of the Role Alias:

https://docs.aws.amazon.com/iot/latest/apireference/API_UpdateRoleAlias.html

In Delete, delete the Role Alias:

https://docs.aws.amazon.com/iot/latest/apireference/API_DeleteRoleAlias.html

4. Suggest specific test cases

  • Pass IAM Role Arn as a 'roleArn' parameter as a string or as a !Ref

  • Pass credentialDurationSeconds parameter as an integer. Minimum value of 900. Maximum value of 3600.

6. Category (required) - Will help with tagging and be easier to find by other users to +1

Use the categories as displayed in the AWS Management Console (simplified):

  • Other (IoT, Migration, Budgets...)
internet of things

Most helpful comment

Can't wait for this

All 7 comments

Any update? This shouldn't take over a year...

come on guys. hurry up

Can't wait for this

Need this aswell :) also in CDK

Eventually i had to implement it as a CustomResource, as i'm using CDK - i'll just share the code with several notes you should take into account - this is the custom lambda i'm running as singleton lambda:

import logging

import boto3
import cfnresponse

def main(event, context):
    client = boto3.client('iot')
    logging.getLogger().setLevel(logging.INFO)
    physical_id = "role_alias_iot"


    try:
        logging.info('Input event: %s', event)
        request_type = event['RequestType']

        alias_name = event['ResourceProperties']['Role_alias_name']
        iam_role_arn = event['ResourceProperties']['Role_arn']

        # Check if this is a Create
        if request_type == 'Create':
            client.create_role_alias(
                roleAlias=alias_name,
                roleArn=iam_role_arn)
        elif request_type == 'Update':
            client.update_role_alias(
                roleAlias=alias_name,
                roleArn=iam_role_arn)
        elif request_type == 'Delete':
            client.delete_role_alias(
                roleAlias=alias_name)

        cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physical_id)
    except Exception as e:
        logging.exception(e)
        # cfnresponse's error message is always "see CloudWatch"
        cfnresponse.send(event, context, cfnresponse.FAILED, {}, physical_id)

The permissions i gave to the lambda (as initial policy) is:

"PolicyDocument": {
          "Statement": [
            {
              "Action": [
                "IoT:CreateRoleAlias",
                "IoT:DeleteRoleAlias",
                "IoT:UpdateRoleAlias"
              ],
              "Effect": "Allow",
              "Resource": "*"
            },
            {
              "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
              ],
              "Effect": "Allow",
              "Resource": "*"
            },
            {
              "Action": "iam:PassRole",
              "Effect": "Allow",
              "Resource": {
                "Fn::GetAtt": [
                  "iot2awspolicyCE236506",
                  "Arn"
                ]
              }
            }
          ],
          "Version": "2012-10-17"
        },

Thanks to @arielb135 's comment, I found that it is also possible to use AwsCustomResource to manage resources not yet available to the CDK by automatically creating a Lambda with the AWS SDK to create those resources. You may use the AwsCustomResource with any programming language supported by the CDK, but inside the Lambda it will use the createRoleAlias of the JavaScript SDK. All you need to know from JS SDK is the proper service and action because those are case-sensitive.

In this example, it creates a role for logging to CloudWatch as usual, and then it creates an IoT Role Alias using the AWS SDK. The role alias can be used in IoT devices to do X.509 auth.

const loggerRole = new Role(this, 'LoggerRole', {
      assumedBy: new ServicePrincipal('credentials.iot.amazonaws.com'),
      roleName: 'device-logger-role',
    });

loggerRole.addToPolicy(new PolicyStatement({
  resources: ['*'],
  actions: [
    'cloudwatch:PutMetricData', 
    'logs:CreateLogGroup',
    'logs:CreateLogStream',
    'logs:DescribeLogGroups',
    'logs:DescribeLogStreams',
    'logs:PutLogEvents',
  ],
}));

// Creating Role alias is not supported by CDK yet
// AWS Custom Resource automatically uses a lambda to call the JS AWS SDK to create the Role Alias
const loggerRoleAliasName = 'device-logger-role-alias';
const loggerRoleAlias = new AwsCustomResource(this, 'LoggerRoleAlias', {
  onCreate: {
    service: 'Iot',
    action: 'createRoleAlias', 
    parameters: {
      roleAlias: loggerRoleAliasName,
      roleArn: loggerRole.roleArn,
    },
    physicalResourceId: PhysicalResourceId.of(loggerRoleAliasName),
  },
  onUpdate: {
    service: 'Iot',
    action: 'updateRoleAlias', 
    parameters: {
      roleAlias: loggerRoleAliasName,
      roleArn: loggerRole.roleArn,
    },
    physicalResourceId: PhysicalResourceId.of(loggerRoleAliasName),
  },
  onDelete: {
    service: 'Iot',
    action: 'deleteRoleAlias', 
    parameters: {
      roleAlias: loggerRoleAliasName,
    },
    physicalResourceId: PhysicalResourceId.of(loggerRoleAliasName),
  },
  policy: AwsCustomResourcePolicy.fromStatements([
    new PolicyStatement({
      effect: Effect.ALLOW,
      actions: ['iot:CreateRoleAlias', 'iot:UpdateRoleAlias', 'iot:DeleteRoleAlias'],
      resources: ['*']
    }), 
    new PolicyStatement({
      effect: Effect.ALLOW,
      actions: ['iam:GetRole','iam:PassRole',],
      resources: [loggerRole.roleArn]
    })
  ]),
  installLatestAwsSdk: false, // avoid installing the lastest version of the sdk (saves 60 seconds)
  logRetention: RetentionDays.ONE_WEEK,
});

I've created a construct that actually helps in creating it, i'll add it also here (for CDK purposes) - if it helps anyone.

import json
import os
from typing import List

from aws_cdk import (
    aws_cloudformation as cfn,
    aws_lambda as lambda_,
    core,
    aws_logs as logs,
)
from aws_cdk.aws_iam import PolicyStatement, Role, IPrincipal


class CustomResource(core.Construct):

    def __init__(
        self, scope: core.Construct, id: str, Description: str, uuid: str, handler_path: str, policies: List[PolicyStatement], event_properties, pass_role_grants: List[IPrincipal] = None
    ) -> None:
        '''
        @param self:
        @param scope: scope of this construct
        @param id: id of the construct
        @param Description: Lambda description
        @param uuid: Unique ID that should be constant - this makes sure the lambda isnt created multiple times (on stack changes)
        @param handler_path: the python code path of the lambda - make sure the main lambda function is defined as main(event, context)
        @param policies: list of policies the lambda will run with, note that every role you use in your lambda code should be added to pass_role_grants
        @param event_properties - python object that contains parameters to pass to the lambda, note that it adds camelCase to the parameters (i.e. bla => Bla) - not sure why, but not critical
        @param pass_role_grants - those are roles/users (IPrincipals) that should be added here if you use them in your lambda code
        @return:
        '''
        super().__init__(scope, id)

        with open(handler_path, encoding="utf-8") as fp:
            code_body = fp.read()

        singleton_func = lambda_.SingletonFunction(
                    self,
                    "Singleton",
                    description=Description,
                    uuid=uuid,
                    code=lambda_.InlineCode(code_body),
                    handler="index.main",
                    timeout=core.Duration.seconds(300),
                    runtime=lambda_.Runtime.PYTHON_3_7,
                    initial_policy=policies,
                    log_retention=logs.RetentionDays.ONE_DAY,
                )

        # Any principals that are used by this lambda MUST grant THIS lambda the pass role permission
        if pass_role_grants:
            for role in pass_role_grants:
                role.grant_pass_role(singleton_func.role)

        resource = cfn.CustomResource(
            self,
            "Resource",
            provider=cfn.CustomResourceProvider.lambda_(
                singleton_func
            ),
            properties=event_properties.__dict__,
        )
        # response
        self.response = resource.get_att("Response")


Usage:

create_alias = CustomResource(self, "createrolealiasid", "Creates role alias in IOT",
                                      "27fb8fb9-2d7d-42ca-932d-7e6ca15be957",
                                      "create_role_alias_lambda.py", [PolicyStatement(effect=Effect.ALLOW, actions=[
                "IoT:CreateRoleAlias",
                "IoT:DeleteRoleAlias",
                "IoT:UpdateRoleAlias"
            ], resources=['*']),
                                                                      PolicyStatement(effect=Effect.ALLOW, actions=[
                                                                          "logs:CreateLogGroup",
                                                                          "logs:CreateLogStream",
                                                                          "logs:PutLogEvents"
                                                                      ], resources=['*'])
                                                                      ],
        RoleAliasProperties(role_alias_name="aws-access", role_arn=assumed_iot_role.role_arn), [assumed_iot_role])

assumed_iot_role - is just a simple IAM Role that i use to access AWS resources, which contains the IOT specific parameters (For example - usage of ${{credentials-iot:ThingTypeName}}

RoleAliasProperties is just some class i've created to hold the parameters..

class RoleAliasProperties:
    def __init__(self, role_alias_name, role_arn):
        self.role_alias_name = role_alias_name
        self.role_arn = role_arn

hope it helps anyone

Just a note that if you're creating a custom resource from scratch, it's often worth it to go with the newer CloudFormation resource provider system, with which you can register proper CloudFormation types.

Was this page helpful?
0 / 5 - 0 ratings