Skip to content

Commit 02d4d69

Browse files
nina9753ava-silver
andauthored
Add Serverless Framework Plugin support for AWS SSM API Key (#653)
* Add Serverless Framework Plugin support for AWS SSM API Key * update checkForMultipleApiKeys * update log * update tests * Update index.ts to list check Co-authored-by: Ava Silver <ava.silver@datadoghq.com> * lint format --------- Co-authored-by: Ava Silver <ava.silver@datadoghq.com>
1 parent 08ada35 commit 02d4d69

5 files changed

Lines changed: 219 additions & 20 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ To further configure your plugin, use the following custom parameters in your `s
3030
| `site` | Set which Datadog site to send data to, such as `datadoghq.com` (default), `datadoghq.eu`, `us3.datadoghq.com`, `us5.datadoghq.com`, `ap1.datadoghq.com`, `ap2.datadoghq.com`, or `ddog-gov.com`. This parameter is required when collecting telemetry using the Datadog Lambda Extension. |
3131
| `apiKey` | [Datadog API key][7]. This parameter is required when collecting telemetry using the Datadog Lambda Extension. Alternatively, you can also set the `DATADOG_API_KEY` environment variable in your deployment environment. |
3232
| `appKey` | Datadog app key. Only needed when the `monitors` field is defined. Alternatively, you can also set the `DATADOG_APP_KEY` environment variable in your deployment environment. |
33-
| `apiKeySecretArn` | An alternative to using the `apiKey` field. The ARN of the secret that is storing the Datadog API key in AWS Secrets Manager. Remember to add the `secretsmanager:GetSecretValue` permission to the Lambda execution role. |
33+
| `apiKeySecretArn` | An alternative to using the `apiKey` field. The ARN of the secret that is storing the Datadog API key in AWS Secrets Manager. Remember to add the `secretsmanager:GetSecretValue` permission to the Lambda execution role. |
34+
| `apiKeySsmArn` | An alternative to using the `apiKey` field. The ARN of the parameter that is storing the Datadog API key in AWS Systems Manager Parameter Store. Remember to add the `ssm:GetParameter` and `kms:Decrypt` (for encrypted SecureString parameters) permission to the Lambda execution role. |
3435
| `apiKMSKey` | An alternative to using the `apiKey` field. Datadog API key encrypted using KMS. Remember to add the `kms:Decrypt` permission to the Lambda execution role. |
3536
| `env` | When set along with `addExtension`, a `DD_ENV` environment variable is added to all Lambda functions with the provided value. Otherwise, an `env` tag is added to all Lambda functions with the provided value. Defaults to the `stage` value of the serverless deployment. |
3637
| `service` | When set along with `addExtension`, a `DD_SERVICE` environment variable is added to all Lambda functions with the provided value. Otherwise, a `service` tag is added to all Lambda functions with the provided value. Defaults to the `service` value of the serverless project.
@@ -77,7 +78,7 @@ To use any of these parameters, add a `custom` > `datadog` section to your `serv
7778
```yaml
7879
custom:
7980
datadog:
80-
apiKeySecretArn: "{Datadog_API_Key_Secret_ARN}"
81+
apiKeySecretArn: "{Datadog_API_Key_Secret_ARN}" # or use apiKeySsmArn for AWS Systems Manager Parameter Store
8182
enableXrayTracing: false
8283
enableDDTracing: true
8384
enableDDLogs: true

src/env.spec.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ describe("setEnvConfiguration", () => {
496496
addLayers: false,
497497
apiKey: "1234",
498498
apiKeySecretArn: "some-resource:from:aws:secrets-manager:arn",
499+
apiKeySsmArn: "some-resource:from:aws:ssm:arn",
499500
apiKMSKey: "0912",
500501
site: "datadoghq.eu",
501502
subdomain: "app",
@@ -525,6 +526,7 @@ describe("setEnvConfiguration", () => {
525526
environment: {
526527
DD_API_KEY: "1234",
527528
DD_API_KEY_SECRET_ARN: "some-resource:from:aws:secrets-manager:arn",
529+
DD_API_KEY_SSM_ARN: "some-resource:from:aws:ssm:arn",
528530
DD_CAPTURE_LAMBDA_PAYLOAD: false,
529531
DD_KMS_API_KEY: "0912",
530532
DD_LOG_LEVEL: "debug",
@@ -544,6 +546,7 @@ describe("setEnvConfiguration", () => {
544546
environment: {
545547
DD_API_KEY: "1234",
546548
DD_API_KEY_SECRET_ARN: "some-resource:from:aws:secrets-manager:arn",
549+
DD_API_KEY_SSM_ARN: "some-resource:from:aws:ssm:arn",
547550
DD_CAPTURE_LAMBDA_PAYLOAD: false,
548551
DD_KMS_API_KEY: "0912",
549552
DD_LOG_LEVEL: "debug",
@@ -564,6 +567,7 @@ describe("setEnvConfiguration", () => {
564567
environment: {
565568
DD_API_KEY: "1234",
566569
DD_API_KEY_SECRET_ARN: "some-resource:from:aws:secrets-manager:arn",
570+
DD_API_KEY_SSM_ARN: "some-resource:from:aws:ssm:arn",
567571
DD_CAPTURE_LAMBDA_PAYLOAD: false,
568572
DD_KMS_API_KEY: "0912",
569573
DD_LOG_LEVEL: "debug",
@@ -583,6 +587,7 @@ describe("setEnvConfiguration", () => {
583587
environment: {
584588
DD_API_KEY: "1234",
585589
DD_API_KEY_SECRET_ARN: "some-resource:from:aws:secrets-manager:arn",
590+
DD_API_KEY_SSM_ARN: "some-resource:from:aws:ssm:arn",
586591
DD_CAPTURE_LAMBDA_PAYLOAD: false,
587592
DD_KMS_API_KEY: "0912",
588593
DD_LOG_LEVEL: "debug",
@@ -1275,6 +1280,190 @@ describe("setEnvConfiguration", () => {
12751280
"apiKeySecretArn` is not supported for Node runtimes when using Synchronous Metrics. Set DATADOG_API_KEY in your environment, or use `apiKmsKey` in the configuration.",
12761281
);
12771282
});
1283+
1284+
it("successfully sets DD_API_KEY_SSM_ARN for Node runtime with sync metrics", () => {
1285+
const handlers: FunctionInfo[] = [
1286+
{
1287+
handler: {
1288+
environment: {},
1289+
events: [],
1290+
runtime: "nodejs20.x",
1291+
},
1292+
name: "function",
1293+
type: RuntimeType.NODE,
1294+
},
1295+
];
1296+
1297+
setEnvConfiguration(
1298+
{
1299+
addLayers: false,
1300+
apiKeySsmArn: "arn:aws:ssm:us-east-1:123456789012:parameter/datadog/api-key",
1301+
site: "datadoghq.eu",
1302+
subdomain: "app",
1303+
logLevel: "debug",
1304+
flushMetricsToLogs: false,
1305+
enableXrayTracing: true,
1306+
enableDDTracing: true,
1307+
enableDDLogs: true,
1308+
subscribeToAccessLogs: true,
1309+
subscribeToExecutionLogs: false,
1310+
subscribeToStepFunctionLogs: false,
1311+
addExtension: false,
1312+
enableTags: true,
1313+
injectLogContext: true,
1314+
exclude: ["dd-excluded-function"],
1315+
enableSourceCodeIntegration: true,
1316+
uploadGitMetadata: false,
1317+
captureLambdaPayload: false,
1318+
failOnError: false,
1319+
skipCloudformationOutputs: false,
1320+
},
1321+
handlers,
1322+
);
1323+
1324+
expect(handlers).toEqual([
1325+
{
1326+
handler: {
1327+
environment: {
1328+
DD_API_KEY_SSM_ARN: "arn:aws:ssm:us-east-1:123456789012:parameter/datadog/api-key",
1329+
DD_CAPTURE_LAMBDA_PAYLOAD: false,
1330+
DD_FLUSH_TO_LOG: false,
1331+
DD_LOGS_INJECTION: true,
1332+
DD_LOG_LEVEL: "debug",
1333+
DD_MERGE_XRAY_TRACES: true,
1334+
DD_SERVERLESS_LOGS_ENABLED: true,
1335+
DD_SITE: "datadoghq.eu",
1336+
DD_TRACE_ENABLED: true,
1337+
},
1338+
events: [],
1339+
runtime: "nodejs20.x",
1340+
},
1341+
name: "function",
1342+
type: RuntimeType.NODE,
1343+
},
1344+
]);
1345+
});
1346+
1347+
it("successfully sets DD_API_KEY_SSM_ARN for Python runtime with extension", () => {
1348+
const handlers: FunctionInfo[] = [
1349+
{
1350+
handler: {
1351+
environment: {},
1352+
events: [],
1353+
runtime: "python3.11",
1354+
},
1355+
name: "function",
1356+
type: RuntimeType.PYTHON,
1357+
},
1358+
];
1359+
1360+
setEnvConfiguration(
1361+
{
1362+
addLayers: true,
1363+
apiKeySsmArn: "arn:aws:ssm:us-east-1:123456789012:parameter/datadog/api-key",
1364+
site: "datadoghq.com",
1365+
subdomain: "app",
1366+
logLevel: "info",
1367+
flushMetricsToLogs: true,
1368+
enableXrayTracing: false,
1369+
enableDDTracing: true,
1370+
enableDDLogs: true,
1371+
subscribeToAccessLogs: true,
1372+
subscribeToExecutionLogs: false,
1373+
subscribeToStepFunctionLogs: false,
1374+
addExtension: true,
1375+
enableTags: true,
1376+
injectLogContext: false,
1377+
exclude: [],
1378+
enableSourceCodeIntegration: true,
1379+
uploadGitMetadata: false,
1380+
captureLambdaPayload: false,
1381+
failOnError: false,
1382+
skipCloudformationOutputs: false,
1383+
},
1384+
handlers,
1385+
);
1386+
1387+
expect(handlers).toEqual([
1388+
{
1389+
handler: {
1390+
environment: {
1391+
DD_API_KEY_SSM_ARN: "arn:aws:ssm:us-east-1:123456789012:parameter/datadog/api-key",
1392+
DD_CAPTURE_LAMBDA_PAYLOAD: false,
1393+
DD_LOGS_INJECTION: false,
1394+
DD_LOG_LEVEL: "info",
1395+
DD_MERGE_XRAY_TRACES: false,
1396+
DD_SERVERLESS_LOGS_ENABLED: true,
1397+
DD_SITE: "datadoghq.com",
1398+
DD_TRACE_ENABLED: true,
1399+
},
1400+
events: [],
1401+
runtime: "python3.11",
1402+
},
1403+
name: "function",
1404+
type: RuntimeType.PYTHON,
1405+
},
1406+
]);
1407+
});
1408+
1409+
it("doesn't set DD_API_KEY from environment if apiKeySsmArn is defined", () => {
1410+
process.env = {};
1411+
process.env.DATADOG_API_KEY = "dd-api-key";
1412+
const handlers: FunctionInfo[] = [
1413+
{
1414+
handler: {
1415+
environment: {},
1416+
events: [],
1417+
},
1418+
name: "function",
1419+
type: RuntimeType.NODE,
1420+
},
1421+
];
1422+
1423+
setEnvConfiguration(
1424+
{
1425+
addLayers: false,
1426+
apiKeySsmArn: "arn:aws:ssm:us-east-1:123456789012:parameter/datadog/api-key",
1427+
site: "datadoghq.com",
1428+
subdomain: "app",
1429+
logLevel: "info",
1430+
flushMetricsToLogs: false,
1431+
enableXrayTracing: true,
1432+
enableDDTracing: true,
1433+
enableDDLogs: true,
1434+
addExtension: true,
1435+
enableTags: true,
1436+
injectLogContext: true,
1437+
subscribeToAccessLogs: true,
1438+
subscribeToExecutionLogs: false,
1439+
subscribeToStepFunctionLogs: false,
1440+
exclude: [],
1441+
enableSourceCodeIntegration: true,
1442+
uploadGitMetadata: false,
1443+
failOnError: false,
1444+
skipCloudformationOutputs: false,
1445+
},
1446+
handlers,
1447+
);
1448+
expect(handlers).toEqual([
1449+
{
1450+
handler: {
1451+
environment: {
1452+
DD_API_KEY_SSM_ARN: "arn:aws:ssm:us-east-1:123456789012:parameter/datadog/api-key",
1453+
DD_LOG_LEVEL: "info",
1454+
DD_LOGS_INJECTION: false,
1455+
DD_SERVERLESS_LOGS_ENABLED: true,
1456+
DD_SITE: "datadoghq.com",
1457+
DD_TRACE_ENABLED: true,
1458+
DD_MERGE_XRAY_TRACES: true,
1459+
},
1460+
events: [],
1461+
},
1462+
name: "function",
1463+
type: RuntimeType.NODE,
1464+
},
1465+
]);
1466+
});
12781467
it("defines `DD_COLD_START_TRACING` when enableColdStartTracing is set", () => {
12791468
const handlers: FunctionInfo[] = [
12801469
{

src/env.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export interface Configuration {
2828
monitorsAppKey?: string;
2929
// The ARN of the secret in AWS Secrets Manager containing the Datadog API key.
3030
apiKeySecretArn?: string;
31+
// The ARN of the parameter in AWS Systems Manager Parameter Store containing the Datadog API key.
32+
apiKeySsmArn?: string;
3133
// Datadog API Key encrypted using KMS, only necessary when using metrics without log forwarding
3234
apiKMSKey?: string;
3335
// Whether to capture and store the payload and response of a lambda invocation
@@ -147,6 +149,7 @@ const webpackPluginName = "serverless-webpack";
147149
const apiKeyEnvVar = "DD_API_KEY";
148150
const apiKeyKMSEnvVar = "DD_KMS_API_KEY";
149151
const apiKeySecretArnEnvVar = "DD_API_KEY_SECRET_ARN";
152+
const apiKeySsmArnEnvVar = "DD_API_KEY_SSM_ARN";
150153
const siteURLEnvVar = "DD_SITE";
151154
const logLevelEnvVar = "DD_LOG_LEVEL";
152155
const logForwardingEnvVar = "DD_FLUSH_TO_LOG";
@@ -221,11 +224,12 @@ export function setEnvConfiguration(config: Configuration, handlers: FunctionInf
221224
environment[apiKeyEnvVar] === undefined &&
222225
// Only set this from the environment if all other methods of authentication
223226
// are not in use. This will set DATADOG_API_KEY on the lambda from the environment
224-
// variable directly if they haven't set one of the below three options
227+
// variable directly if they haven't set one of the below four options
225228
// in the configuration.
226229
config.apiKMSKey === undefined &&
227230
config.apiKey === undefined &&
228-
config.apiKeySecretArn === undefined
231+
config.apiKeySecretArn === undefined &&
232+
config.apiKeySsmArn === undefined
229233
) {
230234
environment[apiKeyEnvVar] = process.env.DATADOG_API_KEY;
231235
logMessage("Using DATADOG_API_KEY environment variable for authentication");
@@ -246,6 +250,9 @@ export function setEnvConfiguration(config: Configuration, handlers: FunctionInf
246250
}
247251
environment[apiKeySecretArnEnvVar] = config.apiKeySecretArn;
248252
}
253+
if (config.apiKeySsmArn !== undefined && environment[apiKeySsmArnEnvVar] === undefined) {
254+
environment[apiKeySsmArnEnvVar] = config.apiKeySsmArn;
255+
}
249256
if (environment[siteURLEnvVar] === undefined) {
250257
environment[siteURLEnvVar] = config.site;
251258
}

src/index.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -886,7 +886,7 @@ describe("ServerlessPlugin", () => {
886886
}
887887
expect(threwError).toBe(true);
888888
expect(thrownErrorMessage).toEqual(
889-
"The environment variable `DATADOG_API_KEY` or configuration variable `apiKMSKey` or `apiKeySecretArn` must be set because `addExtension` is set to true as default.",
889+
"The environment variable `DATADOG_API_KEY` or configuration variable `apiKMSKey` or `apiKeySecretArn` or `apiKeySsmArn` must be set because `addExtension` is set to true as default.",
890890
);
891891
});
892892
});

src/index.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -634,10 +634,11 @@ function validateConfiguration(config: Configuration): void {
634634
config.apiKey === undefined &&
635635
process.env.DATADOG_API_KEY === undefined &&
636636
config.apiKMSKey === undefined &&
637-
config.apiKeySecretArn === undefined
637+
config.apiKeySecretArn === undefined &&
638+
config.apiKeySsmArn === undefined
638639
) {
639640
throw new Error(
640-
"The environment variable `DATADOG_API_KEY` or configuration variable `apiKMSKey` or `apiKeySecretArn` must be set because `addExtension` is set to true as default.",
641+
"The environment variable `DATADOG_API_KEY` or configuration variable `apiKMSKey` or `apiKeySecretArn` or `apiKeySsmArn` must be set because `addExtension` is set to true as default.",
641642
);
642643
}
643644
}
@@ -656,18 +657,19 @@ function validateConfiguration(config: Configuration): void {
656657
}
657658

658659
function checkForMultipleApiKeys(config: Configuration): void {
659-
let multipleApiKeysMessage;
660-
if (config.apiKey !== undefined && config.apiKMSKey !== undefined && config.apiKeySecretArn !== undefined) {
661-
multipleApiKeysMessage = "`apiKey`, `apiKMSKey`, and `apiKeySecretArn`";
662-
} else if (config.apiKey !== undefined && config.apiKMSKey !== undefined) {
663-
multipleApiKeysMessage = "`apiKey` and `apiKMSKey`";
664-
} else if (config.apiKey !== undefined && config.apiKeySecretArn !== undefined) {
665-
multipleApiKeysMessage = "`apiKey` and `apiKeySecretArn`";
666-
} else if (config.apiKMSKey !== undefined && config.apiKeySecretArn !== undefined) {
667-
multipleApiKeysMessage = "`apiKMSKey` and `apiKeySecretArn`";
668-
}
669-
670-
if (multipleApiKeysMessage) {
671-
throw new Error(`${multipleApiKeysMessage} should not be set at the same time.`);
660+
const definedKeys = ["apiKey", "apiKMSKey", "apiKeySecretArn", "apiKeySsmArn"]
661+
.filter((key) => key in config)
662+
.map((key) => `\`${key}\``);
663+
664+
if (definedKeys.length > 1) {
665+
let message;
666+
if (definedKeys.length === 2) {
667+
message = `${definedKeys[0]} and ${definedKeys[1]}`;
668+
} else {
669+
const last = definedKeys[definedKeys.length - 1];
670+
const rest = definedKeys.slice(0, -1).join(", ");
671+
message = `${rest}, and ${last}`;
672+
}
673+
throw new Error(`${message} should not be set at the same time.`);
672674
}
673675
}

0 commit comments

Comments
 (0)