Uptime monitoring เกี่ยวกับการตรวจสอบ availability ของ websites, APIs และ servers Monitor ตรวจสอบ endpoint ที่กำหนดภายใน interval ที่ระบุ เพื่อตัดสินว่า available หรือไม่ เป้าหมายคือบรรลุ contracted level of availability ตาม SLA ของระบบ และวัดความแตกต่างเมื่อ contract ไม่ถูกปฏิบัติ
ในบทความนี้เราจะสร้าง uptime monitoring system based on Prometheus blackbox_exporter แม้การสร้าง custom HTTP monitoring system อาจ trivial แต่การสร้าง wrapper รอบ exporter ช่วยเข้าถึง probe techniques อื่นๆ มาก และ monitor องค์ประกอบอื่นๆ ของระบบได้เร็ว
บทความครอบคลุมหลาย technologies จะอธิบายแต่ละ component ก่อนลงลึก uptime system
Google Compute Engine (GCE) คืออะไร?
Compute Engine เป็น cloud computing service ของ Google คล้าย EC2 ของ AWS GCE secure และ customizable พอสำหรับ workloads ต่างๆ จาก small machines (รองรับ 32 vCPUs และ 128 GB memory) ไป standard machines (224 vCPUs และ 896 GB memory) และ high-end machines สำหรับ intensive workloads ใช้ compute on-demand scale ตามความต้องการ
GCE รองรับ deployment mechanisms ต่างๆ สำหรับ app deployment รวม containers, instance templates และ managed instance groups สำหรับบทความนี้ เราจะ bundle Ruby uptime monitor เข้า docker container สำหรับ deployment
Cloud Storage คืออะไร?
Google Cloud Storage เป็น highly available object-storage service คล้าย S3 ของ AWS Cloud Storage มี storage features หลายอย่าง enable use-cases ต่างๆ สำหรับ modern apps เริ่มต้น Cloud Storage ใน Ruby ใช้ google-cloud-storage gem authenticate, upload และ download files:
require 'google/cloud/storage'
def upload_file(bucket_name:, file_path:, file_name: nil)
storage = Google::Cloud::Storage.new
bucket = storage.bucket(bucket_name)
file = bucket.create_file(file_path, file_name)
end
def download_file(bucket_name:, file_path:, file_name: nil)
storage = Google::Cloud::Storage.new
bucket = storage.bucket(bucket_name)
file = bucket.file(file_name)
file.download(file_path)
end
Note: ต้องตั้งค่า GOOGLE_APPLICATION_CREDENTIALS ใน environment 指向 service account key ที่ถูกต้อง Google client gems ทุกตัวค้นหา environment variable นี้สำหรับ authorization มิฉะนั้นต้องส่ง auth parameters ไป Google::Cloud::Storage.new หาก app รันใน GCE VM อยู่แล้ว ตั้งค่านี้ใน environment แล้ว
Cloud PubSub คืออะไร?
Cloud PubSub เป็น publish/subscribe messaging service ของ Google Cloud ใช้ facilitate asynchronous service-to-service communication คล้าย SNS ของ AWS การสร้าง systems ด้วย asynchronous communication ช่วย improve performance, scalability และ reliability เริ่มต้น Cloud PubSub ใน Ruby ใช้ google-cloud-pubsub gem authenticate, publish และ listen events:
require 'google/cloud/pubsub'
def publish_message(topic_id:, message: nil)
pubsub = Google::Cloud::Pubsub.new
topic = pubsub.topic(topic_id)
topic.publish_async(message) do |result|
raise "Failed to publish message" unless result.succeeded?
puts "Message published asynchronously"
end
topic.async_publisher.stop.wait!
rescue StandardError => e
puts "Received error while publishing: #{e.message}"
end
def receive_message(subscription_id: nil, wait_time: 200.seconds)
pubsub = Google::Cloud::Pubsub.new
subscription = pubsub.subscription(subscription_id)
subscriber = subscription.listen do |received_message|
puts "Received message: #{received_message.data}"
received_message.acknowledge!
end
subscriber.start
sleep(wait_time)
end
Note: Authentication ที่อธิบายสำหรับ Cloud Storage ใช้ได้ที่นี่ด้วย
เมื่อใช้ Cloud Storage และ PubSub สร้าง solutions น่าสนใจได้ บ่อยครั้งเราอยาก upload object และ track updates - life-cycle - create, update, delete และ take specific actions based on events หากยัง abstract ลอง explore 2 use-cases:
- Image Service: สร้าง Image Service สมมติอยากสร้างอย่าง Cloudinary ที่ให้ image/video storage และ transformations บน data Cloud Storage ช่วย store และ version data ด้วย PubSub เรา listen events จาก bucket และ pre-process data แม้ก่อน customer request pre-processed version
- Distribute Configuration Files: ปัญหา infrastructure engineering คือ roll out configurations ไปหลาย servers และ easy rollbacks สมมติอยากมี central server จัดการ server configs และ update config ครั้งเดียว distribute config ไป fleet ของ servers ด้วย Cloud Storage และ Cloud PubSub สร้าง agents บน servers listen ผ่าน PubSub ได้ object notifications และ take action based on events เพิ่มเติม หากเป็น bad change (wrong config changes เป็นเหตุผล downtime 😩) rollback ด้วย object versioning
ในบทความนี้เราจะสร้าง Ruby wrapper สำหรับ Blackbox Exporter ใช้ use-case ที่สองข้างบน Wrapper จะรัน exporter ใน process หนึ่ง และอีก process watch configuration changes จาก bucket ใน GCP แล้ว live reload exporter พร้อมไหม? Let's have fun!
Blackbox Exporter คืออะไร?
Blackbox Exporter เป็น open-source tool ที่ Prometheus team สร้าง probe endpoints ผ่าน HTTP, HTTPS, DNS, TCP และ ICMP Exporter ควร deploy alongside Grafana และ Prometheus deployment Complete setup เป็นแบบนี้:

Blackbox wrapper probe endpoints ที่ configured ทั้งหมด Prometheus scrape exporter เหมือน target อื่นๆ Grafana retrieve data จาก Prometheus เพื่อ graph เรารัน exporter binary แบบ blackbox_exporter --config.file blackbox.yml Blackbox Exporter ยัง allow live reload exporter ด้วย configuration ใหม่โดยไม่ shutdown binary และ restart ใช้งานได้ดีเมื่อ scraping endpoints ด้วย intervals วินาที
BlackboxWrapper Service Specs
ก่อน deep-dive โค้ด มา highlight service specs:
BlackboxWrapperservice จะรัน 2 processes- Process แรก รัน
blackbox_exporterbinary - Process ที่สอง listen bucket changes จาก GCP และ restart process แรก
- Process แรก รัน
- Service จะ deploy เป็น docker image ทำให้ package service พร้อม
blackbox_exporterbinary

เริ่มสร้างกัน
ก่อนอื่น สร้าง app directory และเข้าไป:
mkdir blackbox-wrapper && cd blackbox-wrapper
เหมือน Ruby application มาตรฐาน ใช้ bundler จัดการ dependencies ของ wrapper สร้าง Gemfile:
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
ruby '2.7.2'
gem 'google-cloud-storage'
gem 'google-cloud-pubsub'
gem 'rake'
gem 'pry'
รัน bundle install
สร้างไฟล์เก็บโค้ด: app.rb
ไฟล์นี้เป็น entry point ของ service เนื่องจาก deploy ใน container ไฟล์นี้จะระบุใน CMD command ใน Dockerfile ภายหลัง
touch app.rb
สร้าง Dockerfile
แม้บางรายการถูกละเว้นโดยเจตนา โค้ดด้านล่าง highlight critical components สำหรับบทความ:
FROM ruby:2.7.2
RUN mkdir /app
WORKDIR /app
COPY . .
# Install other dependencies ...
# Download & Install blackbox exporter
RUN curl -SL \
https://github.com/prometheus/blackbox_exporter/releases/download/v0.18.0/blackbox_exporter-0.18.0.linux-386.tar.gz | \
tar xvz -C /tmp && \
mv /tmp/blackbox_exporter-0.18.0.linux-386/blackbox_exporter /usr/local/bin && \
mkdir /etc/blackbox && \
mv /tmp/blackbox_exporter-0.18.0.linux-386/blackbox.yml /etc/blackbox/
# Specify entry point.
CMD ["bundle", "exec", "ruby", "app.rb"]
จากข้างบน ควรสังเกต:
- ใช้ Ruby image -
ruby:2.7.2- เป็น base image พร้อม Ruby - ติดตั้ง
blackbox_exporterbinary และย้ายไป directory accessible จากPATH - ระบุ entrypoint ของ container รัน
app.rbตอน container start up
สร้าง Wrapper Service
นี่คือ Ruby service ที่ผูกทุกอย่าง ใน main.rb วางโค้ด:
require 'rubygems'
require 'bundler/setup'
require "google/cloud/pubsub"
require "google/cloud/storage"
CONFIG_BUCKET = ENV['BUCKET_NAME']
TOPIC = ENV['PUBSUB_TOPIC']
TOPIC_SUBSCRIPTION = ENV['TOPIC_SUBSCRIPTION']
class ProcessNotification
def initialize(file, attr, blackbox_exporter)
@file = file
@attr = attr
@blackbox_exporter = blackbox_exporter
end
def call
return if @attr['eventType'] == 'OBJECT_DELETE'
@blackbox_exporter.write(@file)
@blackbox_exporter.reload
end
end
class BlackBoxExporter
CONFIG_FILE = '/etc/blackbox/blackbox.yml'
def initialize
@blackbox_pid = nil
end
def start
return unless @blackbox_pid.nil?
@blackbox_pid = fork do
exec('blackbox_exporter', '--config.file', CONFIG_FILE)
end
end
def write(file)
file.download(CONFIG_FILE)
end
def reload
# Send SIGHUP signal
Process.kill('HUP', @blackbox_pid)
end
def shutdown
Process.kill('KILL', @blackbox_pid)
end
end
class Subscriber
class NotificationConfigError < StandardError; end
SUPPORTED_FILE_TYPES = ['blackbox.yml']
def initialize(blackbox_exporter)
@pubsub = Google::Cloud::Pubsub.new
@storage = Google::Cloud::Storage.new
@subscription_name = ENV['TOPIC_SUBSCRIPTION']
# Retrieve a subscription
@bucket = @storage.bucket(CONFIG_BUCKET)
@subscription = @pubsub.subscription(@subscription_name)
@blackbox_exporter = blackbox_exporter
end
def listen
create_notification_config
puts "Starting subscriber"
@subscriber = @subscription.listen do |received_message|
process_notification(received_message)
end
@subscriber.on_error do |exception|
process_exception(exception)
end
@subscriber.start
end
def process_notification(received_message)
data = received_message.message.data
published_at = received_message.message.published_at
attributes = received_message.message.attributes
puts "Data: #{data}, published at #{published_at}, Attr: #{attributes}"
received_message.acknowledge!
parsed_data = JSON.parse(data)
file_name = parsed_data['name']
return unless SUPPORTED_FILE_TYPES.include?(file_name)
file = @bucket.file(file_name)
process_notification = ProcessNotification.new(file, attributes, @blackbox_exporter)
process_notification.call
end
def process_exception(exception)
puts "Exception: #{exception.class} #{exception.message}"
end
def shutdown
@subscriber.stop!(10)
end
def create_notification_config
topic = @pubsub.topic(TOPIC)
notification_exists = @bucket.notifications.count == 1
unless notification_exists
@bucket.notifications.each do |notification|
notification.delete
end
end
@bucket.create_notification(topic.name)
rescue StandardError => e
raise NotificationConfigError, e.message
end
end
class BlackboxWrapper
def initialize
@blackbox_exporter = BlackBoxExporter.new
@subscriber = Subscriber.new(@blackbox_exporter)
end
def start
@blackbox_exporter.start
@subscriber.listen
at_exit do
@blackbox_exporter.shutdown
@subscriber.shutdown
end
# Block, letting processing threads continue in the background
sleep
end
end
blackbox_wrapper = BlackboxWrapper.new
blackbox_wrapper.start
โค้ดเยอะมาก ลองแยกส่วนจากท้ายขึ้นไป:
BlackboxWrapper: entrypoint ของ service- เมธอด
.startทำ:- Start
blackbox_exporterbinary ใน process แยก probe endpoints - Start
subscriberใน process อีกอัน listen bucket changes - เรียก
sleepใน main process ให้ app รัน infinitely
- Start
- เมธอด
BlackboxExporterทำงานยังไง?.startใช้execkernel method รันblackbox_exporterbinary ใน process อื่น.reloadส่ง signalSIGHUPlive reloadblackbox_exporterbinary ด้วย config ใหม่ ตามที่เห็นจากProcessNotificationclass config file ใหม่ถูกเขียนไปที่ config file location ก่อน reload exporter
Subscriberทำงานยังไง?.listenเริ่มด้วยการสร้างNotificationConfigurationrule ที่ระบุ 3 สิ่ง:- Topic ใน pub/sub รับ notifications
- Event ที่ trigger notifications ส่ง คลิกดู event types
- Information ใน notifications
#create_notification_configรับรองว่ามีแค่ 1NotificationConfigurationมิฉะนั้น delete ทั้งหมดและสร้างใหม่ รับรอง notifications ส่งแค่ครั้งเดียว.listenเรียก@subscription.listenเริ่ม listen notification changes ใน bucket ที่ subscribe อยู่ รัน infinitely ใน process อื่น#process_notificationเรียกสำหรับทุก notification update ส่งSUPPORTED_FILE_TYPESidentify files ใน bucket ที่เราสนใจ และไม่ทำอะไรกับไฟล์อื่น
ProcessNotification: จัดการ notifications download updated config เขียนไฟล์ และ reloadblackbox_exporterbinary
รัน Service ในเครื่อง
รัน service ในเครื่องและทดสอบ รันใน root ของ app directory:
export BUCKET_NAME='{insert-bucket-name}'
export PUBSUB_TOPIC='{insert-pubsub-topic}'
export TOPIC_SUBSCRIPTION='{insert-subscription-name}'
export GOOGLE_APPLICATION_CREDENTIALS='{insert-path-to-service-key-json}'
bundle exec ruby app.rb
Deploy Service ไป Google Compute Engine
เหมือนหลายด้านของ cloud มีหลายวิธีได้ผลเดียวกัน แต่ modern software engineering ส่งเสริม CI/CD processes ด้วยเหตุผลดีๆ เราจะโฟกัส deploy จาก Github Actions ใช้ setup-gcloud
ตั้งค่า deployment file (.github/workflows/deploy.yml):
name: Build and Deploy to Google Compute Engine
on:
push:
branches:
- main
env:
PROJECT_ID: ${{ secrets.GCE_PROJECT }}
GCE_INSTANCE: ${{ secrets.GCE_INSTANCE }}
GCE_INSTANCE_ZONE: us-central1-a
BUCKET_NAME: demo-configurations
PUBSUB_TOPIC: demo-configurations-bucket-notifications
TOPIC_SUBSCRIPTION: demo-bucket-changes-subscription
jobs:
setup-build-publish-deploy:
name: Setup, Build, Publish, and Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# Setup gcloud CLI
- uses: google-github-actions/setup-gcloud@master
with:
version: '290.0.1'
service_account_key: ${{ secrets.GCE_SA_KEY }}
project_id: ${{ secrets.GCE_PROJECT }}
# Configure Docker to use the gcloud command-line tool as a credential
# helper for authentication
- run: |
gcloud --quiet auth configure-docker
# Build the Docker image
- name: Build
run: |
docker build --tag "gcr.io/$PROJECT_ID/$GCE_INSTANCE-image:$GITHUB_SHA" .
# Push the Docker image to Google Container Registry
- name: Publish
run: |
docker push "gcr.io/$PROJECT_ID/$GCE_INSTANCE-image:$GITHUB_SHA"
- name: Deploy
run: |
gcloud compute instances update-container "$GCE_INSTANCE" \
--zone "$GCE_INSTANCE_ZONE" \
--container-image "gcr.io/$PROJECT_ID/$GCE_INSTANCE-image:$GITHUB_SHA" \
--container-env "BUCKET_NAME=$BUCKET_NAME,PUBSUB_TOPIC=$PUBSUB_TOPIC,TOPIC_SUBSCRIPTION=$TOPIC_SUBSCRIPTION"
สังเกต flag --container-env ตั้งใน deploy phase รับรองว่า pass environment variables ที่จำเป็นจาก Github Actions secrets ไป container อย่างปลอดภัย
Secrets & Environment Variables
ตั้งค่า secrets สำหรับ github actions

ตั้งค่า environment variables สำหรับ container ด้วย flag --container-env เนื่องจากตั้งจาก Github actions ใช้ secrets สำหรับ sensitive data หรือ env variables สำหรับ non-sensitive data
สร้าง GCP Resources
สร้าง bucket ใน GCP console

สร้าง PubSub topic ใน GCP console

ตั้งค่า service agent ของ cloud storage bucket - IAM role - pubsub.publisher ใน console แต่ละ project มี Cloud Storage service account สำหรับ background actions เช่น PubSub notifications คลิกเรียนรู้หาได้ยังไง

สุดท้าย สร้าง subscription ใน GCP Console

Voila! 🎉 Cloud function ของเราถูก deploy เรียบร้อยแล้ว
สรุป
ถ้าคุณอ่านจนถึงตรงนี้ คุณสมควรได้ cookie 🍪 คิดว่าเป็น first version ของ potentially great solution พร้อม multiple optimizations ที่จะ achieve ตัวอย่าง:
- Deploy blackbox_exporter เป็น serverless function รองรับ multiple regions ideal สำหรับ uptime monitoring และ deploy master server จัดการ bucket configuration ใน Cloud Storage
- จากจุดก่อน อาจ abstract เป็น app ที่ integrate เข้า popular cloud providers เพื่อ functionality เดียวกัน ทำให้ cloud-agnostic P.S: Popular cloud providers (GCP, AWS, & Azure) ให้ functionalities เหมือนกันข้าม services
- ในบทความต่อไป เราจะ build on this solution ให้ rollbacks ด้วย cloud-storage object versioning ช่วย recover จากการ update config ด้วย incorrect updates
- Deploy ด้วย Docker แค่แก้ packaging problem แต่รู้แล้วว่ามีหลายวิธี package services เลือก Docker ในบทความเพื่อ simplicity
Glossary
- Prometheus คือ open-source systems monitoring และ alerting toolkit รวม server scrape และ store time-series data, client libraries instrument application code, alert manager handle alerts
- Grafana คือ visualization system query, visualize, alert และ understand metrics ไม่ว่าจะเก็บที่ไหน
- Blackbox Exporter คือ open-source tool โดย Prometheus team probe endpoints ผ่าน HTTP, HTTPS, DNS, TCP, ICMP