Skip to main content

CDK Construct for managing SSM Documents

Project description

CDK SSM Document

Source Test GitHub Docs

npm package PyPI package

Downloads npm PyPI

AWS CDK L3 construct for managing SSM Documents.

CloudFormation's support for SSM Documents currently 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 document
  • Old versions are permanently lost

This construct provides document support in a way you'd expect it:

  • Changes on documents will cerate new versions
  • Versions cannot be deleted

Installation

This package has peer dependencies, which need to be installed along in the expected version.

For TypeScript/NodeJS, add these to your dependencies in package.json. For Python, add these to your requirements.txt:

  • cdk-ssm-document
  • aws-cdk-lib (^2.0.0)
  • constructs (^10.0.0)

CDK compatibility

  • Version 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.

Usage

Creating a document from a YAML or JSON file

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Document } from 'cdk-ssm-document';
import fs = require('fs');
import path = require('path');

export class TestStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);

    const file = path.join(__dirname, '../documents/hello-world.yml');
    new Document(this, 'SSM-Document-HelloWorld', {
      name: 'HelloWorld',
      content: fs.readFileSync(file).toString(),
    });
  }
}

Creating a document via inline definition

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Document } from 'cdk-ssm-document';
import fs = require('fs');
import path = require('path');

export class TestStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);

    new Document(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 directory

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Document } from 'cdk-ssm-document';
import fs = require('fs');
import path = require('path');

export class TestStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);

    const dir = path.join(__dirname, '../documents');
    const files = fs.readdirSync(dir);

    for (const i in files) {
      const name = files[i];
      const shortName = name.split('.').slice(0, -1).join('.'); // removes file extension
      const file = `${dir}/${name}`;

      new Document(this, `SSM-Document-${shortName}`, {
        name: shortName,
        content: fs.readFileSync(file).toString(),
      });
    }
  }
}

Creating a distributor package

import { aws_iam, aws_s3, aws_s3_deployment, Stack, StackProps } from 'aws-cdk-lib';
import { Document } from 'cdk-ssm-document';
import { Construct } from 'constructs';
import fs = require('fs');
import path = require('path');

export class TestStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const bucketName = `${Stack.of(this).account}-cdk-ssm-document-storage`;
    const bucket = new aws_s3.Bucket(this, 'DistributorPackages', {
      bucketName: bucketName,
    });
    const packageDeploy = new aws_s3_deployment.BucketDeployment(
      this,
      'distribution-packages',
      {
        sources: [aws_s3_deployment.Source.asset('../location/to/distributor/packages')],
        destinationBucket: bucket,
      }
    );

    const file = path.join(
      __dirname,
      '../location/to/distributor/packages/v1/manifest.json'
    );
    const doc = new Document(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`] }],
    });

    /**
     * The owner/creator of the document must have read access to the
     * s3 files that make up a distribution. Since that is the lambda in this
     * case we must give it `GetObject` permissions before they will can become `Active`.
     *
     * If access is not granted to the role that created the document you may see
     * an error like the following :
     *
     * ```
     * Permanent download error: Source URL 's3://cdk-ssm-document-storage/v1/package.zip' reported:
     * Access Denied (Service: Amazon S3; Status Code: 403;
     * Error Code: AccessDenied; Request  *ID:DES1XEHZTJ9R; S3 Extended Request ID:
     * A+u8sTGQ6bZpAwl2eXDLq4KTkoeYyQR2XEV+I=; Proxy: null)
     * ```
     */
    doc.lambda.role?.addToPrincipalPolicy(
      new aws_iam.PolicyStatement({
        actions: ['s3:GetObject'],
        resources: [`${bucket.arnForObjects('*')}`],
      })
    );
    doc.node.addDependency(packageDeploy);
  }
}

Deploying many documents in a single stack

When 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.

const docA = new Document(this, 'doc-A', {...})
const docB = new Document(this, 'doc-B', {...})
const docC = new Document(this, 'doc-C', {...})

docC.node.addDependency(docB);
docB.node.addDependency(docA);

When looping through a directory of documents it could look like this:

var last: Document | undefined = undefined;
for (const i in files) {
  const doc = new Document(this, `SSM-Document-${shortName}`, {...});
  if (typeof last !== 'undefined') {
    last.node.addDependency(doc);
  }
  last = doc;
}

Using the Lambda as a custom resource in CloudFormation - without CDK

If you're still not convinced to use the AWS CDK, you can still use the Lambda as a custom resource in your CFN template. Here is how:

  1. Create a zip file for the Lambda:

    To create a zip from the Lambda source run:

    lambda/build
    

    This will generate the file lambda/code.zip.

  2. Upload the Lambda function:

    Upload this zip file to an S3 bucket via cli, Console or however you like.

    Example via cli:

    aws s3 cp lambda/code.zip s3://example-bucket/code.zip
    
  3. Deploy a CloudFormation stack utilizing the zip as a custom resource provider:

    Example CloudFormation template:

    ---
    AWSTemplateFormatVersion: "2010-09-09"
    Resources:
      SSMDocExecutionRole:
        Type: AWS::IAM::Role
        Properties:
          RoleName: CFN-Resource-Custom-SSM-Document
          AssumeRolePolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Principal:
                  Service: lambda.amazonaws.com
                Action: sts:AssumeRole
          ManagedPolicyArns:
            - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
            - Ref: SSMDocExecutionPolicy
    
      SSMDocExecutionPolicy:
        Type: AWS::IAM::ManagedPolicy
        Properties:
          ManagedPolicyName: CFN-Resource-Custom-SSM-Document
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ssm:ListDocuments
                  - ssm:ListTagsForResource
                Resource: "*"
              - Effect: Allow
                Action:
                  - ssm:CreateDocument
                  - ssm:AddTagsToResource
                Resource: "*"
                Condition:
                  StringEquals:
                    aws:RequestTag/CreatedByCfnCustomResource: CFN::Resource::Custom::SSM-Document
              - Effect: Allow
                Action:
                  - ssm:DeleteDocument
                  - ssm:DescribeDocument
                  - ssm:GetDocument
                  - ssm:ListDocumentVersions
                  - ssm:ModifyDocumentPermission
                  - ssm:UpdateDocument
                  - ssm:UpdateDocumentDefaultVersion
                  - ssm:AddTagsToResource
                  - ssm:RemoveTagsFromResource
                Resource: "*"
                Condition:
                  StringEquals:
                    aws:ResourceTag/CreatedByCfnCustomResource: CFN::Resource::Custom::SSM-Document
    
      SSMDocFunction:
        Type: AWS::Lambda::Function
        Properties:
          FunctionName: CFN-Resource-Custom-SSM-Document-Manager
          Code:
            S3Bucket: example-bucket
            S3Key: code.zip
          Handler: index.handler
          Runtime: nodejs10.x
          Timeout: 3
          Role: !GetAtt SSMDocExecutionRole.Arn
    
      MyDocument:
        Type: Custom::SSM-Document
        Properties:
          Name: MyDocument
          ServiceToken: !GetAtt SSMDocFunction.Arn
          StackName: !Ref AWS::StackName
          UpdateDefaultVersion: true # default: true
          Content:
            schemaVersion: "2.2"
            description: Echo Hello World!
            parameters:
              text:
                type: String
                description: Text to echo
                default: Hello World!
            mainSteps:
              - name: echo
                action: aws:runShellScript
                inputs:
                  runCommand:
                    - echo "{{text}}"
                precondition:
                  StringEquals:
                    - platformType
                    - Linux
          DocumentType: Command # default: Command
          TargetType: / # default: /
          Tags:
            CreatedByCfnCustomResource: CFN::Resource::Custom::SSM-Document # required, see above policy conditions
    

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cdk-ssm-document-3.1.1.tar.gz (345.4 kB view details)

Uploaded Source

Built Distribution

cdk_ssm_document-3.1.1-py3-none-any.whl (343.6 kB view details)

Uploaded Python 3

File details

Details for the file cdk-ssm-document-3.1.1.tar.gz.

File metadata

  • Download URL: cdk-ssm-document-3.1.1.tar.gz
  • Upload date:
  • Size: 345.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.2.0 pkginfo/1.5.0.1 requests/2.24.0 setuptools/50.3.0 requests-toolbelt/0.9.1 tqdm/4.50.0 CPython/3.7.9

File hashes

Hashes for cdk-ssm-document-3.1.1.tar.gz
Algorithm Hash digest
SHA256 de29065dc1386b4a47d65ad3816efe5af54cdcbe30fddf4a79ffeedadbe7825c
MD5 77576f03017cfc6c86936486baa9f1bb
BLAKE2b-256 ae9e7f9f0a01e185a275a381a587adbed7a3d280e3b7224392bf01c04798d562

See more details on using hashes here.

File details

Details for the file cdk_ssm_document-3.1.1-py3-none-any.whl.

File metadata

  • Download URL: cdk_ssm_document-3.1.1-py3-none-any.whl
  • Upload date:
  • Size: 343.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.2.0 pkginfo/1.5.0.1 requests/2.24.0 setuptools/50.3.0 requests-toolbelt/0.9.1 tqdm/4.50.0 CPython/3.7.9

File hashes

Hashes for cdk_ssm_document-3.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a6ee395439c751fd1c4fc34b5424ebedfd14fd3abfa30092b52b63d2efaca9cf
MD5 40dc8e5f1a0e4f466010e91bd5cfd946
BLAKE2b-256 573d97aa2a5380f8b46ae9c7f1fdfca62972c440dd1a1a4558899a8368d9cf36

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page