Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

สร้างระบบ Uptime Monitoring น่าเชื่อถือด้วย Ruby ใช้ GCE, Cloud Storage และ Pub/Sub

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 เป็นแบบนี้:

สร้างระบบ Uptime Monitoring น่าเชื่อถือด้วย Ruby ใช้ GCE, Cloud Storage และ Pub/Sub

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:

  • BlackboxWrapper service จะรัน 2 processes
    • Process แรก รัน blackbox_exporter binary
    • Process ที่สอง listen bucket changes จาก GCP และ restart process แรก
  • Service จะ deploy เป็น docker image ทำให้ package service พร้อม blackbox_exporter binary

สร้างระบบ Uptime Monitoring น่าเชื่อถือด้วย Ruby ใช้ GCE, Cloud Storage และ Pub/Sub

เริ่มสร้างกัน

ก่อนอื่น สร้าง 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_exporter binary และย้ายไป 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_exporter binary ใน process แยก probe endpoints
      • Start subscriber ใน process อีกอัน listen bucket changes
      • เรียก sleep ใน main process ให้ app รัน infinitely
  • BlackboxExporter ทำงานยังไง?
    • .start ใช้ exec kernel method รัน blackbox_exporter binary ใน process อื่น
    • .reload ส่ง signal SIGHUP live reload blackbox_exporter binary ด้วย config ใหม่ ตามที่เห็นจาก ProcessNotification class config file ใหม่ถูกเขียนไปที่ config file location ก่อน reload exporter
  • Subscriber ทำงานยังไง?
    • .listen เริ่มด้วยการสร้าง NotificationConfiguration rule ที่ระบุ 3 สิ่ง:
      • Topic ใน pub/sub รับ notifications
      • Event ที่ trigger notifications ส่ง คลิกดู event types
      • Information ใน notifications
    • #create_notification_config รับรองว่ามีแค่ 1 NotificationConfiguration มิฉะนั้น delete ทั้งหมดและสร้างใหม่ รับรอง notifications ส่งแค่ครั้งเดียว
    • .listen เรียก @subscription.listen เริ่ม listen notification changes ใน bucket ที่ subscribe อยู่ รัน infinitely ใน process อื่น
    • #process_notification เรียกสำหรับทุก notification update ส่ง SUPPORTED_FILE_TYPES identify files ใน bucket ที่เราสนใจ และไม่ทำอะไรกับไฟล์อื่น
  • ProcessNotification: จัดการ notifications download updated config เขียนไฟล์ และ reload blackbox_exporter binary

รัน 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

สร้างระบบ Uptime Monitoring น่าเชื่อถือด้วย Ruby ใช้ GCE, Cloud Storage และ Pub/Sub

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

สร้าง GCP Resources

สร้าง bucket ใน GCP console

สร้างระบบ Uptime Monitoring น่าเชื่อถือด้วย Ruby ใช้ GCE, Cloud Storage และ Pub/Sub

สร้าง PubSub topic ใน GCP console

สร้างระบบ Uptime Monitoring น่าเชื่อถือด้วย Ruby ใช้ GCE, Cloud Storage และ Pub/Sub

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

สร้างระบบ Uptime Monitoring น่าเชื่อถือด้วย Ruby ใช้ GCE, Cloud Storage และ Pub/Sub

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

สร้างระบบ Uptime Monitoring น่าเชื่อถือด้วย Ruby ใช้ GCE, Cloud Storage และ Pub/Sub

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