It’s possible to simplify this serverless application by moving the runtime code into the infrastructure definition. This isn’t always the right way to design your infrastructure as code, but for “fully serverless” applications like this one, where the boundary between application and infrastructure is intentionally blurred, this can be a great way to go.
First, delete the IAM handlerRole
and handlerPolicy
definitions altogether.
Next, replace your API Gateway site
with the following code:
const site = new awsx.apigateway.API("site", {
routes: [{
path: "/",
method: "GET",
eventHandler: async () => {
const dc = new AWS.DynamoDB.DocumentClient();
const result = await dc.update({
TableName: hits.name.get(),
Key: { "Site": "ACMECorp" },
UpdateExpression: "SET Hits = if_not_exists(Hits, :zero) + :incr",
ExpressionAttributeValues: { ":zero": 0, ":incr": 1 },
ReturnValues: "UPDATED_NEW",
}).promise();
return {
statusCode: 200,
headers: { "Content-Type": "text/html" },
body: "<h1>Welcome to ACMECorp!</h1>\n"+
`<p>${result.Attributes!.Hits} hits.</p>\n`,
};
},
}],
});
Remember to keep the line at the end to export the url. It is safe to also delete the handler/index.js
file altogether now.
The index.ts
file should now have the following contents:
import * as AWS from "aws-sdk";
import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";
import * as pulumi from "@pulumi/pulumi";
const hits = new aws.dynamodb.Table("hits", {
attributes: [{ name: "Site", type: "S" }],
hashKey: "Site",
billingMode: "PAY_PER_REQUEST",
});
const site = new awsx.apigateway.API("site", {
routes: [{
path: "/",
method: "GET",
eventHandler: async () => {
const dc = new AWS.DynamoDB.DocumentClient();
const result = await dc.update({
TableName: hits.name.get(),
Key: { "Site": "ACMECorp" },
UpdateExpression: "SET Hits = if_not_exists(Hits, :zero) + :incr",
ExpressionAttributeValues: { ":zero": 0, ":incr": 1 },
ReturnValues: "UPDATED_NEW",
}).promise();
return {
statusCode: 200,
headers: { "Content-Type": "text/html" },
body: "<h1>Welcome to ACMECorp!</h1>\n"+
`<p>${result.Attributes!.Hits} hits.</p>\n`,
};
},
}],
});
export const url = site.url;
Next, run an update:
pulumi up
The output will look something like this:
Updating (dev):
Type Name Status Info
pulumi:pulumi:Stack serverless-demo-dev
├─ aws:apigateway:x:API site
+ │ ├─ aws:iam:Role site4c238266 created
+ │ ├─ aws:iam:RolePolicyAttachment site4c238266-32be53a2 created
+ │ ├─ aws:lambda:Function site4c238266 created
~ │ ├─ aws:apigateway:RestApi site updated [diff: ~body]
+- │ ├─ aws:apigateway:Deployment site replaced [diff: ~variables]
+- │ ├─ aws:lambda:Permission site-fa520765 replaced [diff: ~function]
~ │ └─ aws:apigateway:Stage site updated [diff: ~deployment]
- ├─ aws:lambda:Function get-handler deleted
- ├─ aws:iam:RolePolicy handler-policy deleted
- └─ aws:iam:Role handler-role deleted
Outputs:
url: "https://02fpixl9jf.execute-api.us-west-2.amazonaws.com/stage/"
Resources:
+ 3 created
~ 2 updated
- 3 deleted
+-2 replaced
10 changes. 3 unchanged
Duration: 50s
Now, curl the endpoint a few more times:
for i in {1..5}; do curl $(pulumi stack output url); done
Notice that the counter increases:
<h1>Welcome to ACMECorp!</h1>
<p>6 hits.</p>
<h1>Welcome to ACMECorp!</h1>
<p>7 hits.</p>
<h1>Welcome to ACMECorp!</h1>
<p>8 hits.</p>
<h1>Welcome to ACMECorp!</h1>
<p>9 hits.</p>
<h1>Welcome to ACMECorp!</h1>
<p>10 hits.</p>
Because we reused the same table from the prior update, the counter has continued where the prior commands left off.