Skip to content

Commit ca727a9

Browse files
lvthillokaiz-ioMichael Kaiser
authored
feat(cdk): add ssm association example (#1158)
* feat(cdk): add ssm association example * docs(cdk): fix order * Remove used test command * Fix: CachinContext requires a lookup that cause build to fail without valid aws creds --------- Co-authored-by: Michael Kaiser <gh@kaiz.io> Co-authored-by: Michael Kaiser <kaiser@kaiz.io>
1 parent edbbb81 commit ca727a9

File tree

6 files changed

+352
-0
lines changed

6 files changed

+352
-0
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# SSM Document Association
2+
3+
<!--BEGIN STABILITY BANNER-->
4+
---
5+
6+
![Stability: Stable](https://img.shields.io/badge/stability-Stable-success.svg?style=for-the-badge)
7+
8+
> **This is a stable example. It should successfully build out of the box**
9+
>
10+
> This example is built on Construct Libraries marked "Stable" and does not have any infrastructure prerequisites to build.
11+
---
12+
<!--END STABILITY BANNER-->
13+
14+
## Overview
15+
16+
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.
17+
18+
## How it works
19+
20+
1. SSM Document is created with a command to write the current timestamp to a file.
21+
2. SSM Document Association is created with a target tag, parameter, and schedule.
22+
3. An EC2 instance is created with the same tag-value combination as the SSM Document Association target.
23+
4. You can connect to the EC2 instance using AWS Session Manager.
24+
5. Verify the existence of the file with the timestamp.
25+
26+
27+
## Build and Deploy
28+
29+
1. Ensure aws-cdk is installed and your AWS account/region is [bootstrapped](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html).
30+
31+
```bash
32+
npm install -g aws-cdk
33+
cdk bootstrap
34+
```
35+
36+
2. Build and deploy.
37+
_You will need to have [Docker](https://docs.docker.com/get-docker/) installed and running._
38+
39+
```bash
40+
npm run build
41+
cdk deploy
42+
```
43+
44+
You should see some useful outputs in the terminal:
45+
46+
```bash
47+
✅ SsmDocumentAssociationStack
48+
49+
✨ Deployment time: 175.86s
50+
51+
Outputs:
52+
SsmDocumentAssociationStack.DocumentName = WriteTimeToFile
53+
SsmDocumentAssociationStack.InstanceId = <INSTANCE_ID>
54+
Stack ARN: <STACK_ARN>
55+
56+
✨ Total time: 67.29s
57+
```
58+
59+
## Try it out
60+
61+
1. Deploy the stack and connect to the EC2 instance using AWS Session Manager.
62+
63+
2. Verify the existence of the file with the timestamp.
64+
65+
```bash
66+
$ ls /opt/aws/time_records/
67+
time_20250414_195134.txt
68+
$ cat /opt/aws/time_records/time_20250414_195134.txt
69+
Mon Apr 14 19:51:34 UTC 2025
70+
```
71+
72+
3. Try again, 30 minutes later, and see the new file created.
73+
74+
```bash
75+
$ ls /opt/aws/time_records/
76+
time_20250414_195134.txt time_20250414_201930.txt
77+
$ cat /opt/aws/time_records/time_20250414_201930.txt
78+
Mon Apr 14 20:19:30 UTC 2025
79+
```
80+
81+
82+
## Useful commands
83+
84+
* `npm run build` compile typescript to js
85+
* `npm run watch` watch for changes and compile
86+
* `npm run test` perform the jest unit tests
87+
* `cdk deploy` deploy this stack to your default AWS account/region
88+
* `cdk diff` compare deployed stack with current state
89+
* `cdk synth` emits the synthesized CloudFormation template
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env node
2+
import * as cdk from 'aws-cdk-lib';
3+
import { SsmDocumentAssociationStack } from '../lib/ssm-document-association-stack';
4+
5+
const app = new cdk.App();
6+
new SsmDocumentAssociationStack(app, 'SsmDocumentAssociationStack');
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
{
2+
"app": "npx ts-node --prefer-ts-exts bin/ssm-document-association.ts",
3+
"watch": {
4+
"include": [
5+
"**"
6+
],
7+
"exclude": [
8+
"README.md",
9+
"cdk*.json",
10+
"**/*.d.ts",
11+
"**/*.js",
12+
"tsconfig.json",
13+
"package*.json",
14+
"yarn.lock",
15+
"node_modules",
16+
"test"
17+
]
18+
},
19+
"context": {
20+
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
21+
"@aws-cdk/core:checkSecretUsage": true,
22+
"@aws-cdk/core:target-partitions": [
23+
"aws",
24+
"aws-cn"
25+
],
26+
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
27+
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
28+
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
29+
"@aws-cdk/aws-iam:minimizePolicies": true,
30+
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
31+
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
32+
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
33+
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
34+
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
35+
"@aws-cdk/core:enablePartitionLiterals": true,
36+
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
37+
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
38+
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
39+
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
40+
"@aws-cdk/aws-route53-patters:useCertificate": true,
41+
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
42+
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
43+
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
44+
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
45+
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
46+
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
47+
"@aws-cdk/aws-redshift:columnId": true,
48+
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
49+
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
50+
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
51+
"@aws-cdk/aws-kms:aliasNameRef": true,
52+
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
53+
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
54+
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
55+
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
56+
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
57+
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
58+
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
59+
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
60+
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
61+
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
62+
"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
63+
"@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
64+
"@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
65+
"@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
66+
"@aws-cdk/aws-eks:nodegroupNameAttribute": true,
67+
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
68+
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
69+
"@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
70+
"@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
71+
"@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false,
72+
"@aws-cdk/aws-ecs:disableEcsImdsBlocking": true,
73+
"@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
74+
"@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
75+
"@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true,
76+
"@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
77+
"@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
78+
"@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
79+
"@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
80+
"@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true,
81+
"@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true,
82+
"@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true,
83+
"@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true,
84+
"@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true,
85+
"@aws-cdk/core:enableAdditionalMetadataCollection": true,
86+
"@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": true,
87+
"@aws-cdk/aws-s3:setUniqueReplicationRoleName": true,
88+
"@aws-cdk/aws-events:requireEventBusPolicySid": true
89+
}
90+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import * as cdk from 'aws-cdk-lib';
2+
import { Construct } from 'constructs';
3+
import * as ssm from 'aws-cdk-lib/aws-ssm';
4+
import * as iam from 'aws-cdk-lib/aws-iam';
5+
import * as ec2 from 'aws-cdk-lib/aws-ec2';
6+
7+
export class SsmDocumentAssociationStack extends cdk.Stack {
8+
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
9+
super(scope, id, props);
10+
11+
// Create an SSM Document that writes current time to a new file
12+
const ssmDocument = new ssm.CfnDocument(this, 'TimeWriterDocument', {
13+
name: 'WriteTimeToFile',
14+
documentType: 'Command',
15+
content: {
16+
schemaVersion: '2.2',
17+
description: 'Write current timestamp to a new file',
18+
parameters: {
19+
DirectoryPath: {
20+
type: 'String',
21+
description: 'Directory where the time files will be written',
22+
default: '/tmp/time_logs'
23+
}
24+
},
25+
mainSteps: [
26+
{
27+
action: 'aws:runShellScript',
28+
name: 'writeTimeToNewFile',
29+
inputs: {
30+
runCommand: [
31+
'mkdir -p {{DirectoryPath}}',
32+
'TIMESTAMP=$(date +"%Y%m%d_%H%M%S")',
33+
'FILENAME="time_$TIMESTAMP.txt"',
34+
'FILEPATH="{{DirectoryPath}}/$FILENAME"',
35+
'echo "Creating new time file: $FILEPATH"',
36+
'date > $FILEPATH',
37+
'echo "Current time written to $FILEPATH: $(cat $FILEPATH)"',
38+
'echo "Total files in directory: $(ls -1 {{DirectoryPath}} | wc -l)"',
39+
'echo "Operation completed on $(hostname)"'
40+
]
41+
}
42+
}
43+
]
44+
}
45+
});
46+
47+
// Create an association for the document
48+
// Apply the document to all EC2 instances with the tag Environment:Development
49+
// The association will run every 30 minutes
50+
new ssm.CfnAssociation(this, 'DocumentAssociation', {
51+
name: ssmDocument.ref,
52+
targets: [
53+
{
54+
key: 'tag:Environment',
55+
values: ['Development']
56+
}
57+
],
58+
parameters: {
59+
// overwrite default parameter
60+
'DirectoryPath': ['/opt/aws/time_records']
61+
},
62+
scheduleExpression: 'rate(30 minutes)'
63+
});
64+
65+
66+
// Testing infrastructure
67+
// A VPC + EC2 + Connect using AWS Session Manager
68+
// No NAT / Private Subnets
69+
const vpc = new ec2.Vpc(this, 'SSMDocumentTestVpc', {
70+
maxAzs: 1,
71+
natGateways: 0,
72+
subnetConfiguration: [
73+
{
74+
name: 'public',
75+
subnetType: ec2.SubnetType.PUBLIC,
76+
}
77+
]
78+
});
79+
80+
const role = new iam.Role(this, 'EC2SSMRole', {
81+
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
82+
managedPolicies: [
83+
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')
84+
]
85+
});
86+
87+
// Create an EC2 instance with Environment tag set to Development
88+
// Use AMI that contains SSM agent
89+
const instance = new ec2.Instance(this, 'SSMTestInstance', {
90+
vpc,
91+
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.NANO),
92+
machineImage: ec2.MachineImage.latestAmazonLinux2023(),
93+
role,
94+
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
95+
});
96+
97+
// Add the Environment:Development tag that matches our SSM Document association
98+
cdk.Tags.of(instance).add('Environment', 'Development');
99+
100+
// Outputs
101+
new cdk.CfnOutput(this, 'DocumentName', {
102+
value: ssmDocument.ref,
103+
description: 'The name of the SSM document'
104+
});
105+
106+
new cdk.CfnOutput(this, 'InstanceId', {
107+
value: instance.instanceId,
108+
description: 'The ID of the test EC2 instance'
109+
});
110+
}
111+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "ssm-document-association",
3+
"version": "0.1.0",
4+
"bin": {
5+
"ssm-document-association": "bin/ssm-document-association.js"
6+
},
7+
"scripts": {
8+
"build": "tsc",
9+
"watch": "tsc -w",
10+
"cdk": "cdk"
11+
},
12+
"devDependencies": {
13+
"@types/jest": "^29.5.14",
14+
"@types/node": "22.7.9",
15+
"jest": "^29.7.0",
16+
"ts-jest": "^29.2.5",
17+
"aws-cdk": "2.1007.0",
18+
"ts-node": "^10.9.2",
19+
"typescript": "~5.6.3"
20+
},
21+
"dependencies": {
22+
"aws-cdk-lib": "2.186.0",
23+
"constructs": "^10.0.0"
24+
}
25+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"module": "commonjs",
5+
"lib": [
6+
"es2020",
7+
"dom"
8+
],
9+
"declaration": true,
10+
"strict": true,
11+
"noImplicitAny": true,
12+
"strictNullChecks": true,
13+
"noImplicitThis": true,
14+
"alwaysStrict": true,
15+
"noUnusedLocals": false,
16+
"noUnusedParameters": false,
17+
"noImplicitReturns": true,
18+
"noFallthroughCasesInSwitch": false,
19+
"inlineSourceMap": true,
20+
"inlineSources": true,
21+
"experimentalDecorators": true,
22+
"strictPropertyInitialization": false,
23+
"typeRoots": [
24+
"./node_modules/@types"
25+
]
26+
},
27+
"exclude": [
28+
"node_modules",
29+
"cdk.out"
30+
]
31+
}

0 commit comments

Comments
 (0)