Skip to content

Commit

Permalink
Add a serverless-raw example app (using C#)
Browse files Browse the repository at this point in the history
Adds a simple serverless example using raw AWS primitives - API Gateway, Lambda, DynamoDB, etc.

The app that is deployed is a simple C# app which tracks how many times a route has been accessed in Dynamo.

This highlights another option for packaging and deploying code via Pulumi.

Fixes pulumi#19.
  • Loading branch information
lukehoban committed Mar 22, 2018
1 parent 60d8a71 commit 2ec7e6a
Show file tree
Hide file tree
Showing 10 changed files with 336 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.pulumi
Pulumi.*.yaml
7 changes: 7 additions & 0 deletions serverless-raw/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/bin/
/node_modules/
/yarn.lock
/package-lock.json

/app/**/obj
/app/**/bin
3 changes: 3 additions & 0 deletions serverless-raw/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: serverless
runtime: nodejs
description: Basic example of a serverless AWS application.
58 changes: 58 additions & 0 deletions serverless-raw/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# serverless-raw

An example using some serverless AWS resources, including:

* AWS Lambda Function
* AWS IAM Role
* AWS DynamoDB Table
* AWS APIGateway RestAPI

The Lambda function is a C# application using dotnet2.0 (a similar approach works for any other language supported by
AWS Lambda). To deploy the complete application:

```bash
# Create and configure a new stack
$ pulumi stack init testing
$ pulumi config set aws:region us-east-2

# Install dependencies
$ yarn install

# Build the C# app
$ cd ./app
$ dotnet publish
$ cd ..

# Build the Pulumi program
$ npm run build

# Preview the deployment
$ pulumi preview
Previewing changes:
[snip...]
info: 9 changes previewed:
+ 9 resources to create

# Deploy the update
$ pulumi update
Performing changes:
[snip...]
info: 9 changes performed:
+ 9 resources created
Update duration: 25.017340162s

# Test it out
$ curl $(pulumi stack output endpoint)/hello
{"Path":"/hello","Count":0}

# See the logs
$ pulumi logs -f
2018-03-21T18:24:52.670-07:00[ mylambda-d719650] START RequestId: d1e95652-2d6f-11e8-93f6-2921c8ae65e7 Version: $LATEST
2018-03-21T18:24:56.171-07:00[ mylambda-d719650] Getting count for '/hello'
2018-03-21T18:25:01.327-07:00[ mylambda-d719650] Got count 0 for '/hello'
2018-03-21T18:25:02.267-07:00[ mylambda-d719650] END RequestId: d1e95652-2d6f-11e8-93f6-2921c8ae65e7
2018-03-21T18:25:02.267-07:00[ mylambda-d719650] REPORT RequestId: d1e95652-2d6f-11e8-93f6-2921c8ae65e7 Duration: 9540.93 ms Billed Duration: 9600 ms Memory Size: 128 MB Max Memory Used: 37 MB

# Remove the app
$ pulumi destroy
```
81 changes: 81 additions & 0 deletions serverless-raw/app/Functions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

using Amazon.Lambda.Core;
using Amazon.Lambda.APIGatewayEvents;

using Amazon;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;

using Newtonsoft.Json;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

namespace app
{
public class Counter
{
public string Id { get; set; }
public Int32 Count { get; set; }
}

public class Functions
{
// This const is the name of the environment variable that the serverless.template will use to set
// the name of the DynamoDB table used to store blog posts.
const string TABLENAME_ENVIRONMENT_VARIABLE_LOOKUP = "COUNTER_TABLE";

IDynamoDBContext DDBContext { get; set; }

/// <summary>
/// Default constructor that Lambda will invoke.
/// </summary>
public Functions()
{
// Check to see if a table name was passed in through environment variables and if so
// add the table mapping.
var tableName = System.Environment.GetEnvironmentVariable(TABLENAME_ENVIRONMENT_VARIABLE_LOOKUP);
if(!string.IsNullOrEmpty(tableName))
{
AWSConfigsDynamoDB.Context.TypeMappings[typeof(Counter)] = new Amazon.Util.TypeMapping(typeof(Counter), tableName);
}

var config = new DynamoDBContextConfig { Conversion = DynamoDBEntryConversion.V2 };
this.DDBContext = new DynamoDBContext(new AmazonDynamoDBClient(), config);
}

/// <summary>
/// A Lambda function that returns a simple JSON object.
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public async Task<APIGatewayProxyResponse> GetAsync(APIGatewayProxyRequest request, ILambdaContext context)
{
context.Logger.LogLine($"Getting count for '{request.Path}'");

var counter = await DDBContext.LoadAsync<Counter>(request.Path);
if (counter == null) {
counter = new Counter { Id = request.Path, Count = 0 };
}
var count = counter.Count++;
await DDBContext.SaveAsync<Counter>(counter);

context.Logger.LogLine($"Got count {count} for '{request.Path}'");

var response = new APIGatewayProxyResponse
{
StatusCode = (int)HttpStatusCode.OK,
Body = JsonConvert.SerializeObject(new { Path = request.Path, Count = count }),
Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
};

return response;
}

}
}
22 changes: 22 additions & 0 deletions serverless-raw/app/app.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.DynamoDBv2" Version="3.3.5" />

<PackageReference Include="Amazon.Lambda.Core" Version="1.0.0" />
<PackageReference Include="Amazon.Lambda.Serialization.Json" Version="1.1.0" />
<PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="1.1.2" />

<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
</ItemGroup>

<ItemGroup>
<DotNetCliToolReference Include="Amazon.Lambda.Tools" Version="2.1.1" />
</ItemGroup>

</Project>
127 changes: 127 additions & 0 deletions serverless-raw/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.

import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";

// The location of the built dotnet2.0 application to deploy
let dotNetApplicationPublishFolder = "./app/bin/Debug/netcoreapp2.0/publish";
let dotNetApplicationEntryPoint = "app::app.Functions::GetAsync";
// The stage name to use for the API Gateway URL
let stageName = "api";

///////////////////
// DynamoDB Table
///////////////////

// A DynamoDB table with a single primary key
let counterTable = new aws.dynamodb.Table("counterTable", {
attributes: [
{ name: "Id", type: "S" },
],
hashKey: "Id",
readCapacity: 1,
writeCapacity: 1,
});

///////////////////
// Lambda Function
///////////////////

// Create a Role giving our Lambda access.
let policy: aws.iam.PolicyDocument = {
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com",
},
"Effect": "Allow",
"Sid": "",
},
],
};
let role = new aws.iam.Role("mylambda-role", {
assumeRolePolicy: JSON.stringify(policy),
});
let fullAccess = new aws.iam.RolePolicyAttachment("mylambda-access", {
role: role,
policyArn: aws.iam.AWSLambdaFullAccess,
});

// Create a Lambda function, using code from the `./app` folder.
let lambda = new aws.lambda.Function("mylambda", {
runtime: aws.lambda.DotnetCore2d0Runtime,
code: new pulumi.asset.AssetArchive({
".": new pulumi.asset.FileArchive(dotNetApplicationPublishFolder),
}),
timeout: 300,
handler: dotNetApplicationEntryPoint,
role: role.arn,
environment: {
variables: {
"COUNTER_TABLE": counterTable.name
}
},
}, { dependsOn: [fullAccess] });

///////////////////
// APIGateway RestAPI
///////////////////

// Create the Swagger spec for a proxy which forwards all HTTP requests through to the Lambda function.
function swaggerSpec(lambdaArn: string): string {
let swaggerSpec = {
swagger: "2.0",
info: { title: "api", version: "1.0" },
paths: {
"/{proxy+}": swaggerRouteHandler(lambdaArn),
},
};
return JSON.stringify(swaggerSpec);
}

// Create a single Swagger spec route handler for a Lambda function.
function swaggerRouteHandler(lambdaArn: string) {
let region = aws.config.requireRegion();
return {
"x-amazon-apigateway-any-method": {
"x-amazon-apigateway-integration": {
uri: `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations`,
passthroughBehavior: "when_no_match",
httpMethod: "POST",
type: "aws_proxy",
},
}
};
}

// Create the API Gateway Rest API, using a swagger spec.
let restApi = new aws.apigateway.RestApi("api", {
body: lambda.arn.apply(lambdaArn => swaggerSpec(lambdaArn)),
});

// Create a deployment of the Rest API.
let deployment = new aws.apigateway.Deployment("api-deployment", {
restApi: restApi,
// Note: Set to empty to avoid creating an implicit stage, we'll create it explicitly below instead.
stageName: "",
});

// Create a stage, which is an addressable instance of the Rest API. Set it to point at the latest deployment.
let stage = new aws.apigateway.Stage("api-stage", {
restApi: restApi,
deployment: deployment,
stageName: stageName
});

// Give permissions from API Gateway to invoke the Lambda
let invokePermission = new aws.lambda.Permission("api-lambda-permission", {
action: "lambda:invokeFunction",
function: lambda,
principal: "apigateway.amazonaws.com",
sourceArn: deployment.executionArn.apply(arn => arn + "*/*"),
});

// Export the https endpoint of the running Rest API
export let endpoint = deployment.invokeUrl.apply(url => url + stageName);
16 changes: 16 additions & 0 deletions serverless-raw/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "serverless-raw",
"version": "0.1.0",
"main": "bin/index.js",
"typings": "bin/index.d.ts",
"scripts": {
"build": "tsc"
},
"devDependencies": {
"typescript": "^2.5.3"
},
"dependencies": {
"@pulumi/aws": "^0.11.0",
"@pulumi/pulumi": "^0.11.0"
}
}
21 changes: 21 additions & 0 deletions serverless-raw/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"outDir": "bin",
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"sourceMap": true,
"stripInternal": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true
},
"files": [
"index.ts"
]
}
1 change: 0 additions & 1 deletion video-thumbnailer/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/node_modules/
/yarn.lock
/package-lock.json
/Pulumi.*.yaml

0 comments on commit 2ec7e6a

Please sign in to comment.