Serverless Ruby with Google Cloud Run

Outline

What We Will Discuss

  • Revisiting Serverless
  • Ruby on Serverless
  • Benefits of Serverless
  • Concerns about Serverless

Revisiting Serverless

Before we begin...

Definition

"The phrase 'serverless' doesn’t mean servers are no longer involved. It simply means that developers no longer have to think that much about them. Computing resources get used as services without having to manage around physical capacities or limits."

- Ken Fromm, 2012

Characteristics of Serverless

  • No management of server hosts or processes
  • Self auto-scale and auto-provision based on load
  • Cost based on precise usage
  • Performance capabilities defined in terms other than host size/count

- Mike Roberts

Architectural Model

Google Cloud Run Resource Model

Ruby on Serverless

What We Will Deploy

  • A simple Rails app
  • with Google Cloud SQL database
  • storing blob files in GCS Bucket

Day 0

Setup Project and Enable Services

PROJECT_ID=<project_id> 
REGION=asia-northeast1
gcloud config set project $PROJECT_ID
gcloud config set run/region $REGION
gcloud services enable run.googleapis.com
gcloud services enable sqladmin.googleapis.com
gcloud services enable cloudkms.googleapis.com

Generate Rails Master Key

EDITOR=vim rails credentials:edit

Setup Cloud SQL Database

gcloud sql instances create [instance_name] --tier=db-g1-small --region=$REGION --assign-ip
gcloud sql users set-password root --host % --instance [instance_name] --password [password]
gcloud sql users create [user_name] --instance [instance_name] --host % --password [password]

And edit `config/database.yml`:

production:
  <<: *default
  database: [database_name]
  username: [user_name]
  password: <%= ENV['DATABASE_PASSWORD'] %>
  socket: '/cloudsql/[projcet_id]:asia-northeast1:[instance_name]'

Setup GCS Bucket for Storage

gsutil mb -p $PROJECT_ID -l $REGION gs://[bucket_name]

And edit `config/storage.yml`:

google:
  service: GCS
  project: [project_id]
  credentials: <%= Rails.root.join(“config/[runner_key]”) %>
  bucket: [bucket_name]

Setup GCP Service Account

gcloud iam service-accounts create [web_runner_name] --display-name [description]

Grant client roles for Cloud SQL and admin roles for GCS:

SVC_ACCOUNT=[web_runner_name]@$PROJECT_ID.iam.gserviceaccount.com
gcloud projects add-iam-policy-binding $PROJECT_ID --member serviceAccount:$SVC_ACCOUNT --role roles/cloudsql.client
gcloud projects add-iam-policy-binding $PROJECT_ID --member serviceAccount:$SVC_ACCOUNT --role roles/cloudsql.editor
gcloud projects add-iam-policy-binding $PROJECT_ID --member serviceAccount:$SVC_ACCOUNT --role roles/storage.admin

Generate runner key:

gcloud iam service-accounts keys create ./config/[runner_key] --iam-account [runner_name]@$PROJECT_ID.iam.gserviceaccount.com

Setup GCP Key Management Service

KEYRING=[keyring_name]
gcloud kms keyrings create $KEYRING --location=$REGION

Encrypt runner key, Rails master key, and database password:

gcloud kms keys create [runner_key_name] --location $REGION --keyring $KEYRING --purpose encryption
gcloud kms encrypt --location $REGION --keyring $KEYRING \
  --key [runner_key_name] --plaintext-file ./config/[runner_key_name] \
  --ciphertext-file ./config/[runner_key_name].key.enc
  
gcloud kms keys create rails_master_key --location $REGION --keyring $KEYRING --purpose encryption
gcloud kms encrypt --location $REGION --keyring $KEYRING \
  --key rails_master_key --plaintext-file ./config/master.key \
  --ciphertext-file ./config/master.key.enc
  
gcloud kms keys create [db_key_name] --location $REGION --keyring $KEYRING --purpose encryption
echo -n “[password]” | gcloud kms encrypt --location $REGION --keyring $KEYRING --key [db_key_name] --plaintext-file - --ciphertext-file - | base64

Prepare Google Cloud Build

GCB_SVC_ACCOUNT=[service_account_id]@cloudbuild.gserviceaccount.com

Grant Cloud Build the right to decrypt the keys:

gcloud kms keys add-iam-policy-binding rails_master_key --location=$REGION \
  --keyring=$KEYRING --member=serviceAccount:$GCB_SVC_ACCOUNT \
  --role=roles/cloudkms.cryptoKeyDecrypter
  
 gcloud kms keys add-iam-policy-binding [db_key_name] --location=$REGION \
  --keyring=$KEYRING --member=serviceAccount:$GCB_SVC_ACCOUNT \
  --role=roles/cloudkms.cryptoKeyDecrypter
  
gcloud kms keys add-iam-policy-binding [runner_key_name] --location=$REGION \
  --keyring=$KEYRING --member=serviceAccount:$GCB_SVC_ACCOUNT \
  --role=roles/cloudkms.cryptoKeyDecrypter

Save this as `cloudbuild.yaml`:

# Decrypt Rails Master key file
- name: gcr.io/cloud-builders/gcloud
  args: ["kms", "decrypt", "--ciphertext-file=./config/master.key.enc",
    "--plaintext-file=./config/master.key",
    "--location=asia-northeast1","--keyring=[keyring_name]", 
    "--key=rails_master_key"]

# Decrypt Kelimun service account credentials
- name: gcr.io/cloud-builders/gcloud
  args: ["kms", "decrypt", "--ciphertext-file=./config/[runner_name].key.enc",
    "--plaintext-file=./config/[runner_name].key",
    "--location=asia-northeast1","--keyring=[keyring_name]", 
    "--key=[runner_name]"]

# Build image with tag 'latest' and pass decrypted Rails DB password as argument
- name: 'gcr.io/cloud-builders/docker'
  args: ['build', '--tag', 'gcr.io/[project_name]/[image_name]:latest', '--build-arg', 'DB_PWD', '.']
  secretEnv: ['DB_PWD']

# Push new image to Google Container Registry       
- name: 'gcr.io/cloud-builders/docker'
  args: ['push', 'gcr.io/[project_name]/[image_name]-web:latest']

secrets:
- kmsKeyName: projects/[project_name]/locations/asia-northeast1/keyRings/[image_name]/cryptoKeys/[db_key_name]
  secretEnv:
    DB_PWD: "[result_of_gcloud_kms_encrypt_command]"

Configure Cloud Build Manifest

# Leverage the official Ruby image from Docker Hub
# https://hub.docker.com/_/ruby
FROM ruby:2.5.7

# Install recent versions of nodejs (10.x) and yarn pkg manager
# Needed to properly pre-compile Rails assets
RUN (curl -sL https://deb.nodesource.com/setup_10.x | bash -) && apt-get update && apt-get install -y nodejs 

RUN (curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -) && \
  echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
  apt-get update && apt-get install -y yarn

# Install MySQL client (needed for the connection to Google CloudSQL instance)
RUN apt-get install -y default-mysql-client

# Install production dependencies (Gems installation in
# local vendor directory)
WORKDIR /usr/src/app
COPY Gemfile Gemfile.lock ./
ENV BUNDLE_FROZEN=true
RUN gem install bundler -v 2.0.1
RUN bundle install

# Copy application code to the container image.
# Note: files listed in .gitignore are not copied
# (e.g.secret files)
COPY . .

# Pre-compile Rails assets (master key needed)
RUN RAILS_ENV=production bundle exec rake assets:precompile

# Set Google App Credentials environment variable with Service Account
ENV GOOGLE_APPLICATION_CREDENTIALS=/usr/src/app/config/kelimun_runner.key

# Setup Rails DB password passed on docker command line (see Cloud Build file)
ARG DB_PWD
ENV DATABASE_PASSWORD=${DB_PWD}

# For now we don't have a Nginx/Apache frontend so tell 
# the Puma HTTP server to serve static content
# (e.g. CSS and Javascript files)
ENV RAILS_SERVE_STATIC_FILES=true

# Redirect Rails log to STDOUT for Cloud Run to capture
ENV RAILS_LOG_TO_STDOUT=true

# Designate the initial sript to run on container startup
RUN chmod +x /usr/src/app/entrypoint.sh
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]

Configure Dockerfile Manifest

#!/usr/bin/env bash

cd /usr/src/app

# Create the Rails production DB on first run
RAILS_ENV=production bundle exec rake db:create

# Make sure we are using the most up to date
# database schema
RAILS_ENV=production bundle exec rake db:migrate

# Do some protective cleanup
> log/production.log
rm -f tmp/pids/server.pid

# Run the web service on container startup
# $PORT is provided as an environment variable by Cloud Run
bundle exec rails server -e production -b 0.0.0.0 -p $PORT

Configure Entrypoint

Save this as `entrypoint.sh`:

Day 1 to N

gcloud builds submit --config cloudbuild.yaml

gcloud beta run deploy kelimun --image gcr.io/$PROJECT_ID/[image_name] \
  --set-cloudsql-instances $PROJECT_ID:$REGION:[db_name] \
  --region $REGION --allow-unauthenticated --platform managed

Build and Deploy to Google Cloud Run

Benefits of Serverless

Ease of Deployment

As we have demonstrated, while the initial setup requires a handful things to prepare, afterwards the deployment process is relatively easy.

No Servers to Manage

List of things you don't need to manage:

  • Provisioning and configuring servers (or automating these things)
  • Applying (security) patches
  • Configuring networking and firewalls
  • Automating application deployment
  • Making and testing system backups
  • Setting up logging and metrics monitoring that provide good insight into system performance.

Scalable Out of The Box

Workloads are automatically scaled up and down depending on traffic. Stable and consistent performance, regardless of scale.

Resilience

Of course, a big part of the responsibility to build a resilient system lies with you, the developer. However, with serverless infrastructure, the characteristics of the platform provide a framework that gently pushes you towards building your application in a way that makes it bounce back from failure when it occurs.

Different Cost Model

You pay for actual usage only, not for the allocation of capacity.

Concerns about Serverless

Portability

Vendor lock-in is a real issue. Be careful of choosing tech stack that does not have open source alternatives.

Unpredictable Costs

While you pay only for your usage, it means that at any given time period, you can't really be sure in advance how much you will have to pay.

When Things Really Go Wrong

When you have production issue that you can't reproduce in staging environment, debugging it can be a little bit tricky. Although, you can mitigate this by setting up proper logging and monitoring infrastructure.

Migration Effort

You can't migrate your existing serverful app to serverless infrastructure without a proper modification.

Parting Words...

"Serverless is any developer-focused product which abstracts out the underlying servers."

- Sam Newman

Discussions

Thank You!

Serverless Ruby with Google Cloud Run

By qblfrb

Serverless Ruby with Google Cloud Run

  • 426