Skip to content

Commit

Permalink
Attach iam to instance (#77)
Browse files Browse the repository at this point in the history
* added automation document to attach a role to an ec2 instance

* added test case where an instance is already associated to a role

* added optional lambda role for automation cloudformation

* changed lambda role policy to have more specific actions
  • Loading branch information
sdechgan authored and nanalakshmanan committed Feb 12, 2018
1 parent f3a9106 commit a95a540
Show file tree
Hide file tree
Showing 11 changed files with 943 additions and 0 deletions.
48 changes: 48 additions & 0 deletions Automation/AttachIAMToInstance/Design/Design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Attach an IAM role to an Instance

## Notes

To associate a role to an instance, we have to create an instance profile.
By AWS Design, we can only have one role associated to an instance profile.
So the new instance profile name will be the name of the role.
Then we can associate the instance to an instance profile.

There are two things to consider about attaching a role to an instance
1. Instance is already associated to a role
2. Instance is not associated to any role

## Document Design

Refer to schema.json

Document Steps:
1. aws:createStack - Execute CloudFormation Template to create lambda.
* Inputs:
* StackName: {{DocumentStackName}} - Stack name or Unique ID
* Parameters:
* LambdaRole: {{LambdaAssumeRole}} - role assumed by lambda
* LambdaName: UpdateCFTemplate-{{automation:EXECUTION_ID}}
2. aws:invokeLambdaFunction - Execute Lambda to detach the volume
* Inputs:
* FunctionName: AttachIAMToInstanceLambda-{{automation:EXECUTION_ID}} - Lambda name to use
* Payload:
* Instance: {{Instance}} - The ID of the instance.
* RoleName: {{RoleName}} - Role Name to associate the Instance
3. aws:deleteStack - Delete CloudFormation Template.
* Inputs:
* StackName: {{DocumentStackName}} - Stack name or Unique ID

## Test script

Instance is already associated to a role:
1. Create a test stack with an instance already associated to a role
2. Execute automation document to attach the role to an instance
3. Verify instance profile is replaced
4. Clean up test stack

Instance is not associated to a role:
1. Create a test stack with an instance that is not associated to any role
2. Execute automation document to attach the role to an instance
3. Verify instance is associated to the role
4. Clean up test stack

30 changes: 30 additions & 0 deletions Automation/AttachIAMToInstance/Design/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"description": "Attach IAM to Instance",
"schemaVersion": "0.3",
"assumeRole": "{{ AutomationAssumeRole }}",
"parameters": {
"InstanceId": {
"type": "String",
"description": "(Required) The ID of the instance."
},
"RoleName": {
"type": "String",
"description": "(Required) Role Name to add"
},
"LambdaAssumeRole": {
"type": "String",
"description": "(Optional) The ARN of the role assumed by lambda",
"default": ""
},
"AutomationAssumeRole": {
"type": "String",
"description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf. ",
"default": ""
}
},
"mainSteps": [],
"outputs":[
"attachIAMToInstance.LogResult",
"attachIAMToInstance.Payload"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
LambdaRoleArn:
Description: >
The ARN of the role that allows Lambda created by Automation to perform the action on your behalf
Type: String
Default: ""
LambdaName:
Description: >
The lambda function name
Type: String
Conditions:
LambdaAssumeRoleNotSpecified:
!Or
- !Equals [!Ref LambdaRoleArn, '']
- !Equals [!Ref LambdaRoleArn, 'undefined']
Resources:
# Assume role used by the lambda function (only created if not passed in)
LambdaRole:
Type: AWS::IAM::Role
Condition: LambdaAssumeRoleNotSpecified
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Action: ["sts:AssumeRole"]
Effect: "Allow"
Principal:
Service:
- lambda.amazonaws.com
Policies:
- PolicyName: AttachIAMToInstanceLambdaPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
Action:
- ec2:AssociateIamInstanceProfile
- ec2:DescribeIamInstanceProfileAssociations
- ec2:DisassociateIamInstanceProfile
- iam:AddRoleToInstanceProfile
- iam:CreateInstanceProfile
- iam:ListInstanceProfilesForRole
- iam:PassRole
Effect: Allow
Resource: "*"
Path: "/"
AttachIAMToInstanceLambda:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: "{}"
FunctionName: !Ref LambdaName
Role: !If ["LambdaAssumeRoleNotSpecified", !GetAtt LambdaRole.Arn, !Ref LambdaRoleArn]
Timeout: 60
Handler: "index.handler"
Runtime: python2.7
MemorySize: 128
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import boto3
import time
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

iam_client = boto3.client('iam')
ec2_client = boto3.client('ec2')


def find_or_create_instance_profile(role_name):
response = iam_client.list_instance_profiles_for_role(RoleName=role_name)
if len(response['InstanceProfiles']) != 0:
logger.info("Instance profile with role " + role_name + " already exists")
instance_profile = response['InstanceProfiles'][0]
else:
logger.info("Creating instance profile for role " + role_name)
response = iam_client.create_instance_profile(
InstanceProfileName=role_name,
Path='/'
)
instance_profile = response['InstanceProfile']

# Now assign the role to the profile
iam_client.add_role_to_instance_profile(
InstanceProfileName=instance_profile['InstanceProfileName'],
RoleName=role_name
)

return {
'InstanceProfileName': instance_profile['InstanceProfileName'],
'Arn': instance_profile['Arn']
}


def associate_instance_profile(profile_name, profile_arn, instance_id):
logger.info("Associating instance profile: " + profile_name + " to " + instance_id)
# For whatever reason, new instance profiles are not available immediately. So we try again
retry_count = 6
while True:
try:
return ec2_client.associate_iam_instance_profile(
IamInstanceProfile={
'Arn': profile_arn,
'Name': profile_name
},
InstanceId=instance_id
)
except Exception as e:
retry_count -= 1
if retry_count == 0:
raise e

logger.info("Unable to associate instance profile... trying again in 10 sec")
time.sleep(10)


def handler(event, context):
instance_id = event['InstanceId']
role_name = event['RoleName']

response = ec2_client.describe_iam_instance_profile_associations(Filters=[{
'Name': 'instance-id',
'Values': [instance_id]
}])

if len(response['IamInstanceProfileAssociations']) != 0:
logger.info("Instance Profile already exists. Will attach role to existing instance profile")

iam_instance_profile_association = response['IamInstanceProfileAssociations'][0]
association_id = iam_instance_profile_association['AssociationId']
ec2_client.disassociate_iam_instance_profile(AssociationId=association_id)

instance_profile = find_or_create_instance_profile(role_name)
association_response = associate_instance_profile(instance_profile['InstanceProfileName'], instance_profile['Arn'],
instance_id)
association_id = association_response['IamInstanceProfileAssociation']['AssociationId']

return {
"InstanceProfileName": instance_profile['InstanceProfileName'],
"Arn": instance_profile['Arn'],
"RoleName": event['RoleName'],
"AssociationId": association_id
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"description": "Attach IAM to Instance",
"schemaVersion": "0.3",
"assumeRole": "{{ AutomationAssumeRole }}",
"parameters": {
"InstanceId": {
"type": "String",
"description": "(Required) The ID of the instance."
},
"RoleName": {
"type": "String",
"description": "(Required) Role Name to add"
},
"LambdaAssumeRole": {
"type": "String",
"description": "(Optional) The ARN of the role assumed by lambda",
"default": ""
},
"AutomationAssumeRole": {
"type": "String",
"description": "(Optional) The ARN of the role that allows Automation to perform the actions on your behalf. ",
"default": ""
}
},
"mainSteps": [
{
"name": "createDocumentStack",
"action": "aws:createStack",
"inputs": {
"Capabilities": [
"CAPABILITY_IAM"
],
"StackName": "AttachIAMToInstanceStack{{automation:EXECUTION_ID}}",
"Parameters": [
{
"ParameterKey": "LambdaRoleArn",
"ParameterValue": "{{LambdaAssumeRole}}"
},
{
"ParameterKey": "LambdaName",
"ParameterValue": "AttachIAMToInstanceLambda-{{automation:EXECUTION_ID}}"
}
],
"TemplateBody": "..."
}
},
{
"name": "attachIAMToInstance",
"action": "aws:invokeLambdaFunction",
"inputs": {
"FunctionName": "AttachIAMToInstanceLambda-{{automation:EXECUTION_ID}}",
"Payload": "{\"InstanceId\": \"{{InstanceId}}\", \"RoleName\": \"{{RoleName}}\"}",
"LogType": "Tail"
}
},
{
"name": "deleteCloudFormationTemplate",
"action": "aws:deleteStack",
"inputs": {
"StackName": "AttachIAMToInstanceStack{{automation:EXECUTION_ID}}"
}
}
],
"outputs":[
"attachIAMToInstance.LogResult",
"attachIAMToInstance.Payload"
]
}
18 changes: 18 additions & 0 deletions Automation/AttachIAMToInstance/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
TARGET_DIR = "./Output"

documents: targetdir createdocuments
@echo "Done making documents"

targetdir:
@echo "Making $(TARGET_DIR)"
mkdir -p ./Output

createdocuments:
python ./Setup/create_document.py > ./Output/aws-AttachIAMToInstance.json

test: documents
python -m unittest discover Tests

clean:
@echo "Removing $(TARGET_DIR)"
@rm -rf ./Output
Loading

0 comments on commit a95a540

Please sign in to comment.