diff --git a/aws-ts-containers/Pulumi.yaml b/aws-ts-containers/Pulumi.yaml new file mode 100644 index 000000000..518a93bd6 --- /dev/null +++ b/aws-ts-containers/Pulumi.yaml @@ -0,0 +1,8 @@ +name: container-quickstart +description: NGINX container example +runtime: nodejs +template: + config: + aws:region: + description: The AWS region to deploy into + default: us-west-2 diff --git a/aws-ts-containers/README.md b/aws-ts-containers/README.md new file mode 100644 index 000000000..3dcc288d7 --- /dev/null +++ b/aws-ts-containers/README.md @@ -0,0 +1,65 @@ +[![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new) + +# Easy container example + +Companion to the tutorial [Provision containers on AWS](https://pulumi.io/quickstart/aws-containers.html). + +## Prerequisites + +To run this example, make sure [Docker](https://docs.docker.com/engine/installation/) is installed and running. + +## Running the App + +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 containers-dev + ``` + +1. Configure Pulumi to use an AWS region that supports Fargate. This 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 + ``` + +1. Restore NPM modules via `npm install` or `yarn install`. + +1. Preview and deploy the app via `pulumi up`. The preview will take a few minutes, as it builds a Docker container. A total of 19 resources are created. + + ``` + $ pulumi up + ``` + +1. View the endpoint URL, and run curl: + + ```bash + $ pulumi stack output + Current stack outputs (1) + OUTPUT VALUE + hostname http://***.elb.us-west-2.amazonaws.com + + $ curl $(pulumi stack output hostname) + + + Hello, Pulumi! + +

Hello, S3!

+

Made with ❤️ with Pulumi

+ + ``` + +1. To view the runtime logs from the container, use the `pulumi logs` command. To get a log stream, use `pulumi logs --follow`. + + ``` + $ pulumi logs --follow + Collecting logs for stack container-quickstart-dev since 2018-05-22T14:25:46.000-07:00. + 2018-05-22T15:33:22.057-07:00[ pulumi-nginx] 172.31.13.248 - - [22/May/2018:22:33:22 +0000] "GET / HTTP/1.1" 200 189 "-" "curl/7.54.0" "-" + ``` + +## Clean up + +To clean up resources, run `pulumi destroy` and answer the confirmation question at the prompt. + diff --git a/aws-ts-containers/app/Dockerfile b/aws-ts-containers/app/Dockerfile new file mode 100644 index 000000000..4f9d60fb7 --- /dev/null +++ b/aws-ts-containers/app/Dockerfile @@ -0,0 +1,2 @@ +FROM nginx +COPY content /usr/share/nginx/html diff --git a/aws-ts-containers/app/content/favicon.png b/aws-ts-containers/app/content/favicon.png new file mode 100644 index 000000000..ad4baeb6f Binary files /dev/null and b/aws-ts-containers/app/content/favicon.png differ diff --git a/aws-ts-containers/app/content/index.html b/aws-ts-containers/app/content/index.html new file mode 100644 index 000000000..d0fe5c694 --- /dev/null +++ b/aws-ts-containers/app/content/index.html @@ -0,0 +1,7 @@ + + + Hello, Pulumi! + +

Hello, containers!

+

Made with ❤️ with Pulumi

+ \ No newline at end of file diff --git a/aws-ts-containers/index.ts b/aws-ts-containers/index.ts new file mode 100644 index 000000000..8ff8af032 --- /dev/null +++ b/aws-ts-containers/index.ts @@ -0,0 +1,24 @@ +import * as awsx from "@pulumi/aws-infra"; + +// Create an elastic network listener to listen for requests and route them to the container. +// See https://docs.aws.amazon.com/elasticloadbalancing/latest/network/introduction.html +// for more details. +let listener = new awsx.elasticloadbalancingv2.NetworkListener("nginx", { port: 80 }); + +// Define the service to run. We pass in the listener to hook up the network load balancer +// to the containers the service will launch. +let service = new awsx.ecs.FargateService("nginx", { + desiredCount: 2, + taskDefinitionArgs: { + containers: { + nginx: { + image: awsx.ecs.Image.fromPath("./app"), + memory: 512, + portMappings: [listener], + }, + }, + }, +}); + +// export just the hostname property of the container frontend +export const hostname = listener.endpoint().apply(e => `http://${e.hostname}`); diff --git a/aws-ts-containers/package.json b/aws-ts-containers/package.json new file mode 100644 index 000000000..e59736feb --- /dev/null +++ b/aws-ts-containers/package.json @@ -0,0 +1,9 @@ +{ + "name": "container-quickstart", + "main": "index.js", + "dependencies": { + "@pulumi/pulumi": "dev", + "@pulumi/aws": "dev", + "@pulumi/aws-infra": "=0.16.3-dev.1546999531" + } +} diff --git a/aws-ts-voting-app/Pulumi.yaml b/aws-ts-voting-app/Pulumi.yaml new file mode 100644 index 000000000..620e31c4d --- /dev/null +++ b/aws-ts-voting-app/Pulumi.yaml @@ -0,0 +1,11 @@ +name: voting-app +runtime: nodejs +description: Voting app that uses containers +template: + config: + aws:region: + description: The AWS region to deploy into + default: us-west-2 + redisPassword: + description: The Redis password + secret: true diff --git a/aws-ts-voting-app/README.md b/aws-ts-voting-app/README.md new file mode 100644 index 000000000..4afacd222 --- /dev/null +++ b/aws-ts-voting-app/README.md @@ -0,0 +1,111 @@ +[![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new) + +# Voting app with two containers + +A simple voting app that uses Redis for a data store and a Python Flask app for the frontend. The example has been ported from https://github.com/Azure-Samples/azure-voting-app-redis. + +The example shows how easy it is to deploy containers into production and to connect them to one another. Since the example defines a custom container, Pulumi does the following: +- Builds the Docker image +- Provisions AWS Container Registry (ECR) instance +- Pushes the image to the ECR instance +- Creates a new ECS task definition, pointing to the ECR image definition + +## Prerequisites + +To use this example, make sure [Docker](https://docs.docker.com/engine/installation/) is installed and running. + +## Deploying and running the program + +### Configure the deployment + +Note: some values in this example will be different from run to run. These values are indicated +with `***`. + +1. Login via `pulumi login`. + +1. Create a new stack: + + ``` + $ pulumi stack init voting-app-testing + ``` + +1. Set AWS as the provider: + + ``` + $ pulumi config set cloud:provider aws + ``` + +1. Configure Pulumi to use an AWS region that supports 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 + ``` + +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.testing.yaml` (since `testing` is the current stack name). + + ``` + $ pulumi config set --secret redisPassword S3cr37Password + ``` + +### Install dependencies + +1. Restore NPM modules via `npm install` or `yarn install`. + +### Preview and deploy + +1. Ensure the Docker daemon is running on your machine, then preview and deploy the program with `pulumi up`. The program deploys 24 resources and takes about 10 minutes to complete. + +1. View the stack output properties via `pulumi stack output`. The stack output property `frontendUrl` is the URL and port of the deployed app: + + ```bash + $ pulumi stack output frontendURL + ***.elb.us-west-2.amazonaws.com + ``` + +1. In a browser, navigate to the URL for `frontendURL`. You should see the voting app webpage. + + ![Voting app screenshot](./voting-app-webpage.png) + +### Delete resources + +When you're done, run `pulumi destroy` to delete the program's resources: + +``` +$ pulumi destroy +This will permanently destroy all resources in the 'testing' stack! +Please confirm that this is what you'd like to do by typing ("testing"): testing +``` + +## About the code + +At the start of the program, the following lines retrieve the value for the Redis password by reading a [configuration value](https://pulumi.io/reference/config.html). This is the same value that was set above with the command `pulumi config set redisPassword `: + +```typescript +let config = new pulumi.Config(); +let redisPassword = config.require("redisPassword"); +``` + +In the program, the value can be used like any other variable. + +### Resources + +The program provisions two top-level resources with the following commands: + +```typescript +let redisCache = new awsx.ecs.FargateService("voting-app-cache", ... ) +let frontend = new awsx.ecs.FargateService("voting-app-frontend", ... ) +``` + +The definition of `redisCache` uses the `image` property of `FargateService.taskDefinitionArgs` to point to an existing Docker image. In this case, this is the image `redis` at tag `alpine` on Docker Hub. The `redisPassword` variable is passed to the startup command for this image. + +The definition of `frontend` is more interesting, as it uses `image` property of `FargateService.taskDefinitionArgs` to point to a folder with a Dockerfile, which in this case is a Python Flask app. Pulumi automatically invokes `docker build` for you and pushes the container to ECR. + +So that the `frontend` container can connect to `redisCache`, the environment variables `REDIS`, `REDIS_PORT` are defined. Using the `redisListenre.endpoint` property, it's easy to declare the connection between the two containers. + +The Flask app uses these environment variables to connect to the Redis cache container. See the following in [`frontend/app/main.py`](frontend/app/main.py): + +```python +redis_server = os.environ['REDIS'] +redis_port = os.environ['REDIS_PORT'] +redis_password = os.environ['REDIS_PWD'] +``` diff --git a/aws-ts-voting-app/frontend/Dockerfile b/aws-ts-voting-app/frontend/Dockerfile new file mode 100755 index 000000000..690a4775b --- /dev/null +++ b/aws-ts-voting-app/frontend/Dockerfile @@ -0,0 +1,3 @@ +FROM tiangolo/uwsgi-nginx-flask:python3.6 +RUN pip install redis +COPY /app /app diff --git a/aws-ts-voting-app/frontend/LICENSE b/aws-ts-voting-app/frontend/LICENSE new file mode 100644 index 000000000..d1ca00f20 --- /dev/null +++ b/aws-ts-voting-app/frontend/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE \ No newline at end of file diff --git a/aws-ts-voting-app/frontend/app/config_file.cfg b/aws-ts-voting-app/frontend/app/config_file.cfg new file mode 100755 index 000000000..23c142d8f --- /dev/null +++ b/aws-ts-voting-app/frontend/app/config_file.cfg @@ -0,0 +1,5 @@ +# UI Configurations +TITLE = 'Pulumi Voting App' +VOTE1VALUE = 'Tabs' +VOTE2VALUE = 'Spaces' +SHOWHOST = 'false' \ No newline at end of file diff --git a/aws-ts-voting-app/frontend/app/main.py b/aws-ts-voting-app/frontend/app/main.py new file mode 100755 index 000000000..d1fe50f42 --- /dev/null +++ b/aws-ts-voting-app/frontend/app/main.py @@ -0,0 +1,71 @@ +# Copied from https://github.com/Azure-Samples/azure-voting-app-redis + +from flask import Flask, request, render_template +import os +import random +import redis +import socket +import sys + +app = Flask(__name__) + +# Load configurations +app.config.from_pyfile('config_file.cfg') +button1 = app.config['VOTE1VALUE'] +button2 = app.config['VOTE2VALUE'] +title = app.config['TITLE'] + +# Redis configurations +redis_server = os.environ['REDIS'] +redis_port = os.environ['REDIS_PORT'] +redis_password = os.environ['REDIS_PWD'] + +# Redis Connection +try: + r = redis.StrictRedis(host=redis_server, port=redis_port, password=redis_password) + r.ping() +except redis.ConnectionError: + exit('Failed to connect to Redis, terminating.') + +# Init Redis +if not r.get(button1): r.set(button1,0) +if not r.get(button2): r.set(button2,0) + +@app.route('/', methods=['GET', 'POST']) +def index(): + + if request.method == 'GET': + + # Get current values + vote1 = r.get(button1).decode('utf-8') + vote2 = r.get(button2).decode('utf-8') + + # Return index with values + return render_template("index.html", value1=int(vote1), value2=int(vote2), button1=button1, button2=button2, title=title) + + elif request.method == 'POST': + + if request.form['vote'] == 'reset': + + # Empty table and return results + r.set(button1,0) + r.set(button2,0) + vote1 = r.get(button1).decode('utf-8') + vote2 = r.get(button2).decode('utf-8') + return render_template("index.html", value1=int(vote1), value2=int(vote2), button1=button1, button2=button2, title=title) + + else: + + # Insert vote result into DB + vote = request.form['vote'] + r.incr(vote,1) + + # Get current values + vote1 = r.get(button1).decode('utf-8') + vote2 = r.get(button2).decode('utf-8') + + # Return results + return render_template("index.html", value1=int(vote1), value2=int(vote2), button1=button1, button2=button2, title=title) + +if __name__ == "__main__": + app.run() diff --git a/aws-ts-voting-app/frontend/app/static/default.css b/aws-ts-voting-app/frontend/app/static/default.css new file mode 100755 index 000000000..cb2cb0ada --- /dev/null +++ b/aws-ts-voting-app/frontend/app/static/default.css @@ -0,0 +1,96 @@ +body { + background-color:#F8F8F8; +} + +div#container { + margin-top:5%; +} + +div#space { + display:block; + margin: 0 auto; + width: 500px; + height: 10px; + +} + +div#logo { + display:block; + margin: 0 auto; + width: 500px; + text-align: center; + font-size:30px; + font-family: 'PT Sans', sans-serif; + /*border-bottom: 1px solid black;*/ +} + +div#form { + padding: 20px; + padding-right: 20px; + padding-top: 20px; + display:block; + margin: 0 auto; + width: 500px; + text-align: center; + font-size:30px; + font-family: 'PT Sans', sans-serif; + border-bottom: 1px solid black; + border-top: 1px solid black; +} + +div#results { + display:block; + margin: 0 auto; + width: 500px; + text-align: center; + font-size:30px; + font-family: 'PT Sans', sans-serif; +} + +.button { + background-color: #4CAF50; /* Green */ + border: none; + color: white; + padding: 16px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; + -webkit-transition-duration: 0.4s; /* Safari */ + transition-duration: 0.4s; + cursor: pointer; + width: 250px; +} + +.button1 { + background-color: white; + color: black; + border: 2px solid #008CBA; +} + +.button1:hover { + background-color: #008CBA; + color: white; +} +.button2 { + background-color: white; + color: black; + border: 2px solid #555555; +} + +.button2:hover { + background-color: #555555; + color: white; +} + +.button3 { + background-color: white; + color: black; + border: 2px solid #f44336; +} + +.button3:hover { + background-color: #f44336; + color: white; +} \ No newline at end of file diff --git a/aws-ts-voting-app/frontend/app/templates/index.html b/aws-ts-voting-app/frontend/app/templates/index.html new file mode 100755 index 000000000..c92e65976 --- /dev/null +++ b/aws-ts-voting-app/frontend/app/templates/index.html @@ -0,0 +1,31 @@ + + + + + + + {{title}} + + + + + +
+
+ +
+
+ + + +
+
+
{{button1}}: {{ value1 }} · {{button2}}: {{ value2 }}
+ +
+
+ + \ No newline at end of file diff --git a/aws-ts-voting-app/index.ts b/aws-ts-voting-app/index.ts new file mode 100644 index 000000000..a363fcb6e --- /dev/null +++ b/aws-ts-voting-app/index.ts @@ -0,0 +1,51 @@ +// Copyright 2017, Pulumi Corporation. All rights reserved. + +import * as pulumi from "@pulumi/pulumi"; +import * as awsx from "@pulumi/aws-infra"; + +// Get the password to use for Redis from config. +let config = new pulumi.Config(); +let redisPassword = config.require("redisPassword"); +let redisPort = 6379; + +// The data layer for the application +// Use the 'image' property to point to a pre-built Docker image. +let redisListener = new awsx.elasticloadbalancingv2.NetworkListener("voting-app-cache", { port: redisPort }); +let redisCache = new awsx.ecs.FargateService("voting-app-cache", { + taskDefinitionArgs: { + containers: { + redis: { + image: "redis:alpine", + memory: 512, + portMappings: [redisListener], + command: ["redis-server", "--requirepass", redisPassword], + }, + }, + }, +}); + +let redisEndpoint = redisListener.endpoint(); + +// A custom container for the frontend, which is a Python Flask app +// Use the 'build' property to specify a folder that contains a Dockerfile. +// Pulumi builds the container for you and pushes to an ECR registry +let frontendListener = new awsx.elasticloadbalancingv2.NetworkListener("voting-app-frontend", { port: 80 }); +let frontend = new awsx.ecs.FargateService("voting-app-frontend", { + taskDefinitionArgs: { + containers: { + votingAppFrontend: { + image: awsx.ecs.Image.fromPath("./frontend"), // path to the folder containing the Dockerfile + memory: 512, + portMappings: [frontendListener], + environment: redisEndpoint.apply(e => [ + { name: "REDIS", value: e.hostname }, + { name: "REDIS_PORT", value: e.port.toString() }, + { name: "REDIS_PWD", value: redisPassword }, + ]), + }, + }, + }, +}); + +// Export a variable that will be displayed during 'pulumi up' +export let frontendURL = frontendListener.endpoint(); diff --git a/aws-ts-voting-app/package.json b/aws-ts-voting-app/package.json new file mode 100644 index 000000000..56ab14618 --- /dev/null +++ b/aws-ts-voting-app/package.json @@ -0,0 +1,13 @@ +{ + "name": "voting-app", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^8.0.27" + }, + "dependencies": { + "@pulumi/pulumi": "dev", + "@pulumi/aws": "dev", + "@pulumi/aws-infra": "dev", + "@pulumi/cloud-aws": "dev" + } +} diff --git a/aws-ts-voting-app/tsconfig.json b/aws-ts-voting-app/tsconfig.json new file mode 100644 index 000000000..ae4e90d4b --- /dev/null +++ b/aws-ts-voting-app/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "outDir": "bin", + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "experimentalDecorators": true, + "pretty": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "forceConsistentCasingInFileNames": true, + "strictNullChecks": true + }, + "files": [ + "index.ts" + ] +} diff --git a/aws-ts-voting-app/voting-app-webpage.png b/aws-ts-voting-app/voting-app-webpage.png new file mode 100644 index 000000000..b612eba0d Binary files /dev/null and b/aws-ts-voting-app/voting-app-webpage.png differ