Go ChatGPT AWS Serverless API

이재호·2023년 2월 28일
0
post-custom-banner

Go ChatGPT AWS Serverless API

Go 기반 ChatGPT 서버리스 REST API를 작업한 후기를 공유합니다.

ChatGPT

ChatGPT 는 OpenAI에서 만든 AI입니다. 홈페이지에서 로그인을 한 후 다음과 같이 REST API를 만들 때 사용할 Secret Key 를 먼저 발급합니다.

처음에 발급하게 되면, 무료로 18$만큼의 쿼타가 주어집니다. API를 사용할 때 마다 주어진 무료 쿼타는 소진되며, 그 뒤에는 결제를 해야 이용할 수 있습니다.

우리의 목표는 무료 쿼타가 모두 소진되기 전에 API를 만드는 것입니다.

Terraform

이번 작업에서 AWS의 모든 리소스는 Terraform으로 작업했습니다. 앞으로 리소스를 사용하게 될 때는 Terraform을 사용하는 것을 추천합니다.

AWS

작업하려는 API와 관련된 모든 리소스는 AWS기반입니다. 리소스를 생성하기 위해서 AWS 계정의 Access Key와 Secret Key가 필요합니다.

Go Text completion server

먼저 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 코드

main.tf

다음으로는 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 도 같이 넘겨주었습니다.

provider.tf

프로바이더에서는 사용할 프로바이더와 함께 버전을 명시해 주었습니다.

terraform {
  required_version = "~> 1.1.2"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 3.15"
    }
  }
}

provider "aws" {
  region  = "ap-northeast-2"
  profile = "dev"
}

api-gateway/policy.tf

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
}

api-gateway/secrets.tf

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)
}

api-gateway/lambda.tf

빌드해서 만들어놓은 소스코드를 이용해, 람다 함수를 만드는 코드를 작성했습니다. 추후에 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/main.tf

마지막으로 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"
}

Apply

작업이 모두 완료되었다면 Terraform apply를 통해서 배포해줍니다. api-gateway 라는 모듈을 만들어주었기 때문에, 처음에는 Terraform init 을 활용해 모듈을 초기화해주는 과정이 필요합니다.

API 테스트

배포가 완료되었다면 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 호출을 할 때마다 비용을 산정해주는 서버리스가 좋은 솔루션이 될 수 있습니다.

profile
복세편살
post-custom-banner

0개의 댓글