From a857962770fa61c53ab1156b2d2148efffd04b9f Mon Sep 17 00:00:00 2001 From: diana esteves Date: Thu, 26 Sep 2024 09:29:06 -0500 Subject: [PATCH] Adds aws-ts-lambda-slack example (#1709) Co-authored-by: diana esteves --- aws-ts-lambda-slack/.gitignore | 3 + aws-ts-lambda-slack/Pulumi.yaml | 11 ++++ aws-ts-lambda-slack/README.md | 51 +++++++++++++++ aws-ts-lambda-slack/index.ts | 96 +++++++++++++++++++++++++++++ aws-ts-lambda-slack/lambda/index.js | 53 ++++++++++++++++ aws-ts-lambda-slack/package.json | 12 ++++ aws-ts-lambda-slack/tsconfig.json | 18 ++++++ 7 files changed, 244 insertions(+) create mode 100644 aws-ts-lambda-slack/.gitignore create mode 100644 aws-ts-lambda-slack/Pulumi.yaml create mode 100644 aws-ts-lambda-slack/README.md create mode 100644 aws-ts-lambda-slack/index.ts create mode 100644 aws-ts-lambda-slack/lambda/index.js create mode 100644 aws-ts-lambda-slack/package.json create mode 100644 aws-ts-lambda-slack/tsconfig.json diff --git a/aws-ts-lambda-slack/.gitignore b/aws-ts-lambda-slack/.gitignore new file mode 100644 index 000000000..71a733f9d --- /dev/null +++ b/aws-ts-lambda-slack/.gitignore @@ -0,0 +1,3 @@ +/bin/ +/node_modules/ +Pulumi.dev.yaml \ No newline at end of file diff --git a/aws-ts-lambda-slack/Pulumi.yaml b/aws-ts-lambda-slack/Pulumi.yaml new file mode 100644 index 000000000..a882648cd --- /dev/null +++ b/aws-ts-lambda-slack/Pulumi.yaml @@ -0,0 +1,11 @@ +name: ${PROJECT} +description: ${DESCRIPTION} +runtime: nodejs + +template: + description: Deploy a Slack webhook Lambda function. + config: + aws:region: + description: AWS Region + default: us-west-2 + \ No newline at end of file diff --git a/aws-ts-lambda-slack/README.md b/aws-ts-lambda-slack/README.md new file mode 100644 index 000000000..3c71d8b9d --- /dev/null +++ b/aws-ts-lambda-slack/README.md @@ -0,0 +1,51 @@ +# AWS Lambda for Slack Notification + +A Pulumi example to: + +- Creates an AWS Lambda function to post a message on Slack via a Webhook URL. +- Adds an AWS API Gateway so the Lambda can be invoked externally, e.g, via GitHub Webhooks. +- Uses a Pulumi ESC Environment to dynamically retrieve AWS OIDC Credentials and the Slack URL from AWS Secrets Manager. + +Last update: September 2024 + +## ๐Ÿ“‹ Pre-requisites + +- AWS OIDC configured in an Pulumi ESC Environment +- AWS Secrets Manager with a Slack Webhook URL secret +- A properly configured Slack Webhook URL +- [Pulumi CLI](https://www.pulumi.com/docs/get-started/install/) +- [Pulumi Cloud account](https://app.pulumi.com/signup) +- [npm](https://www.npmjs.com/get-npm) + +## ๐Ÿ‘ฉโ€๐Ÿซ Get started + +This Pulumi example is written as a template. It is meant to be copied via `pulumi new` + +```bash +# login to your Pulumi Cloud if you haven't already +$ pulumi login + +# create a new dir and cd to it +$ mkdir my-slack-demo +$ cd my-slack-demo + +# start your pulumi project +$ pulumi new https://github.com/pulumi/examples/aws-ts-lambda-slack +``` + +```bash +# Add your Pulumi ESC Environment +$ pulumi config env add YOUR_ESC_ENV --yes --non-interactive +$ pulumi up +# select 'yes' to confirm the expected changes +# ๐ŸŽ‰ Ta-Da! +``` + +## ๐Ÿงน Clean up + +To clean up your infrastructure, run: + +```bash +$ pulumi destroy +# select 'yes' to confirm the expected changes +``` diff --git a/aws-ts-lambda-slack/index.ts b/aws-ts-lambda-slack/index.ts new file mode 100644 index 000000000..e7f3fabd0 --- /dev/null +++ b/aws-ts-lambda-slack/index.ts @@ -0,0 +1,96 @@ + +// Copyright 2024, Pulumi Corporation. All rights reserved. + +import * as aws from "@pulumi/aws"; +import * as pulumi from "@pulumi/pulumi"; + +const config = new pulumi.Config(); +const slackWebhookUrl = config.requireSecret("slackWebhookUrl"); + +// Create an IAM role for the Lambda function +const lambdaRole = new aws.iam.Role("lambdaRole", { + assumeRolePolicy: { + Version: "2012-10-17", + Statement: [{ + Action: "sts:AssumeRole", + Principal: { + Service: "lambda.amazonaws.com", + }, + Effect: "Allow", + }], + }, +}); + +// Attach a policy to the role to allow Lambda to log to CloudWatch +const rpa = new aws.iam.RolePolicyAttachment("lambdaRolePolicy", { + role: lambdaRole.name, + policyArn: aws.iam.ManagedPolicies.AWSLambdaBasicExecutionRole, +}); + +// Create the Lambda function +const lambdaFunction = new aws.lambda.Function("myLambda", { + // https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html#runtimes-supported + runtime: "nodejs20.x", + role: lambdaRole.arn, + handler: "index.handler", + code: new pulumi.asset.AssetArchive({ + ".": new pulumi.asset.FileArchive("./lambda"), + }), + environment: { + variables: { + "SLACK_WEBHOOK_URL": slackWebhookUrl, + }, + }, + memorySize: 128, + timeout: 30, + tags: { + "Environment": "dev", + }, +}); + +// Export the Lambda function name +// export const lambdaFunctionName = lambdaFunction.name; + +// Create an API Gateway +const api = new aws.apigateway.RestApi("myApi", { + description: "API Gateway for Lambda function", +}); + + +// Create a root resource +const rootResource = api.rootResourceId; + +// Create a method for the root resource +const rootMethod = new aws.apigateway.Method("rootMethod", { + restApi: api.id, + resourceId: rootResource, + httpMethod: "ANY", + authorization: "NONE", +}); + +// Integrate the Lambda function with the root method +const rootIntegration = new aws.apigateway.Integration("rootIntegration", { + restApi: api.id, + resourceId: rootResource, + httpMethod: rootMethod.httpMethod, + integrationHttpMethod: "POST", + type: "AWS_PROXY", + uri: lambdaFunction.invokeArn, +}); + +// Grant API Gateway permission to invoke the Lambda function +const lambdaPermission = new aws.lambda.Permission("apiGatewayPermission", { + action: "lambda:InvokeFunction", + function: lambdaFunction.arn, + principal: "apigateway.amazonaws.com", + sourceArn: pulumi.interpolate`${api.executionArn}/*/*`, +}); + +// Deploy the API +const deployment = new aws.apigateway.Deployment("myDeployment", { + restApi: api.id, + stageName: "dev", +}, { dependsOn: [rootIntegration] }); + +// Export the URL of the API +export const url = pulumi.interpolate`${deployment.invokeUrl}`; diff --git a/aws-ts-lambda-slack/lambda/index.js b/aws-ts-lambda-slack/lambda/index.js new file mode 100644 index 000000000..4e078938f --- /dev/null +++ b/aws-ts-lambda-slack/lambda/index.js @@ -0,0 +1,53 @@ +const https = require('https'); + +exports.handler = async (event) => { + // Get the Slack webhook URL from environment variables + const slackWebhookUrl = process.env.SLACK_WEBHOOK_URL; + + // Define the message payload + const slackMessage = JSON.stringify({ + text: event.body + }); + + // Slack Webhook URL parsed + const url = new URL(slackWebhookUrl); + + const options = { + hostname: url.hostname, + path: url.pathname, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': slackMessage.length + } + }; + + // Create a promise to post the message + return new Promise((resolve, reject) => { + const req = https.request(options, (res) => { + let response = ''; + + res.on('data', (chunk) => { + response += chunk; + }); + + res.on('end', () => { + resolve({ + statusCode: res.statusCode, + body: response + }); + }); + }); + + req.on('error', (error) => { + reject({ + statusCode: 500, + body: JSON.stringify(error) + }); + }); + + // Send the Slack message + req.write(slackMessage); + req.end(); + }); +}; diff --git a/aws-ts-lambda-slack/package.json b/aws-ts-lambda-slack/package.json new file mode 100644 index 000000000..433ed82c2 --- /dev/null +++ b/aws-ts-lambda-slack/package.json @@ -0,0 +1,12 @@ +{ + "name": "aws-ts-lambda-slack", + "main": "index.ts", + "devDependencies": { + "@types/node": "^22", + "typescript": "^5.6.2" + }, + "dependencies": { + "@pulumi/aws": "^6.52.0", + "@pulumi/pulumi": "^3.133.0" + } +} diff --git a/aws-ts-lambda-slack/tsconfig.json b/aws-ts-lambda-slack/tsconfig.json new file mode 100644 index 000000000..f960d5171 --- /dev/null +++ b/aws-ts-lambda-slack/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "strict": true, + "outDir": "bin", + "target": "es2020", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "experimentalDecorators": true, + "pretty": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.ts" + ] +}