diff --git a/gcp-py-functions/.gitignore b/gcp-py-functions/.gitignore new file mode 100644 index 000000000..a3807e5bd --- /dev/null +++ b/gcp-py-functions/.gitignore @@ -0,0 +1,2 @@ +*.pyc +venv/ diff --git a/gcp-py-functions/Pulumi.yaml b/gcp-py-functions/Pulumi.yaml new file mode 100644 index 000000000..e1ecb2f83 --- /dev/null +++ b/gcp-py-functions/Pulumi.yaml @@ -0,0 +1,3 @@ +name: gcp-py-functions +runtime: python +description: A minimal Python Pulumi program using Google Cloud Functions. diff --git a/gcp-py-functions/README.md b/gcp-py-functions/README.md new file mode 100644 index 000000000..68a3469f8 --- /dev/null +++ b/gcp-py-functions/README.md @@ -0,0 +1,136 @@ +[![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new) + +# GCP Functions + +This example shows how to deploy a Python-based Google Cloud Function. + +The deployed Cloud Function allows you to notify a friend via SMS about how long it will take to +arrive at a location. This uses the Google Maps API and Twilio, and also benefits from using a +[Flic button](https://flic.io) and [IFTTT](https://ifttt.com). But none of that is necessary to +use Pulumi to provision the Google Cloud Platform resources. + +## Creating the Stack + +1. Create a new stack: + + ```bash + pulumi stack init gcp-py-functions + ``` + +1. Configure GCP project and region: + + ```bash + pulumi config set gcp:project + pulumi config set gcp:region + ``` + +1. Download dependencies: + + ```bash + # (Optional) Create a virtualenv environment. + virtualenv -p python3 venv + source venv/bin/activate + + pip3 install -r requirements.txt + ``` + +1. Run `pulumi up` to preview and deploy changes: + + ```bash + $ pulumi up + Previewing changes: + ... + + Performing changes: + ... + Resources: + + 4 created + Duration: 1m2s + ``` + +Once the application is deployed, you can start accessing the Google Cloud Function by making an +HTTP request to the function's endpoint. It is exported from the stack's as `fxn_url`. + +```bash +$ pulumi stack output fxn_url +https://us-central1-pulumi-gcp-dev.cloudfunctions.net/eta_demo_function... +``` + +You can specify a starting location via latitude and longitude coordinates via URL query +parameters. (You can find your current location on [https://www.latlong.net/](https://www.latlong.net/).) + +```bash +$ curl "$(pulumi stack output fxn_url)?lat=&long=" +Sent text message to... +``` + +## Configuration + +### Google Maps for Travel Time + +The application uses the [Google Maps API](https://developers.google.com/maps/documentation/) to estimate travel time. To set this up: + +1. Get a [Google Maps](https://cloud.google.com/maps-platform/) API key by clicking 'Get started'. + + * Check the Routes and then click continue. + * Select the GCP project you are deploying your Cloud function to. + +1. Update the stack's configuration, encrypting the API key value: + + ```bash + pulumi config set googleMapsApiKey --secret + ``` + +1. Set the target destination to compute directions to: + + ```bash + pulumi config set destination + ``` + +1. (Optional) Add a travel time offset, e.g. add 5 minutes to the estimate. + + ```bash + pulumi config set travelOffset + ``` + +1. Run `pulumi up` to re-deploy your cloud function with the new configuration. + +### Twilio for SMS Notifications + +To have the Cloud Function send a text message, you'll need to a Twilio key too: + +1. Log into your [Twilio](https://www.twilio.com/) account, and create a new access token + and/or phone number to send SMS messages from. + +1. Add the Twilio configuration data to your Pulumi stack: + + ```bash + pulumi config set twillioAccessToken --secret + pulumi config set twillioAccountSid --secret + pulumi config set fromPhoneNumber + ``` + +1. Enter the phone number the Cloud Function will send messages to: + + ```bash + pulumi config set toPhoneNumber --secret + ``` + +1. Run `pulumi up` to re-deploy your cloud function with the new configuration. + +### Flic Button to Trigger the Cloud Function + +With Pulumi having setup the cloud infrastructure, the next step is to have a simple way to trigger +it. With [Flic](https://flic.io) you can trigger the Cloud Function with literally the push +of a button. + +To make sure to include the button presser's location, you can use [IFTTT](https://ifttt.com). + +1. Install the Flic app on your phone and pair your button. Enable location services for the Flic app + and add an IFTTT for one of the click gestures. + +1. Create a new Applet on IFTTT: "If You click a Flic, then Make a web request" + * For "If" select the "Flic" service then "Flic is clicked". + * Select your Flic button and the appropriate gesture from the menu. + * For "Then" select the "Make a web request" service + * Under URL enter following (replace `` with the value from `pulumi stack output fxn_url`): `?long={{Longitude}}&lat={{Latitude}}` diff --git a/gcp-py-functions/__main__.py b/gcp-py-functions/__main__.py new file mode 100644 index 000000000..ec7b3e7da --- /dev/null +++ b/gcp-py-functions/__main__.py @@ -0,0 +1,72 @@ +"""Python Pulumi program for creating Google Cloud Functions. + +Create a single Google Cloud Function. The deployed application will calculate +the estimated travel time to a given location, sending the results via SMS. +""" + +import time +import os +import pulumi + +from pulumi_gcp import storage +from pulumi_gcp import cloudfunctions + +# Disable rule for that module-level exports be ALL_CAPS, for legibility. +# pylint: disable=C0103 + +# File path to where the Cloud Function's source code is located. +PATH_TO_SOURCE_CODE = "./functions" + +# Get values from Pulumi config to use as environment variables in our Cloud Function. +config = pulumi.Config(name=None) +config_values = { + # Target destination and travel time offset. + "DESTINATION": config.get("destination"), + "TRAVEL_OFFSET": config.get("travelOffset"), + + # Google Maps API key. + "GOOGLE_MAPS_API_KEY": config.get("googleMapsApiKey"), + + # Twilio account for sending SMS messages. + "TWILLIO_ACCESS_TOKEN": config.get("twillioAccessToken"), + "TWILLIO_ACCOUNT_SID": config.get("twillioAccountSid"), + "TO_PHONE_NUMBER": config.get("toPhoneNumber"), + "FROM_PHONE_NUMBER": config.get("fromPhoneNumber"), +} + +# We will store the source code to the Cloud Function in a Google Cloud Storage bucket. +bucket = storage.Bucket("eta_demo_bucket") + +# The Cloud Function source code itself needs to be zipped up into an +# archive, which we create using the pulumi.AssetArchive primitive. +assets = {} +for file in os.listdir(PATH_TO_SOURCE_CODE): + location = os.path.join(PATH_TO_SOURCE_CODE, file) + asset = pulumi.FileAsset(path=location) + assets[file] = asset + +archive = pulumi.AssetArchive(assets=assets) + +# Create the single Cloud Storage object, which contains all of the function's +# source code. ("main.py" and "requirements.txt".) +source_archive_object = storage.BucketObject( + "eta_demo_object", + name="main.py-%f" % time.time(), + bucket=bucket.name, + source=archive) + +# Create the Cloud Function, deploying the source we just uploaded to Google +# Cloud Storage. +fxn = cloudfunctions.Function( + "eta_demo_function", + entry_point="get_demo", + environment_variables=config_values, + region="us-central1", + runtime="python37", + source_archive_bucket=bucket.name, + source_archive_object=source_archive_object.name, + trigger_http=True) + +# Export the DNS name of the bucket and the cloud function URL. +pulumi.export("bucket_name", bucket.url) +pulumi.export("fxn_url", fxn.https_trigger_url) diff --git a/gcp-py-functions/functions/main.py b/gcp-py-functions/functions/main.py new file mode 100644 index 000000000..30fd48027 --- /dev/null +++ b/gcp-py-functions/functions/main.py @@ -0,0 +1,77 @@ +"""Google Cloud Function source code for an ETA messaging app. + +Defines a single Cloud Function endpoint, get_demo, which will compute the +estimated travel time to a location. If configured, will also send the result +via SMS. +""" + +import os +from datetime import datetime +import googlemaps +import twilio.rest + + +def get_travel_time(origin, destination, offset): + """Returns the estimated travel time using the Google Maps API. + + Returns: A string, such as '3 minutes'""" + key = os.getenv("GOOGLE_MAPS_API_KEY", "") + if key == "": + return "[ENABLE GOOGLE MAPS TO DETERMINE TRAVEL TIME]" + + gmaps = googlemaps.Client(key=key) + now = datetime.now() + directions_result = gmaps.directions( + origin=origin, + destination=destination, + mode="driving", + departure_time=now) + + travel_time = directions_result[0]["legs"][0]["duration"]["value"] + travel_time /= 60 # seconds to minutes + travel_time += offset + + return "%d minutes" % travel_time + +def send_text(message_body): + """Sends an SMS using the Twilio API.""" + to_number = os.getenv("TO_PHONE_NUMBER", "") + from_number = os.getenv("FROM_PHONE_NUMBER", "") + account_sid = os.getenv("TWILLIO_ACCOUNT_SID", "") + auth_token = os.getenv("TWILLIO_ACCESS_TOKEN", "") + + if account_sid and auth_token and to_number and from_number: + client = twilio.rest.Client(account_sid, auth_token) + client.messages.create( + to=to_number, + from_=from_number, + body=message_body) + return "Sent text message to %s\n%s" % (to_number, message_body) + return "[ENABLE TWILIO TO SEND A TEXT]: \n%s" % (message_body) + +def get_demo(request): + """The Google Cloud Function computing estimated travel time.""" + + # Get origin location from URL-query parameters. + lat = request.args.get("lat") + long = request.args.get("long") + if lat and long: + origin = "%s, %s" % (lat, long) + else: + origin = "Pulumi HQ, Seattle, WA" + + destination = os.getenv( + "DESTINATION", + "Space Needle, Seattle, WA") + + # Optional travel time offset, e.g. add a static 5m delay. + travel_offset_str = os.getenv("TRAVEL_OFFSET", "0") + travel_offset = int(travel_offset_str) + + travel_time_str = get_travel_time( + origin=origin, destination=destination, offset=travel_offset) + + # Send the message. Returns a summary in the Cloud Function's response. + message = "Hey! I'm leaving now, I'll be at '%s' to pick you up in about %s." % ( + destination, travel_time_str) + return send_text(message) diff --git a/gcp-py-functions/functions/requirements.txt b/gcp-py-functions/functions/requirements.txt new file mode 100644 index 000000000..fbd89910d --- /dev/null +++ b/gcp-py-functions/functions/requirements.txt @@ -0,0 +1,2 @@ +twilio>=6.26.0 +googlemaps>=3.0.2 diff --git a/gcp-py-functions/requirements.txt b/gcp-py-functions/requirements.txt new file mode 100644 index 000000000..7b3076dd0 --- /dev/null +++ b/gcp-py-functions/requirements.txt @@ -0,0 +1,4 @@ +pulumi>=0.16.4 +pulumi_gcp>=0.16.2 +twilio>=6.26.0 +googlemaps>=3.0.2