Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions typescript/ssm-document-association/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# SSM Document Association

<!--BEGIN STABILITY BANNER-->
---

![Stability: Stable](https://img.shields.io/badge/stability-Stable-success.svg?style=for-the-badge)

> **This is a stable example. It should successfully build out of the box**
>
> This example is built on Construct Libraries marked "Stable" and does not have any infrastructure prerequisites to build.
---
<!--END STABILITY BANNER-->

## Overview

An example that shows how to create an SSM document and associate it with targets that meet certain conditions — in this case, based on a tag and value. Additionally, an EC2 instance is deployed with this specific tag-value combination, so the document will be executed on that instance. The document will write the current timestamp to a file on the instance every 30 minutes.

## How it works

1. SSM Document is created with a command to write the current timestamp to a file.
2. SSM Document Association is created with a target tag, parameter, and schedule.
3. An EC2 instance is created with the same tag-value combination as the SSM Document Association target.
4. You can connect to the EC2 instance using AWS Session Manager.
5. Verify the existence of the file with the timestamp.


## Build and Deploy

1. Ensure aws-cdk is installed and your AWS account/region is [bootstrapped](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html).

```bash
npm install -g aws-cdk
cdk bootstrap
```

2. Build and deploy.
_You will need to have [Docker](https://docs.docker.com/get-docker/) installed and running._

```bash
npm run build
cdk deploy
```

You should see some useful outputs in the terminal:

```bash
✅ SsmDocumentAssociationStack

✨ Deployment time: 175.86s

Outputs:
SsmDocumentAssociationStack.DocumentName = WriteTimeToFile
SsmDocumentAssociationStack.InstanceId = <INSTANCE_ID>
Stack ARN: <STACK_ARN>

✨ Total time: 67.29s
```

## Try it out

1. Deploy the stack and connect to the EC2 instance using AWS Session Manager.

2. Verify the existence of the file with the timestamp.

```bash
$ ls /opt/aws/time_records/
time_20250414_195134.txt
$ cat /opt/aws/time_records/time_20250414_195134.txt
Mon Apr 14 19:51:34 UTC 2025
```

3. Try again, 30 minutes later, and see the new file created.

```bash
$ ls /opt/aws/time_records/
time_20250414_195134.txt time_20250414_201930.txt
$ cat /opt/aws/time_records/time_20250414_201930.txt
Mon Apr 14 20:19:30 UTC 2025
```


## Useful commands

* `npm run build` compile typescript to js
* `npm run watch` watch for changes and compile
* `npm run test` perform the jest unit tests
* `cdk deploy` deploy this stack to your default AWS account/region
* `cdk diff` compare deployed stack with current state
* `cdk synth` emits the synthesized CloudFormation template
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { SsmDocumentAssociationStack } from '../lib/ssm-document-association-stack';

const app = new cdk.App();
new SsmDocumentAssociationStack(app, 'SsmDocumentAssociationStack');
90 changes: 90 additions & 0 deletions typescript/ssm-document-association/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"app": "npx ts-node --prefer-ts-exts bin/ssm-document-association.ts",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
"@aws-cdk/aws-kms:aliasNameRef": true,
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
"@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
"@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
"@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
"@aws-cdk/aws-eks:nodegroupNameAttribute": true,
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
"@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
"@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
"@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false,
"@aws-cdk/aws-ecs:disableEcsImdsBlocking": true,
"@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
"@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
"@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true,
"@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
"@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
"@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
"@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
"@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true,
"@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true,
"@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true,
"@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true,
"@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true,
"@aws-cdk/core:enableAdditionalMetadataCollection": true,
"@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": true,
"@aws-cdk/aws-s3:setUniqueReplicationRoleName": true,
"@aws-cdk/aws-events:requireEventBusPolicySid": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ec2 from 'aws-cdk-lib/aws-ec2';

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

// Create an SSM Document that writes current time to a new file
const ssmDocument = new ssm.CfnDocument(this, 'TimeWriterDocument', {
name: 'WriteTimeToFile',
documentType: 'Command',
content: {
schemaVersion: '2.2',
description: 'Write current timestamp to a new file',
parameters: {
DirectoryPath: {
type: 'String',
description: 'Directory where the time files will be written',
default: '/tmp/time_logs'
}
},
mainSteps: [
{
action: 'aws:runShellScript',
name: 'writeTimeToNewFile',
inputs: {
runCommand: [
'mkdir -p {{DirectoryPath}}',
'TIMESTAMP=$(date +"%Y%m%d_%H%M%S")',
'FILENAME="time_$TIMESTAMP.txt"',
'FILEPATH="{{DirectoryPath}}/$FILENAME"',
'echo "Creating new time file: $FILEPATH"',
'date > $FILEPATH',
'echo "Current time written to $FILEPATH: $(cat $FILEPATH)"',
'echo "Total files in directory: $(ls -1 {{DirectoryPath}} | wc -l)"',
'echo "Operation completed on $(hostname)"'
]
}
}
]
}
});

// Create an association for the document
// Apply the document to all EC2 instances with the tag Environment:Development
// The association will run every 30 minutes
new ssm.CfnAssociation(this, 'DocumentAssociation', {
name: ssmDocument.ref,
targets: [
{
key: 'tag:Environment',
values: ['Development']
}
],
parameters: {
// overwrite default parameter
'DirectoryPath': ['/opt/aws/time_records']
},
scheduleExpression: 'rate(30 minutes)'
});


// Testing infrastructure
// A VPC + EC2 + Connect using AWS Session Manager
// No NAT / Private Subnets
const vpc = new ec2.Vpc(this, 'SSMDocumentTestVpc', {
maxAzs: 1,
natGateways: 0,
subnetConfiguration: [
{
name: 'public',
subnetType: ec2.SubnetType.PUBLIC,
}
]
});

const role = new iam.Role(this, 'EC2SSMRole', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')
]
});

// Create an EC2 instance with Environment tag set to Development
// Use AMI that contains SSM agent
const instance = new ec2.Instance(this, 'SSMTestInstance', {
vpc,
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.NANO),
machineImage: ec2.MachineImage.latestAmazonLinux2023(),
role,
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
});

// Add the Environment:Development tag that matches our SSM Document association
cdk.Tags.of(instance).add('Environment', 'Development');

// Outputs
new cdk.CfnOutput(this, 'DocumentName', {
value: ssmDocument.ref,
description: 'The name of the SSM document'
});

new cdk.CfnOutput(this, 'InstanceId', {
value: instance.instanceId,
description: 'The ID of the test EC2 instance'
});
}
}
25 changes: 25 additions & 0 deletions typescript/ssm-document-association/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "ssm-document-association",
"version": "0.1.0",
"bin": {
"ssm-document-association": "bin/ssm-document-association.js"
},
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"cdk": "cdk"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "22.7.9",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"aws-cdk": "2.1007.0",
"ts-node": "^10.9.2",
"typescript": "~5.6.3"
},
"dependencies": {
"aws-cdk-lib": "2.186.0",
"constructs": "^10.0.0"
}
}
31 changes: 31 additions & 0 deletions typescript/ssm-document-association/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": [
"es2020",
"dom"
],
"declaration": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": false,
"inlineSourceMap": true,
"inlineSources": true,
"experimentalDecorators": true,
"strictPropertyInitialization": false,
"typeRoots": [
"./node_modules/@types"
]
},
"exclude": [
"node_modules",
"cdk.out"
]
}
Loading