Parameters: VpcIdParameter: Type: AWS::EC2::VPC::Id Description: "The id of the Vpc into which the EC2 ImageBuilder will be deployed" ConstraintDescription: "Must be a valid AWS VPC Id" SubnetIdParameter: Type: AWS::EC2::Subnet::Id Description: "The id of the subnet in the Vpc into which the EC2 ImageBuilder will be deployed" ConstraintDescription: "Must be a valid AWS VPC Subnet Id" AmiPublishingRegionsParameter: Type: CommaDelimitedList Description: "Comma delimited list of the AWS regions to which the AMI should be published" AmiPublishingTargetIdsParameter: Type: String Description: "Comma delimited list of the AWS account ids to which the AMI should be published" AmiSharingAccountIdsParameter: Type: String Description: "Comma delimited list of the AWS account ids with whom to share the AMI" Resources: AmiShareKmsKey: Type: 'AWS::KMS::Key' Properties: KeyPolicy: Statement: - Action: 'kms:*' Effect: Allow Principal: AWS: 'Fn::Join': - '' - - 'arn:' - Ref: 'AWS::Partition' - ':iam::' - Ref: 'AWS::AccountId' - ':root' Resource: '*' - Action: - 'kms:Create*' - 'kms:Describe*' - 'kms:Enable*' - 'kms:List*' - 'kms:Put*' - 'kms:Update*' - 'kms:Revoke*' - 'kms:Disable*' - 'kms:Get*' - 'kms:Delete*' - 'kms:TagResource' - 'kms:UntagResource' - 'kms:ScheduleKeyDeletion' - 'kms:CancelKeyDeletion' Effect: Allow Principal: AWS: 'Fn::Join': - '' - - 'arn:' - Ref: 'AWS::Partition' - ':iam::' - Ref: 'AWS::AccountId' - ':root' Resource: '*' - Action: - 'kms:Decrypt' - 'kms:Encrypt' - 'kms:ReEncrypt*' - 'kms:GenerateDataKey*' Effect: Allow Principal: Service: 'Fn::Join': - '' - - imagebuilder. - Ref: 'AWS::URLSuffix' Resource: '*' - Action: - 'kms:Decrypt' - 'kms:Encrypt' - 'kms:ReEncrypt*' - 'kms:GenerateDataKey*' Effect: Allow Principal: Service: 'Fn::Join': - '' - - sns. - Ref: 'AWS::URLSuffix' Resource: '*' Version: '2012-10-17' Description: KMS key used with EC2 Imagebuilder Ami Share project Enabled: true EnableKeyRotation: true UpdateReplacePolicy: Delete DeletionPolicy: Delete AmiShareKmsKeyAlias: Type: 'AWS::KMS::Alias' Properties: AliasName: alias/AmiShareKmsKeyAlias TargetKeyId: 'Fn::GetAtt': - AmiShareKmsKey - Arn AmiShareImageRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Statement: - Action: 'sts:AssumeRole' Effect: Allow Principal: Service: ec2.amazonaws.com Version: '2012-10-17' ManagedPolicyArns: - 'Fn::Join': - '' - - 'arn:' - Ref: 'AWS::Partition' - ':iam::aws:policy/AmazonSSMManagedInstanceCore' - 'Fn::Join': - '' - - 'arn:' - Ref: 'AWS::Partition' - ':iam::aws:policy/EC2InstanceProfileForImageBuilder' AmiShareImageRoleDefaultPolicy: Type: 'AWS::IAM::Policy' Properties: PolicyDocument: Statement: - Action: - 'kms:Decrypt' - 'kms:Encrypt' - 'kms:ReEncrypt*' - 'kms:GenerateDataKey*' Effect: Allow Resource: 'Fn::GetAtt': - AmiShareKmsKey - Arn - Action: 'kms:Describe*' Effect: Allow Resource: 'Fn::GetAtt': - AmiShareKmsKey - Arn - Action: - 'logs:CreateLogStream' - 'logs:CreateLogGroup' - 'logs:PutLogEvents' Effect: Allow Resource: 'Fn::Join': - '' - - 'arn:' - Ref: 'AWS::Partition' - ':logs:' - Ref: 'AWS::Region' - ':' - Ref: 'AWS::AccountId' - ':log-group/aws/imagebuilder/*' - Action: 'sns:Publish' Effect: Allow Resource: Ref: AmiShareDistributionTopic Version: '2012-10-17' PolicyName: AmiShareImageRoleDefaultPolicy Roles: - Ref: AmiShareImageRole AmiShareImageBuilderInstanceProfile: Type: 'AWS::IAM::InstanceProfile' Properties: Roles: - Ref: AmiShareImageRole InstanceProfileName: ami-share-imagebuilder-instance-profile AmiShareDistributionList: Type: 'AWS::SSM::Parameter' Properties: Type: StringList Value: 'account1,account2' Name: /master-AmiSharePipeline/DistributionList AmiShareDistributionTopic: Type: 'AWS::SNS::Topic' Properties: KmsMasterKeyId: 'Fn::GetAtt': - AmiShareKmsKey - Arn TopicName: ami-share-imagebuilder-topic AmiShareImageBuilderSubscription: Type: 'AWS::SNS::Subscription' Properties: Protocol: email TopicArn: Ref: AmiShareDistributionTopic Endpoint: email@domian.com AmiShareImageBuilderSecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: >- Security group for the EC2 Image Builder Pipeline: EC2ImageBuilderAmiShare-Pipeline GroupName: ami-share-imagebuilder-sg SecurityGroupEgress: - CidrIp: 0.0.0.0/0 Description: Allow all outbound traffic by default IpProtocol: '-1' VpcId: Ref: VpcIdParameter AmiShareInfrastructureConfig: Type: 'AWS::ImageBuilder::InfrastructureConfiguration' Properties: InstanceProfileName: ami-share-imagebuilder-instance-profile Name: ami-share-infra-config InstanceTypes: - t2.medium ResourceTags: project: ec2-imagebuilder-ami-share SecurityGroupIds: - 'Fn::GetAtt': - AmiShareImageBuilderSecurityGroup - GroupId SnsTopicArn: Ref: AmiShareDistributionTopic SubnetId: Ref: SubnetIdParameter TerminateInstanceOnFailure: true DependsOn: - AmiShareImageBuilderInstanceProfile AmiShareImageRecipie: Type: 'AWS::ImageBuilder::ImageRecipe' Properties: Components: - ComponentArn: 'Fn::Join': - '' - - 'arn:' - Ref: 'AWS::Partition' - ':imagebuilder:' - Ref: 'AWS::Region' - ':aws:component/aws-cli-version-2-linux/x.x.x' Name: ami-share-image-recipe ParentImage: 'Fn::Join': - '' - - 'arn:' - Ref: 'AWS::Partition' - ':imagebuilder:' - Ref: 'AWS::Region' - ':aws:image/amazon-linux-2-x86/2021.4.29' Version: 1.0.0 BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: DeleteOnTermination: true Encrypted: false VolumeSize: 8 VolumeType: gp2 Description: Recipe to build and validate AmiShareImageRecipe Tags: project: ec2-imagebuilder-ami-share WorkingDirectory: /imagebuilder AmiShareDistributionConfig: Type: 'AWS::ImageBuilder::DistributionConfiguration' Properties: Distributions: - AmiDistributionConfiguration: Name: 'Fn::Sub': 'AmiShare-ImageRecipe-{{ imagebuilder:buildDate }}' AmiTags: project: ec2-imagebuilder-ami-share Pipeline: AmiSharePipeline Region: Ref: AWS::Region Name: ami-share-distribution-config AmiSharePipeline: Type: 'AWS::ImageBuilder::ImagePipeline' Properties: InfrastructureConfigurationArn: 'Fn::GetAtt': - AmiShareInfrastructureConfig - Arn Name: ami-share-pipeline Description: 'Image Pipeline for: AmiSharePipeline' DistributionConfigurationArn: 'Fn::GetAtt': - AmiShareDistributionConfig - Arn EnhancedImageMetadataEnabled: true ImageRecipeArn: 'Fn::GetAtt': - AmiShareImageRecipie - Arn ImageTestsConfiguration: ImageTestsEnabled: true TimeoutMinutes: 90 Status: ENABLED Tags: project: ec2-imagebuilder-ami-share DependsOn: - AmiShareInfrastructureConfig AmiDistributionLambdaRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Statement: - Action: 'sts:AssumeRole' Effect: Allow Principal: Service: lambda.amazonaws.com Version: '2012-10-17' ManagedPolicyArns: - 'Fn::Join': - '' - - 'arn:' - Ref: 'AWS::Partition' - ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' AmiDistributionLambdaRoleDefaultPolicy: Type: 'AWS::IAM::Policy' Properties: PolicyDocument: Statement: - Action: 'imagebuilder:UpdateDistributionConfiguration' Effect: Allow Resource: 'Fn::GetAtt': - AmiShareDistributionConfig - Arn - Action: - 'ssm:GetParameter' - 'ssm:GetParameters' - 'ssm:GetParametersByPath' Effect: Allow Resource: 'Fn::Join': - '' - - 'arn:aws:ssm:' - Ref: 'AWS::Region' - ':' - Ref: 'AWS::AccountId' - ':parameter/master-AmiSharing/*' Version: '2012-10-17' PolicyName: AmiDistributionLambdaRoleDefaultPolicy Roles: - Ref: AmiDistributionLambdaRole AmiDistributionLambda: Type: 'AWS::Lambda::Function' Properties: Code: ZipFile: | ################################################## ## EC2 ImageBuilder AMI distribution setting targetAccountIds ## is not supported by CloudFormation (as of September 2021). ## https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-distributionconfiguration.html ## ## This lambda function uses Boto3 for EC2 ImageBuilder in order ## to set the AMI distribution settings which are currently missing from ## CloudFormation - specifically the targetAccountIds attribute ## https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/imagebuilder.html ################################################## import os import boto3 import botocore import json import logging import cfnresponse def get_ssm_parameter(ssm_param_name: str, aws_ssm_region: str): ssm = boto3.client('ssm', region_name=aws_ssm_region) parameter = ssm.get_parameter(Name=ssm_param_name, WithDecryption=False) return parameter['Parameter'] def get_distributions_configurations( aws_distribution_regions, ami_distribution_name, publishing_account_ids, sharing_account_ids ): distribution_configs = [] for aws_region in aws_distribution_regions: distribution_config = { 'region': aws_region, 'amiDistributionConfiguration': { 'name': ami_distribution_name, 'description': f'AMI Distribution configuration for {ami_distribution_name}', 'targetAccountIds': publishing_account_ids, 'amiTags': { 'PublishTargets': ",".join(publishing_account_ids), 'SharingTargets': ",".join(sharing_account_ids) }, 'launchPermission': { 'userIds': sharing_account_ids } } } distribution_configs.append(distribution_config) return distribution_configs def handler(event, context): # set logging logger = logging.getLogger() logger.setLevel(logging.DEBUG) # print the event details logger.debug(json.dumps(event, indent=2)) props = event['ResourceProperties'] aws_region = os.environ['AWS_REGION'] aws_distribution_regions = props['AwsDistributionRegions'] imagebuiler_name = props['ImageBuilderName'] ami_distribution_name = props['AmiDistributionName'] ami_distribution_arn = props['AmiDistributionArn'] ssm_publishing_account_ids_param_name = props['PublishingAccountIds'] ssm_sharing_account_ids_param_name = props['SharingAccountIds'] publishing_account_ids = get_ssm_parameter(ssm_publishing_account_ids_param_name, aws_region)['Value'].split(",") sharing_account_ids = get_ssm_parameter(ssm_sharing_account_ids_param_name, aws_region)['Value'].split(",") logger.info(publishing_account_ids) logger.info(sharing_account_ids) if event['RequestType'] != 'Delete': try: client = boto3.client('imagebuilder') response = client.update_distribution_configuration( distributionConfigurationArn=ami_distribution_arn, description=f"AMI Distribution settings for: {imagebuiler_name}", distributions=get_distributions_configurations( aws_distribution_regions=aws_distribution_regions, ami_distribution_name=ami_distribution_name, publishing_account_ids=publishing_account_ids, sharing_account_ids=sharing_account_ids ) ) cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) except botocore.exceptions.ClientError as err: logger.critical(err) cfnresponse.send(event, context, cfnresponse.FAILED, {}) # nothing to do on delete so send a success response cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) Role: 'Fn::GetAtt': - AmiDistributionLambdaRole - Arn Handler: index.handler Runtime: python3.6 Timeout: 30 DependsOn: - AmiDistributionLambdaRoleDefaultPolicy - AmiDistributionLambdaRole AmiPublishingTargetIds: Type: 'AWS::SSM::Parameter' Properties: Type: StringList Value: Ref: AmiPublishingTargetIdsParameter Name: /master-AmiSharing/AmiPublishingTargetIds AmiSharingAccountIds: Type: 'AWS::SSM::Parameter' Properties: Type: StringList Value: Ref: AmiSharingAccountIdsParameter Name: /master-AmiSharing/AmiSharingAccountIds AmiDistributionCustomResource: Type: 'AWS::CloudFormation::CustomResource' Properties: ServiceToken: 'Fn::GetAtt': - AmiDistributionLambda - Arn AwsDistributionRegions: Ref: AmiPublishingRegionsParameter ImageBuilderName: AmiDistributionConfig AmiDistributionName: 'AmiShare-{{ imagebuilder:buildDate }}' AmiDistributionArn: 'Fn::GetAtt': - AmiShareDistributionConfig - Arn PublishingAccountIds: Ref: AmiPublishingTargetIds SharingAccountIds: Ref: AmiSharingAccountIds DependsOn: - AmiShareDistributionConfig UpdateReplacePolicy: Delete DeletionPolicy: Delete Outputs: ExportAmiShareSnsTopicArn: Description: Ami Share Sns Topic Value: Ref: AmiShareDistributionTopic Export: Name: AmiShare-SnsTopicArn ExportAmiShareKmsKeyArn: Description: Ami Share KMS Key ARN Value: 'Fn::GetAtt': - AmiShareKmsKey - Arn Export: Name: AmiShare-KmsKeyArn ExportAmiSharePipelineArn: Description: Ami Share Pipeline Arn Value: 'Fn::GetAtt': - AmiSharePipeline - Arn Export: Name: AmiShare-PipelineArn