Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

HTTP 서버를 Compute Module 함수로 연결

HTTP 서버를 SDK나 작업 polling adapter 없이 Compute Module에 연결할 수 있습니다. 이 기능은 Experimental이며 플랫폼 인스턴스에 따라 사용할 수 없고 개발 중에 동작이 바뀔 수 있습니다.

서버의 OpenAPI 명세를 이미지의 server.openapi label에 넣으면 Functions에서 엔드포인트를 함수로 가져옵니다.

OpenAPI 조건

  • 버전은 3.0.0 이상이어야 합니다.
  • server는 하나이며 URL 형식은 http://localhost:port입니다.
  • 각 operation에 operationId를 지정하며 그 값이 Foundry 함수 이름이 됩니다.
  • HTTP 메서드는 GET, PUT, POST와 DELETE만 사용합니다.
  • 모든 parameter에 schema를 지정하며 cookie 위치의 parameter는 지원하지 않습니다.
  • anyOf, oneOf, allOf 및 여러 타입을 가진 schema는 사용할 수 없습니다.
  • 각 endpoint에는 응답 코드를 하나만 선언하며 응답 Content-Type은 application/json입니다.

일반 OpenAPI가 허용하는 모든 조합을 이 연동에서 사용할 수 있는 것은 아니므로 위 조건을 만족하는 명세를 준비합니다.

Flask 도서 예제

다음 가상 앱은 메모리 안에 도서를 추가하고 ID로 조회합니다. title과 author는 문자열, published_year는 정수입니다. 선택 User-ID 헤더는 added_by 메타데이터이며 인증된 사용자 신원을 증명하지 않습니다.

src/app.py:

from flask import Flask, abort, jsonify, request
from werkzeug.exceptions import HTTPException
import uuid

app = Flask(__name__)
books = {}


@app.errorhandler(HTTPException)
def http_error(error):
    response = error.get_response()
    response.data = app.json.dumps({
        "error": error.name, "message": error.description
    })
    response.content_type = "application/json"
    return response


@app.post("/books")
def add_book():
    data = request.get_json()
    if not isinstance(data, dict):
        abort(400, description="The request body must be an object")
    if not isinstance(data.get("title"), str) or not isinstance(data.get("author"), str):
        abort(400, description="title and author must be strings")
    year = data.get("published_year")
    if not isinstance(year, int) or isinstance(year, bool):
        abort(400, description="published_year must be an integer")
    book_id = str(uuid.uuid4())
    book = {"title": data["title"], "author": data["author"], "published_year": year}
    user_id = request.headers.get("User-ID")
    if user_id is not None:
        book["added_by"] = user_id
    books[book_id] = book
    return jsonify(book_id), 200


@app.get("/books/<book_id>")
def get_book(book_id):
    if book_id not in books:
        abort(404, description="Book not found")
    return jsonify(books[book_id]), 200

저장 상태는 프로세스 메모리에만 있습니다. 이 예제를 한 개의 Gunicorn worker와 한 복제본으로 실행하며, 재시작하면 도서 목록이 사라집니다. 여러 worker나 복제본이 메모리를 공유하지 않습니다. 영구 업무 데이터에는 별도 데이터 저장소가 필요합니다.

잘못된 JSON이나 본문 유형은 400, 잘못된 Content-Type은 415, 존재하지 않는 도서는 404로 응답합니다. 지원하지 않는 HTTP 메서드는 허용 메서드를 담은 Allow 헤더와 함께 405를 반환합니다. 오류 처리기는 상태와 헤더를 유지하면서 본문을 JSON으로 바꿉니다. 함수의 정상 출력으로 사용할 응답은 200 한 종류입니다.

openapi.json

다음 명세를 프로젝트 루트의 openapi.json에 저장합니다. 각 endpoint의 정상 출력에 맞는 응답 하나를 선언합니다.

{
  "openapi": "3.0.0",
  "info": {
    "title": "In-memory book example",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "http://localhost:8000"
    }
  ],
  "paths": {
    "/books": {
      "post": {
        "operationId": "addBook",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "author": {
                    "type": "string"
                  },
                  "published_year": {
                    "type": "integer"
                  }
                },
                "required": [
                  "title",
                  "author",
                  "published_year"
                ]
              }
            }
          }
        },
        "parameters": [
          {
            "name": "User-ID",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The new book identifier",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/books/{book_id}": {
      "get": {
        "operationId": "getBook",
        "parameters": [
          {
            "name": "book_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The stored book",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "title": {
                      "type": "string"
                    },
                    "author": {
                      "type": "string"
                    },
                    "published_year": {
                      "type": "integer"
                    },
                    "added_by": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "title",
                    "author",
                    "published_year"
                  ]
                }
              }
            }
          }
        }
      }
    }
  }
}

이미지 label과 시작 명령

Dockerfile은 OpenAPI JSON을 build argument로 받아 image label에 기록합니다. 앱은 Flask 개발 서버 대신 Gunicorn으로 실행합니다.

FROM python:3.12
WORKDIR /app
RUN pip install --no-cache-dir flask gunicorn
COPY src/app.py ./app.py
ARG SERVER_OPENAPI
LABEL server.openapi="$SERVER_OPENAPI"
EXPOSE 8000
USER 5000
CMD ["gunicorn", "--workers", "1", "--bind", "0.0.0.0:8000", "app:app"]
SERVER_OPENAPI="$(cat openapi.json)" && \
docker buildx build --platform=linux/amd64 --load \
  --build-arg "SERVER_OPENAPI=$SERVER_OPENAPI" \
  -t book-module:0.0.1 .

빌드한 이미지를 Artifacts 저장소에 게시하고 Compute Module의 컨테이너로 선택한 뒤 구성을 저장합니다.

함수 가져오기

Functions 탭에서 Detect from OpenAPI specification을 선택합니다.

검색된 addBook 및 getBook을 가져오고 사용한 OpenAPI 명세를 확인할 수 있습니다.

가져온 함수의 정의를 이미지의 OpenAPI 명세와 별도로 수동 변경하지 않습니다. 서버와 명세를 함께 변경하고 새 이미지에 맞는 함수 정의를 사용합니다.