AWSTemplateFormatVersion: 2010-09-09 Description: >- After Golden Images are built from EC2 Image Builder Pipelines, a manual approval step is added before AMIs are shared to different accounts Parameters: NamingPrefix: Type: String Description: Prefix given to resource names Default: blog InstanceType: Type: String Description: Image Builder instance type Default: t3.micro LambdaCloudWatchLogGroupRetentionInDays: Type: Number Description: The number of days to retain the log events in the specified log group. AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 2192, 2557, 2922, 3288, 3653] TargetAccountIds: Type: CommaDelimitedList Description: Account IDs within the same region for shared AMIs. ApproverEmail: Type: String Description: Approver account email address that receives email notifications when a new AMI is created by image builder. Approver to click to approve sharing of AMI to target accounts. TargetAccountEmail: Type: String Description: Target account email address that receives updates whenever a new image has been shared with their account. ApprovalTimeout: Type: Number Description: Specifies the execution timeout value for the aws:approve action in seconds Default: 3600 IAMPrincipalAssumeRoleARN: Type: String Description: ARN of the IAM principal that will assume the approver role. Resources: #============================# # EC2 Image Builder resources #============================# EC2ImageBuilderPipeline: Type: AWS::ImageBuilder::ImagePipeline Properties: Name: !Sub ${NamingPrefix}-image-pipeline ImageRecipeArn: !Ref EC2ImageBuilderRecipe InfrastructureConfigurationArn: !Ref EC2ImageBuilderInfrastructureConfiguration DistributionConfigurationArn: !Ref EC2ImageBuilderDistributionConfiguration EC2ImageBuilderRecipe: Type: AWS::ImageBuilder::ImageRecipe Properties: Name: !Sub ${NamingPrefix}-recipe Description: This recipe installs the Amazon CloudWatch Agent on an Amazon Linux 2 AMI ParentImage: !Sub arn:${AWS::Partition}:imagebuilder:${AWS::Region}:aws:image/amazon-linux-2-x86/x.x.x Components: - ComponentArn: !Sub arn:${AWS::Partition}:imagebuilder:${AWS::Region}:aws:component/amazon-cloudwatch-agent-linux/x.x.x Version: 1.0.0 EC2ImageBuilderInfrastructureConfiguration: Type: AWS::ImageBuilder::InfrastructureConfiguration Properties: Name: !Sub ${NamingPrefix}-infra-config InstanceTypes: - !Ref InstanceType InstanceProfileName: !Ref EC2ImageBuilderIAMInstanceProfile SnsTopicArn: !Ref ImageBuilderSNSTopic SubnetId: !Ref PublicSubnet1 SecurityGroupIds: - !GetAtt VPC.DefaultSecurityGroup TerminateInstanceOnFailure: true EC2ImageBuilderDistributionConfiguration: Type: AWS::ImageBuilder::DistributionConfiguration Properties: Name: !Sub ${NamingPrefix}-distribution-config Distributions: - AmiDistributionConfiguration: Name: amazon-linux2-golden-ami - {{ imagebuilder:buildDate }} LaunchPermissionConfiguration: UserIds: - !Ref AWS::AccountId Region: !Ref AWS::Region EC2ImageBuilderIAMRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/EC2InstanceProfileForImageBuilder - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore EC2ImageBuilderIAMInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Roles: - !Ref EC2ImageBuilderIAMRole ImageBuilderSNSTopic: Type: AWS::SNS::Topic Properties: Subscription: - Endpoint: !GetAtt StartAutomationExecutionHandler.Arn Protocol: lambda KmsMasterKeyId: alias/aws/sns ImageBuilderSNSTopicPolicy: Type: AWS::SNS::TopicPolicy DependsOn: EC2ImageBuilderInfrastructureConfiguration Properties: PolicyDocument: Version: 2012-10-17 Statement: - Effect: Deny Principal: "*" Action: - sns:Publish Resource: !Ref ImageBuilderSNSTopic Condition: StringNotEquals: aws:PrincipalArn: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/aws-service-role/imagebuilder.amazonaws.com/AWSServiceRoleForImageBuilder Topics: - !Ref ImageBuilderSNSTopic VPC: Type: AWS::EC2::VPC Properties: CidrBlock: "10.0.0.0/16" EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: !Sub ${NamingPrefix}-vpc Metadata: cfn_nag: rules_to_suppress: - id: W60 reason: Flow Log not required for blog since only the CloudWatch agent is installed on the launched EC2 instance InternetGateway: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: !Sub ${NamingPrefix}-igw InternetGatewayAttachment: Type: AWS::EC2::VPCGatewayAttachment Properties: InternetGatewayId: !Ref InternetGateway VpcId: !Ref VPC PublicSubnet1: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [ 0, !GetAZs '' ] CidrBlock: "10.0.0.0/24" MapPublicIpOnLaunch: true Tags: - Key: Name Value: !Sub ${NamingPrefix}-public-subnet Metadata: cfn_nag: rules_to_suppress: - id: W33 reason: Public Subnet used to reduce the need for a Nat Gateway, reducing the cost of the blog PublicRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${NamingPrefix}-public-rtb DefaultPublicRoute: Type: AWS::EC2::Route DependsOn: InternetGatewayAttachment Properties: RouteTableId: !Ref PublicRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway PublicSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicRouteTable SubnetId: !Ref PublicSubnet1 #=================================# # Trigger SSM Automation Document resources #=================================# StartAutomationExecutionHandler: Type: AWS::Lambda::Function Properties: Handler: index.lambda_handler Runtime: python3.9 Description: This lambda triggers an SSM Automation Document with a Approval Step Environment: Variables: SNS_TOPIC_ARN: !Ref ApproverSNSEmailTopic SSM_DOCUMENT_NAME: !Ref ManualApprovalSSMDocument Role: !GetAtt StartAutomationExecutionHandlerLambdaRole.Arn Code: ZipFile: | import boto3 import json import logging import os logger = logging.getLogger() logger.setLevel(logging.INFO) ssm_client = boto3.client('ssm') sns_client = boto3.client('sns') SNS_TOPIC_ARN = os.environ['SNS_TOPIC_ARN'] SSM_DOCUMENT_NAME = os.environ['SSM_DOCUMENT_NAME'] def lambda_handler(event, context): ib_notification = json.loads(event['Records'][0]['Sns']['Message']) if ib_notification['state']['status'] != 'AVAILABLE': status = ib_notification['state']['status'] logger.info(f'Image Builder Pipeline Status: {status}') source_pipeline_arn = ib_notification['sourcePipelineArn'] build_execution_id = ib_notification['buildExecutionId'] message = f'Source Pipeline ARN: {source_pipeline_arn}.\nBuild Execution Id: {build_execution_id}\n\nPlease investigate the Image Builder logs. Thank you.' if 'reason' in ib_notification['state']: reason_formatted = f"Reason: {ib_notification['state']['reason']}\n" message = reason_formatted + message try: sns_client.publish( TargetArn = SNS_TOPIC_ARN, Subject = f'Image Builder Pipeline Status: {status}', Message = message ) logger.info('Published to SNS Topic') except Exception as e: logger.error(e) return { 'statusCode': 500, 'body': 'Something went wrong went publishing to the SNS Topic' } warning_message = 'No action taken. EC2 Image not available.' logging.warning(warning_message) return { 'statusCode': 500, 'body': warning_message } ami_event = ib_notification['outputResources']['amis'][0] ami_name = ami_event['name'] ami_id = ami_event['image'] logger.info(f'AMI ID: {ami_id}') # Trigger SSM Automation Document try: ssm_client.start_automation_execution( DocumentName = SSM_DOCUMENT_NAME, Parameters = { 'amiId': [ ami_id, ], 'amiName': [ ami_name, ], }, ) logger.info('SSM Automation Document triggered') except Exception as e: logger.error(e) return { 'statusCode': 500, 'body': 'Something went wrong went triggering SSM Automation Document' } return { 'statusCode': 200, 'body': 'SSM Automation Document triggered' } Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: Lambda function has permissions to write to CloudWatch logs via a Managed Policy that allows logs:CreateLogStream and logs:PutLogEvents to the CloudWatch Log Group ARN - id: W89 reason: If Lambda is in a VPC, it requires a VPC endpoint to call the start_automation_execution API (incurs cost), or requires security group egress to 0.0.0.0/0. - id: W92 reason: ReservedConcurrentExecutions not needed since this function can either be triggered only once by this blog or potentially be scaled to more Image Builder Pipelines StartAutomationExecutionHandlerLambdaRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / StartAutomationExecutionPolicy: Type: AWS::IAM::ManagedPolicy Properties: PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - ssm:StartAutomationExecution # https://docs.aws.amazon.com/service-authorization/latest/reference/list_awssystemsmanager.html#awssystemsmanager-automation-definition Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${ManualApprovalSSMDocument}* - Effect: Allow Action: - iam:PassRole Resource: !GetAtt AutomationServiceRole.Arn Condition: StringEquals: iam:PassedToService: ssm.amazonaws.com StringLike: iam:AssociatedResourceARN: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-definition/${ManualApprovalSSMDocument}* # SNS publish permissions for Image Builder Pipeline failures - Effect: Allow Action: - sns:Publish Resource: !Ref ApproverSNSEmailTopic # https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-identity-based-access-control-cwl.html - Effect: Allow Action: - logs:CreateLogStream - logs:PutLogEvents Resource: !GetAtt StartAutomationExecutionHandlerLogGroup.Arn Roles: - !Ref StartAutomationExecutionHandlerLambdaRole StartAutomationExecutionPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt StartAutomationExecutionHandler.Arn Principal: sns.amazonaws.com SourceArn: !Ref ImageBuilderSNSTopic ApproverSSMExecuteRole: Type: AWS::IAM::Role Properties: Path: / MaxSessionDuration: 3600 AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: AWS: !Ref IAMPrincipalAssumeRoleARN Action: - sts:AssumeRole ApproverSSMExecutePolicy: Type: AWS::IAM::ManagedPolicy Properties: PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - ssm:GetAutomationExecution - ssm:SendAutomationSignal - ssm:DescribeAutomationStepExecutions Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-execution/* Roles: - !Ref ApproverSSMExecuteRole StartAutomationExecutionHandlerLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Join ['/', ['/aws/lambda', !Ref StartAutomationExecutionHandler]] RetentionInDays: !Ref LambdaCloudWatchLogGroupRetentionInDays Metadata: cfn_nag: rules_to_suppress: - id: W84 reason: Logs do not contain sensitive data. #=================================# # SSM Automation Document Resources #=================================# # https://docs.aws.amazon.com/systems-manager/latest/userguide/automation-cf.html#automation-cf-create AutomationServiceRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ssm.amazonaws.com Action: sts:AssumeRole Condition: StringEquals: aws:SourceAccount: !Ref AWS::AccountId ArnLike: aws:SourceArn: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:automation-execution/* ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonSSMAutomationRole Path: / AutomationServicePolicy: Type: AWS::IAM::ManagedPolicy Properties: PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - sns:Publish Resource: !Ref ApproverSNSEmailTopic - Effect: Allow Action: - lambda:InvokeFunction Resource: !GetAtt ShareAMIHandler.Arn Roles: - !Ref AutomationServiceRole ApproverSNSEmailTopic: Type: AWS::SNS::Topic Properties: Subscription: - Endpoint: !Ref ApproverEmail Protocol: email KmsMasterKeyId: alias/aws/sns ApproverSNSEmailTopicPolicy: Type: AWS::SNS::TopicPolicy Properties: PolicyDocument: Version: 2012-10-17 Statement: - Effect: Deny Principal: "*" Action: - sns:Publish Resource: !Ref ApproverSNSEmailTopic Condition: StringNotEquals: aws:PrincipalArn: - !GetAtt AutomationServiceRole.Arn - !GetAtt StartAutomationExecutionHandlerLambdaRole.Arn Topics: - !Ref ApproverSNSEmailTopic ManualApprovalSSMDocument: Type: AWS::SSM::Document Properties: DocumentType: Automation DocumentFormat: YAML Content: description: Automation document with a manual approval step that if approved, triggers a Lambda that shares an AMI schemaVersion: '0.3' assumeRole: "{{ assumeRole }}" parameters: assumeRole: type: String description: The IAM Role that the Automation Document Assumes default: !GetAtt AutomationServiceRole.Arn amiId: type: String description: The AMI ID of the Golden Image amiName: type: String description: The AMI Name of the Golden Image mainSteps: - name: manualApprovalStep action: aws:approve timeoutSeconds: !Ref ApprovalTimeout onFailure: Abort inputs: NotificationArn: !Ref ApproverSNSEmailTopic Message: 'AMI Name: {{amiName}}, with ID: {{amiId}} has been created. Please verify the AMI before approving or denying it.' MinRequiredApprovals: 1 Approvers: - !GetAtt ApproverSSMExecuteRole.Arn - name: shareAMI action: aws:invokeLambdaFunction inputs: FunctionName: !Ref ShareAMIHandler Payload: '{"ami_id":"{{amiId}}"}' isEnd: true #=================================# # Share AMI resources #=================================# ShareAMIHandler: Type: AWS::Lambda::Function Properties: Handler: index.lambda_handler Runtime: python3.9 Description: This lambda triggers an SSM Automation Document with a Approval Step Environment: Variables: SNS_TOPIC_ARN: !Ref TargetAccountSNSEmailTopic TARGET_ACCOUNT_IDS: !Join [ ",", !Ref TargetAccountIds ] Role: !GetAtt ShareAMIHandlerLambdaRole.Arn Timeout: 30 Code: ZipFile: | import boto3 import os import logging logger = logging.getLogger() logger.setLevel(logging.INFO) ec2 = boto3.client('ec2') sns_client = boto3.client('sns') TARGET_ACCOUNT_IDS = os.environ['TARGET_ACCOUNT_IDS'] SNS_TOPIC_ARN = os.environ['SNS_TOPIC_ARN'] def lambda_handler(event, context): image_id = event['ami_id'] logger.info(f'AMI ID: {image_id}') response = ec2.modify_image_attribute( ImageId=image_id, OperationType='add', Attribute='launchPermission', UserIds=TARGET_ACCOUNT_IDS.split(",") ) logger.info('AMI Shared') # Send success email to target account email address sns_client.publish( TargetArn = SNS_TOPIC_ARN, Subject = f'New AMI ({image_id}) has been successfully shared', Message = f'AMI ID: {image_id}\n\nTarget Accounts: {TARGET_ACCOUNT_IDS}\n\nA new AMI ({image_id}) has been created and successfully shared with the accounts above.\n\nThank you.' ) logger.info('Published to SNS Topic') return { 'statusCode': 200, 'body': 'Successfully shared AMI' } Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: Lambda function has permissions to write to CloudWatch logs via a Managed Policy that allows logs:CreateLogStream and logs:PutLogEvents to the CloudWatch Log Group ARN - id: W89 reason: If Lambda is in a VPC, it requires a VPC endpoint to call the start_automation_execution API (incurs cost), or requires security group egress to 0.0.0.0/0. - id: W92 reason: ReservedConcurrentExecutions not needed since this function can either be triggered only once by this blog or potentially be scaled to more Image Builder Pipelines ShareAMIHandlerLambdaRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / ShareAMIPolicy: Type: AWS::IAM::ManagedPolicy Properties: PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - ec2:ModifyImageAttribute Resource: !Sub arn:${AWS::Partition}:ec2:${AWS::Region}::image/* Condition: StringEquals: "ec2:ResourceTag/CreatedBy": "EC2 Image Builder" StringLike: "ec2:ResourceTag/Ec2ImageBuilderArn": !Sub arn:${AWS::Partition}:imagebuilder:${AWS::Region}:${AWS::AccountId}:image/${EC2ImageBuilderRecipe.Name}/* # use a wildcard because versions may change - Effect: Allow Action: - sns:Publish Resource: !Ref TargetAccountSNSEmailTopic # https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-identity-based-access-control-cwl.html - Effect: Allow Action: - logs:CreateLogStream - logs:PutLogEvents Resource: !GetAtt ShareAMIHandlerLogGroup.Arn Roles: - !Ref ShareAMIHandlerLambdaRole ShareAMIHandlerLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Join ['/', ['/aws/lambda', !Ref ShareAMIHandler]] RetentionInDays: !Ref LambdaCloudWatchLogGroupRetentionInDays Metadata: cfn_nag: rules_to_suppress: - id: W84 reason: Logs do not contain sensitive data. TargetAccountSNSEmailTopic: Type: AWS::SNS::Topic Properties: Subscription: - Endpoint: !Ref TargetAccountEmail Protocol: email KmsMasterKeyId: alias/aws/sns TargetAccountSNSEmailTopicPolicy: Type: AWS::SNS::TopicPolicy Properties: PolicyDocument: Version: 2012-10-17 Statement: - Effect: Deny Principal: "*" Action: - sns:Publish Resource: !Ref TargetAccountSNSEmailTopic Condition: StringNotEquals: aws:PrincipalArn: !GetAtt ShareAMIHandlerLambdaRole.Arn Topics: - !Ref TargetAccountSNSEmailTopic Outputs: ApproverSSMExecuteRoleName: Description: Name of the IAM Role that can approve SSM Automation Approval Step Value: !Ref ApproverSSMExecuteRole