Serverless Functions là một mô hình lập trình mới để phát triển và triển khai các dịch vụ đám mây. Trong thế giới không có máy chủ, chúng tôi tóm tắt việc cung cấp, bảo trì và mở rộng các dịch vụ phụ trợ của chúng tôi cho nhà cung cấp đám mây. Điều này cải thiện đáng kể năng suất của nhà phát triển bằng cách cho phép các nhà phát triển tập trung vào giải quyết một vấn đề cụ thể. Mặc dù có nhiều ưu điểm và nhược điểm của việc xây dựng các chức năng serverless, nhưng một điều cần cân nhắc khi xây dựng chúng là hỗ trợ ngôn ngữ. Gần đây, Google đã công bố hỗ trợ Ruby 2.7 cho Google Cloud Functions và trong bài viết này, tôi sẽ tập trung vào việc xây dựng, thử nghiệm và triển khai một chức năng serverless trong Ruby on Google Cloud Functions và những ưu nhược điểm của các chức năng serverless.
Xây dựng Hệ thống OTP không máy chủ
Mật khẩu dùng một lần (OTP) là các mã số ngắn được sử dụng cho mục đích xác thực, chẳng hạn như khi ngân hàng của bạn gửi OPT qua tin nhắn văn bản để xác minh danh tính của bạn.
Trong bài viết này, chúng tôi sẽ xây dựng một chức năng OTP xử lý ba trách nhiệm cốt lõi:
POST /otp
:Tạo và gửi tin nhắn OTP đến phone_number
được cung cấp .
# 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
:Xác minh OTP với OTP do người dùng cung cấp.
# Request
{
"phone_number": "+2347012345678",
"otp": 7116
}
# Response
{
"status": true,
"message": "OTP verified",
"data": {}
}
PUT /otp/resend
:Cố gắng tạo và gửi lại mã OTP đến phone_number
được cung cấp .
# 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"
}
}
Vì mục đích đơn giản, chức năng đám mây của chúng tôi sẽ được hỗ trợ bởi Cloud MemoryStore (Redis hoặc Memcache trên GCP) chứ không phải là Cơ sở dữ liệu SQL hoặc NoSQL đầy đủ. Điều này cũng sẽ cho phép chúng tôi tìm hiểu về các trạng thái được chia sẻ trong môi trường không trạng thái.
Viết hàm Google Cloud với Ruby
Để viết các hàm trong GCF, chúng tôi sẽ dựa vào Functions Framework
do nhóm Google Cloud cung cấp để xây dựng các chức năng GCF (sẽ nói thêm về điều này sau).
Đầu tiên, Tạo thư mục ứng dụng và nhập thư mục.
mkdir otp-cloud-function && cd otp-cloud-function
Tiếp theo, tạo Gemfile của bạn và cài đặt.
Giống như hầu hết các ứng dụng Ruby tiêu chuẩn, chúng tôi sẽ sử dụng bundler
để quản lý các phần phụ thuộc vào hàm của chúng tôi
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
Tạo hàm
Nói chung, các môi trường lưu trữ khác nhau cho phép bạn chỉ định một tệp khác nơi các chức năng của bạn được viết. Tuy nhiên, Google Cloud Functions yêu cầu nó phải là app.rb
trong thư mục gốc của thư mục dự án của bạn. Bây giờ, chúng tôi đã sẵn sàng để viết hàm của mình.
Mở app.rb
và tạo hàm:
# 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
Đây là rất nhiều mã, vì vậy chúng tôi sẽ chia nhỏ ra:
-
Functions_Framework.on_startup
là một khối mã chạy trên mỗi phiên bản Ruby trước khi các hàm bắt đầu xử lý các yêu cầu. Lý tưởng nhất là chạy bất kỳ hình thức khởi tạo nào trước khi các hàm của chúng ta được gọi. Trong trường hợp này, tôi đang sử dụng nó để tạo và chia sẻ nhóm kết nối với máy chủ Redis của chúng tôi:set_global :redis_client, ConnectionPool.new(size: 5, timeout: 5) { Redis.new }
Điều này cho phép chúng tôi chia sẻ một nhóm các đối tượng kết nối Redis qua nhiều lệnh gọi hàm đồng thời mà không sợ hãi. Nhiều công ty khởi nghiệp có thể được xác định. Chúng sẽ chạy theo thứ tự mà chúng đã được xác định. Cũng cần lưu ý rằng
Functions Framework
không cung cấp bất kỳ móc đặc biệt nào để chạy sau khi hoàn thành chức năng. -
Functions_Framework.http 'otp' do |request|
xử lý yêu cầu và xử lý phản hồi các chức năng của chúng tôi. Chức năng này hỗ trợ ba mẫu tuyến đường khác nhau. Có thể xác định các loại hàm khác (ví dụ:Functions_Framework.cloud_event 'otp' do |event|
) xử lý các sự kiện từ các dịch vụ khác của Google. Cũng có thể xác định nhiều hàm trong cùng một tệp nhưng được triển khai độc lập. -
Trong
store = Store.new(global(:redis_client))
,global
phương thức được sử dụng để truy xuất bất kỳ đối tượng nào được lưu trữ trong trạng thái chia sẻ toàn cục. Như nó được sử dụng ở trên, chúng tôi truy xuất ứng dụng khách Redis từ nhóm Kết nối được xác định trong thiết lập chung trongstartup
của chúng tôi khối. -
Response
vàModels::OtpResponse
xử lý tuần tự hóa phản hồi vớiactive_model_serializers
để đưa ra các phản hồi JSON được định dạng đúng.
Kiểm tra chức năng của chúng tôi tại địa phương
Functions Framework
thư viện cho phép chúng tôi dễ dàng kiểm tra các chức năng của mình cục bộ trước khi triển khai chúng lên đám mây. Để kiểm tra cục bộ, chúng tôi chạy
bundle exec functions-framework-ruby --target=otp --port=3000
--target
được sử dụng để chọn chức năng để triển khai.
Trong khi kiểm tra thủ công là tuyệt vời, kiểm tra tự động và phần mềm tự kiểm tra là chén thánh trong kiểm thử. Functions Framework
cung cấp các phương thức trợ giúp cho cả Minitest
và RSpec
để giúp kiểm tra các chức năng của chúng tôi cho cả http
và cloudevents
người xử lý. Đây là một ví dụ về bài kiểm tra:
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
Triển khai chức năng của chúng tôi
Trước tiên, chúng tôi cần triển khai máy chủ Redis bằng Google Cloud Memorystore
, mà chức năng của chúng ta phụ thuộc vào. Tôi sẽ không đi sâu vào chi tiết ở đây về cách triển khai máy chủ Redis cho GCP, vì nó nằm ngoài phạm vi của bài viết này.
Có nhiều cách để triển khai chức năng của chúng tôi vào môi trường của Chức năng đám mây của Google:triển khai từ máy của bạn, triển khai từ bảng điều khiển GCP và triển khai từ kho mã của chúng tôi. Kỹ thuật phần mềm hiện đại khuyến khích các quy trình CI / CD cho hầu hết quá trình phát triển của chúng tôi và với mục đích của bài viết này, tôi sẽ tập trung vào việc triển khai Chức năng đám mây của chúng tôi từ Github với Hành động Github bằng cách sử dụng các chức năng đám mây.
Hãy thiết lập tệp triển khai của chúng tôi (.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 }}"
Biến môi trường
Trong đoạn mã trên, dòng cuối cùng cho phép chúng tôi chỉ định các biến môi trường sẽ có sẵn cho chức năng của chúng tôi trong môi trường Google Cloud. Lưu ý rằng vì lý do bảo mật, chúng tôi không để lộ các biến này trong cơ sở mã của chúng tôi; thay vào đó, chúng tôi đang sử dụng bí mật hành động trên Github để giữ thông tin này ở chế độ riêng tư. Để kiểm tra xem các mã thông báo của chúng tôi đã được triển khai đúng cách hay chưa, hãy kiểm tra chức năng đám mây của bạn trong Google Console, như được hiển thị bên dưới:
Xác thực
Tạo Service Account
với Cloud Functions Admin
và Service Account User
vai trò.
Tài khoản dịch vụ được sử dụng cho IAM giữa máy và máy. Do đó, khi một hệ thống, bất kể nó đang chạy trên Google Cloud, nói chuyện với một hệ thống khác trên Google Cloud, thì cần có tài khoản dịch vụ để giúp xác định ai đang yêu cầu quyền truy cập vào tài nguyên Google của chúng tôi. Các vai trò Cloud Functions Admin
và Service Account User
cho phép chúng tôi xác định xem người dùng có được phép truy cập tài nguyên hay không. Trong trường hợp này, người chạy Github Action giao tiếp với Google Cloud xác thực dưới dạng tài khoản dịch vụ với các quyền cần thiết để triển khai chức năng của chúng tôi.
Tạo Khóa tài khoản dịch vụ, tải xuống JSON và thêm nó vào GitHub Secrets.
Thì đấy! 🎉 Chức năng đám mây của chúng tôi đã được triển khai thành công.
Giới hạn chức năng đám mây so với Giới hạn AWS
Dưới đây là so sánh chi tiết về hai trong số các nhà cung cấp chức năng Serverless lớn nhất:
Hợp đồng Khung chức năng so với Khung không máy chủ
Trong bài viết này, chúng tôi đã tập trung vào việc xây dựng các chức năng đám mây cho Google Cloud Functions. Trong phân đoạn này, tôi muốn so sánh việc xây dựng với Khung chức năng với Khung không máy chủ.
-
Serverless Framework
dựa trênserverless.yml
, trong khi Khung chức năng dựa trênFunctions Framework Contract
, được sử dụng để triển khai các chức năng không máy chủ trên Cơ sở hạ tầng đám mây của Google. - Với
Serverless Framework
, chỉ có một số ví dụ và không hoàn toàn rõ ràng về cách xây dựng và triển khai các chức năng không máy chủ với Ruby cho các môi trường không máy chủ khác nhau của Google (Các chức năng đám mây, các môi trường Cloud Run và Knative). VớiFunctions Framework Contract
, việc xây dựng với Ruby trên các sản phẩm khác nhau này của Google rất đơn giản.- Tiếp theo từ điểm trước,
Functions Framework Contract
giúp bạn dễ dàng chuyển đổi ngôn ngữ hỗ trợ đằng sau chức năng của mình mà không nhất thiết phải thay đổi nhiều trong quá trình triển khai.
- Tiếp theo từ điểm trước,
- Theo văn bản này,
Functions Framework
chỉ hỗ trợ khả năng tương tác trên các môi trường Google Cloud Serverless và môi trường Knative.Serverless Framework
tuy nhiên, hỗ trợ nhiều nền tảng trên nhiều nhà cung cấp.
Để tham khảo, mã đầy đủ có sẵn tại đây.