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

การสร้าง การทดสอบ และการปรับใช้ฟังก์ชัน Google Cloud ด้วย Ruby

Serverless Functions เป็นกระบวนทัศน์การเขียนโปรแกรมใหม่ของการพัฒนาและการปรับใช้บริการคลาวด์ ในโลกที่ไร้เซิร์ฟเวอร์ เราสรุปการจัดเตรียม การบำรุงรักษา และการปรับขนาดบริการแบ็กเอนด์ของเราไปยังผู้ให้บริการระบบคลาวด์ สิ่งนี้ช่วยปรับปรุงประสิทธิภาพการทำงานของนักพัฒนาอย่างมากโดยให้นักพัฒนามุ่งเน้นไปที่การแก้ปัญหาเฉพาะ แม้ว่าจะมีข้อดีและข้อเสียมากมายในการสร้างฟังก์ชันแบบไร้เซิร์ฟเวอร์ สิ่งหนึ่งที่ต้องพิจารณาในการสร้างฟังก์ชันเหล่านี้คือการสนับสนุนด้านภาษา เมื่อเร็วๆ นี้ Google ได้ประกาศการรองรับ Ruby 2.7 สำหรับ Google Cloud Functions และในบทความนี้ ฉันจะเน้นที่การสร้าง ทดสอบ และปรับใช้ฟังก์ชันแบบไร้เซิร์ฟเวอร์ใน Ruby บน Google Cloud Functions และข้อดีและข้อเสียของฟังก์ชันแบบไร้เซิร์ฟเวอร์

การสร้างระบบ OTP แบบไร้เซิร์ฟเวอร์

รหัสผ่านแบบใช้ครั้งเดียว (OTP) คือรหัสตัวเลขสั้นๆ ที่ใช้สำหรับการตรวจสอบสิทธิ์ เช่น เมื่อธนาคารของคุณส่ง OPT ผ่านข้อความเพื่อยืนยันตัวตนของคุณ

ในบทความนี้ เราจะสร้างฟังก์ชัน OTP ที่จัดการความรับผิดชอบหลักสามประการ:

POST /otp :สร้างและส่งข้อความ OTP ไปยัง phone_number ที่ให้มา .

# Request
{
  "phone_number": "+2347012345678"
}

# Response
{
  "status": true,
  "message": "OTP sent successfully",
  "data": {
    "phone_number": "+2347012345678",
    "otp": 6872,
    "expires_at": "2021-02-09 07:15:25 +0100"
  }
}

PUT /otp/verify :ตรวจสอบ OTP กับ OTP ที่ผู้ใช้ให้มา

# Request
{
  "phone_number": "+2347012345678",
  "otp": 7116
}

# Response
{
  "status": true,
  "message": "OTP verified",
  "data": {}
}

PUT /otp/resend :พยายามสร้างและส่ง OTP ไปยัง phone_number ที่ให้มาอีกครั้ง .

# Request
{
  "phone_number": "+2347012345678"
}

# Response
{
  "status": true,
  "message": "OTP sent successfully",
  "data": {
    "phone_number": "+2347012345678",
    "otp": 8533,
    "expires_at": "2021-02-09 08:59:16 +0100"
  }
}

เพื่อความเรียบง่าย ฟังก์ชันระบบคลาวด์ของเราจะได้รับการสนับสนุนโดย Cloud MemoryStore (Redis หรือ Memcache บน GCP) แทนที่จะเป็นฐานข้อมูล SQL หรือ NoSQL แบบเต็ม นอกจากนี้ยังช่วยให้เราเรียนรู้เกี่ยวกับสถานะที่ใช้ร่วมกันในสภาพแวดล้อมที่ไม่มีสัญชาติอีกด้วย

การเขียนฟังก์ชัน Google Cloud ด้วย Ruby

ในการเขียนฟังก์ชันใน GCF เราจะอาศัย Functions Framework จัดทำโดยทีม Google Cloud สำหรับสร้างฟังก์ชัน GCF (เพิ่มเติมในภายหลัง)

ขั้นแรก สร้างไดเร็กทอรี App และป้อนไดเร็กทอรี

mkdir otp-cloud-function && cd otp-cloud-function

ถัดไป สร้าง Gemfile และติดตั้ง

เช่นเดียวกับแอปพลิเคชัน Ruby มาตรฐานส่วนใหญ่ เราจะใช้ bundler เพื่อจัดการการขึ้นต่อกันของฟังก์ชันของเรา

source "https://rubygems.org"

# Core
gem "functions_framework", "~> 0.7"

# Twilio for Sms
gem 'twilio-ruby', '~> 5.43.0'

# Database
gem 'redis'

# Connection Pooling
gem 'connection_pool'

# Time management
gem 'activesupport'

# API Serialization
gem 'active_model_serializers', '~> 0.10.0'

group :development, :test do
  gem 'pry'
  gem 'rspec'
  gem 'rspec_junit_formatter'
  gem 'faker', '~> 2.11.0'
end
bundle install

การสร้างฟังก์ชัน

โดยทั่วไป สภาพแวดล้อมการโฮสต์ที่แตกต่างกันจะอนุญาตให้คุณระบุไฟล์อื่นที่ฟังก์ชันของคุณถูกเขียนขึ้น อย่างไรก็ตาม Google Cloud Functions กำหนดให้เป็น app.rb ในรูทของไดเร็กทอรีโครงการของคุณ ตอนนี้เราพร้อมที่จะเขียนฟังก์ชันของเราแล้ว

เปิด app.rb และสร้างฟังก์ชัน:

# Cloud Functions Entrypoint

require 'functions_framework'
require 'connection_pool'
require 'active_model_serializers'
require './lib/store'
require './lib/send_sms_notification'
require './lib/response'
require './lib/serializers/models/base_model'
require './lib/serializers/models/otp_response'
require './lib/serializers/application_serializer'
require './lib/serializers/base_model_serializer'
require './lib/serializers/otp_response_serializer'

FunctionsFramework.on_startup do |function|
  # Setup Shared Redis Client
  require 'redis'
  set_global :redis_client, ConnectionPool.new(size: 5, timeout: 5) { Redis.new }
end

# Define HTTP Function
FunctionsFramework.http "otp" do |request|

  store = Store.new(global(:redis_client))
  data = JSON.parse(request.body.read)

  if  request.post? && request.path == '/otp'
    phone_number = data['phone_number']
    record = store.get(phone_number)
    unless record.nil? || record.expired?
      data = Models::OtpResponse.new(phone_number: phone_number,
                                      otp: record['otp'],
                                      expires_at: record['expires_at'])
      json = Response.generate_json(status: true,
                            message: 'OTP previously sent',
                            data: data)

      return json
    end

    otp = rand(1111..9999)
    record = store.set(phone_number, otp)
    SendSmsNotification.new(phone_number, otp).call

    data = Models::OtpResponse.new(phone_number: phone_number,
                                    otp: record['otp'],
                                    expires_at: record['expires_at'])

    Response.generate_json(status: true,
                          message: 'OTP sent successfully',
                          data: data)

  elsif request.put? && request.path == '/otp/verify'
    phone_number = data['phone_number']
    record = store.get(phone_number)

    if record.nil?
      return Response.generate_json(status: false, message: "OTP not sent to number")
    elsif record.expired?
      return Response.generate_json(status: false,  message: 'OTP code expired')
    end

    is_verified = data['otp'] == record['otp']

    if is_verified
      return Response.generate_json(status: true, message: 'OTP verified')
    else
      return Response.generate_json(status: false, message: 'OTP does not match')
    end

  elsif request.put? && request.path == '/otp/resend'
    phone_number = data['phone_number']
    store.del(phone_number)

    otp = rand(1111..9999)
    record = store.set(phone_number, otp)
    SendSmsNotification.new(phone_number, otp).call

    data = Models::OtpResponse.new(phone_number: phone_number,
                                    otp: record['otp'],
                                    expires_at: record['expires_at'])

    json = Response.generate_json(status: true,
                          message: 'OTP sent successfully',
                          data: data)
  else
    Response.generate_json(status: false,
                            message: 'Request method and path did not match')
  end
end

นี่เป็นรหัสจำนวนมาก ดังนั้นเราจะแยกย่อย:

  • Functions_Framework.on_startup เป็นบล็อกของโค้ดที่ทำงานต่ออินสแตนซ์ Ruby ก่อนที่ฟังก์ชันจะเริ่มประมวลผลคำขอ เป็นการดีที่จะเรียกใช้การเริ่มต้นรูปแบบใดๆ ก่อนที่ฟังก์ชันของเราจะถูกเรียกใช้ ในกรณีนี้ ฉันใช้มันเพื่อสร้างและแบ่งปันพูลการเชื่อมต่อกับเซิร์ฟเวอร์ Redis ของเรา:

    set_global :redis_client, ConnectionPool.new(size: 5, timeout: 5) { Redis.new }
    

    ซึ่งช่วยให้เราแชร์พูลของอ็อบเจ็กต์การเชื่อมต่อ Redis ในการเรียกใช้ฟังก์ชันพร้อมกันหลายรายการโดยไม่ต้องกลัว สามารถกำหนดการเริ่มต้นหลายรายการได้ พวกเขาจะทำงานตามลำดับที่กำหนดไว้ สิ่งสำคัญที่ควรทราบคือ Functions Framework ไม่ได้จัดเตรียมเบ็ดพิเศษให้ทำงานหลังจากเสร็จสิ้นการทำงาน

  • Functions_Framework.http 'otp' do |request| จัดการคำขอและการดำเนินการตอบสนองของฟังก์ชันของเรา ฟังก์ชันนี้รองรับรูปแบบเส้นทางที่แตกต่างกันสามรูปแบบ เป็นไปได้ที่จะกำหนดประเภทฟังก์ชันอื่นๆ (เช่น Functions_Framework.cloud_event 'otp' do |event| ) ที่จัดการกิจกรรมจากบริการอื่นๆ ของ Google นอกจากนี้ยังสามารถกำหนดหลายฟังก์ชันในไฟล์เดียวกันได้ แต่ปรับใช้อย่างอิสระ

  • ใน store = Store.new(global(:redis_client)) , global เมธอดใช้เพื่อดึงวัตถุใด ๆ ที่เก็บไว้ในสถานะที่ใช้ร่วมกันทั่วโลก ตามที่ใช้ข้างต้น เราดึงไคลเอ็นต์ Redis จากพูลการเชื่อมต่อที่กำหนดไว้ในการตั้งค่าส่วนกลางใน startup ของเรา บล็อก

  • Response และ Models::OtpResponse จัดการการทำให้เป็นอันดับการตอบสนองด้วย active_model_serializers เพื่อให้การตอบสนอง JSON ที่มีรูปแบบถูกต้อง

การทดสอบฟังก์ชันของเราในเครื่อง

Functions Framework ไลบรารีช่วยให้เราทดสอบฟังก์ชันของเราในเครื่องได้อย่างง่ายดายก่อนที่จะปรับใช้กับระบบคลาวด์ เพื่อทดสอบในเครื่อง เราเรียกใช้

bundle exec functions-framework-ruby --target=otp --port=3000

--target ใช้สำหรับเลือกฟังก์ชันที่จะใช้งาน

ในขณะที่การทดสอบด้วยตนเองนั้นยอดเยี่ยม การทดสอบอัตโนมัติและซอฟต์แวร์ทดสอบตัวเองถือเป็นจอกศักดิ์สิทธิ์ในการทดสอบ Functions Framework มีวิธีช่วยเหลือสำหรับทั้ง Minitest และ RSpec เพื่อช่วยทดสอบฟังก์ชันของเราสำหรับทั้ง http และ cloudevents ตัวจัดการ นี่คือตัวอย่างการทดสอบ:

require './spec/spec_helper.rb'
require 'functions_framework/testing'

describe  'OTP Functions' do
  include FunctionsFramework::Testing

  describe 'Send OTP', redis: true do
    let(:phone_number) { "+2347012345678" }
    let(:body) { { phone_number: phone_number }.to_json }
    let(:headers) { ["Content-Type: application/json"] }

    it 'should send OTP successfully' do
      load_temporary "app.rb" do
        request = make_post_request "/otp", body, headers

        response = call_http "otp", request
        expect(response.status).to eq 200
        expect(response.content_type).to eq("application/json")

        parsed_response = JSON.parse(response.body.join)
        expect(parsed_response['status']).to eq true
        expect(parsed_response['message']).to eq 'OTP sent successfully'
      end
    end
  end
end

การปรับใช้ฟังก์ชันของเรา

ก่อนอื่น เราต้องทำให้เซิร์ฟเวอร์ Redis ใช้งานได้โดยใช้ Google Cloud Memorystore ซึ่งขึ้นอยู่กับหน้าที่ของเรา ฉันจะไม่ลงรายละเอียดเพิ่มเติมเกี่ยวกับวิธีการปรับใช้เซิร์ฟเวอร์ Redis กับ GCP เนื่องจากอยู่นอกขอบเขตของบทความนี้

มีหลายวิธีในการปรับใช้ฟังก์ชันของเรากับสภาพแวดล้อมของ Google Cloud Function:การปรับใช้จากเครื่องของคุณ การปรับใช้จากคอนโซล GCP และการปรับใช้จากที่เก็บโค้ดของเรา วิศวกรรมซอฟต์แวร์สมัยใหม่สนับสนุนกระบวนการ CI/CD สำหรับการพัฒนาส่วนใหญ่ของเรา และสำหรับจุดประสงค์ของบทความนี้ ฉันจะเน้นที่การปรับใช้งาน Cloud Function จาก Github ด้วย Github Actions โดยใช้ Deploy-cloud-functions

มาตั้งค่าไฟล์การปรับใช้ของเรา (.github/workflows/deploy.yml)

name: Deployment
on:
  push:
    branches:
      - main
jobs:
  deploy:
    name: Function Deployment
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - id: deploy
        uses: google-github-actions/deploy-cloud-functions@main
        with:
          name: otp-cloud-function
          runtime: ruby26
          credentials: ${{ secrets.gcp_credentials }}
          env_vars: "TWILIO_ACCOUNT_SID=${{ secrets.TWILIO_ACCOUNT_SID }},TWILIO_AUTH_TOKEN=${{ secrets.TWILIO_AUTH_TOKEN }},TWILIO_PHONE_NUMBER=${{ secrets.TWILIO_PHONE_NUMBER }},REDIS_URL=${{ secrets.REDIS_URL }}"

ตัวแปรสภาพแวดล้อม

ในโค้ดด้านบน บรรทัดสุดท้ายช่วยให้เราระบุตัวแปรสภาพแวดล้อมที่จะใช้ได้กับฟังก์ชันของเราในสภาพแวดล้อม Google Cloud โปรดทราบว่าสำหรับเหตุผลด้านความปลอดภัย เราจะไม่เปิดเผยตัวแปรเหล่านี้ในฐานรหัสของเรา แต่เรากำลังใช้ความลับของการดำเนินการของ Github เพื่อเก็บข้อมูลนี้เป็นส่วนตัว ในการตรวจสอบว่าโทเค็นของเราได้รับการปรับใช้อย่างถูกต้องหรือไม่ ให้ตรวจสอบฟังก์ชันระบบคลาวด์ของคุณใน Google Console ดังที่แสดงด้านล่าง:

การสร้าง การทดสอบ และการปรับใช้ฟังก์ชัน Google Cloud ด้วย Ruby

การตรวจสอบสิทธิ์

สร้าง Service Account ด้วย Cloud Functions Admin และ Service Account User บทบาท

บัญชีบริการใช้สำหรับ IAM ระหว่างเครื่องกับเครื่อง ดังนั้น เมื่อระบบไม่ว่าจะทำงานบน Google Cloud หรือไม่ก็ตาม พูดคุยกับระบบอื่นบน Google Cloud จำเป็นต้องมีบัญชีบริการเพื่อช่วยระบุว่าใครกำลังร้องขอการเข้าถึงทรัพยากร Google ของเรา บทบาท Cloud Functions Admin และ Service Account User ทำให้เราสามารถระบุได้ว่าผู้ใช้ได้รับอนุญาตให้เข้าถึงทรัพยากรหรือไม่ ในสถานการณ์นี้ ตัวดำเนินการ Github Action จะสื่อสารกับ Google Cloud ในการตรวจสอบสิทธิ์เป็นบัญชีบริการที่มีสิทธิ์ที่จำเป็นในการปรับใช้ฟังก์ชันของเรา

การสร้าง การทดสอบ และการปรับใช้ฟังก์ชัน Google Cloud ด้วย Ruby

การสร้าง การทดสอบ และการปรับใช้ฟังก์ชัน Google Cloud ด้วย Ruby

สร้างรหัสบัญชีบริการ ดาวน์โหลด JSON และเพิ่มลงใน GitHub Secrets

การสร้าง การทดสอบ และการปรับใช้ฟังก์ชัน Google Cloud ด้วย Ruby

การสร้าง การทดสอบ และการปรับใช้ฟังก์ชัน Google Cloud ด้วย Ruby

โว้ว! 🎉 ใช้งานระบบคลาวด์ของเราเรียบร้อยแล้ว

ขีดจำกัดของฟังก์ชันระบบคลาวด์เทียบกับขีดจำกัดของ AWS

ด้านล่างนี้คือการเปรียบเทียบโดยละเอียดของผู้ให้บริการฟังก์ชันไร้เซิร์ฟเวอร์รายใหญ่ที่สุดสองราย:

การสร้าง การทดสอบ และการปรับใช้ฟังก์ชัน Google Cloud ด้วย Ruby

Functions Framework Contract vs. Serverless Framework

ในบทความนี้ เราได้เน้นที่การสร้างฟังก์ชันระบบคลาวด์สำหรับฟังก์ชัน Google Cloud ในส่วนนี้ ฉันต้องการเปรียบเทียบสิ่งปลูกสร้างกับ Functions Framework กับ Serverless Framework

  • The Serverless Framework ขึ้นอยู่กับ serverless.yml ในขณะที่ Functions Framework อิงตาม Functions Framework Contract ซึ่งใช้ในการปรับใช้ฟังก์ชันแบบไร้เซิร์ฟเวอร์ทั่วทั้ง Google Cloud Infrastructure
  • ด้วย Serverless Framework มีตัวอย่างเพียงไม่กี่ตัวอย่าง และยังไม่ชัดเจนว่าจะสร้างและปรับใช้ฟังก์ชันแบบไร้เซิร์ฟเวอร์ด้วย Ruby กับสภาพแวดล้อมแบบไร้เซิร์ฟเวอร์ของ Google ได้อย่างไร (Cloud Functions, Cloud Run และ Knative) ด้วย Functions Framework Contract การทำงาน การสร้าง Ruby ในผลิตภัณฑ์ต่างๆ ของ Google นั้นตรงไปตรงมา
    • ต่อจากข้อที่แล้ว Functions Framework Contract ทำให้ง่ายต่อการเปลี่ยนภาษาสำรองที่อยู่เบื้องหลังฟังก์ชันของคุณโดยไม่จำเป็นต้องเปลี่ยนแปลงอะไรมากมายในกระบวนการปรับใช้ของคุณ
  • ในขณะที่เขียนนี้ Functions Framework รองรับการทำงานร่วมกันใน Google Cloud Serverless Environments และ Knative เท่านั้น Serverless Framework อย่างไรก็ตาม รองรับหลายแพลตฟอร์มจากผู้ให้บริการหลายราย

สำหรับการอ้างอิง รหัสเต็มมีอยู่ที่นี่