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 ดังที่แสดงด้านล่าง:
การตรวจสอบสิทธิ์
สร้าง Service Account
ด้วย Cloud Functions Admin
และ Service Account User
บทบาท
บัญชีบริการใช้สำหรับ IAM ระหว่างเครื่องกับเครื่อง ดังนั้น เมื่อระบบไม่ว่าจะทำงานบน Google Cloud หรือไม่ก็ตาม พูดคุยกับระบบอื่นบน Google Cloud จำเป็นต้องมีบัญชีบริการเพื่อช่วยระบุว่าใครกำลังร้องขอการเข้าถึงทรัพยากร Google ของเรา บทบาท Cloud Functions Admin
และ Service Account User
ทำให้เราสามารถระบุได้ว่าผู้ใช้ได้รับอนุญาตให้เข้าถึงทรัพยากรหรือไม่ ในสถานการณ์นี้ ตัวดำเนินการ Github Action จะสื่อสารกับ Google Cloud ในการตรวจสอบสิทธิ์เป็นบัญชีบริการที่มีสิทธิ์ที่จำเป็นในการปรับใช้ฟังก์ชันของเรา
สร้างรหัสบัญชีบริการ ดาวน์โหลด JSON และเพิ่มลงใน GitHub Secrets
โว้ว! 🎉 ใช้งานระบบคลาวด์ของเราเรียบร้อยแล้ว
ขีดจำกัดของฟังก์ชันระบบคลาวด์เทียบกับขีดจำกัดของ AWS
ด้านล่างนี้คือการเปรียบเทียบโดยละเอียดของผู้ให้บริการฟังก์ชันไร้เซิร์ฟเวอร์รายใหญ่ที่สุดสองราย:
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
อย่างไรก็ตาม รองรับหลายแพลตฟอร์มจากผู้ให้บริการหลายราย
สำหรับการอ้างอิง รหัสเต็มมีอยู่ที่นี่