App Engine Standard and SQLAlchemy

December 3, 2017
appengine cloudsql flask

Lets admit it. Google wants you to use their super duper NoSQL Datastore product as the default storage option for new app engine projects. While datastore is quite nice it also has its own drawbacks and shortcomings. One of the big ones is that you actually have to learn it and try to wrap your head around it! That takes time. Time that is better spent delivering business value instead. Lets take what we already know and use plain ol’ MySQL that Google so nicely packages for us under the fancy name of Cloud SQL.

But even if you want to use SQL there are still a few unknowns left. How do you connect to a Cloud SQL instance from your local computer? Or how do you use a local instance of MySQL while developing?

Part 1: Setup Cloud SQL

First you have to create a new Cloud SQL instance. An instance is basically a server where you can specify tons of options on how MySQL should be set up and run. Lets create the smallest instance possible with default values for our example.

# create smallest possible instance
$ gcloud sql instances create cloudy-mysql \
    --database-version=MYSQL_5_7 \
    --region=us-east1 \
    --tier=db-f1-micro

# set root password
$ gcloud sql users set-password root % --instance cloudy-mysql --password pa$$w0rd

# connect to your cloud sql instance and create a sample database
$ gcloud sql connect cloudy-mysql -u root
mysql> CREATE DATABASE blog;
mysql> QUIT;

Part 2: Use SQLAlchemy with Google App Engine

Now that we have setup MySQL both remotely and hopefully locally (not part of this post) lets write a simple JSON API using Flask web framework.

Proxy connection to remote instance

If you don’t have a local MySQL server installed you can proxy a connection to your Cloud SQL instance using Cloud SQL Proxy utility. Download it and put it somewhere in your path.

# find your connection name
$ gcloud sql instances describe cloudy-mysql | grep connectionName -

# start sql proxy with a tcp connection
$ cloud_sql_proxy -instances my-gcp-project-id:us-east1:cloudy-mysql=tcp:3306

# now you can connect to proxied Cloud SQL instance locally
$ mysql -h 127.0.0.1 -u root -p

Install Python MySQL client

There are many ways to install Python MySQL client locally. Most of the examples out there recommend using a OS package manager (yum, apt). What many people don’t realise is that it’s quite simple and you can do it using only pip.

# install MySQLdb Python client
$ pip install --user mysqlclient

The problem and common misconception is that the package name is not mysqldb but mysqlclient and when you do a pip search mysqldb you won’t get any matches. Likewise pip install mysqldb will also fail.

For Google App Engine the Python client is already installed but you have to explicitly enable it in app.yaml config file.

libraries:
- name: MySQLdb
  version: latest

There are quite many built-in third-party libraries that you can use with the Google App Engine Python runtime. Just don’t forget to enable them.

Add some code

Now that we have set up a Cloud SQL instance and we know how to connect to it - both locally and remote - it’s time to create tiny little app connecting the dots. For this example lets use the best of both worlds - Flask for our web and SQLAlchemy for our SQL needs - by using the excellent Flask-SQLAlchemy package. We will also use Marshmallow package for simple serialization to JSON.

Create a requirements.txt file with package dependencies

Flask-SQLAlchemy
marshmallow

Install them by running

$ pip install -t lib -r requirements.txt

Create an app.yaml configuration file.

runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /.*
  script: main.app

env_variables:
  CLOUDSQL_CONNECTION_NAME: your-project-id:us-east1:cloudy-mysql
  CLOUDSQL_USER: root
  CLOUDSQL_PASSWORD: pa$$w0rd

libraries:
- name: MySQLdb
  version: latest

Now lets create main.py file

import os
from flask import Flask, jsonify
from datetime import datetime, timedelta
from flask_sqlalchemy import SQLAlchemy
from marshmallow import Schema, fields

def gen_connection_string():
    # if not on Google then use local MySQL
    if not os.getenv('SERVER_SOFTWARE', '').startswith('Google App Engine/'):
        return 'mysql://root@localhost/blog'
    else:
        conn_name = os.environ.get('CLOUDSQL_CONNECTION_NAME' '')
        sql_user = os.environ.get('CLOUDSQL_USER', 'root')
        sql_pass = os.environ.get('CLOUDSQL_PASSWORD', '')
        conn_template = 'mysql+mysqldb://%s:%s@/blog?unix_socket=/cloudsql/%s'
        return conn_template % (sql_user, sql_pass, conn_name)

app = Flask(__name__)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = gen_connection_string()
db = SQLAlchemy(app)

# SQLAchemy model
class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer, primary_key=True)
    author = db.Column(db.Unicode(80), nullable=False)
    title = db.Column(db.Unicode(255), nullable=False)
    body = db.Column(db.UnicodeText())
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    def __repr__(self):
        return '<Post %r>' % self.title

# Marshmallow serialization schema
class PostSerializer(Schema):
    id = fields.Integer()
    author = fields.Str()
    title = fields.Str()
    body = fields.Str()
    created_at = fields.DateTime()

@app.route('/')
def index():
    return 'hello from google app engine + sqlalchemy!'

@app.route('/posts')
def get_posts():
    # Fetch all the posts
    posts = Post.query.all()
    # Create a serialization schema
    schema = PostSerializer(many=True)
    # Return all posts as json
    return jsonify({ 'items': schema.dump(posts).data })

def bootstrap_db():
    # Create db tables if they don't exits. NOT SAFE FOR PRODUCTION USE!
    db.create_all()
    # create a couple of posts if there are none
    if Post.query.count() == 0:
        p1 = Post(author=u'john',
                title=u'Do you know your Latin?',
                body=u'Lorem ipsum dolor sit amet',
                created_at=datetime.utcnow() + timedelta(days=-1))
        p2 = Post(author=u'mary',
                title=u'Easy way to a better life',
                body=u'Eat healthy, move more, lift heavy, laugh.')
        db.session.add_all([p1, p2])
        db.session.commit()

bootstrap_db()

Lets run the app locally with extra debugging enabled to see how it flies.

# start dev server with extra debugging
$ dev_appserver.py --dev_appserver_log_level debug .

# fetch all posts from another terminal
$ curl localhost:8080/posts

Lets deploy it to GCP if everything worked as expected.

$ gcloud app create
$ gcloud app deploy
$ gcloud app browse

This wasn’t too hard but there were a few steps we needed to take to make everything work. Hopefully now you have a little more understanding of Cloud SQL and how it works together with App Engine.

Like what you just read? Sign up for the newsletter!

Read more