forked from pulumi/examples
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add aws/azure cloud shortener example. (pulumi#130)
- Loading branch information
1 parent
d2c414e
commit 164ea9e
Showing
10 changed files
with
435 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
}); | ||
}); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": { | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} |
Large diffs are not rendered by default.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.