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."
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