Package: cdk-ssm-document
CDK SSM DocumentAWS CDKL3 construct for managing SSM Documents.CloudFormation's support for SSM Documentscurrently is lacking updating functionality. Instead of updating a document, CFN will replace it. The old document is destroyed and a new one is created with a different name. This is problematic because:When names potentially change, you cannot directly reference a documentOld versions are permanently lostThis construct provides document support in a way you'd expect it:Changes on documents will cerate new versionsVersions cannot be deletedInstallationThis package has peer dependencies, which need to be installed along in the expected version.For TypeScript/NodeJS, add these to yourdependenciesinpackage.json. For Python, add these to yourrequirements.txt:cdk-ssm-documentaws-cdk-lib (^2.0.0)constructs (^10.0.0)CDK compatibilityVersion 3.x is compatible with the CDK v2.Version 2.x is compatible with the CDK v1. There won't be regular updates for this.UsageCreating a document from a YAML or JSON fileimport*ascdkfrom'aws-cdk-lib';import{Construct}from'constructs';import{Document}from'cdk-ssm-document';importfs=require('fs');importpath=require('path');exportclassTestStackextendscdk.Stack{constructor(scope:Construct,id:string,props:cdk.StackProps){super(scope,id,props);constfile=path.join(__dirname,'../documents/hello-world.yml');newDocument(this,'SSM-Document-HelloWorld',{name:'HelloWorld',content:fs.readFileSync(file).toString(),});}}Creating a document via inline definitionimport*ascdkfrom'aws-cdk-lib';import{Construct}from'constructs';import{Document}from'cdk-ssm-document';importfs=require('fs');importpath=require('path');exportclassTestStackextendscdk.Stack{constructor(scope:Construct,id:string,props:cdk.StackProps){super(scope,id,props);newDocument(this,'SSM-Document-HelloWorld',{name:'HelloWorld',content:{schemaVersion:'2.2',description:'Echo Hello World!',parameters:{text:{default:'Hello World!',description:'Text to echo',type:'String',},},mainSteps:[{name:'echo',action:'aws:runShellScript',inputs:{runCommand:['echo "{{text}}"'],},precondition:{StringEquals:['platformType','Linux'],},},],},});}}Deploy all YAML/JSON files from a directoryimport*ascdkfrom'aws-cdk-lib';import{Construct}from'constructs';import{Document}from'cdk-ssm-document';importfs=require('fs');importpath=require('path');exportclassTestStackextendscdk.Stack{constructor(scope:cdk.Construct,id:string,props:cdk.StackProps){super(scope,id,props);constdir=path.join(__dirname,'../documents');constfiles=fs.readdirSync(dir);for(constiinfiles){constname=files[i];constshortName=name.split('.').slice(0,-1).join('.');//removesfileextensionconstfile=`${dir}/${name}`;newDocument(this,`SSM-Document-${shortName}`,{name:shortName,content:fs.readFileSync(file).toString(),});}}}Creating a distributor packageimport{aws_iam,aws_s3,aws_s3_deployment,Stack,StackProps}from'aws-cdk-lib';import{Document}from'cdk-ssm-document';import{Construct}from'constructs';importfs=require('fs');importpath=require('path');exportclassTestStackextendsStack{constructor(scope:Construct,id:string,props:StackProps){super(scope,id,props);constbucketName=`${Stack.of(this).account}-cdk-ssm-document-storage`;constbucket=newaws_s3.Bucket(this,'DistributorPackages',{bucketName:bucketName,});constpackageDeploy=newaws_s3_deployment.BucketDeployment(this,'distribution-packages',{sources:[aws_s3_deployment.Source.asset('../location/to/distributor/packages')],destinationBucket:bucket,});constfile=path.join(__dirname,'../location/to/distributor/packages/v1/manifest.json');constdoc=newDocument(this,`SSM-Distribution-Package`,{documentType:'Package',name:'Test-Distribution-Package',content:fs.readFileSync(file).toString(),versionName:'1.0-Custom-Name',attachments:[{key:'SourceUrl',values:[`s3://${bucketName}/v1`]}],});/***Theowner/creatorofthedocumentmusthavereadaccesstothe*s3filesthatmakeupadistribution.Sincethatisthelambdainthis*casewemustgiveit`GetObject`permissionsbeforetheywillcanbecome`Active`.**Ifaccessisnotgrantedtotherolethatcreatedthedocumentyoumaysee*anerrorlikethefollowing:**```*Permanentdownloaderror:SourceURL's3://cdk-ssm-document-storage/v1/package.zip'reported:*AccessDenied(Service:AmazonS3;StatusCode:403;*ErrorCode:AccessDenied;Request*ID:DES1XEHZTJ9R;S3ExtendedRequestID:*A+u8sTGQ6bZpAwl2eXDLq4KTkoeYyQR2XEV+I=;Proxy:null)*```*/doc.lambda.role?.addToPrincipalPolicy(newaws_iam.PolicyStatement({actions:['s3:GetObject'],resources:[`${bucket.arnForObjects('*')}`],}));doc.node.addDependency(packageDeploy);}}Deploying many documents in a single stackWhen you want to create multiple documents in the same stack, you will quickly exceed the SSM API rate limit. One ugly but working solution for this is to ensure that only a single document is created/updated at a time by adding resource dependencies. When document C depends on document B and B depends on document A, the documents will be created/updated in that order.constdocA=newDocument(this,'doc-A',{...})constdocB=newDocument(this,'doc-B',{...})constdocC=newDocument(this,'doc-C',{...})docC.node.addDependency(docB);docB.node.addDependency(docA);When looping through a directory of documents it could look like this:varlast:Document|undefined=undefined;for(constiinfiles){constdoc=newDocument(this,`SSM-Document-${shortName}`,{...});if(typeoflast!=='undefined'){last.node.addDependency(doc);}last=doc;}Using the Lambda as a custom resource in CloudFormation - without CDKIf you're still not convinced to use theAWS CDK, you can still use the Lambda as acustom resourcein your CFN template. Here is how:Create a zip file for the Lambda:To create a zip from the Lambda source run:lambda/buildThis will generate the filelambda/code.zip.Upload the Lambda function:Upload this zip file to an S3 bucket via cli, Console or however you like.Example via cli:awss3cplambda/code.zips3://example-bucket/code.zipDeploy a CloudFormation stack utilizing the zip as a custom resource provider:Example CloudFormation template:---AWSTemplateFormatVersion:"2010-09-09"Resources:SSMDocExecutionRole:Type:AWS::IAM::RoleProperties:RoleName:CFN-Resource-Custom-SSM-DocumentAssumeRolePolicyDocument:Version:"2012-10-17"Statement:-Effect:AllowPrincipal:Service:lambda.amazonaws.comAction:sts:AssumeRoleManagedPolicyArns:-arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole-Ref:SSMDocExecutionPolicySSMDocExecutionPolicy:Type:AWS::IAM::ManagedPolicyProperties:ManagedPolicyName:CFN-Resource-Custom-SSM-DocumentPolicyDocument:Version:"2012-10-17"Statement:-Effect:AllowAction:-ssm:ListDocuments-ssm:ListTagsForResourceResource:"*"-Effect:AllowAction:-ssm:CreateDocument-ssm:AddTagsToResourceResource:"*"Condition:StringEquals:aws:RequestTag/CreatedByCfnCustomResource:CFN::Resource::Custom::SSM-Document-Effect:AllowAction:-ssm:DeleteDocument-ssm:DescribeDocument-ssm:GetDocument-ssm:ListDocumentVersions-ssm:ModifyDocumentPermission-ssm:UpdateDocument-ssm:UpdateDocumentDefaultVersion-ssm:AddTagsToResource-ssm:RemoveTagsFromResourceResource:"*"Condition:StringEquals:aws:ResourceTag/CreatedByCfnCustomResource:CFN::Resource::Custom::SSM-DocumentSSMDocFunction:Type:AWS::Lambda::FunctionProperties:FunctionName:CFN-Resource-Custom-SSM-Document-ManagerCode:S3Bucket:example-bucketS3Key:code.zipHandler:index.handlerRuntime:nodejs10.xTimeout:3Role:!GetAttSSMDocExecutionRole.ArnMyDocument:Type:Custom::SSM-DocumentProperties:Name:MyDocumentServiceToken:!GetAttSSMDocFunction.ArnStackName:!RefAWS::StackNameUpdateDefaultVersion:true# default: trueContent:schemaVersion:"2.2"description:Echo Hello World!parameters:text:type:Stringdescription:Text to echodefault:Hello World!mainSteps:-name:echoaction:aws:runShellScriptinputs:runCommand:-echo "{{text}}"precondition:StringEquals:-platformType-LinuxDocumentType:Command# default: CommandTargetType:/# default: /Tags:CreatedByCfnCustomResource:CFN::Resource::Custom::SSM-Document# required, see above policy conditions