Go 기반 ChatGPT 서버리스 REST API를 작업한 후기를 공유합니다.
ChatGPT 는 OpenAI에서 만든 AI입니다. 홈페이지에서 로그인을 한 후 다음과 같이 REST API를 만들 때 사용할 Secret Key
를 먼저 발급합니다.
처음에 발급하게 되면, 무료로 18$만큼의 쿼타가 주어집니다. API를 사용할 때 마다 주어진 무료 쿼타는 소진되며, 그 뒤에는 결제를 해야 이용할 수 있습니다.
우리의 목표는 무료 쿼타가 모두 소진되기 전에 API를 만드는 것입니다.
이번 작업에서 AWS의 모든 리소스는 Terraform으로 작업했습니다. 앞으로 리소스를 사용하게 될 때는 Terraform을 사용하는 것을 추천합니다.
작업하려는 API와 관련된 모든 리소스는 AWS기반입니다. 리소스를 생성하기 위해서 AWS 계정의 Access Key와 Secret Key가 필요합니다.
먼저 Golang기반의 textcompletion API를 사용하는 서버를 작업했습니다. 다음과 같이 prompt
쿼리 파람을 받아서 그것을 OpenAI의 Text completion API에 전달해서 응답을 받아오도록 했습니다.
func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
prompt := req.QueryStringParameters["prompt"]
options := gogpt.CompletionRequest{
Model: gogpt.GPT3TextDavinci003,
MaxTokens: 50,
Temperature: 1,
Prompt: prompt,
}
resp, err := service.GptCompletionCaller(options)
if err != nil {
errorMessage := err.Error()
log.Infoln(errorMessage)
return events.APIGatewayProxyResponse{Body: errorMessage, StatusCode: 500}, nil
}
responseMessage := resp.Choices[0].Text
log.Infoln(responseMessage)
return events.APIGatewayProxyResponse{Body: responseMessage, StatusCode: 200}, nil
}
package service
import (
"context"
"os"
gogpt "github.com/sashabaranov/go-gpt3"
)
func GptCompletionCaller(req gogpt.CompletionRequest) (r gogpt.CompletionResponse, e error) {
token := os.Getenv("GPT_TOKEN")
c := gogpt.NewClient(token)
ctx := context.Background()
resp, err := c.CreateCompletion(ctx, req)
return resp, err
}
원하는 대로 go-gpt3 라이브러리의 옵션을 설정해줍니다. API와 관련된 설정 및 그것에 대한 설명에는 다음을 참고합니다. go-gpt3는 Go로 만든 OpenAI API wrapper이기 때문에 거의 모든 옵션이 동일하게 들어가 있습니다.
위 코드는 로컬에서 vscode debug mode
등을 사용해서 테스트 해줍니다. 테스트가 잘 되었다면, AWS lambda의 소스코드로 전달하기 위해서 먼저 빌드를 해줍니다. 빌드에 사용할 Makefile
을 다음과 같이 작성해줍니다.
build:
GOOS=linux GOARCH=amd64 /usr/local/go/bin/go build -v -ldflags '-d -s -w' -a -tags netgo -installsuffix netgo -o ./${PATH}/build/bin/app ./${PATH}
run: build
코드에서는 PATH
를 environment variable로 받아 그 위치에 빌드 파일을 남기도록 했습니다. 적절하게 작성한 파일의 위치를 PATH로 넘겨 빌드해줍니다.
make build PATH=post/textcompletion
저는 post라는 폴더 밑에 textcompletion이라는 Go 모듈을 만들었기 때문에 위와 같이 PATH를 주었습니다. 서버를 Go 언어로 작성하고 빌드까지 마쳤기 때문에 코드 준비는 모두 완료되었습니다.
다음으로는 Terraform을 이용해서 AWS Serverless 리소스를 만들어보겠습니다.
제가 가장 먼저 작업한 부분은 루트 폴더에서 사용하게 될 메인과 프로바이더 입니다. 그런 후 api-gateway
라는 모듈을 따로 만들어 main에서 참조할 수 있도록 해줬습니다.
module "api-gateway" {
source = "./api-gateway"
providers = {
aws = aws
}
service_name = local.service_name
aws_common_tags = local.aws_common_tags
chat_secret = var.chat_secret
}
또한 모듈에서 사용할 variables
도 같이 넘겨주었습니다.
프로바이더에서는 사용할 프로바이더와 함께 버전을 명시해 주었습니다.
terraform {
required_version = "~> 1.1.2"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 3.15"
}
}
}
provider "aws" {
region = "ap-northeast-2"
profile = "dev"
}
AWS Lambda에서 사용할 IAM/Role
을 작성했습니다. 그리고 CloudWatch
에 로깅할 수 있도록 관련 로깅권한을 부여해주었습니다.
resource "aws_iam_role" "lambda_role" {
name = "${var.service_name}_lambda_role"
assume_role_policy = data.aws_iam_policy_document.lambda_assume_role_policy.json
tags = var.aws_common_tags
}
data "aws_iam_policy_document" "lambda_assume_role_policy" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}
resource "aws_iam_policy" "lambda_logging" {
name = "${var.service_name}_lambda_logging"
path = "/"
description = "IAM policy for logging from a lambda"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*",
"Effect": "Allow"
}
]
}
EOF
tags = var.aws_common_tags
}
resource "aws_iam_role_policy_attachment" "lambda_logs" {
role = aws_iam_role.lambda_role.name
policy_arn = aws_iam_policy.lambda_logging.arn
}
Go lambda소스에서 사용할 ChatGPT 시크릿키를 AWS의 시크릿 매니저를 통해서 전달해줄 수 있도록 하기 위해 먼저, AWS secretsmanager를 작성햇습니다. chat_secret
변수는 [main.tf](http://main.tf)
에서 따로 전달해주고 있지 않기 때문에 추후에 테라폼 적용 시 콘솔에서 직접 시크릿키를 입력해주어야 합니다.
resource "aws_secretsmanager_secret" "chatgpt" {
name = "chatgpt"
}
resource "aws_secretsmanager_secret_version" "chat_secret" {
secret_id = aws_secretsmanager_secret.chatgpt.id
secret_string = jsonencode(var.chat_secret)
}
빌드해서 만들어놓은 소스코드를 이용해, 람다 함수를 만드는 코드를 작성했습니다. 추후에 Gateway
에서 invoke할 수 있도록 권한도 설정하고, 빌드한 파일의 위치를 입력하여 람다코드를 만들 수 있도록 해주었습니다. 또한 GPT_TOKEN
이라는 환경변수를 이전에 만든 시크릿매니저 리소스를 활용해 전달합니다.
data "archive_file" "textcompletion" {
type = "zip"
source_file = "${path.module}/../../go/post/textcompletion/build/bin/app"
output_path = "${path.module}/../../go/post/textcompletion/build/bin/textcompletion.zip"
}
resource "aws_lambda_permission" "apigw_lambda" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.textcompletion.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*"
depends_on = [
aws_lambda_function.textcompletion
]
}
resource "aws_lambda_function" "textcompletion" {
function_name = "textcompletion"
filename = data.archive_file.textcompletion.output_path
source_code_hash = filebase64sha256(data.archive_file.textcompletion.output_path)
handler = "app"
runtime = "go1.x"
timeout = 10
role = aws_iam_role.lambda_role.arn
tags = var.aws_common_tags
environment {
variables = {
GPT_TOKEN = jsondecode(aws_secretsmanager_secret_version.chat_secret.secret_string)["GPT_TOKEN"]
}
}
}
resource "aws_cloudwatch_log_group" "textcompletion" {
name = "/aws/lambda/textcompletion"
retention_in_days = 7
tags = var.aws_common_tags
}
마지막으로 api-gateway
관련 리소스를 만들고 배포를 진행해 보겠습니다. 다음과 같이 루트 리소스와 메소드 뿐만 아니라 작성한 Go 소스가 사용될 리소스와 메소드를 정의합니다. 저는 textcompletion
라는 path로 리소스를 정의했고 메소드는 POST
를 사용할 수 있도록 했습니다.
resource "aws_api_gateway_rest_api" "api" {
name = "${var.service_name}_api"
description = "api gateway"
tags = var.aws_common_tags
}
# root methods
resource "aws_api_gateway_method" "proxy_root" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_rest_api.api.root_resource_id
http_method = "GET"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "lambda_root" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_method.proxy_root.resource_id
http_method = aws_api_gateway_method.proxy_root.http_method
connection_type = "INTERNET"
type = "MOCK"
}
# textcompletion resourcd and methods
resource "aws_api_gateway_resource" "textcompletion" {
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_rest_api.api.root_resource_id
path_part = "textcompletion"
}
resource "aws_api_gateway_method" "textcompletion" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.textcompletion.id
http_method = "POST"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "textcompletion" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_method.textcompletion.resource_id
http_method = aws_api_gateway_method.textcompletion.http_method
connection_type = "INTERNET"
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.textcompletion.invoke_arn
depends_on = [
aws_lambda_function.textcompletion
]
}
resource "aws_api_gateway_method_response" "response_200" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.textcompletion.id
http_method = aws_api_gateway_method.textcompletion.http_method
status_code = "200"
}
resource "aws_api_gateway_deployment" "deployment" {
depends_on = [
aws_api_gateway_integration.textcompletion,
aws_api_gateway_integration.lambda_root,
]
rest_api_id = aws_api_gateway_rest_api.api.id
stage_name = "dev1"
}
작업이 모두 완료되었다면 Terraform apply
를 통해서 배포해줍니다. api-gateway
라는 모듈을 만들어주었기 때문에, 처음에는 Terraform init
을 활용해 모듈을 초기화해주는 과정이 필요합니다.
배포가 완료되었다면 API가 잘 배포되었는지 확인하고 API를 직접 사용해봅니다.
AWS 콘솔에서 api gateway
서비스로 이동하여, 생성된 리소스로 접근해 다음과 같이 요청을 테스트 해봅니다. 저의 경우에는 who are you?
라는 prompt에 I am a human being
이라는 답변을 전달받았습니다.
이번에는 Postman
으로 테스트 해봅니다. 테스트를 위해서 관련 endpoint를 API Gateway → Stages 에서 생성한 디플로이먼트의 Invoke URL을 사용합니다.
똑같이 who are you?
라는 prompt를 사용했을 때 I'm a person who is trying to determine the answer to your question.
라는 다른 답변을 전달받았습니다.
이상으로 Go 언어로 AWS 서버리스 ChatGPT API를 만들어 보았습니다. 프론트엔드에서는 ChatGPT API를 직접 호출할 수 없기 때문에, 만약에 서비스에서 사용하려면 서버는 필수불가결 합니다. 하지만 클라우드 프로바이더를 사용하게 되면, 서버에 들어가는 비용이 만만치 않기 때문에 API 호출을 할 때마다 비용을 산정해주는 서버리스가 좋은 솔루션이 될 수 있습니다.