Skip to content

Commit

Permalink
Add aws/azure cloud shortener example. (pulumi#130)
Browse files Browse the repository at this point in the history
  • Loading branch information
CyrusNajmabadi committed Sep 10, 2018
1 parent d2c414e commit 164ea9e
Show file tree
Hide file tree
Showing 10 changed files with 435 additions and 0 deletions.
3 changes: 3 additions & 0 deletions cloud-ts-url-shortener-cache-http/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: url-shortener-cache-http
runtime: nodejs
description: URL shortener with cache and HttpServer.
87 changes: 87 additions & 0 deletions cloud-ts-url-shortener-cache-http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Serverless URL Shortener with Redis Cache and HttpServer

A sample URL shortener SPA that uses the high-level `cloud.Table` and `cloud.HttpServer` components. The example shows to combine serverless functions along with containers. This shows that you can create your own `cloud.*`-like
abstractions for your own use, your team's, or to share with the community using your language's package manager.

## Deploying and running the program

Note: some values in this example will be different from run to run. These values are indicated
with `***`.

1. Create a new stack:

```
$ pulumi stack init url-cache-testing
```

1. Set AWS or Azure as the provider:

```
$ pulumi config set cloud:provider aws
# or
$ pulumi config set cloud:provider azure
```

1. If using AWS configure Pulumi to use AWS Fargate, which is currently only available in `us-east-1`, `us-east-2`, `us-west-2`, and `eu-west-1`:

```
$ pulumi config set aws:region us-west-2
$ pulumi config set cloud-aws:useFargate true
```

1. If using Azure set an appropriate location like:

```
$ pulumi config set cloud-azure:location "West US 2"
```

1. Set a value for the Redis password. The value can be an encrypted secret, specified with the `--secret` flag. If this flag is not provided, the value will be saved as plaintext in `Pulumi.url-cache-testing.yaml` (since `url-cache-testing` is the current stack name).

```
$ pulumi config set --secret redisPassword S3cr37Password
```

1. Add the 'www' directory to the uploaded function code so it can be served from the http server:

```
$ pulumi config set cloud-aws:functionIncludePaths
#or
$ pulumi config set cloud-azure:functionIncludePaths
```

1. Restore NPM modules via `npm install` or `yarn install`.

1. Compile the program via `tsc` or `npm run build` or `yarn run build`.

1. Preview and run the deployment via `pulumi update`. The operation will take about 5 minutes to complete.

```
$ pulumi update
Previewing stack 'url-cache-testing'
...
Updating stack 'url-cache-testing'
Performing changes:
#: Resource Type Name
1: pulumi:pulumi:Stack url-shortener-cache-url-
...
49: aws:apigateway:Stage urlshortener
info: 49 changes performed:
+ 49 resources created
Update duration: ***
```

1. To view the API endpoint, use the `stack output` command:

```
$ pulumi stack output endpointUrl
https://***.us-east-1.amazonaws.com/stage/
```

1. Open this page in a browser and you'll see a single page app for creating and viewing short URLs.

## Clean up

To clean up resources, run `pulumi destroy` and answer the confirmation question at the prompt.
60 changes: 60 additions & 0 deletions cloud-ts-url-shortener-cache-http/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.

import * as pulumi from "@pulumi/pulumi";
import * as cloud from "@pulumi/cloud";
import * as config from "./config";

// A simple cache abstraction that wraps Redis.
export class Cache {
private readonly redis: cloud.Service;
private readonly endpoint: pulumi.Output<cloud.Endpoint>;

constructor(name: string, memory: number = 128) {
let pw = config.redisPassword;
this.redis = new cloud.Service(name, {
containers: {
redis: {
image: "redis:alpine",
memory: memory,
ports: [{ port: 6379, external: true }],
command: ["redis-server", "--requirepass", pw],
},
},
});

this.endpoint = this.redis.endpoints.apply(endpoints => endpoints.redis[6379]);
}

public get(key: string): Promise<string> {
let ep = this.endpoint.get();
console.log(`Getting key '${key}' on Redis@${ep.hostname}:${ep.port}`);

let client = require("redis").createClient(ep.port, ep.hostname, { password: config.redisPassword });
return new Promise<string>((resolve, reject) => {
client.get(key, (err: any, v: any) => {
if (err) {
reject(err);
} else {
resolve(v);
}
});
});
}

public set(key: string, value: string): Promise<void> {
let ep = this.endpoint.get();
console.log(`Setting key '${key}' to '${value}' on Redis@${ep.hostname}:${ep.port}`);

let client = require("redis").createClient(ep.port, ep.hostname, { password: config.redisPassword });
return new Promise<void>((resolve, reject) => {
client.set(key, value, (err: any, v: any) => {
if (err) {
reject(err);
} else {
console.log("Set succeeed: " + JSON.stringify(v))
resolve();
}
});
});
};
}
8 changes: 8 additions & 0 deletions cloud-ts-url-shortener-cache-http/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.

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

let config = new pulumi.Config("url-shortener-cache");

// Get the Redis password from config
export let redisPassword = config.require("redisPassword");
134 changes: 134 additions & 0 deletions cloud-ts-url-shortener-cache-http/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.

import * as cloud from "@pulumi/cloud";
import * as cache from "./cache";
import * as express from "express";
import * as fs from "fs";
import * as mime from "mime-types";

type AsyncRequestHandler = (req: express.Request, res: express.Response, next: express.NextFunction) => Promise<void>;

const asyncMiddleware = (fn: AsyncRequestHandler) => {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}

// Create a table `urls`, with `name` as primary key.
let urlTable = new cloud.Table("urls", "name");

// Create a cache of frequently accessed urls.
let urlCache = new cache.Cache("urlcache");

// Create a web server.
let endpoint = new cloud.HttpServer("urlshortener", () => {
let app = express();

// GET /url lists all URLs currently registered.
app.get("/url", asyncMiddleware(async (req, res) => {
try {
let items = await urlTable.scan();
res.status(200).json(items);
console.log(`GET /url retrieved ${items.length} items`);
} catch (err) {
res.status(500).json(err.stack);
console.log(`GET /url error: ${err.stack}`);
}
}));

// GET /url/{name} redirects to the target URL based on a short-name.
app.get("/url/:name", asyncMiddleware(async (req, res) => {
let name = req.params.name
try {
// First try the Redis cache.
let url = await urlCache.get(name);
if (url) {
console.log(`Retrieved value from Redis: ${url}`);
res.setHeader("X-Powered-By", "redis");
}
else {
// If we didn't find it in the cache, consult the table.
let value = await urlTable.get({name});
url = value && value.url;
if (url) {
urlCache.set(name, url); // cache it for next time.
}
}

// If we found an entry, 301 redirect to it; else, 404.
if (url) {
res.setHeader("Location", url);
res.status(302);
res.end("");
console.log(`GET /url/${name} => ${url}`)
}
else {
res.status(404);
res.end("");
console.log(`GET /url/${name} is missing (404)`)
}
} catch (err) {
res.status(500).json(err.stack);
console.log(`GET /url/${name} error: ${err.stack}`);
}
}));

// POST /url registers a new URL with a given short-name.
app.post("/url", asyncMiddleware(async (req, res) => {
const url = <string>req.query["url"];
const name = <string>req.query["name"];
try {
await urlTable.insert({ name, url });
await urlCache.set(name, url);
res.json({ shortenedURLName: name });
console.log(`POST /url/${name} => ${url}`);
} catch (err) {
res.status(500).json(err.stack);
console.log(`POST /url/${name} => ${url} error: ${err.stack}`);
}
}));

// Serve all files in the www directory to the root.
// Note: www will be auto-included using config. either
// cloud-aws:functionIncludePaths or
// cloud-azure:functionIncludePaths

// staticRoutes(app, "/", "www");
app.use("/", express.static("www"));

app.get("*", (req, res) => {
res.json({ uncaught: { url: req.url, baseUrl: req.baseUrl, originalUrl: req.originalUrl, version: process.version } });
});

return app;
});

function staticRoutes(app: express.Express, path: string, root: string) {
for (const child of fs.readdirSync("./" + root)) {
app.get(path + child, (req, res) => {
try
{
// console.log("Trying to serve: " + path + child)
// res.json({ serving: child });
const localPath = "./" + root + "/" + child;
const contents = fs.readFileSync(localPath);

var type = mime.contentType(child)
if (type) {
res.setHeader('Content-Type', type);
}

const stat = fs.statSync(path);

res.setHeader("Content-Length", stat.size);
res.end(contents);
}
catch (err) {
console.log(JSON.stringify({ message: err.message, stack: err.stack }));
res.json({ message: err.message, stack: err.stack });
}
});
}
}

export let endpointUrl = endpoint.url.apply(u => u + "index.html");
32 changes: 32 additions & 0 deletions cloud-ts-url-shortener-cache-http/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "url-shortener",
"version": "1.0.0",
"main": "bin/index.js",
"typings": "bin/index.d.ts",
"scripts": {
"build": "tsc"
},
"devDependencies": {
"@types/node": "^8.0.27",
"@types/express": "^4.16.0",
"@types/parseurl": "^1.3.1",
"@types/send": "^0.14.4",
"typescript": "^2.7.2",
"@types/mime-types": "^2.1.0"
},
"dependencies": {
"@pulumi/pulumi": "^0.15.2-dev",
"@pulumi/azure": "^0.15.2-dev",
"@pulumi/azure-serverless": "^0.15.1-rc1",
"@pulumi/cloud": "^0.15.2-dev",
"@pulumi/cloud-aws": "^0.15.2-dev",
"@pulumi/cloud-azure": "^0.15.2-dev",
"redis": "^2.8.0",
"express": "^4.16.3",
"parseurl": "^1.3.2",
"send": "^0.16.2",
"mime-types": "^2.1.20"
},
"peerDependencies": {
}
}
24 changes: 24 additions & 0 deletions cloud-ts-url-shortener-cache-http/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"outDir": "bin",
"target": "es6",
"lib": [
"es6"
],
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true
},
"files": [
"index.ts",
"cache.ts",
"config.ts"
]
}
11 changes: 11 additions & 0 deletions cloud-ts-url-shortener-cache-http/www/bootstrap.min.css

Large diffs are not rendered by default.

Binary file added cloud-ts-url-shortener-cache-http/www/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 164ea9e

Please sign in to comment.