If you’ve been following my blog in the last months, you might remember my last post (you can read it here), where I present an alternative of how to import manually created AWS resources using Terraform. One of the points I mentioned for using Terraform was because I was willing to play with TF import CloudFormation doesn’t provide such feature: either you create your resources with CloudFormation since the beginning or you have to deal managing them manually (or recreate afterward using CF).
Turns out that on November 13rd, 2019, AWS announced the support to Resource Import, which makes my previous statement outdated.
So, let’s see how CloudFormation Resouce Import works and how we can import our manually created role & policies using it.
The setup
Before we start, we need to have a manually created role with policies. Similar to the previous post, we’re gonna create a role (foobar
) with three policies:
AmazonS3ReadOnlyAccess
, an AWS managed policy giving read-only access to S3 bucketsfoobar-user-managed-policy
, a user managed policy giving full tag permissions for S3 bucketsfoobar-inline-policy
, an inline policy attached to the role giving list access for S3 buckets
So, heading to AWS CLI:
# Define the assume role policy document
$ cat > assume-role-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"AWS": "*"
}
}
]
}
EOF
# Create the IAM role
$ aws iam create-role --role-name foobar --assume-role-policy-document file://assume-role-policy.json
# Attach the "AmazonS3ReadOnlyAccess" policy to the role
$ aws iam attach-role-policy --role-name foobar --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
# Define the user managed role document
$ cat > user-managed-policy-document.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:DeleteObjectTagging",
"s3:PutBucketTagging",
"s3:ReplicateTags",
"s3:PutObjectVersionTagging",
"s3:PutObjectTagging",
"s3:DeleteObjectVersionTagging"
],
"Resource": "*"
}
]
}
EOF
# Create the "foobar-user-managed-policy" policy and attach to the role
$ aws iam create-policy --policy-name foobar-user-managed-policy --policy-document file://user-managed-policy-document.json
# Attach the user managed policy to the role
$ aws iam attach-role-policy --role-name foobar --policy-arn "[ARN of the policy created in the previous step]"
# Define the inline policy document
$ cat > foobar-inline-policy-document.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets",
"s3:ListBucket",
"s3:HeadBucket"
],
"Resource": "*"
}
]
}
EOF
# Create the "foobar-inline-policy" policy and attach to the role
$ aws iam put-role-policy --role-name foobar --policy-name foobar-inline-policy --policy-document file://foobar-inline-policy-document.json
By now, you should have a role setup similar to the following:
Role & policies created, let’s move to the resource import.
How CloudFormation Resource Import works
Resource import works by adding existing resources in a CloudFormation stack. This stack can be created in advance or during the import time. Why is the stack necessary? Because the resources are imported via changeset on the stack. The distinction between a regular changeset and an import one is defined by the changeset type, provided in the creation of the changeset: in the case of an import, the type is set to IMPORT
. Additionally, when an import changeset is created, you need to provide the parameter --resources-to-import
, which informs CloudFormation what physical resources are being imported and to which logical resources in the stack they will be attached.
Importing the role
Let’s start importing the role with no policies.
First, let’s define our initial CloudFormation stack:
# role.yaml
Resources:
Role:
Type: AWS::IAM::Role
DeletionPolicy: Retain # This is mandatory for import operations
Properties:
RoleName: "foobar"
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: "sts:AssumeRole"
Principal:
AWS: "*"
Now let’s create the stack, informing CloudFormation that the Role
resource above will be used to import our existing role:
$ aws cloudformation create-change-set \
--stack-name foobar \
--template-body file://role.yaml \
--change-set-name ImportRoleChangeSet \
--change-set-type IMPORT \
--resources-to-import '[{"ResourceType": "AWS::IAM::Role", "LogicalResourceId": "Role", "ResourceIdentifier": {"RoleName": "foobar"}}]' \
--capabilities CAPABILITY_NAMED_IAM
OK, a lot of stuff happening here, so let’s go by steps:
- The first four lines are self-explanatory: a CloudFormation changeset is created and, since the stack still doesn’t exist, we provide the initial template (
role.yaml
) - Fifth and sixth lines are related to the import action: the
change-set-type
is set toIMPORT
and theresources-to-import
defines the resource to be imported (thefoobar
role), mapping to the logical resourceRole
in our stack template above. The format for this field follows a JSON specification defined by CloudFormation and is described here. - The last line adds the
CAPABILITY_NAMED_IAM
capability to the stack. It’s required because the stack is changing IAM resources
You should now have a new CloudFormation stack (foobar
) and a changeset (ImportRoleChangeSet
). The stack remains in the REVIEW_IN_PROGRESS
state since we need to decide to move forward with the changeset or not.
Before proceeding, let’s check the changeset and see what CloudFormation identifies as change:
$ aws cloudformation describe-change-set --stack-name foobar --change-set-name ImportRoleChangeSet
{
"Changes": [
{
"Type": "Resource",
"ResourceChange": {
"Action": "Import",
"LogicalResourceId": "Role",
"PhysicalResourceId": "foobar",
"ResourceType": "AWS::IAM::Role",
"Scope": [],
"Details": []
}
}
],
"ChangeSetName": "ImportRoleChangeSet",
"ChangeSetId": "[OMITTED]",
"StackId": "[OMITTED]",
"StackName": "foobar",
"Description": null,
"Parameters": null,
"CreationTime": "2019-12-22T22:30:02.743Z",
"ExecutionStatus": "AVAILABLE",
"Status": "CREATE_COMPLETE",
"StatusReason": null,
"NotificationARNs": [],
"RollbackConfiguration": {},
"Capabilities": [
"CAPABILITY_NAMED_IAM"
],
"Tags": null
}
As described in the Changes
section, CloudFormation is importing the physical resource (our foobar
role) in the stack’s logical resource (Role
). This is what we want, so let’s execute the changeset:
$ aws cloudformation execute-change-set --stack-name foobar --change-set-name ImportRoleChangeSet
The stack now changes to the IMPORT_IN_PROGRESS
state and, as soon the import is done, it goes to the IMPORT_COMPLETE
state. At this point, your stack should contain a single resource, which is the role we just imported:
Finally, let’s check if there is any drift between our role definition and the imported role:
# Create the drift check
$ aws cloudformation detect-stack-drift --stack-name foobar
# Display the drift check result
$ aws cloudformation describe-stack-drift-detection-status --stack-drift-detection-id "[StackDriftDetectionId returned by the previous command]"
{
"StackId": "[OMITTED]",
"StackDriftDetectionId": "[OMITTED]",
"StackDriftStatus": "IN_SYNC",
"DetectionStatus": "DETECTION_COMPLETE",
"DriftedStackResourceCount": 0,
"Timestamp": "2019-12-22T22:43:55.390Z"
}
Great! We now have the role imported and no drifts, but also no policies. So, let’s move ahead and import our next resource: the inline policy.
Importing the Inline Policy
For the inline policy, we follow the same procedure performed in the role:
- Update the stack definition
- Create the changeset
- Execute the changeset
The difference is that, for CloudFormation, the inline policy is part of the IAM::Role
, resource, so no real import operation is performed. Instead, we’re doing a regular changeset. Also, at the time I’m writing this post, inline policies aren’t detected by CloudFormation drifts (you can check the official relation of drift supported resources here).
So, let’s update our stack definition to include the inline policy:
# role.yaml
Resources:
Role:
Type: AWS::IAM::Role
DeletionPolicy: Retain
Properties:
RoleName: "foobar"
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: "sts:AssumeRole"
Principal:
AWS: "*"
Policies:
# Inline policy
- PolicyName: "foobar-inline-policy"
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- "s3:ListAllMyBuckets"
- "s3:ListBucket"
- "s3:HeadBucket"
Resource: "*"
Now, let’s create the changeset and check what will change on our stack:
# Create the changeset
$ aws cloudformation create-change-set \
--stack-name foobar \
--template-body file://role.yaml \
--change-set-name ImportInlinePolicyChangeset \
--capabilities CAPABILITY_NAMED_IAM
# Check the detected changes
$ aws cloudformation describe-change-set --stack-name foobar --change-set-name ImportInlinePolicyChangeset
{
"Changes": [
{
"Type": "Resource",
"ResourceChange": {
"Action": "Modify",
"LogicalResourceId": "Role",
"PhysicalResourceId": "foobar",
"ResourceType": "AWS::IAM::Role",
"Replacement": "False",
"Scope": [
"Properties"
],
"Details": [
{
"Target": {
"Attribute": "Properties",
"Name": "Policies",
"RequiresRecreation": "Never"
},
"Evaluation": "Static",
"ChangeSource": "DirectModification"
}
]
}
}
],
"ChangeSetName": "ImportInlinePolicyChangeset",
"ChangeSetId": "[OMITTED]",
"StackId": "[OMITTED]",
"StackName": "foobar",
"Description": null,
"Parameters": null,
"CreationTime": "2019-12-22T23:03:19.154Z",
"ExecutionStatus": "AVAILABLE",
"Status": "CREATE_COMPLETE",
"StatusReason": null,
"NotificationARNs": [],
"RollbackConfiguration": {},
"Capabilities": [
"CAPABILITY_NAMED_IAM"
],
"Tags": null
}
As expected, CloudFormation detected a change in our existing role, and the change is in the Policies
attribute (exactly where we added the inline policy).
All looks good, let’s execute the changeset:
$ aws cloudformation execute-change-set --stack-name foobar --change-set-name ImportInlinePolicyChangeset
This time, the stack transitions to the state UPDATE_IN_PROGRESS
and, then, UPDATE_COMPLETED
. You won’t see the inline policy in the resources list because CloudFormation treats inline policies as part of the role.
All good! Now we have our foobar
role and its inline policy imported. Let’s move to the user managed policy.
Importing the User Managed Policy
Since user managed roles have a dedicated logical resource on CloudFormation (i.e. they aren’t part of the IAM::Role
resource) we can perform an import operation.
However, this operation needs to be performed in two steps:
- Import the user managed policy
- Attach the imported policy to the role
The reason for this separation is because CloudFormation doesn’t support stack changes during import operations. So, while we’re importing the policy, we can’t change the role definition on the same step.
Let’s start our policy import operation, by updating our stack definition:
# role.yaml
Resources:
Role:
Type: AWS::IAM::Role
DeletionPolicy: Retain
Properties:
RoleName: "foobar"
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: "sts:AssumeRole"
Principal:
AWS: "*"
Policies:
- PolicyName: "foobar-inline-policy"
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- "s3:ListAllMyBuckets"
- "s3:ListBucket"
- "s3:HeadBucket"
Resource: "*"
# The user managed policy definition
UserManagedPolicy:
Type: AWS::IAM::ManagedPolicy
DeletionPolicy: Retain
Properties:
ManagedPolicyName: "foobar-user-managed-policy"
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- "s3:PutBucketTagging"
- "s3:ReplicateTags"
- "s3:PutObjectVersionTagging"
- "s3:PutObjectTagging"
- "s3:DeleteObjectVersionTagging"
Resource: "*"
And the respective changeset:
$ aws cloudformation create-change-set \
--stack-name foobar \
--change-set-name ImportUserManagedPolicyChangeSet \
--change-set-type IMPORT \
--resources-to-import '[{"ResourceType": "AWS::IAM::ManagedPolicy", "LogicalResourceId": "UserManagedPolicy", "ResourceIdentifier": {"PolicyArn": "[USER MANAGED POLICY ARN]"}}]' \
--template-body file://role.yaml \
--capabilities CAPABILITY_NAMED_IAM
Important: make sure to fill the policy ARN field in the command above with the ARN of the user managed policy. You can find the policy, along with the ARN, running
aws iam list-policies --scope Local
Now let’s check what will change when we apply our changeset:
$ aws cloudformation describe-change-set --stack-name foobar --change-set-name ImportUserManagedPolicyChangeSet
{
"Changes": [
{
"Type": "Resource",
"ResourceChange": {
"Action": "Import",
"LogicalResourceId": "UserManagedPolicy",
"PhysicalResourceId": "[OMITTED]",
"ResourceType": "AWS::IAM::ManagedPolicy",
"Scope": [],
"Details": []
}
}
],
"ChangeSetName": "ImportUserManagedPolicyChangeSet",
"ChangeSetId": "[OMITTED]",
"StackId": "[OMITTED]",
"StackName": "foobar",
"Description": null,
"Parameters": null,
"CreationTime": "2019-12-23T09:20:42.378Z",
"ExecutionStatus": "AVAILABLE",
"Status": "CREATE_COMPLETE",
"StatusReason": null,
"NotificationARNs": [],
"RollbackConfiguration": {},
"Capabilities": [
"CAPABILITY_NAMED_IAM"
],
"Tags": null
}
As expected, a new resource will be imported on the stack. So, let’s proceed with the changeset execution:
$ aws cloudformation execute-change-set --stack-name foobar --change-set-name ImportUserManagedPolicyChangeSet
Wait for CloudFormation to fully perform the import and you now might have two resources in the stack: the role and the user managed policy
Finally, let’s check for drifts between our stack description and the imported resource:
$ aws cloudformation detect-stack-drift --stack-name foobar
$ aws cloudformation describe-stack-resource-drifts --stack-name foobar
{
"StackId": "[OMITTED]",
"LogicalResourceId": "UserManagedPolicy",
"PhysicalResourceId": "[OMITTED]",
"ResourceType": "AWS::IAM::ManagedPolicy",
"ExpectedProperties": "{\"ManagedPolicyName\":\"foobar-user-managed-policy\",\"PolicyDocument\":{\"Statement\":[{\"Action\":[\"s3:PutBucketTagging\",\"s3:DeleteObjectVersionTagging\",\"s3:PutObjectTagging\",\"s3:ReplicateTags\",\"s3:PutObjectVersionTagging\"],\"Effect\":\"Allow\",\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}}",
"ActualProperties": "{\"ManagedPolicyName\":\"foobar-user-managed-policy\",\"PolicyDocument\":{\"Statement\":[{\"Action\":[\"s3:PutBucketTagging\",\"s3:DeleteObjectVersionTagging\",\"s3:PutObjectTagging\",\"s3:ReplicateTags\",\"s3:PutObjectVersionTagging\",\"s3:DeleteObjectTagging\"],\"Effect\":\"Allow\",\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}}",
"PropertyDifferences": [
{
"PropertyPath": "/PolicyDocument/Statement/0/Action/5",
"ExpectedValue": "null",
"ActualValue": "s3:DeleteObjectTagging",
"DifferenceType": "ADD"
}
],
"StackResourceDriftStatus": "MODIFIED",
"Timestamp": "2019-12-23T16:13:12.280Z"
}
Ops! Looks like we have a drift on our stack. By the PropertyDifferences
field, we can see that we forgot one action on our user managed role description: s3:DeleteObjectTagging
.
So, let’s fix this, by adjusting our stack description and running a new changeset
# role.yaml
Resources:
Role:
Type: AWS::IAM::Role
DeletionPolicy: Retain
Properties:
RoleName: "foobar"
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: "sts:AssumeRole"
Principal:
AWS: "*"
Policies:
- PolicyName: "foobar-inline-policy"
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- "s3:ListAllMyBuckets"
- "s3:ListBucket"
- "s3:HeadBucket"
Resource: "*"
UserManagedPolicy:
Type: AWS::IAM::ManagedPolicy
DeletionPolicy: Retain
Properties:
ManagedPolicyName: "foobar-user-managed-policy"
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- "s3:DeleteObjectTagging" # <--- the missing action
- "s3:PutBucketTagging"
- "s3:ReplicateTags"
- "s3:PutObjectVersionTagging"
- "s3:PutObjectTagging"
- "s3:DeleteObjectVersionTagging"
Resource: "*"
# Create the changeset
$ aws cloudformation create-change-set \
--stack-name foobar \
--template-body file://role.yaml \
--change-set-name ImportManagedPolicyChangeset \
--capabilities CAPABILITY_NAMED_IAM
# Execute the changeset
$ aws cloudformation execute-change-set --stack-name foobar --change-set-name ImportManagedPolicyChangeset
# Wait for the update to finish...
# Check for drifts
$ aws cloudformation detect-stack-drift --stack-name foobar
$ aws cloudformation describe-stack-resource-drifts --stack-name foobar
{
"StackId": "[OMITTED]",
"LogicalResourceId": "UserManagedPolicy",
"PhysicalResourceId": "[OMITTED]",
"ResourceType": "AWS::IAM::ManagedPolicy",
"ExpectedProperties": "{\"ManagedPolicyName\":\"foobar-user-managed-policy\",\"PolicyDocument\":{\"Statement\":[{\"Action\":[\"s3:PutBucketTagging\",\"s3:DeleteObjectVersionTagging\",\"s3:PutObjectTagging\",\"s3:ReplicateTags\",\"s3:PutObjectVersionTagging\",\"s3:DeleteObjectTagging\"],\"Effect\":\"Allow\",\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}}",
"ActualProperties": "{\"ManagedPolicyName\":\"foobar-user-managed-policy\",\"PolicyDocument\":{\"Statement\":[{\"Action\":[\"s3:PutBucketTagging\",\"s3:DeleteObjectVersionTagging\",\"s3:PutObjectTagging\",\"s3:ReplicateTags\",\"s3:PutObjectVersionTagging\",\"s3:DeleteObjectTagging\"],\"Effect\":\"Allow\",\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}}",
"PropertyDifferences": [],
"StackResourceDriftStatus": "IN_SYNC",
"Timestamp": "2019-12-23T16:20:17.461Z"
}
Drift solved! Now, let’s attach the policy to the role. This can be done by a simple stack update and respective changeset:
# role.yaml
Resources:
Role:
Type: AWS::IAM::Role
DeletionPolicy: Retain
Properties:
RoleName: "foobar"
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: "sts:AssumeRole"
Principal:
AWS: "*"
ManagedPolicyArns:
- !Ref UserManagedPolicy # <--- attach the user managed policy to the role
Policies:
- PolicyName: "foobar-inline-policy"
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- "s3:ListAllMyBuckets"
- "s3:ListBucket"
- "s3:HeadBucket"
Resource: "*"
UserManagedPolicy:
Type: AWS::IAM::ManagedPolicy
DeletionPolicy: Retain
Properties:
ManagedPolicyName: "foobar-user-managed-policy"
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- "s3:DeleteObjectTagging"
- "s3:PutBucketTagging"
- "s3:ReplicateTags"
- "s3:PutObjectVersionTagging"
- "s3:PutObjectTagging"
- "s3:DeleteObjectVersionTagging"
Resource: "*"
# Create the changeset
$ aws cloudformation create-change-set \
--stack-name foobar \
--template-body file://role.yaml \
--change-set-name RolePolicyAssociationChangeset \
--capabilities CAPABILITY_NAMED_IAM
# Execute the changeset
$ aws cloudformation execute-change-set --stack-name foobar --change-set-name RolePolicyAssociationChangeset
# Wait for the update to finish...
# Check for drifts
$ aws cloudformation detect-stack-drift --stack-name foobar
$ aws cloudformation describe-stack-resource-drifts --stack-name foobar
Done! We now have our role, inline policy and user managed policy imported in our stack.
However, if you check the IAM::Role
drift detection output, you’ll see a drift:
{
"StackId": "[OMITTED]",
"LogicalResourceId": "Role",
"PhysicalResourceId": "foobar",
"ResourceType": "AWS::IAM::Role",
"PropertyDifferences": [
{
"PropertyPath": "/ManagedPolicyArns/1",
"ExpectedValue": "null",
"ActualValue": "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess",
"DifferenceType": "ADD"
}
],
"StackResourceDriftStatus": "MODIFIED",
"Timestamp": "2019-12-23T17:20:18.721Z"
}
This is because we’re missing our last resource: the AWS managed policy.
Importing the AWS Managed Policy
AWS managed policies, the same way as inline policies, don’t hold a specific logical resource in CloudFormation, being just attached to an existing role.
In this case, the import process becomes adjusting the stack definition and running a changeset.
# role.yaml
Resources:
Role:
Type: AWS::IAM::Role
DeletionPolicy: Retain
Properties:
RoleName: "foobar"
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: "sts:AssumeRole"
Principal:
AWS: "*"
ManagedPolicyArns:
- !Ref UserManagedPolicy
- arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess # <--- the AWS managed policy
Policies:
- PolicyName: "foobar-inline-policy"
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- "s3:ListAllMyBuckets"
- "s3:ListBucket"
- "s3:HeadBucket"
Resource: "*"
UserManagedPolicy:
Type: AWS::IAM::ManagedPolicy
DeletionPolicy: Retain
Properties:
ManagedPolicyName: "foobar-user-managed-policy"
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- "s3:DeleteObjectTagging"
- "s3:PutBucketTagging"
- "s3:ReplicateTags"
- "s3:PutObjectVersionTagging"
- "s3:PutObjectTagging"
- "s3:DeleteObjectVersionTagging"
Resource: "*"
# Create the changeset
$ aws cloudformation create-change-set \
--stack-name foobar \
--template-body file://role.yaml \
--change-set-name ImportAWSManagedPolicyChangeset \
--capabilities CAPABILITY_NAMED_IAM
# Check the detected changes
$ aws cloudformation describe-change-set --stack-name foobar --change-set-name ImportAWSManagedPolicyChangeset
{
"changes": [
{
"Type": "Resource",
"ResourceChange": {
"Action": "Modify",
"LogicalResourceId": "Role",
"PhysicalResourceId": "foobar",
"ResourceType": "AWS::IAM::Role",
"Replacement": "False",
"Scope": [
"Properties"
],
"Details": [
{
"Target": {
"Attribute": "Properties",
"Name": "ManagedPolicyArns",
"RequiresRecreation": "Never"
},
"Evaluation": "Static",
"ChangeSource": "DirectModification"
}
]
}
}
],
The changeset correctly recognized a change on the role’s ManagedPolicyArns
field, so let’s proceed with the execution:
$ aws cloudformation execute-change-set --stack-name foobar --change-set-name ImportAWSManagedPolicyChangeset
Finally, let’s check for drifts again and make sure that we no longer have a drift in the role’s definition
$ aws cloudformation detect-stack-drift --stack-name foobar
$ aws cloudformation describe-stack-resource-drifts --stack-name foobar
{
"StackId": "[OMITTED]",
"LogicalResourceId": "Role",
"PhysicalResourceId": "foobar",
"ResourceType": "AWS::IAM::Role",
"PropertyDifferences": [],
"StackResourceDriftStatus": "IN_SYNC",
"Timestamp": "2019-12-23T17:30:25.119Z"
}
Fantastic! We now have our role & policies fully imported in our CloudFormation stack.
But there’s only one way to make sure everything works as expected: let’s nuke our manually created role & policies and see how CloudFormation reacts to that.
Detect stack changes
The moment of truth: let’s delete our IAM role & policies manually using the AWS CLI and see how our stack behaves related to drifts:
# List all managed policies attached to the role
# (should return two: the user managed policy and the AWS managed policy)
$ aws iam list-attached-role-policies --role-name foobar
# For (managed policies returned by the command above) do:
$ aws iam detach-role-policy --role-name foobar --policy-arn "[POLICY ARN]"
# Delete the inline policy
$ aws iam delete-role-policy --role-name foobar --policy-name foobar-inline-policy
# Delete the user managed policy
$ aws iam delete-policy --policy-arn "[USER MANAGED POLICY (returned by the first command)]"
# Delete the role
$ aws iam delete-role --role-name foobar
Our role & policies are gone.
Let’s now trigger a drift detection on our CloudFormation stack and see the output:
$ aws cloudformation detect-stack-drift --stack-name foobar
$ aws cloudformation describe-stack-resource-drifts --stack-name foobar
{
"StackResourceDrifts": [
{
"StackId": "[OMITTED]",
"LogicalResourceId": "Role",
"PhysicalResourceId": "foobar",
"ResourceType": "AWS::IAM::Role",
"StackResourceDriftStatus": "DELETED",
"Timestamp": "2019-12-24T00:48:03.855Z"
},
{
"StackId": "[OMITTED]",
"LogicalResourceId": "UserManagedPolicy",
"PhysicalResourceId": "[OMITTED]",
"ResourceType": "AWS::IAM::ManagedPolicy",
"StackResourceDriftStatus": "DELETED",
"Timestamp": "2019-12-24T00:48:04.491Z"
}
]
}
As expected, CloudFormation detected that our imported resources no longer exist and, thus, are reported as DELETED
.
(Optional) Update the delete policy
Since our import operation is now complete, CloudFormation no longer requires a Retain
deletion policy.
So, we can remove this deletion policy from our stack definition, so in case a resource is removed from the stack (or the entire stack is removed), the associated resource is deleted.
# role.yaml
Resources:
Role:
Type: AWS::IAM::Role
DeletionPolicy: Retain # <-- REMOVE THIS LINE
Properties:
RoleName: "foobar"
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: "sts:AssumeRole"
Principal:
AWS: "*"
ManagedPolicyArns:
- !Ref UserManagedPolicy
- arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
Policies:
- PolicyName: "foobar-inline-policy"
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- "s3:ListAllMyBuckets"
- "s3:ListBucket"
- "s3:HeadBucket"
Resource: "*"
UserManagedPolicy:
Type: AWS::IAM::ManagedPolicy
DeletionPolicy: Retain # <-- REMOVE THIS LINE
Properties:
ManagedPolicyName: "foobar-user-managed-policy"
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- "s3:DeleteObjectTagging"
- "s3:PutBucketTagging"
- "s3:ReplicateTags"
- "s3:PutObjectVersionTagging"
- "s3:PutObjectTagging"
- "s3:DeleteObjectVersionTagging"
Resource: "*"
$ aws cloudformation update-stack --stack-name foobar --template-body file://role.yaml --capabilities CAPABILITY_NAMED_IAM
Conclusion
CloudFormation Resource Import is a brand new feature, so there are still some limitations, such as resource types supported by the import operation (a full updated list can be found here) and the way how we inform and perform the imports (compared to other solutions, such as Terraform, the update process for CloudFormation isn’t still very user friendly).
However, this is a big step for CloudFormation, since it addresses a limitation raised by customers for a long time.
Worth keeping an eye on Resource Import, since it can be of great help moving existing legacy infrastructure to IaC, something mandatory nowadays.