"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."
- Mike Roberts
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
EDITOR=vim rails credentials:edit
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]'
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]
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
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
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]"
# 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"]
#!/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
Save this as `entrypoint.sh`:
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
As we have demonstrated, while the initial setup requires a handful things to prepare, afterwards the deployment process is relatively easy.
List of things you don't need to manage:
Workloads are automatically scaled up and down depending on traffic. Stable and consistent performance, regardless of scale.
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.
You pay for actual usage only, not for the allocation of capacity.
Vendor lock-in is a real issue. Be careful of choosing tech stack that does not have open source alternatives.
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 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.
You can't migrate your existing serverful app to serverless infrastructure without a proper modification.
"Serverless is any developer-focused product which abstracts out the underlying servers."
- Sam Newman