Skip to content

Commit

Permalink
Infra: Sharing my Bazel rules for GAE and GCF deployments.
Browse files Browse the repository at this point in the history
  • Loading branch information
weisi committed Sep 16, 2018
0 parents commit 4527798
Show file tree
Hide file tree
Showing 21 changed files with 752 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__/
bazel-*
*.py[co]
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"python.formatting.provider": "yapf",
"python.pythonPath": "/usr/bin/python3",
"files.exclude": {
"**/__pycache__": true,
"**/bazel-*": true
},
}
13 changes: 13 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright 2018 Google LLC. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
103 changes: 103 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
================================================================
Bazel Rules: Python 3 Google App Engine & Google Cloud Functions
================================================================

This repository contains the `Bazel <https://bazel.build>`_ (`Starlark <https://docs.bazel.build/versions/master/skylark/language.html>`_) rules to build Python 3 and deploy to Google App Engine and Google Cloud Functions.

Usage
=====

There is a `GAE app example <examples/app_engine/BUILD>`_ and `a GCF example <examples/function/BUILD>`_ in the ``examples`` directory.

Google App Engine (Python 3)
----------------------------

Put this in the ``BUILD`` file::

py_binary(
name = "app",
srcs = [
"app.py",
],
)

py_app_engine(
name = "app_deploy",
src = ":app",
descriptor = "app.yaml",
entry = "app",
requirements = [
"flask",
],
)

and run::

examples$ bazel run //app_engine:app_deploy

Google Cloud Functions (Python 3)
---------------------------------

Put this in the ``BUILD`` file::

py_binary(
name = "hello",
srcs = [
"hello.py",
],
)

py_cloud_function(
name = "hello_deploy",
src = ":hello",
entry = "hello",
)

and run::

examples$ bazel run //function:hello_deploy

Features
========

In the BUILD rule you can also specify:

PyPI (pip) requirements
either as a package list or pointing to a ``requirements.txt`` file

The GCP project name
if you don't want to use the default one used by ``gcloud``

Version (App Engine only)
the version string of the GAE deployment

Deploy name (Cloud Functions only)
the name of the deployed function (in GCP) and also in the HTTP path

Pub/Sub topic trigger (Cloud Functions only)
if you want your function to be triggered by a Cloud Pub/Sub message

GCS bucket trigger (Cloud Functions only)
if you want your function to be triggered by events from a GCS bucket

Memory in MiB (Cloud Functions only)
the default is 256 MiB but you can request more, up to 2,048 MiB

Timeout in seconds (Cloud Functions only)
the default is 60 s but you can request up to 540 s.

Requirements
============

This solution depends on these commands being available:

* The Python 3 interpreter at ``/usr/bin/python3``
* `The fish shell <http://fishshell.com/>`_ at ``fish``
* `The Google Cloud SDK <https://cloud.google.com/sdk/>`_ at ``gcloud``

Your Bazel workspace should be set to generate a ZIP package for ``py_binary`` targets, by setting some parameters in the ``.bazelrc`` file, `like this <examples/.bazelrc>`_.

LICENSE
=======

``bazel_for_gcloud_python`` is released under `the Apache 2.0 License <LICENSE>`_.
1 change: 1 addition & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
workspace(name = "bazel_for_gcloud_python")
3 changes: 3 additions & 0 deletions examples/.bazelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Use Python 3 and build to ZIP.
run --python_top=//dev/env:python3 --build_python_zip
build --python_top=//dev/env:python3 --build_python_zip
14 changes: 14 additions & 0 deletions examples/WORKSPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
workspace(name = "bazel_for_gcloud_python_examples")

local_repository(
name = "bazel_for_gcloud_python",
path = "../",
)

# Or, use the git repository (and comment the above):
# load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
# git_repository(
# name = "bazel_for_gcloud_python",
# remote = "https://github.com/weisi/bazel_for_gcloud_python.git",
# commit = "Replace this with a Git commit SHA",
# )
44 changes: 44 additions & 0 deletions examples/app_engine/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
load("@bazel_for_gcloud_python//infra/serverless:gae_rules.bzl", "py_app_engine")

py_library(
name = "gae_utils",
srcs = ["gae_utils.py"],
deps = [
"//function:gcf_utils",
],
visibility = ["//visibility:public"],
)

py_binary(
name = "app",
srcs = [
"app.py",
],
data = [
"static/file.html",
],
deps = [
":gae_utils",
],
)

# 'bazel run' this rule to trigger deployment.
py_app_engine(
# Required parameters:
name = "app_deploy",
src = ":app",
descriptor = "app.yaml",
entry = "app",

# Specify your pip requirements here
requirements = [
# flask is required.
"flask",
],

# Specify a GCP project name instead of using the default:
# gcloud_project = "my-gcp-project",

# Print the arguments for debugging when running the rule:
# debug = True,
)
19 changes: 19 additions & 0 deletions examples/app_engine/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from flask import Flask, request

from app_engine import gae_utils

app = Flask(__name__)

@app.route('/')
@gae_utils.plain_text_response
def homepage():
messages = list()
messages.append('Hi there,')
messages.append(f'You are visiting path "{request.path}".')
messages.append(
f'This app "{gae_utils.current_application_name()}" '
f'is running on GCP project {gae_utils.current_gcloud_project()}.'
)
messages.append(f'Your IP address is {gae_utils.remote_ip()}')
messages.append('Visit any other path to see a static file.')
return '\n'.join(messages)
14 changes: 14 additions & 0 deletions examples/app_engine/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
runtime: python37
# service: service-name
instance_class: F1
automatic_scaling:
min_instances: 0
max_instances: 1
handlers:
- url: /$
secure: always
script: auto
- url: /.*$
static_files: app_engine/static/file.html
upload: app_engine/static/file\.html
secure: always
30 changes: 30 additions & 0 deletions examples/app_engine/gae_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Helper methods for using Python 3 on Google App Engine.
# Don't use gcf_utils directly if running on GAE, since some details are different.

from flask import request, Response
import http
import os

from function.gcf_utils import \
plain_text_response, \
http_no_content, \
current_gcloud_project, \
remote_ip


def current_application_name():
# In the form of 's~xcode-dev'.
return os.environ.get('GAE_APPLICATION')


def current_service_name():
return os.environ.get('GAE_SERVICE')


def current_version():
return os.environ.get('GAE_VERSION')


def current_instance():
# It's the GAE instance ID, a.k.a. "clone_id".
return os.environ.get('GAE_INSTANCE')
14 changes: 14 additions & 0 deletions examples/app_engine/static/file.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<title>Hello, static file</title>
</head>

<body>
<h3>Hello, static file</h3>
<p>This is a static file, served on Google App Engine.</p>
</body>

</html>
6 changes: 6 additions & 0 deletions examples/dev/env/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
py_runtime(
name = "python3",
files = [],
interpreter_path = "/usr/bin/python3",
visibility = ["//visibility:public"],
)
56 changes: 56 additions & 0 deletions examples/function/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
load("@bazel_for_gcloud_python//infra/serverless:gcf_rules.bzl", "py_cloud_function")

py_library(
name = "gcf_utils",
srcs = [
"gcf_utils.py",
],
visibility = ["//visibility:public"],
)

py_binary(
name = "hello",
srcs = [
"hello.py",
],
deps = [
":gcf_utils",
]
)

# 'bazel run' this rule to trigger deployment.
py_cloud_function(
# Required parameters:
name = "hello_deploy",
src = ":hello",
entry = "hello",

# Specify your pip requirements here:
# requirements = [
# "google-cloud-logging",
# ],

# or specify them in another file:
# requirements_file = "//function:requirements.txt"

# Specify a GCP project name instead of using the default:
# gcloud_project = "my-gcp-project",

# The function name that appears in HTTP path:
# deploy_name = "function_name",

# Use pubsub as a trigger instead of HTTP:
# trigger_topic = "pubsub_topic",

# Use GCS as a trigger instead of HTTP:
# trigger_bucket = "my_gcs_bucket",

# Specify the memory limit in MiB (default is 256 MiB):
# memory = 2048,

# Specify the timeout of the function in seconds (default is 60 s):
# timeout = 200,

# Print the arguments for debugging when running the rule:
# debug = True,
)
42 changes: 42 additions & 0 deletions examples/function/gcf_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from flask import request, Response
import http
import os


def plain_text_response(fn):
def _wrapper(*args, **kwargs):
return Response(fn(*args, **kwargs), mimetype='text/plain')

_wrapper.__name__ = fn.__name__
return _wrapper


def http_no_content(fn):
def _wrapper(*args, **kwargs):
fn(*args, **kwargs)
return (str(), http.HTTPStatus.NO_CONTENT)

_wrapper.__name__ = fn.__name__
return _wrapper


def execution_id():
return request.headers.get('Function-Execution-Id')


def remote_ip():
return request.headers.get('X-Appengine-User-Ip')


def current_gcloud_project():
return os.environ.get('GCP_PROJECT') \
or os.environ.get('GCLOUD_PROJECT') \
or os.environ.get('GOOGLE_CLOUD_PROJECT')


def current_function_name():
return os.environ.get('FUNCTION_NAME')


def current_function_region():
return os.environ.get('FUNCTION_REGION')
13 changes: 13 additions & 0 deletions examples/function/hello.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from function import gcf_utils

@gcf_utils.plain_text_response
def hello(request):
messages = list()
messages.append('Hi there,')
messages.append(f'You are visiting path "{request.path}".')
messages.append(
f'This function "{gcf_utils.current_function_name()}" '
f'is running on GCP project {gcf_utils.current_gcloud_project()}.'
)
messages.append(f'Your IP address is {gcf_utils.remote_ip()}')
return '\n'.join(messages)
Loading

0 comments on commit 4527798

Please sign in to comment.