Tổ chức mã nguồn AWS Serverless của bạn để tránh xung đột khi gộp

Làm thế nào để bạn ngăn ngừa các xung đột gộp phổ biến nhất khi nhóm của bạn đang làm việc trên một ứng dụng Serverless? Làm thế nào để bạn đảm bảo rằng nhóm của bạn duy trì hiệu suất và tránh các vấn đề gộp lớn trong khi cố gắng cập nhật cùng một tệp quan trọng cùng một lúc? – Câu trả lời cho cả hai câu hỏi là tổ chức mã nguồn! Bạn có thể sử dụng cfn-include và swagger-cli để tổ chức, cộng tác và duy trì một ứng dụng serverless lớn cũng như hỗ trợ một nhóm phát triển lớn hoặc phân tán.

Sự truyền cảm hứng từ cuộc sống thực tế

WRAP Technologies Inc. (WRAP) tạo ra các công nghệ tiên tiến cho việc bảo vệ và đảm bảo an ninh công cộng. Sản phẩm WRAP Reality của họ cho phép các cơ quan thực thi pháp luật đào tạo các viên cảnh sát của họ bằng các tình huống dựa trên thực tế ảo.

Quá nhiều người làm việc trên một dự án

Khi nhiều nhà phát triển cộng tác trên một kiến trúc serverless được xây dựng bằng AWS CloudFormation và các phần mở rộng của nó như AWS Serverless Application Model (SAM), tính chất của việc chỉ định tài nguyên trong cả tệp template.yaml và tài liệu OpenAPI.yaml tùy chọn cho Amazon API Gateway dẫn đến xung đột gộp, như minh họa trong hình sau, khi hai nhà phát triển đang thêm các điểm cuối API khác nhau cùng một lúc. Những xung đột này làm mất thời gian và tính linh hoạt của nhà phát triển. Hơn nữa, điều hướng và duy trì các tệp template dài được yêu cầu cho một kiến trúc serverless lớn làm chậm quá trình phát triển, khi nhà phát triển quét các tệp lớn để tìm định nghĩa tài nguyên cụ thể.

Hình 1. Những xung đột gộp gây thất vọng.

Bằng cách tái cấu trúc và tổ chức các tệp CloudFormation và OpenAPI của bạn, nhóm phát triển của bạn có thể thấy được một số lợi ích:

  • Nâng cao hiệu suất phát triển bằng cách phân rã các tệp lớn, khó quản lý thành một loạt các tệp được tổ chức cẩn thận và đơn mục đích.
  • Tăng năng suất của nhà phát triển bằng cách cho phép mỗi nhà phát triển sở hữu mã của họ, giảm sự cần thiết phối hợp gộp với đồng đội.
  • Loại bỏ các vấn đề xung đột tiềm năng cho các tệp tạo ra nhiều xung đột nhất trong quá trình phát triển của ứng dụng API Serverless điển hình.

Phát triển nhanh chóng

WRAP đã hợp tác với AWS để phát triển và lưu trữ phần backend cho nền tảng quản lý đào tạo cảnh sát mới của họ. Nền tảng hoàn toàn mới này đã được phát triển, hoàn thành và sẵn sàng sử dụng chỉ trong vài tháng. Hơn nữa, đây là một sự hợp tác của các nhà phát triển đặt rải rác trên nhiều nhóm trên toàn cầu, tất cả đóng góp vào cùng một mã nguồn. Bằng cách thiết lập các quy tắc và kỹ thuật của bài viết này, WRAP đã tạo ra một ứng dụng serverless lớn và có thể duy trì với sự xung đột mã nguồn phát triển tối thiểu.

Việc phát triển hệ thống quản lý đào tạo WRAP Reality đã được thực hiện bằng cách sử dụng CloudFormation để xác định Cơ sở Hạ tầng như Mã (IaC), và một đặc tả Amazon API Gateway OpenAPI để xác định các hợp đồng API. Nhóm phát triển dự án cho dịch vụ quản lý đào tạo WRAP Reality đã tận dụng phát triển linh hoạt để thực hiện nhanh chóng, bao gồm chiến lược nhánh GitHub Flow. Tuy nhiên, vì các thành viên đóng góp vào dự án không cùng một địa điểm, nhiều xem xét đã được đưa ra để đảm bảo tính nhất quán và tốc độ phát triển mã nguồn:

  • Các đặc tả và hợp đồng API đã được xác định trong đặc tả OpenAPI (Swagger) sớm trong quá trình phát triển, xác định rõ cấu trúc dự án từ đầu, và cho phép các nhà phát triển xây dựng các thành phần cơ sở hạ tầng một cách độc lập.
  • Hai tài sản mã nguồn quan trọng cho toàn bộ dự án – template CloudFormation và Đặc tả OpenAPI – đã được phân rã thành các thành phần nhỏ, dễ quản lý. Điều này cho phép các thành phần được tổ chức một cách tăng cường hiệu suất phát triển và gần như loại bỏ hoàn toàn các xung đột gộp không thể tránh khỏi trong các tệp mã nguồn lớn đang được sửa đổi hàng ngày.

Quá trình phát triển đã được tăng tốc bằng cách sử dụng tích hợp OpenAPI với Dịch vụ AWS, cũng như các kỹ thuật quản lý Đặc tả OpenAPI và các tệp template Cloudformation.

Dự án template

Để minh họa những kỹ thuật này, chúng ta sẽ khám phá dự án template sau bao gồm các điểm cuối API để quản lý “widget”, có sẵn trên GitHub. Dự án này cung cấp các điểm cuối sau:

  • /widget PUT: Tạo một widget mới
  • /widget GET: Truy xuất một widget mới
  • /reports/color GET: Truy xuất một tập hợp các widget dựa trên template của widget
  • /reports/filterpage GET: Truy xuất các widget dựa trên các bộ lọc cụ thể

Kiến trúc tổng quan của ứng dụng được hiển thị trong biểu đồ sau:

Hình 2. Biểu đồ Kiến trúc

Ứng dụng bao gồm:

  • Amazon API Gateway là dịch vụ được quản lý đầy đủ giúp các nhà phát triển dễ dàng tạo, xuất bản, duy trì, theo dõi và bảo mật các API ở bất kỳ quy mô nào. Trong ví dụ này, API Gateway phục vụ như dịch vụ web cho các điểm cuối API. Việc ánh xạ dữ liệu vào và ra khỏi các điểm cuối API đến các hàm Lambda được định nghĩa một cách chính thức bằng một tệp đặc tả OpenAPI.
  • AWS Lambda là dịch vụ tính toán không máy chủ cho phép bạn chạy mã mà không cần dự trữ hoặc quản lý máy chủ, tạo lý thuyết về tỷ lệ cụ thể cho các cụm làm việc, duy trì tích hợp sự kiện hoặc quản lý thời gian chạy. Trong ví dụ này, có bốn hàm Lambda được sử dụng để phục vụ mỗi cuộc gọi API trong bốn cuộc gọi API.
  • Amazon DynamoDB là một cơ sở dữ liệu key-value và tài liệu cung cấp hiệu suất trong vòng vài mili giây đơn chữ số ở bất kỳ quy mô nào. DynamoDB được sử dụng như một cửa hàng dữ liệu cố định cho các widget và các thuộc tính liên quan.

Tích hợp OpenAPI và dịch vụ AWS

Khi sử dụng API Gateway, các nhà phát triển có tùy chọn sử dụng tích hợp Lambda proxy hoặc định rõ giao diện API trong tệp yaml OpenAPI. Đặc tả OpenAPI có thể được tận dụng để tài liệu hóa API trước khi phát triển, và tính năng ví dụ/ảo của đặc tả OpenAPI giúp phát triển đồng thời bằng cách nhanh chóng xây dựng cơ sở hạ tầng hoạt động để phát triển. Hơn nữa, tài liệu API có thể được tạo tự động từ đặc tả OpenAPI.

Khi số lượng điểm cuối tăng lên, tệp đặc tả OpenAPI có thể tăng kích thước, đạt hàng nghìn dòng mã cần phải được cập nhật và bảo trì thường xuyên bởi nhiều nhà phát triển. Để hỗ trợ quản lý và sử dụng, tệp OpenAPI có thể được chia thành các tệp riêng biệt cho các điểm cuối, phản hồi, trường và lược đồ.

Bắt đầu với một tệp “skeleton” như điểm vào cho định nghĩa OpenAPI, sau đó thêm một tệp riêng biệt cho định nghĩa của mỗi điểm cuối hoặc cấu trúc. Ví dụ, điểm vào dự án template là api/apiSkeleton.yaml, chứa các định nghĩa toàn cầu và hiệu quả định nghĩa một danh sách đơn giản các điểm cuối và đường dẫn đến định nghĩa của mỗi điểm cuối.

Ứng dụng bao gồm:

/reports/color:

    $ref: ‘./paths/reports/reportsColor.yaml’

  /reports/filterpage:

    $ref: ‘./paths/reports/reportsFilterPage.yaml’

Mở rộng vào tệp được tham chiếu bởi một điểm cuối, chúng ta thấy rằng nó chứa tất cả các chi tiết đặc tả cho điểm cuối đó. Nhìn vào tệp reportsColor.yaml, bạn có thể thấy đặc tả điểm cuối đầy đủ cho /reports/color:

get:

  description: Get widgets by color

  parameters:

    – in: path

      $ref: ‘../../requestParameters/color.yaml’

  responses:

    200:

      description: Get All the Widgets of a color

      content:

        application/json:

          schema:

            $ref: ‘../../schemas/widgetList.yaml’

    . . .

Lượt lại, đặc tả điểm cuối này có thể bao gồm thêm tham chiếu đến các tệp yaml định nghĩa tham số, lược đồ và thậm chí các phản hồi cổng thông tin chung. Ví dụ, tệp color.yaml xác định biến đường template:

 type: string

    description: “The widget’s color”

    example: “Red”

Nói một cách ngắn gọn, “Với một số lượng lớn tệp, đến trách nhiệm lớn về tổ chức.” Để làm được điều này, chúng tôi đề xuất cấu trúc tổ chức sau đây như một khởi đầu. Đặt tất cả các đặc tả API liên quan vào một thư mục con “api” của dự án của bạn. Tạo các thư mục con con cho các tệp định nghĩa trường, siêu dữ liệu và phản hồi cổng thông tin. Sau đó, tạo cấu trúc cây thư mục con cho mỗi nhánh của các điểm cuối của bạn sao cho tương ứng với các đường dẫn điểm cuối. Điều này sẽ dẫn đến một cấu trúc thư mục có tổ chức tốt, như đã thấy trong dự án template:

├── api

│   ├── apiSkeleton.yaml

│   ├── fields

│   │   ├── color.yaml

│   │   ├── metadata

│   │   │   ├── count.yaml

│   │   │   ├── message.yaml

│   │   └── widgetname.yaml

│   ├── gatewayResponses

│   │   ├── error.yaml

│   │   └── notFound.yaml

│   ├── paths

│   │   ├── reports

│   │   │   ├── reportsColor.yaml

│   │   │   └── reportsFilterPage.yaml

│   │   └── widget

│   │       ├── widgetPut.yaml

│   │       └── widgetWidgetnameGet.yaml

Chúng ta vẫn cần một tệp OpenAPI đơn giản được tổng hợp để cung cấp cho CloudFormation trong quá trình triển khai trên AWS. Do đó, các tệp đa dạng được kết hợp và kiểm tra bằng lệnh tổng hợp swagger-cli, tạo ra một tệp duy nhất để triển khai. Lệnh tổng hợp phải được thực hiện trước khi xây dựng CloudFormation. Lệnh này cũng có thể được bao gồm như một lối tắt trong Makefile dưới dạng lệnh “buildOpenApi”:

swagger-cli bundle -o api/api.yaml –dereference –t yaml  api/apiSkeleton.yaml

hoặc

make buildOpenApi

Sau khi được biên soạn, api/api.yaml sau đó được sử dụng bình thường cho tích hợp API Gateway và như một bộ sưu tập API Postman để nhập. Vì api/api.yaml được biên soạn một cách động, nó đã được bao gồm trong .gitignore và không được kiểm tra vào AWS CodeCommit.

cfn-include và các Nested Stacks của CloudFormation

template CloudFormation xác định cơ sở hạ tầng cho một dịch vụ đơn giản có thể phát triển đến độ dài đáng kể, có thể là hàng ngàn dòng mã. Điều này đặt ra những thách thức từ quan điểm hỗ trợ và phát triển liên tục, vì vị trí mã cụ thể trở nên khó tìm kiếm và xung đột hợp nhất trở nên thường xuyên.

Các Nested Stacks của CloudFormation là một phương pháp để chia template CloudFormation lớn thành các template riêng lẻ. Khi có sự phân chia rõ ràng giữa các nhóm tài nguyên trong một template, việc chia thành các Nested Stacks riêng biệt là có lý. Ngoài ra, có giới hạn 500 tài nguyên trong một template CloudFormation duy nhất và để vượt qua giới hạn đó cần có các Nested Stacks hoặc riêng biệt. Tuy nhiên, tùy thuộc vào độ phức tạp của kiến trúc và tần suất cập nhật, các Nested Stacks có thể trở nên lớn hơn. Hơn nữa, trong kiến trúc không máy chủ, sự phân tách logic của các tầng kiến trúc thành các template riêng biệt có thể không trực tiếp, ví dụ khi một hàm Lambda được kích hoạt bởi một sự kiện được gửi đến một bus sự kiện EventBridge, sau đó hàm Lambda đó gửi một sự kiện khác trở lại cùng một bus sự kiện.

Trong những trường hợp như vậy, các template CloudFormation có thể được chia thành các phần nhỏ hơn để tận dụng thêm tính năng cfn-include. Với kỹ thuật này, template CloudFormation cấp độ cao nhất trở thành một tệp skeleton chứa các tham số của template, các đặc điểm toàn cầu, danh sách tên tài nguyên không có thuộc tính và các đầu ra. Các thuộc tính của từng tài nguyên được chứa trong các tệp riêng biệt, được tham chiếu bằng chỉ thị ‘include’.

Tổ chức template CloudFormation

Để tổ chức template CloudFormation của bạn, hãy phân tách template thành một tệp cho mỗi tài nguyên, với một tệp “skeleton” chính là điểm vào chính. Tệp skeleton này chứa toàn bộ thông số, phần toàn cục, điều kiện và đặc tả đầu ra. Tài nguyên được xác định bằng tên tài nguyên trong tệp skeleton này, và sau đó, chỉ thị ‘include’ trỏ đến tệp chứa nội dung của khai báo tài nguyên. Dưới đây là ví dụ về tệp skeleton chính với hai tài nguyên:

AWSTemplateFormatVersion: ‘2010-09-09’

Transform: AWS::Serverless-2016-10-31

Description: >

  Widget API Service

Globals:

  Function:

    Handler: app.lambda_handler

    Runtime: python3.8

Resources:

    WidgetApi:

        !Include ./resources/apigw/widgetApiGW.yaml

    WidgetDdbTable:

        !Include ./resources/dynamodb/widgetDdbTable.yaml

Sau đó, các tệp tài nguyên chứa các thuộc tính của tài nguyên cụ thể đó. Ví dụ, tệp widgetApiGW.yaml xác định một Cổng thông tin API:

Type: AWS::Serverless::Api

    Properties:

      DefinitionBody:

        Fn::Transform:

          Name: AWS::Include

          Parameters:

            Location: api/api.yaml

      EndpointConfiguration:

        Type: REGIONAL

      StageName: prod

      TracingEnabled: true

Cách tiếp cận này có lợi điểm là chia template CloudFormation thành nhiều tệp nhỏ, trong khi vẫn duy trì một cái nhìn tổng quan cấp cao. Các định nghĩa tài nguyên, thường chiếm phần lớn nội dung và có thể gây xung đột hợp nhất, được di chuyển ra khỏi template chính.

Đối với tổ chức, bạn có thể tạo một thư mục trong dự án của bạn để chứa các tập lệnh CloudFormation. Thư mục này cũng chứa tệp skeleton điểm vào. Tạo các thư mục con khác cho các tài nguyên, sau đó tạo thêm các thư mục con theo loại tài nguyên và kiến trúc. Chúng tôi thấy rằng đặt định nghĩa tài nguyên tương ứng với vai trò AWS Identity and Access Management (IAM) có thể áp dụng trong cùng một thư mục với tài nguyên đã được áp dụng giúp dễ dàng điều hướng hơn. Ví dụ:

├── cloudformation

│   ├── resources

│   │   ├── apigw

│   │   │   └── widgetApiGW.yaml

│   │   ├── dynamodb

│   │   │   └── widgetDdbTable.yaml

│   │   └── lambda

│   │       ├── layers

│   │       │   └── lambdaDDBEnv.yaml

│   │       ├── reports

│   │       │   ├── reportsColorLambda.yaml

│   │       │   └── reportsColorLambdaRole.yaml

│   │       └── widget

│   │           ├── widgetGetLambda.yaml

│   │           └── widgetGetLambdaRole.yaml

│   └── templateSkeleton.yaml

Các tệp phải được tạo lại thành một tệp template.yaml duy nhất để xây dựng và triển khai CloudFormation. Điều này được thực hiện bằng cách sử dụng lệnh cfn-include. Một lệnh thuận tiện có thể được bao gồm tùy chọn trong Makefile.

cfn-include –yaml  cloudFormation/templateSkeleton.yaml > template.yaml

hoặc

make buildTemplate

Khi đã biên soạn, tệp template.yaml cuối cùng sau đó được sử dụng bình thường cho tích hợp API Gateway và không được kiểm tra vào CodeCommit.

Kết luận

Bài viết này thể hiện các kỹ thuật được sử dụng bởi WRAP và AWS để phát triển và duy trì nhanh chóng các tệp quan trọng trong một kiến trúc Serverless. Các kỹ thuật được thảo luận trong bài viết này đã giúp đội ngũ WRAP và AWS làm được những điều sau đây:

  • Cải thiện hiệu suất phát triển bằng cách phân tách các tệp lớn, khó quản lý thành một loạt các tệp được tổ chức cẩn thận và có mục đích đơn.
  • Nâng cao năng suất của các nhà phát triển bằng cách cho phép mỗi nhà phát triển có quyền sở hữu một phần riêng của mã mà không cần phải phối hợp với đồng đội.
  • Loại bỏ các vấn đề xung đột tiềm năng trên các tệp thường tạo ra nhiều xung đột nhất trong quá trình phát triển của một ứng dụng API Serverless điển hình.

Việc áp dụng các kỹ thuật này đã là một trong những yếu tố quan trọng trong quá trình phát triển nhanh chóng của hệ thống đào tạo WRAP Reality.