Serverless Functions는 클라우드 서비스를 개발하고 배포하는 새로운 프로그래밍 패러다임입니다. 서버리스 세계에서 우리는 클라우드 공급자에 대한 백엔드 서비스의 프로비저닝, 유지 관리 및 확장을 추상화합니다. 이는 개발자가 특정 문제 해결에 집중할 수 있도록 하여 개발자 생산성을 크게 향상시킵니다. 서버리스 기능을 구축하는 데에는 많은 장점과 단점이 있지만, 이를 구축할 때 고려해야 할 한 가지는 언어 지원입니다. 최근 Google은 Google Cloud Functions용 Ruby 2.7 지원을 발표했으며 이 기사에서는 Ruby on 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
:제공된 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"
}
}
단순화를 위해 클라우드 기능은 전체 SQL 또는 NoSQL 데이터베이스가 아닌 Cloud MemoryStore(GCP의 Redis 또는 Memcache)에 의해 지원됩니다. 이를 통해 상태 비저장 환경의 공유 상태에 대해서도 배울 수 있습니다.
Ruby로 Google Cloud 함수 작성
GCF에서 함수를 작성하기 위해 우리는 Functions Framework
에 의존할 것입니다. GCF 기능 구축을 위해 Google Cloud 팀에서 제공합니다(자세한 내용은 나중에 설명).
먼저 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
메서드는 전역 공유 상태에 저장된 개체를 검색하는 데 사용됩니다. 위에서 사용된 것처럼startup
의 전역 설정에 정의된 연결 풀에서 Redis 클라이언트를 검색합니다. 차단합니다. -
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
기능 배포
먼저 Google Cloud Memorystore
를 사용하여 Redis 서버를 배포해야 합니다. , 우리의 기능이 의존합니다. Redis 서버를 GCP에 배포하는 방법은 이 문서의 범위를 벗어나므로 여기에서 더 자세히 설명하지 않겠습니다.
Google Cloud Functions 환경에 함수를 배포하는 방법에는 머신에서 배포, GCP 콘솔에서 배포, 코드 저장소에서 배포 등 여러 가지가 있습니다. 최신 소프트웨어 엔지니어링은 대부분의 개발에 CI/CD 프로세스를 권장하며, 이 기사의 목적을 위해 deploy-cloud-functions를 사용하여 Github Actions와 함께 Github에서 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 }}"
환경 변수
위의 코드에서 마지막 줄을 사용하면 GCP 환경에서 함수에 사용할 수 있는 환경 변수를 지정할 수 있습니다. 보안상의 이유로 코드 기반에서 이러한 변수를 노출하지 않습니다. 대신 이 정보를 비공개로 유지하기 위해 Github 작업 비밀을 활용하고 있습니다. 토큰이 제대로 배포되었는지 확인하려면 아래와 같이 Google 콘솔에서 클라우드 기능을 확인하세요.
인증
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에 추가합니다.
짜잔! 🎉 Cloud Function이 성공적으로 배포되었습니다.
Cloud Functions 한도와 AWS 한도 비교
다음은 가장 큰 서버리스 기능 제공업체 두 곳을 자세히 비교한 것입니다.
함수 프레임워크 계약 대 서버리스 프레임워크
이 문서에서는 Google Cloud Functions용 클라우드 기능 구축에 중점을 두었습니다. 이 부분에서는 Functions Framework와 Serverless Framework로 빌드를 비교하려고 합니다.
Serverless Framework
serverless.yml
을 기반으로 합니다. , Functions Framework는Functions Framework Contract
를 기반으로 합니다. , Google Cloud Infrastructure 전반에 서버리스 기능을 배포하는 데 사용됩니다.Serverless Framework
사용 , 몇 가지 예만 있으며 Ruby를 사용하여 서버리스 기능을 빌드하고 다양한 Google 서버리스 환경(Cloud Functions, Cloud Run 및 Knative 환경)에 배포하는 방법이 명확하지 않습니다.Functions Framework Contract
사용 다양한 Google 제품에서 Ruby로 빌드하는 것은 간단합니다.- 이전 포인트에 이어
Functions Framework Contract
배포 프로세스를 크게 변경하지 않고도 기능 뒤의 지원 언어를 매우 쉽게 전환할 수 있습니다.
- 이전 포인트에 이어
- 이 글을 쓰는 시점에서
Functions Framework
Google Cloud Serverless 환경 및 Knative 환경 간의 상호 운용성만 지원합니다.Serverless Framework
그러나 여러 제공업체에서 여러 플랫폼을 지원합니다.
참고로 전체 코드는 여기에서 볼 수 있습니다.