সার্ভারলেস ফাংশন ক্লাউড সার্ভিস ডেভেলপ এবং ডেপ্লয় করার একটি নতুন প্রোগ্রামিং প্যারাডাইম। একটি সার্ভারলেস বিশ্বে, আমরা আমাদের ব্যাকএন্ড সার্ভিসের প্রোভিশনিং, মেইনটেনেন্স, এবং স্কেলিং ক্লাউড প্রোভাইডারের ওপর অ্যাবস্ট্রাক্ট করি। এটি ডেভেলপার প্রোডাক্টিভিটি önemliভাবে উন্নত করে ডেভেলপারদের একটি নির্দিষ্ট সমস্যা সমাধান করতে ফোকাস করতে দিয়ে। সার্ভারলেস ফাংশন বিল্ড করার অনেক সুবিধা এবং অসুবিধা থাকলেও, একটি বিষয় বিবেচনা করতে হবে সেটি হলো ল্যাঙ্গুয়েজ সাপোর্ট। gầnে, Google Cloud Functions-এ Ruby 2.7 সাপোর্ট ঘোষণা করেছে Google, এবং এই আর্টিকেলে আমি ফোকাস করব Google Cloud Functions-এ Ruby-তে সার্ভারলেস ফাংশন বিল্ড, টেস্ট, এবং ডেপ্লয় করতে এবং সার্ভারলেস ফাংশনগুলোর সুবিধা-অসুবিধা।
একটি সার্ভারলেস OTP সিস্টেম বিল্ড করা
ওয়ান-টাইম পাসওয়ার্ড (OTP) হলো ছোট Нуমেরিক কোড যা অথেনটিকেশন উদ্দেশ্যে ব্যবহার হয়, যেমন আপনার ব্যাংক আপনার পরিচয় ভেরিফাই করতে OTP ভিয়া টেক্সট পাঠায়।
এই আর্টিকেলে, আমরা একটি OTP ফাংশন বিল্ড করব যা তিনটি কোর রেসপন্সিবিলিটি হ্যান্ডেল করে:
POST /otp: প্রদত্ত phone_number-এ OTP মেসেজ জেনারেট করে এবং পাঠায়।
# 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: প্রদত্ত phone_number-এ OTP জেনারেট করে এবং পুনরায় পাঠাতে চেষ্টা করে।
# 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 (GCP-এ Redis বা Memcache) দিয়ে ব্যাকড থাকবে একটি ফুল SQL বা NoSQL ডেটাবেসের বদলে। এটি আমাদেরকে স্টেটলেস এনভায়রনমেন্টে শেয়ার্ড স্টেট সম্পর্কে শিখতে সক্ষম করব।
Ruby-তে Google Cloud Function লিখুন
GCF-এ ফাংশন লিখতে, আমরা Google Cloud টিম দ্বারা প্রদত্ত Functions Framework-এর উপর নির্ভর করব GCF ফাংশন বিল্ড করার জন্য (এর সম্পর্কে পরে আরো)।
প্রথম, অ্যাপ ডিরেক্টরি তৈরি করুন এবং ডিরেক্টরিতে প্রবেশ করুন।
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 সার্ভারের সাথে একটি কানেকশন পুল তৈরি এবং শেয়ার করতে:
এটি আমাদের একাধিক কনকারেন্ট ফাংশন ইনভোকেশন জুড়ে Redis কানেকশন অবজেক্টের একটি পুল শেয়ার করতে সক্ষম করে কোনো ভয় ছাড়া। একাধিক স্টার্টআপ ডিফাইন করা যাবে। সেগুলো ডিফাইন হওয়ার অর্ডারে রান করবে। এটি নোট করাও গুরুত্বপূর্ণ যেset_global :redis_client, ConnectionPool.new(size: 5, timeout: 5) { Redis.new }Functions Frameworkফাংশন কমপ্লিশনের পর রান করার জন্য কোনো স্পেশাল হুক প্রদান করে না।Functions_Framework.http 'otp' do |request|আমাদের ফাংশনগুলোর রিকোয়েস্ট এবং রেসপন্স প্রসেসিং হ্যান্ডেল করে। এই ফাংশনটি তিনটি ভিন্ন রাউট প্যাটার্ন সাপোর্ট করে। অন্যান্য টাইপের ফাংশন ডিফাইন করা সম্ভব (যেমন,Functions_Framework.cloud_event 'otp' do |event|) যা অন্যান্য Google সার্ভিস থেকে ইভেন্ট হ্যান্ডেল করে। একই ফাইলে একাধিক ফাংশন ডিফাইন করা সম্ভব কিন্তু স্বাধীনভাবে ডেপ্লয় করা।store = Store.new(global(:redis_client))-এ,globalমেথডটি গ্লোবাল শেয়ার্ড স্টেটে স্টোর করা যেকোনো অবজেক্ট রিট্রিভ করতে ব্যবহৃত হয়। উপরে যেমন ব্যবহার করা হয়েছে, আমরা আমাদেরstartupব্লকে গ্লোবাল সেটআপে ডিফাইন করা কানেকশন পুল থেকে Redis ক্লায়েন্ট রিট্রিভ করছি।ResponseএবংModels::OtpResponseactive_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
আমাদের ফাংশন ডেপ্লয় করা
প্রথমে, আমাদের Google Cloud Memorystore ব্যবহার করে একটি Redis সার্ভার ডেপ্লয় করতে হবে, যাতে আমাদের ফাংশনটি ডিপেন্ডেন্ট। আমি এখানে আরও ডিটেইলে যাবো না GCP-এ Redis সার্ভার ডেপ্লয় করার ऊपर, কারণ এটি এই আর্টিকেলের স্কোপের বাইরে।
আমাদের ফাংশনটি Google Cloud Function-এর এনভায়রনমেন্টে ডেপ্লয় করার একাধিক উপায় আছে: আপনার মেশিন থেকে ডেপ্লয়, GCP কনসোল থেকে ডেপ্লয়, এবং আমাদের কোড রিপোজিটরি থেকে ডেপ্লয়। মডার্ন সফ্টওয়্যার ইঞ্জিনিয়ারিং আমাদের অধিকাংশ ডেভেলপমেন্টের জন্য CI/CD প্রসেস এনকourage করে, এবং এই আর্টিকেলের উদ্দেশ্যে, আমি ফোকাস করব GitHub থেকে GitHub Actions ব্যবহার করে deploy-cloud-functions দিয়ে আমাদের Cloud Function ডেপ্লয় করার ওপর।
আমাদের ডেপ্লয়মেন্ট ফাইল সেট আপ করি (.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 Actions secrets ব্যবহার করছি এই ইনফরমেশন প্রাইভেট রাখতে। আমাদের টোকেনগুলো প্রপারলি ডেপ্লয় হয়েছে কিনা চেক করতে, আপনার ক্লাউড ফাংশন Google Console-এ চেক করুন, নিচে দেখানো মত:

অথেনটিকেশন
Cloud Functions Admin এবং Service Account User রোলসহ একটি Service Account তৈরি করুন।
একটি সার্ভিস একাউন্ট মেশিন-টু-মেশিন IAM-এর জন্য ব্যবহৃত হয়। অতএব, যখন একটি সিস্টেম, যেহেতু এটি Google Cloud-এ রান হয় বা না হয়, Google Cloud-এ অন্য একটি সিস্টেমের সাথে কথা বলে, একটি সার্ভিস একাউন্ট দরকার আমাদের Google রিসোর্সে কে এক্সেস রিকোয়েস্ট করছে সেটি আইডেন্টিফাই করতে। Cloud Functions Admin এবং Service Account User রোলস আমাদের সক্ষম করে নির্ধারণ করতে যে ইউজারটি রিসোর্স এক্সেস করার জন্য অথোরাইজড কিনা। এই সিনেরিওতে, একটি GitHub Action রানার Google Cloud-এ কমিউনিকেট করছে একটি সার্ভিস একাউন্ট হিসেবে অথেনটিকেট হয়ে যেটির আপনার ফাংশন ডেপ্লয় করার জন্য आवশ্যক পারমিশন আছে।


একটি সার্ভিস একাউন্ট কী তৈরি করুন, JSON ডাউনলোড করুন, এবং GitHub Secrets-এ যোগ করুন।


Voila! 🎉 আমাদের ক্লাউড ফাংশন সফলভাবে ডেপ্লয় হয়েছে।
Cloud Functions লিমিটস vs AWS লিমিটস
নিচে দুটি বড় সার্ভারলেস ফাংশন প্রোভাইডারের বিস্তারিত তুলনা দেওয়া হলো:

Functions Framework Contract vs Serverless Framework
এই আর্টিকেলে, আমরা Google Cloud Functions-এ ক্লাউড ফাংশন বিল্ড করার ওপর ফোকাস করেছি। এই সেগমেন্টে, আমি Functions Framework vs Serverless Framework বিল্ড করার তুলনা করতে চাই।
Serverless Frameworkserverless.yml-এর ওপর বেসড, অন্যদিকে Functions FrameworkFunctions 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, যাইহোক, একাধিক প্রোভাইডার জুড়ে একাধিক প্ল্যাটফর্ম সাপোর্ট করে।
রেফারেন্সের জন্য, ফুল কোড এখানে উপলব্ধ।