Computer >> Máy Tính >  >> Lập trình >> Ruby

Xây dựng, thử nghiệm và triển khai các chức năng của Google Cloud với Ruby

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 trong startup của chúng tôi khối.

  • ResponseModels::OtpResponse xử lý tuần tự hóa phản hồi với active_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ả MinitestRSpec để giúp kiểm tra các chức năng của chúng tôi cho cả httpcloudevents 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ây dựng, thử nghiệm và triển khai các chức năng của Google Cloud với Ruby

Xác thực

Tạo Service Account với Cloud Functions AdminService 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 AdminService 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.

Xây dựng, thử nghiệm và triển khai các chức năng của Google Cloud với Ruby

Xây dựng, thử nghiệm và triển khai các chức năng của Google Cloud với Ruby

Tạo Khóa tài khoản dịch vụ, tải xuống JSON và thêm nó vào GitHub Secrets.

Xây dựng, thử nghiệm và triển khai các chức năng của Google Cloud với Ruby

Xây dựng, thử nghiệm và triển khai các chức năng của Google Cloud với Ruby

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:

Xây dựng, thử nghiệm và triển khai các chức năng của Google Cloud với Ruby

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ên serverless.yml , trong khi Khung chức năng dựa trên Functions 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ới Functions 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.
  • 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.