From bc7909cac30d626d09fa9201d918dbc6f3f17ebc Mon Sep 17 00:00:00 2001 From: Vova Ivanov Date: Fri, 7 Aug 2020 00:35:54 -0700 Subject: [PATCH 1/8] Uploaded template for aws-py-flask-redis-voting-app example --- aws-py-voting-app/Pulumi.yaml | 8 ++ aws-py-voting-app/README.md | 82 +++++++++++++ aws-py-voting-app/__main__.py | 180 +++++++++++++++++++++++++++++ aws-py-voting-app/requirements.txt | 2 + 4 files changed, 272 insertions(+) create mode 100644 aws-py-voting-app/Pulumi.yaml create mode 100644 aws-py-voting-app/README.md create mode 100644 aws-py-voting-app/__main__.py create mode 100644 aws-py-voting-app/requirements.txt diff --git a/aws-py-voting-app/Pulumi.yaml b/aws-py-voting-app/Pulumi.yaml new file mode 100644 index 0000000000..b29a095cc7 --- /dev/null +++ b/aws-py-voting-app/Pulumi.yaml @@ -0,0 +1,8 @@ +name: webserver-py +runtime: python +description: Basic example of an AWS web server accessible over HTTP (in Python!) +template: + config: + aws:region: + description: The AWS region to deploy into + default: us-west-2 diff --git a/aws-py-voting-app/README.md b/aws-py-voting-app/README.md new file mode 100644 index 0000000000..04998f19ff --- /dev/null +++ b/aws-py-voting-app/README.md @@ -0,0 +1,82 @@ +[![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new) + +# Web Server Using Amazon EC2 + +An example based on the Amazon sample at: +http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/deploying.applications.html. The example deploys an EC2 instance and opens port 80. + +## Prerequisites + +1. [Install Pulumi](https://www.pulumi.com/docs/get-started/install/) +1. [Configure Pulumi for AWS](https://www.pulumi.com/docs/intro/cloud-providers/aws/setup/) +1. [Configure Pulumi for Python](https://www.pulumi.com/docs/intro/languages/python/) + +## Deploying and running the program + +1. Install dependencies (a `virtualenv` is recommended - see [Pulumi Python docs](https://www.pulumi.com/docs/intro/languages/python/)): + + ```bash + $ pip install -r requirements.txt + ``` + +1. Create a new stack: + + ```bash + $ pulumi stack init python-webserver-testing + ``` + +1. Set the AWS region: + + ```bash + $ pulumi config set aws:region us-west-2 + ``` + +1. Create a Python virtualenv, activate it, and install dependencies: + + This installs the dependent packages [needed](https://www.pulumi.com/docs/intro/concepts/how-pulumi-works/) for our Pulumi program. + + ```bash + $ python3 -m venv venv + $ source venv/bin/activate + $ pip3 install -r requirements.txt + ``` + +1. Run `pulumi up` to preview and deploy changes: + + ```bash + $ pulumi up + Previewing stack 'python-webserver-testing' + Previewing changes: + ... + + Do you want to proceed? yes + Updating stack 'python-webserver-testing' + Performing changes: + + #: Resource Type Name Status Extra Info + 1: pulumi:pulumi:Stack webserver-py-python-webserver-testing + created + 2: aws:ec2:SecurityGroup web-secgrp + created + 3: aws:ec2:Instance web-server-www + created + + info: 3 changes performed: + + 3 resources created + Update duration: 26.470339302s + + Permalink: https://pulumi.com/lindydonna/examples/webserver-py/python-webserver-testing/updates/1 + ``` + +1. View the host name and IP address of the instance via `stack output`: + + ```bash + $ pulumi stack output + Current stack outputs (2): + OUTPUT VALUE + public_dns ec2-34-217-176-141.us-west-2.compute.amazonaws.com + public_ip 34.217.176.141 + ``` + +1. Verify that the EC2 instance exists, by either using the AWS Console or running `aws ec2 describe-instances`. + +## Clean up + +To clean up resources, run `pulumi destroy` and answer the confirmation question at the prompt. diff --git a/aws-py-voting-app/__main__.py b/aws-py-voting-app/__main__.py new file mode 100644 index 0000000000..662d60d284 --- /dev/null +++ b/aws-py-voting-app/__main__.py @@ -0,0 +1,180 @@ +# Copyright 2016-2018, Pulumi Corporation. All rights reserved. + +import json +import pulumi +import pulumi_aws as aws +import pulumi_docker as docker + +# Get the password to use for Redis from config. +config = pulumi.Config() +redis_password = config.require("redis-password") +redis_port = 6379 + +app_cluster = aws.ecs.Cluster("app-cluster") + +# Read back the default VPC and public subnets, which we will use. +app_vpc = aws.ec2.get_vpc(default="true") +app_vpc_subnets = aws.ec2.get_subnet_ids(vpc_id=app_vpc.id) + +# Create a SecurityGroup that permits HTTP ingress and unrestricted egress. +app_security_group = aws.ec2.SecurityGroup("web-secgrp", + vpc_id=app_vpc.id, + description="Enables HTTP access", + ingress=[{ + "protocol": "tcp", + "from_port": 80, + "to_port": 80, + "cidr_blocks": ["0.0.0.0/0"], + }], + egress=[{ + "protocol": "-1", + "from_port": 0, + "to_port": 0, + "cidr_blocks": ["0.0.0.0/0"], + }] +) + +# Create an IAM role that can be used by our service's task. +app_exec_role = aws.iam.Role("app-exec-role", + assume_role_policy="""{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Principal": { + "Service": "ec2.amazonaws.com" + }, + "Effect": "Allow", + "Sid": "" + }] + }""") + +exec_policy_attachment = aws.iam.RolePolicyAttachment("task-exec-policy", role=app_exec_role.name, + policy_arn="arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy") + +# The data layer for the application +# Use the 'image' property to point to a pre-built Docker image. +redis_balancer = aws.lb.LoadBalancer("redis-balancer", + security_groups=[app_security_group.id], + subnets=app_vpc_subnets.ids) + +redis_targetgroup = aws.lb.TargetGroup("redis-targetgroup", + port=redis_port, + protocol="HTTP", + target_type="ip", + vpc_id=app_vpc.id) + +redis_listener = aws.lb.Listener("redis-cache", + load_balancer_arn=redis_balancer.arn, + port=redis_port, + default_actions=[{ + "type": "forward", + "target_group_arn": redis_targetgroup.arn + }]) + +redis_task_definition = aws.ecs.TaskDefinition("redis-task-definition", + family="redis-task-definition", + cpu="256", + memory="512", + network_mode="awsvpc", + requires_compatibilities=["FARGATE"], + execution_role_arn=app_exec_role.arn, + container_definitions={ + "containers": { + "redis": { + "image": "redis:alpine", + "memory": 512, + "portMappings": [redis_listener], + "command": ["redis-server", "--requirepass", redis_port], + }, + } + }) + +redis_cache = aws.ecs.Service("redis-cache", + cluster=app_cluster.arn, + desired_count=1, + launch_type="FARGATE", + task_definition=redis_task_definition.arn, + network_configuration={ + "assign_public_ip": "false", + "subnets": app_vpc_subnets.ids, + "security_groups": [app_security_group.id] + }, + load_balancers=[{ + "target_group_arn": redis_targetgroup.arn, + "container_name": "redis", + "container_port": redis_port, + }], + opts=pulumi.ResourceOptions(depends_on=[redis_listener]), +) + +redis_endpoint = redis_listener.default_actions["userInfoEndpoint"] + +# 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 +frontend_balancer = aws.lb.LoadBalancer("frontend-balancer", + security_groups=[app_security_group.id], + subnets=app_vpc_subnets.ids) + +frontend_targetgroup = aws.lb.TargetGroup("frontend-targetgroup", + port=80, + protocol="HTTP", + target_type="ip", + vpc_id=app_vpc.id) + +frontend_listener = aws.lb.Listener("frontend-cache", + load_balancer_arn=frontend_balancer.arn, + port=80, + default_actions=[{ + "type": "forward", + "target_group_arn": frontend_targetgroup.arn + }]) + +frontend_image = docker.Image("frontend-image", + image_name="frontend/app", + build=DockerBuild( + target="dependencies", + ), + skip_push=True, +) + +frontend_task_definition = aws.ecs.TaskDefinition("frontend-task-definition", + family="redis-task-definition", + cpu="256", + memory="512", + network_mode="awsvpc", + requires_compatibilities=["FARGATE"], + execution_role_arn=app_exec_role.arn, + container_definitions={ + "votingAppFrontend": { + "image": frontend_image, + "memory": 512, + "portMappings": [frontendListener], + "environment": redisEndpoint.apply(lambda e => [ + { "name": "REDIS", value: e["hostname"] }, + { "name": "REDIS_PORT", value: e["port"] }, + { "name": "REDIS_PWD", value: redisPassword }, + ]), + }, + }) + +frontend = aws.ecs.Service("frontend-service", + cluster=app_cluster.arn, + desired_count=3, + launch_type="FARGATE", + task_definition=frontend_task_definition.arn, + network_configuration={ + "assign_public_ip": "false", + "subnets": app_vpc_subnets.ids, + "security_groups": [app_security_group.id] + }, + load_balancers=[{ + "target_group_arn": frontend_targetgroup.arn, + "container_name": "redis", + "container_port": 80, + }], + opts=pulumi.ResourceOptions(depends_on=[frontend_listener]), +) + +pulumi.export("app-url", frontendListener.default_actions["userInfoEndpoint"]) diff --git a/aws-py-voting-app/requirements.txt b/aws-py-voting-app/requirements.txt new file mode 100644 index 0000000000..8888be0d3a --- /dev/null +++ b/aws-py-voting-app/requirements.txt @@ -0,0 +1,2 @@ +pulumi>=2.0.0,<3.0.0 +pulumi-aws>=2.0.0,<3.0.0 From 7a47b900e6fecfdc4e303b6fd23e6bd23416d2bd Mon Sep 17 00:00:00 2001 From: Vova Ivanov Date: Fri, 7 Aug 2020 08:39:50 -0700 Subject: [PATCH 2/8] Added dockerfile folder --- aws-py-voting-app/__main__.py | 2 +- aws-py-voting-app/frontend/Dockerfile | 3 + aws-py-voting-app/frontend/LICENSE | 21 ++++ .../frontend/app/config_file.cfg | 5 + aws-py-voting-app/frontend/app/main.py | 71 ++++++++++++++ .../frontend/app/static/default.css | 96 +++++++++++++++++++ .../frontend/app/templates/index.html | 31 ++++++ 7 files changed, 228 insertions(+), 1 deletion(-) create mode 100755 aws-py-voting-app/frontend/Dockerfile create mode 100644 aws-py-voting-app/frontend/LICENSE create mode 100755 aws-py-voting-app/frontend/app/config_file.cfg create mode 100755 aws-py-voting-app/frontend/app/main.py create mode 100755 aws-py-voting-app/frontend/app/static/default.css create mode 100755 aws-py-voting-app/frontend/app/templates/index.html diff --git a/aws-py-voting-app/__main__.py b/aws-py-voting-app/__main__.py index 662d60d284..80155c9064 100644 --- a/aws-py-voting-app/__main__.py +++ b/aws-py-voting-app/__main__.py @@ -132,7 +132,7 @@ }]) frontend_image = docker.Image("frontend-image", - image_name="frontend/app", + image_name="./frontend", build=DockerBuild( target="dependencies", ), diff --git a/aws-py-voting-app/frontend/Dockerfile b/aws-py-voting-app/frontend/Dockerfile new file mode 100755 index 0000000000..690a4775b3 --- /dev/null +++ b/aws-py-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-py-voting-app/frontend/LICENSE b/aws-py-voting-app/frontend/LICENSE new file mode 100644 index 0000000000..d1ca00f20a --- /dev/null +++ b/aws-py-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-py-voting-app/frontend/app/config_file.cfg b/aws-py-voting-app/frontend/app/config_file.cfg new file mode 100755 index 0000000000..23c142d8f9 --- /dev/null +++ b/aws-py-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-py-voting-app/frontend/app/main.py b/aws-py-voting-app/frontend/app/main.py new file mode 100755 index 0000000000..d1fe50f426 --- /dev/null +++ b/aws-py-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-py-voting-app/frontend/app/static/default.css b/aws-py-voting-app/frontend/app/static/default.css new file mode 100755 index 0000000000..cb2cb0ada0 --- /dev/null +++ b/aws-py-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-py-voting-app/frontend/app/templates/index.html b/aws-py-voting-app/frontend/app/templates/index.html new file mode 100755 index 0000000000..c92e65976f --- /dev/null +++ b/aws-py-voting-app/frontend/app/templates/index.html @@ -0,0 +1,31 @@ + + + + + + + {{title}} + + + + + +
+
+ +
+
+ + + +
+
+
{{button1}}: {{ value1 }} ยท {{button2}}: {{ value2 }}
+ +
+
+ + \ No newline at end of file From e2a1be35bf95751cff456be7e8b75253b55be8af Mon Sep 17 00:00:00 2001 From: Vova Ivanov Date: Fri, 7 Aug 2020 09:54:46 -0700 Subject: [PATCH 3/8] Added comments to describe what each resource does --- aws-py-voting-app/__main__.py | 67 ++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/aws-py-voting-app/__main__.py b/aws-py-voting-app/__main__.py index 80155c9064..0fe531e378 100644 --- a/aws-py-voting-app/__main__.py +++ b/aws-py-voting-app/__main__.py @@ -5,18 +5,20 @@ import pulumi_aws as aws import pulumi_docker as docker -# Get the password to use for Redis from config. +# Get the password to use for Redis from the pulumi config config = pulumi.Config() redis_password = config.require("redis-password") redis_port = 6379 +# The ECS cluster in which our application and databse will run app_cluster = aws.ecs.Cluster("app-cluster") -# Read back the default VPC and public subnets, which we will use. +# Creating a default VPC and public subnets app_vpc = aws.ec2.get_vpc(default="true") app_vpc_subnets = aws.ec2.get_subnet_ids(vpc_id=app_vpc.id) +appsd = aws.ec2.get_ -# Create a SecurityGroup that permits HTTP ingress and unrestricted egress. +# Creating a Security Group that restricts incoming traffic to HTTP app_security_group = aws.ec2.SecurityGroup("web-secgrp", vpc_id=app_vpc.id, description="Enables HTTP access", @@ -34,7 +36,7 @@ }] ) -# Create an IAM role that can be used by our service's task. +# Creating an IAM role used by Fargate to execute all our tasks app_exec_role = aws.iam.Role("app-exec-role", assume_role_policy="""{ "Version": "2012-10-17", @@ -49,22 +51,26 @@ }] }""") +# Attaching execution permissions to the IAM role exec_policy_attachment = aws.iam.RolePolicyAttachment("task-exec-policy", role=app_exec_role.name, policy_arn="arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy") -# The data layer for the application -# Use the 'image' property to point to a pre-built Docker image. -redis_balancer = aws.lb.LoadBalancer("redis-balancer", - security_groups=[app_security_group.id], - subnets=app_vpc_subnets.ids) +# The application's backend and data layer: Redis +# Creating a target group through which the Redis backend receives requests redis_targetgroup = aws.lb.TargetGroup("redis-targetgroup", port=redis_port, protocol="HTTP", target_type="ip", vpc_id=app_vpc.id) -redis_listener = aws.lb.Listener("redis-cache", +# Creating a load balancer to spread out incoming requests +redis_balancer = aws.lb.LoadBalancer("redis-balancer", + security_groups=[app_security_group.id], + subnets=app_vpc_subnets.ids) + +# Forwards internal traffic with the Redis port number to the Redis target group +redis_listener = aws.lb.Listener("redis-listener", load_balancer_arn=redis_balancer.arn, port=redis_port, default_actions=[{ @@ -72,6 +78,7 @@ "target_group_arn": redis_targetgroup.arn }]) +# Creating a task definition for the Redis instance. redis_task_definition = aws.ecs.TaskDefinition("redis-task-definition", family="redis-task-definition", cpu="256", @@ -82,14 +89,15 @@ container_definitions={ "containers": { "redis": { - "image": "redis:alpine", + "image": "redis:alpine", # A pre-built docker image with a functioning redis server "memory": 512, "portMappings": [redis_listener], - "command": ["redis-server", "--requirepass", redis_port], + "command": ["redis-server", "--requirepass", redis_password], }, } }) +# Launching our Redis service on Fargate, using our configurations and load balancers redis_cache = aws.ecs.Service("redis-cache", cluster=app_cluster.arn, desired_count=1, @@ -108,22 +116,26 @@ opts=pulumi.ResourceOptions(depends_on=[redis_listener]), ) +# Creating a special endpoint for the Redis backend, which we will later provide +# to the Flask service redis_endpoint = redis_listener.default_actions["userInfoEndpoint"] -# 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 -frontend_balancer = aws.lb.LoadBalancer("frontend-balancer", - security_groups=[app_security_group.id], - subnets=app_vpc_subnets.ids) +# The application's frontend: A Flask service +# Creating a target group through which the Flask frontend receives requests frontend_targetgroup = aws.lb.TargetGroup("frontend-targetgroup", port=80, protocol="HTTP", target_type="ip", vpc_id=app_vpc.id) -frontend_listener = aws.lb.Listener("frontend-cache", +# Creating a load balancer to spread out incoming requests +frontend_balancer = aws.lb.LoadBalancer("frontend-balancer", + security_groups=[app_security_group.id], + subnets=app_vpc_subnets.ids) + +# Forwards all public traffic using port 80 to the Flask target group +frontend_listener = aws.lb.Listener("frontend-listener", load_balancer_arn=frontend_balancer.arn, port=80, default_actions=[{ @@ -131,6 +143,8 @@ "target_group_arn": frontend_targetgroup.arn }]) +# Creating a Docker image from the "./frontend" folder, which we will use +# to upload our app frontend_image = docker.Image("frontend-image", image_name="./frontend", build=DockerBuild( @@ -139,8 +153,9 @@ skip_push=True, ) +# Creating a task definition for the Flask instance. frontend_task_definition = aws.ecs.TaskDefinition("frontend-task-definition", - family="redis-task-definition", + family="frontend-task-definition", cpu="256", memory="512", network_mode="awsvpc", @@ -151,30 +166,32 @@ "image": frontend_image, "memory": 512, "portMappings": [frontendListener], - "environment": redisEndpoint.apply(lambda e => [ + "environment": redisEndpoint.apply(lambda e => [ # The Redis endpoint we created is given to Flask, allowing it to communicate with the former { "name": "REDIS", value: e["hostname"] }, { "name": "REDIS_PORT", value: e["port"] }, - { "name": "REDIS_PWD", value: redisPassword }, + { "name": "REDIS_PWD", value: redis_password }, ]), }, }) -frontend = aws.ecs.Service("frontend-service", +# Launching our Redis service on Fargate, using our configurations and load balancers +flask_frontend = aws.ecs.Service("flask-service", cluster=app_cluster.arn, desired_count=3, launch_type="FARGATE", task_definition=frontend_task_definition.arn, network_configuration={ - "assign_public_ip": "false", + "assign_public_ip": "true", "subnets": app_vpc_subnets.ids, "security_groups": [app_security_group.id] }, load_balancers=[{ "target_group_arn": frontend_targetgroup.arn, - "container_name": "redis", + "container_name": "votingAppFrontend", "container_port": 80, }], opts=pulumi.ResourceOptions(depends_on=[frontend_listener]), ) +# Exporting the url of our Flask frontend. We can now connect to our app pulumi.export("app-url", frontendListener.default_actions["userInfoEndpoint"]) From 5b25bf21deb90f86c73ee53f5a5c29e407af59b1 Mon Sep 17 00:00:00 2001 From: Vova Ivanov Date: Sun, 9 Aug 2020 12:01:39 -0700 Subject: [PATCH 4/8] Fixed syntax errors preventing application from deploying --- aws-py-voting-app/__main__.py | 76 ++++++++++++++------------ aws-py-voting-app/frontend/app/main.py | 2 +- aws-py-voting-app/requirements.txt | 1 + 3 files changed, 43 insertions(+), 36 deletions(-) diff --git a/aws-py-voting-app/__main__.py b/aws-py-voting-app/__main__.py index 0fe531e378..0dd119fa64 100644 --- a/aws-py-voting-app/__main__.py +++ b/aws-py-voting-app/__main__.py @@ -16,7 +16,6 @@ # Creating a default VPC and public subnets app_vpc = aws.ec2.get_vpc(default="true") app_vpc_subnets = aws.ec2.get_subnet_ids(vpc_id=app_vpc.id) -appsd = aws.ec2.get_ # Creating a Security Group that restricts incoming traffic to HTTP app_security_group = aws.ec2.SecurityGroup("web-secgrp", @@ -80,22 +79,24 @@ # Creating a task definition for the Redis instance. redis_task_definition = aws.ecs.TaskDefinition("redis-task-definition", - family="redis-task-definition", + family="redis-task-definition-family", cpu="256", memory="512", network_mode="awsvpc", requires_compatibilities=["FARGATE"], execution_role_arn=app_exec_role.arn, - container_definitions={ - "containers": { - "redis": { - "image": "redis:alpine", # A pre-built docker image with a functioning redis server - "memory": 512, - "portMappings": [redis_listener], - "command": ["redis-server", "--requirepass", redis_password], - }, - } - }) + container_definitions=json.dumps([{ + "name": "redis-container", + "image": "redis:alpine", # A pre-built docker image with a functioning redis server + "memory": 512, + "essential": True, + "portMappings": [{ + "containerPort": redis_port, + "hostPort": redis_port, + "protocol": "tcp" + }], + "command": ["redis-server", "--requirepass", redis_password], + }])) # Launching our Redis service on Fargate, using our configurations and load balancers redis_cache = aws.ecs.Service("redis-cache", @@ -110,15 +111,15 @@ }, load_balancers=[{ "target_group_arn": redis_targetgroup.arn, - "container_name": "redis", + "container_name": "redis-container", "container_port": redis_port, }], opts=pulumi.ResourceOptions(depends_on=[redis_listener]), ) -# Creating a special endpoint for the Redis backend, which we will later provide -# to the Flask service -redis_endpoint = redis_listener.default_actions["userInfoEndpoint"] +# Creating a special endpoint for the Redis backend, which we will provide +# to the Flask service as an environment variable +redis_endpoint = {"host": str(redis_balancer.dns_name), "port": str(redis_listener.port)} # The application's frontend: A Flask service @@ -132,7 +133,7 @@ # Creating a load balancer to spread out incoming requests frontend_balancer = aws.lb.LoadBalancer("frontend-balancer", security_groups=[app_security_group.id], - subnets=app_vpc_subnets.ids) + subnets=app_vpc_subnets.ids) # Forwards all public traffic using port 80 to the Flask target group frontend_listener = aws.lb.Listener("frontend-listener", @@ -143,12 +144,12 @@ "target_group_arn": frontend_targetgroup.arn }]) -# Creating a Docker image from the "./frontend" folder, which we will use +# Creating a Docker image from "./frontend/Dockerfile", which we will use # to upload our app -frontend_image = docker.Image("frontend-image", - image_name="./frontend", - build=DockerBuild( - target="dependencies", +frontend_image = docker.Image("frontend-dockerimage", + image_name="frontend-dockerimage", + build=docker.DockerBuild( + context="./frontend", ), skip_push=True, ) @@ -161,18 +162,22 @@ network_mode="awsvpc", requires_compatibilities=["FARGATE"], execution_role_arn=app_exec_role.arn, - container_definitions={ - "votingAppFrontend": { - "image": frontend_image, - "memory": 512, - "portMappings": [frontendListener], - "environment": redisEndpoint.apply(lambda e => [ # The Redis endpoint we created is given to Flask, allowing it to communicate with the former - { "name": "REDIS", value: e["hostname"] }, - { "name": "REDIS_PORT", value: e["port"] }, - { "name": "REDIS_PWD", value: redis_password }, - ]), - }, - }) + container_definitions=json.dumps([{ + "name": "votingAppFrontend", + "image": "frontend-dockerimage", + "memory": 512, + "essential": True, + "portMappings": [{ + "containerPort": 80, + "hostPort": 80, + "protocol": "tcp" + }], + "environment": [ # The Redis endpoint we created is given to Flask, allowing it to communicate with the former + { "name": "REDIS_HOST", "value": redis_endpoint["host"] }, + { "name": "REDIS_PORT", "value": redis_endpoint["port"] }, + { "name": "REDIS_PWD", "value": redis_password }, + ], + }])) # Launching our Redis service on Fargate, using our configurations and load balancers flask_frontend = aws.ecs.Service("flask-service", @@ -194,4 +199,5 @@ ) # Exporting the url of our Flask frontend. We can now connect to our app -pulumi.export("app-url", frontendListener.default_actions["userInfoEndpoint"]) +pulumi.export("app-url", frontend_balancer.dns_name) +pulumi.export("redis-url", redis_balancer.dns_name) diff --git a/aws-py-voting-app/frontend/app/main.py b/aws-py-voting-app/frontend/app/main.py index d1fe50f426..cfec8f7a2c 100755 --- a/aws-py-voting-app/frontend/app/main.py +++ b/aws-py-voting-app/frontend/app/main.py @@ -16,7 +16,7 @@ title = app.config['TITLE'] # Redis configurations -redis_server = os.environ['REDIS'] +redis_server = os.environ['REDIS_HOST'] redis_port = os.environ['REDIS_PORT'] redis_password = os.environ['REDIS_PWD'] diff --git a/aws-py-voting-app/requirements.txt b/aws-py-voting-app/requirements.txt index 8888be0d3a..7a17446113 100644 --- a/aws-py-voting-app/requirements.txt +++ b/aws-py-voting-app/requirements.txt @@ -1,2 +1,3 @@ pulumi>=2.0.0,<3.0.0 pulumi-aws>=2.0.0,<3.0.0 +pulumi-docker>=1.0.0,<3.0.0 From 070191308b400eab4caa8b589fa145ea4a11cb5e Mon Sep 17 00:00:00 2001 From: Vova Ivanov Date: Mon, 10 Aug 2020 11:42:57 -0700 Subject: [PATCH 5/8] Fixed internet connection errors, and created blog draft --- aws-py-voting-app/__main__.py | 195 ++++++++++++++++++------- aws-py-voting-app/blogpost.md | 119 +++++++++++++++ aws-py-voting-app/frontend/app/main.py | 2 +- 3 files changed, 261 insertions(+), 55 deletions(-) create mode 100644 aws-py-voting-app/blogpost.md diff --git a/aws-py-voting-app/__main__.py b/aws-py-voting-app/__main__.py index 0dd119fa64..ac141e9691 100644 --- a/aws-py-voting-app/__main__.py +++ b/aws-py-voting-app/__main__.py @@ -13,29 +13,51 @@ # The ECS cluster in which our application and databse will run app_cluster = aws.ecs.Cluster("app-cluster") -# Creating a default VPC and public subnets -app_vpc = aws.ec2.get_vpc(default="true") -app_vpc_subnets = aws.ec2.get_subnet_ids(vpc_id=app_vpc.id) +# Creating a VPC and a public subnet +app_vpc = aws.ec2.Vpc("app-vpc", + cidr_block="172.31.0.0/16", + enable_dns_hostnames=True) + +app_vpc_subnet = aws.ec2.Subnet("app-vpc-subnet", + cidr_block="172.31.32.0/20", + vpc_id=app_vpc) + +# Creating a gateway to the web for the VPC +app_gateway = aws.ec2.InternetGateway("app-gateway", + vpc_id=app_vpc.id) + +app_routetable = aws.ec2.RouteTable("app-routetable", + routes=[ + { + "cidr_block": "0.0.0.0/0", + "gateway_id": app_gateway.id, + } + ], + vpc_id=app_vpc.id) + +# Associating our gateway with our VPC, to allow our app to communicate with the greater internet +app_routetable_association = aws.ec2.MainRouteTableAssociation("app_routetable_association", + route_table_id=app_routetable.id, + vpc_id=app_vpc) # Creating a Security Group that restricts incoming traffic to HTTP -app_security_group = aws.ec2.SecurityGroup("web-secgrp", +app_security_group = aws.ec2.SecurityGroup("security-group", vpc_id=app_vpc.id, description="Enables HTTP access", - ingress=[{ - "protocol": "tcp", - "from_port": 80, - "to_port": 80, - "cidr_blocks": ["0.0.0.0/0"], - }], - egress=[{ - "protocol": "-1", - "from_port": 0, - "to_port": 0, - "cidr_blocks": ["0.0.0.0/0"], - }] -) + ingress=[{ + 'protocol': 'tcp', + 'from_port': 0, + 'to_port': 65535, + 'cidr_blocks': ['0.0.0.0/0'], + }], + egress=[{ + 'protocol': '-1', + 'from_port': 0, + 'to_port': 0, + 'cidr_blocks': ['0.0.0.0/0'], + }]) -# Creating an IAM role used by Fargate to execute all our tasks +# Creating an IAM role used by Fargate to execute all our services app_exec_role = aws.iam.Role("app-exec-role", assume_role_policy="""{ "Version": "2012-10-17", @@ -43,35 +65,88 @@ { "Action": "sts:AssumeRole", "Principal": { - "Service": "ec2.amazonaws.com" + "Service": "ecs-tasks.amazonaws.com" }, "Effect": "Allow", "Sid": "" }] }""") -# Attaching execution permissions to the IAM role -exec_policy_attachment = aws.iam.RolePolicyAttachment("task-exec-policy", role=app_exec_role.name, +# Attaching execution permissions to the exec role +exec_policy_attachment = aws.iam.RolePolicyAttachment("app-exec-policy", role=app_exec_role.name, policy_arn="arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy") +# Creating an IAM role used by Fargate to manage tasks +app_task_role = aws.iam.Role("app-task-role", + assume_role_policy="""{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + }, + "Effect": "Allow", + "Sid": "" + }] + }""") + +# Attaching execution permissions to the task role +exec_policy_attachment = aws.iam.RolePolicyAttachment("app-access-policy", role=app_exec_role.name, + policy_arn="arn:aws:iam::aws:policy/AmazonEC2ContainerServiceFullAccess") + +exec_policy_attachment = aws.iam.RolePolicyAttachment("app-lambda-policy", role=app_exec_role.name, + policy_arn="arn:aws:iam::aws:policy/AWSLambdaFullAccess") + +# Creating storage space to upload a docker image of our app to +app_ecr_repo = aws.ecr.Repository("app-ecr-repo", + image_tag_mutability="MUTABLE") + +# Attaching an application life cycle policy to the storage +app_lifecycle_policy = aws.ecr.LifecyclePolicy("app-lifecycle-policy", + repository=app_ecr_repo.name, + policy="""{ + "rules": [ + { + "rulePriority": 10, + "description": "Remove untagged images", + "selection": { + "tagStatus": "untagged", + "countType": "imageCountMoreThan", + "countNumber": 1 + }, + "action": { + "type": "expire" + } + } + ] + }""") + # The application's backend and data layer: Redis # Creating a target group through which the Redis backend receives requests redis_targetgroup = aws.lb.TargetGroup("redis-targetgroup", port=redis_port, - protocol="HTTP", + protocol="TCP", target_type="ip", + stickiness= { + "enabled": False, + "type": "lb_cookie", + }, vpc_id=app_vpc.id) # Creating a load balancer to spread out incoming requests redis_balancer = aws.lb.LoadBalancer("redis-balancer", - security_groups=[app_security_group.id], - subnets=app_vpc_subnets.ids) + load_balancer_type="network", + internal= False, + security_groups=[], + subnets=[app_vpc_subnet.id]) # Forwards internal traffic with the Redis port number to the Redis target group redis_listener = aws.lb.Listener("redis-listener", load_balancer_arn=redis_balancer.arn, port=redis_port, + protocol="TCP", default_actions=[{ "type": "forward", "target_group_arn": redis_targetgroup.arn @@ -85,6 +160,7 @@ network_mode="awsvpc", requires_compatibilities=["FARGATE"], execution_role_arn=app_exec_role.arn, + task_role_arn=app_task_role.arn, container_definitions=json.dumps([{ "name": "redis-container", "image": "redis:alpine", # A pre-built docker image with a functioning redis server @@ -99,14 +175,15 @@ }])) # Launching our Redis service on Fargate, using our configurations and load balancers -redis_cache = aws.ecs.Service("redis-cache", +redis_service = aws.ecs.Service("redis-service", cluster=app_cluster.arn, desired_count=1, launch_type="FARGATE", task_definition=redis_task_definition.arn, + wait_for_steady_state=False, network_configuration={ - "assign_public_ip": "false", - "subnets": app_vpc_subnets.ids, + "assign_public_ip": "true", + "subnets": [app_vpc_subnet.id], "security_groups": [app_security_group.id] }, load_balancers=[{ @@ -118,53 +195,63 @@ ) # Creating a special endpoint for the Redis backend, which we will provide -# to the Flask service as an environment variable -redis_endpoint = {"host": str(redis_balancer.dns_name), "port": str(redis_listener.port)} +# to the Flask frontend as an environment variable +redis_endpoint = {"host": str(redis_balancer.dns_name), "port": str(redis_port)} # The application's frontend: A Flask service # Creating a target group through which the Flask frontend receives requests -frontend_targetgroup = aws.lb.TargetGroup("frontend-targetgroup", +flask_targetgroup = aws.lb.TargetGroup("flask-targetgroup", port=80, - protocol="HTTP", + protocol="TCP", target_type="ip", + stickiness= { + "enabled": False, + "type": "lb_cookie", + }, vpc_id=app_vpc.id) # Creating a load balancer to spread out incoming requests -frontend_balancer = aws.lb.LoadBalancer("frontend-balancer", - security_groups=[app_security_group.id], - subnets=app_vpc_subnets.ids) +flask_balancer = aws.lb.LoadBalancer("flask-balancer", + load_balancer_type="network", + internal=False, + security_groups=[], + subnets=[app_vpc_subnet.id]) # Forwards all public traffic using port 80 to the Flask target group -frontend_listener = aws.lb.Listener("frontend-listener", - load_balancer_arn=frontend_balancer.arn, +flask_listener = aws.lb.Listener("flask-listener", + load_balancer_arn=flask_balancer.arn, port=80, + protocol="TCP", default_actions=[{ "type": "forward", - "target_group_arn": frontend_targetgroup.arn + "target_group_arn": flask_targetgroup.arn }]) # Creating a Docker image from "./frontend/Dockerfile", which we will use # to upload our app -frontend_image = docker.Image("frontend-dockerimage", - image_name="frontend-dockerimage", +flask_image = docker.Image("flask-dockerimage", + image_name="flask-dockerimage", build=docker.DockerBuild( context="./frontend", ), - skip_push=True, + skip_push=False, + registry=app_ecr_repo ) +# docker.build_and_push_image("pulumi-user/flask-dockerimage:v1.0.0", "./frontend", app_ecr_repo.repository_url, flask_listener, flask_listener) # Creating a task definition for the Flask instance. -frontend_task_definition = aws.ecs.TaskDefinition("frontend-task-definition", - family="frontend-task-definition", +flask_task_definition = aws.ecs.TaskDefinition("flask-task-definition", + family="frontend-task-definition-family", cpu="256", memory="512", network_mode="awsvpc", requires_compatibilities=["FARGATE"], execution_role_arn=app_exec_role.arn, + task_role_arn=app_task_role.arn, container_definitions=json.dumps([{ - "name": "votingAppFrontend", - "image": "frontend-dockerimage", + "name": "flask-container", + "image": "flask-dockerimage", "memory": 512, "essential": True, "portMappings": [{ @@ -173,31 +260,31 @@ "protocol": "tcp" }], "environment": [ # The Redis endpoint we created is given to Flask, allowing it to communicate with the former - { "name": "REDIS_HOST", "value": redis_endpoint["host"] }, + { "name": "REDIS", "value": redis_endpoint["host"] }, { "name": "REDIS_PORT", "value": redis_endpoint["port"] }, { "name": "REDIS_PWD", "value": redis_password }, ], }])) # Launching our Redis service on Fargate, using our configurations and load balancers -flask_frontend = aws.ecs.Service("flask-service", +flask_service = aws.ecs.Service("flask-service", cluster=app_cluster.arn, - desired_count=3, + desired_count=1, launch_type="FARGATE", - task_definition=frontend_task_definition.arn, + task_definition=flask_task_definition.arn, + wait_for_steady_state=False, network_configuration={ "assign_public_ip": "true", - "subnets": app_vpc_subnets.ids, + "subnets": [app_vpc_subnet.id], "security_groups": [app_security_group.id] }, load_balancers=[{ - "target_group_arn": frontend_targetgroup.arn, - "container_name": "votingAppFrontend", + "target_group_arn": flask_targetgroup.arn, + "container_name": "flask-container", "container_port": 80, }], - opts=pulumi.ResourceOptions(depends_on=[frontend_listener]), + opts=pulumi.ResourceOptions(depends_on=[flask_listener]), ) # Exporting the url of our Flask frontend. We can now connect to our app -pulumi.export("app-url", frontend_balancer.dns_name) -pulumi.export("redis-url", redis_balancer.dns_name) +pulumi.export("app-url", flask_balancer.dns_name) diff --git a/aws-py-voting-app/blogpost.md b/aws-py-voting-app/blogpost.md new file mode 100644 index 0000000000..18f68ce0d7 --- /dev/null +++ b/aws-py-voting-app/blogpost.md @@ -0,0 +1,119 @@ + Creating a Python AWS application using Flask, Redis, and Pulumi + +Having recently begun developing with Pulumi, I have taken it upon myself to learn as much about its inner workings and processes as I could. To that end, I have decided to construct a production-level application using it, and to document each step that I take and its impact on my progress as I go along. + +This blog post will feature an existing [Typescript voting app example](https://www.pulumi.com/docs/tutorials/aws/aws-ts-voting-app/) be fully re-created step by step in Python, using Flask as the frontend, and Redis as the backend. In future blog posts, we will explore how to change the front and backends, how to upgrade the app with additional AWS services, and even how to migrate from one cloud provider to another. + +--- + +The first few lines of the __main\__.py file indicate which libraries need to be imported, and describe a pair of configuration options that will be used by the application. +``` +import json +import pulumi +import pulumi_aws as aws +import pulumi_docker as docker + +config = pulumi.Config() +redis_password = config.require("redis-password") +redis_port = 6379 +``` + + +After setting up the imports and configurations, we create an Elastic Container Service Cluster. +A Cluster represent a group of tasks and services that work together for a certain purpose. In +this instance, the purpose is to provide users with a voting application. +``` +app_cluster = aws.ecs.Cluster("app-cluster") +``` + + +In order to allow different tasks within our cluster to communicate, we create a Virtual Private +Cloud and an associated subnet. +``` +app_vpc = aws.ec2.Vpc("app-vpc", + cidr_block="172.31.0.0/16", + enable_dns_hostnames=True) + +app_vpc_subnet = aws.ec2.Subnet("app-vpc-subnet", + cidr_block="172.31.32.0/20", + vpc_id=app_vpc) +``` + + +A gateway and routing table are needed to allow the VPC to communicate with the greater internet. +Once created, we declare that the routing table is associated with our VPC. +``` +app_gateway = aws.ec2.InternetGateway("app-gateway", + vpc_id=app_vpc.id) + +app_routetable = aws.ec2.RouteTable("app-routetable", + routes=[ + { + "cidr_block": "0.0.0.0/0", + "gateway_id": app_gateway.id, + } + ], + vpc_id=app_vpc.id) + +app_routetable_association = aws.ec2.MainRouteTableAssociation("app_routetable_association", + route_table_id=app_routetable.id, + vpc_id=app_vpc) +``` + + +To inform what kinds of internet traffic are and aren't allowed to connect with the application, +we create a firewall in the form of a security group. +``` +app_security_group = aws.ec2.SecurityGroup("security-group", + vpc_id=app_vpc.id, + description="Enables HTTP access", + ingress=[{ + 'protocol': 'tcp', + 'from_port': 0, + 'to_port': 65535, + 'cidr_blocks': ['0.0.0.0/0'], + }], + egress=[{ + 'protocol': '-1', + 'from_port': 0, + 'to_port': 0, + 'cidr_blocks': ['0.0.0.0/0'], + }]) +``` + + +In order to allow our services to start, we create an Identity and Access Management role, and +attach execution permissions to it. +``` +app_exec_role = aws.iam.Role("app-exec-role", + assume_role_policy="""{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + }, + "Effect": "Allow", + "Sid": "" + }] + }""") + +exec_policy_attachment = aws.iam.RolePolicyAttachment("app-exec-policy", role=app_exec_role.name, + policy_arn="arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy") +``` + +--- + +The full code for the blog post can be [found on github.](https://github.com/jetvova/examples/tree/vova/aws-py-flask-redis-voting-app/aws-py-voting-app) + + + + + + + + + + + diff --git a/aws-py-voting-app/frontend/app/main.py b/aws-py-voting-app/frontend/app/main.py index cfec8f7a2c..d1fe50f426 100755 --- a/aws-py-voting-app/frontend/app/main.py +++ b/aws-py-voting-app/frontend/app/main.py @@ -16,7 +16,7 @@ title = app.config['TITLE'] # Redis configurations -redis_server = os.environ['REDIS_HOST'] +redis_server = os.environ['REDIS'] redis_port = os.environ['REDIS_PORT'] redis_password = os.environ['REDIS_PWD'] From d7ef88fd3236a27353a7c276d81fae144dc6c05d Mon Sep 17 00:00:00 2001 From: Vova Ivanov Date: Mon, 10 Aug 2020 11:50:00 -0700 Subject: [PATCH 6/8] Changed code blocks to be displayed as python --- aws-py-voting-app/blogpost.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aws-py-voting-app/blogpost.md b/aws-py-voting-app/blogpost.md index 18f68ce0d7..708c2e0650 100644 --- a/aws-py-voting-app/blogpost.md +++ b/aws-py-voting-app/blogpost.md @@ -1,4 +1,4 @@ - Creating a Python AWS application using Flask, Redis, and Pulumi + Creating a Python AWS Application Using Flask, Redis, and Pulumi Having recently begun developing with Pulumi, I have taken it upon myself to learn as much about its inner workings and processes as I could. To that end, I have decided to construct a production-level application using it, and to document each step that I take and its impact on my progress as I go along. @@ -22,14 +22,14 @@ redis_port = 6379 After setting up the imports and configurations, we create an Elastic Container Service Cluster. A Cluster represent a group of tasks and services that work together for a certain purpose. In this instance, the purpose is to provide users with a voting application. -``` +```python app_cluster = aws.ecs.Cluster("app-cluster") ``` In order to allow different tasks within our cluster to communicate, we create a Virtual Private Cloud and an associated subnet. -``` +```python app_vpc = aws.ec2.Vpc("app-vpc", cidr_block="172.31.0.0/16", enable_dns_hostnames=True) @@ -42,7 +42,7 @@ app_vpc_subnet = aws.ec2.Subnet("app-vpc-subnet", A gateway and routing table are needed to allow the VPC to communicate with the greater internet. Once created, we declare that the routing table is associated with our VPC. -``` +```python app_gateway = aws.ec2.InternetGateway("app-gateway", vpc_id=app_vpc.id) @@ -63,7 +63,7 @@ app_routetable_association = aws.ec2.MainRouteTableAssociation("app_routetable_a To inform what kinds of internet traffic are and aren't allowed to connect with the application, we create a firewall in the form of a security group. -``` +```python app_security_group = aws.ec2.SecurityGroup("security-group", vpc_id=app_vpc.id, description="Enables HTTP access", @@ -84,7 +84,7 @@ app_security_group = aws.ec2.SecurityGroup("security-group", In order to allow our services to start, we create an Identity and Access Management role, and attach execution permissions to it. -``` +```python app_exec_role = aws.iam.Role("app-exec-role", assume_role_policy="""{ "Version": "2012-10-17", From 89951c7e7fb615defcc3917208e85003a1a83a44 Mon Sep 17 00:00:00 2001 From: Vova Ivanov Date: Mon, 10 Aug 2020 11:51:07 -0700 Subject: [PATCH 7/8] Changed first code block to be python as well --- aws-py-voting-app/blogpost.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-py-voting-app/blogpost.md b/aws-py-voting-app/blogpost.md index 708c2e0650..8f695e7e95 100644 --- a/aws-py-voting-app/blogpost.md +++ b/aws-py-voting-app/blogpost.md @@ -7,7 +7,7 @@ This blog post will feature an existing [Typescript voting app example](https:// --- The first few lines of the __main\__.py file indicate which libraries need to be imported, and describe a pair of configuration options that will be used by the application. -``` +```python import json import pulumi import pulumi_aws as aws From 02ca8a5817666c17b64b6421b263755078f8c441 Mon Sep 17 00:00:00 2001 From: spara Date: Mon, 10 Aug 2020 14:28:29 -0500 Subject: [PATCH 8/8] edits/formatting --- aws-py-voting-app/blogpost.md | 44 ++++++++++++++--------------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/aws-py-voting-app/blogpost.md b/aws-py-voting-app/blogpost.md index 8f695e7e95..fb8b71678d 100644 --- a/aws-py-voting-app/blogpost.md +++ b/aws-py-voting-app/blogpost.md @@ -1,12 +1,13 @@ - Creating a Python AWS Application Using Flask, Redis, and Pulumi +# Creating a Python AWS Application Using Flask, Redis, and Pulumi -Having recently begun developing with Pulumi, I have taken it upon myself to learn as much about its inner workings and processes as I could. To that end, I have decided to construct a production-level application using it, and to document each step that I take and its impact on my progress as I go along. +I've recently begun developing with Pulumi to learn as much about its inner workings and processes as I could. I decided to construct a production-level application and to document each step that I take and my progress as I go along. -This blog post will feature an existing [Typescript voting app example](https://www.pulumi.com/docs/tutorials/aws/aws-ts-voting-app/) be fully re-created step by step in Python, using Flask as the frontend, and Redis as the backend. In future blog posts, we will explore how to change the front and backends, how to upgrade the app with additional AWS services, and even how to migrate from one cloud provider to another. +This blog post features recreating the existing [Typescript voting app example](https://www.pulumi.com/docs/tutorials/aws/aws-ts-voting-app/) step by step in Python with Flask as the frontend and Redis as the backend. In future blog posts, we will explore how to change the front and backends, how to upgrade the app with additional AWS services, and migrating from one cloud provider to another. --- -The first few lines of the __main\__.py file indicate which libraries need to be imported, and describe a pair of configuration options that will be used by the application. +The first few lines of the `__main\__.py` file indicate which libraries need to be imported, and describe a pair of configuration options that will be used by the application. + ```python import json import pulumi @@ -18,17 +19,17 @@ redis_password = config.require("redis-password") redis_port = 6379 ``` +After setting the imports and configuration, we create an Elastic Container Service Cluster. +A Cluster represents a group of tasks and services that work together for a certain purpose. In +this instance, the purpose is to provide users with a voting application. -After setting up the imports and configurations, we create an Elastic Container Service Cluster. -A Cluster represent a group of tasks and services that work together for a certain purpose. In -this instance, the purpose is to provide users with a voting application. ```python app_cluster = aws.ecs.Cluster("app-cluster") ``` - -In order to allow different tasks within our cluster to communicate, we create a Virtual Private +In order to allow different tasks within our cluster to communicate, we create a Virtual Private Cloud and an associated subnet. + ```python app_vpc = aws.ec2.Vpc("app-vpc", cidr_block="172.31.0.0/16", @@ -39,9 +40,9 @@ app_vpc_subnet = aws.ec2.Subnet("app-vpc-subnet", vpc_id=app_vpc) ``` - -A gateway and routing table are needed to allow the VPC to communicate with the greater internet. +A gateway and routing table are needed to allow the VPC to communicate with the greater internet. Once created, we declare that the routing table is associated with our VPC. + ```python app_gateway = aws.ec2.InternetGateway("app-gateway", vpc_id=app_vpc.id) @@ -60,9 +61,9 @@ app_routetable_association = aws.ec2.MainRouteTableAssociation("app_routetable_a vpc_id=app_vpc) ``` - -To inform what kinds of internet traffic are and aren't allowed to connect with the application, +To control internet traffic that are and aren't allowed to connect with the application, we create a firewall in the form of a security group. + ```python app_security_group = aws.ec2.SecurityGroup("security-group", vpc_id=app_vpc.id, @@ -81,9 +82,9 @@ app_security_group = aws.ec2.SecurityGroup("security-group", }]) ``` - -In order to allow our services to start, we create an Identity and Access Management role, and +In order to allow our services to start, we create an Identity and Access Management role, and attach execution permissions to it. + ```python app_exec_role = aws.iam.Role("app-exec-role", assume_role_policy="""{ @@ -105,15 +106,4 @@ exec_policy_attachment = aws.iam.RolePolicyAttachment("app-exec-policy", role=ap --- -The full code for the blog post can be [found on github.](https://github.com/jetvova/examples/tree/vova/aws-py-flask-redis-voting-app/aws-py-voting-app) - - - - - - - - - - - +The full code for the blog post can be [found on Github.](https://github.com/jetvova/examples/tree/vova/aws-py-flask-redis-voting-app/aws-py-voting-app)