AWS LambdaでChatGPTプラグイン開発を試してみる - AWSデプロイ編

| 9 min read
Author: noboru-kudo noboru-kudoの画像

以下記事の続編です。

前回はAWS LambdaでChatGPTプラグインを動かすことを前提として、ローカル環境(SAM CLI)で起動したプラグインAPIとChatGPTを連携して感覚を掴みました。
今回は、実際にAWS環境へデプロイしてみたいと思います。

最終的な構成を再掲します。

plugin design

このサンプルプラグインのソースコードは以下GitHubレポジトリで公開しています。

AWS環境向けマニフェスト/API仕様を作成する

#

基本的にローカル環境の時と変わりません。
今回はstaticディレクトリを別途用意して、以下のリソースを配置しました。

static
├── .well-known
│   └── ai-plugin.json <- プラグインマニフェスト
├── openapi.yaml <- OpenAPI仕様
├── legal.html <- プライバシーポリシー
└── logo.png <- プラグインロゴ

プラグインマニフェスト、OpenAPI仕様は前回説明のとおりそれぞれマニフェストの説明とプラグインAPIの仕様を記述するものです。
プライバシーポリシーは、プラグインマニフェストのlegal_info_urlで指定したエンドポイントに対応するものです[1]
プラグインロゴもローカル環境ではリンク切れになっていましたが、今回はちゃんと用意しました。

プラグインマニフェストとOpenAPI仕様ですが、ローカル環境と内容は変わりません。
違いはプラグインのエンドポイントのみです。今回はサンプルプラグイン用のドメインchatgpt.mamezou-tech.comを用意しましたので、関連箇所をそれに置き換えています。
以下は各ファイルのGitHubリンクです。

AWS CDKスクリプトを記述する

#

前回はローカル環境向けのスクリプトでしたが、今回はAWS環境向けの部分を記述します。
AWS環境では、プラグインマニフェストやAPI仕様(OAS)等の静的リソースはS3オリジンでCloudFrontから配信します(ローカル環境ではLambda関数として実装)。

この辺りはChatGPTプラグインだから特別といったものはなく、CloudFront経由で静的リソースやAPIを公開したことのある方にとってはお馴染みのものかと思います。

若干長くなるので分割して説明します。
ソースコード全体は、GitHubレポジトリのこちらをご参考ください。

まずは、プラグインAPI本体のLambda関数です。

const stage = this.node.tryGetContext('stage') || 'local';

// AWSでは使いません。ローカル固有です。
const preflightOptions = {
  allowMethods: apigateway.Cors.ALL_METHODS,
  allowOrigins: ['https://chat.openai.com'],
  allowHeaders: ['*']
};

const githubSearchFunction = new nodejs.NodejsFunction(this, 'SearchRepos', {
  functionName: this.stackName,
  entry: '../handler.ts',
  handler: 'search',
  timeout: cdk.Duration.seconds(10),
  memorySize: 256,
  runtime: lambda.Runtime.NODEJS_18_X,
  environment: {
    GITHUB_TOKEN: this.node.getContext('github-token') // for testing
  }
});

const api = new apigateway.RestApi(this, 'GithubSearchApi', {
  restApiName: 'GitHub Search API',
  description: 'ChatGPT Plugin for GitHub Search'
});
const resource = api.root.addResource('api').addResource('search', {
  defaultCorsPreflightOptions: stage === "local" ? preflightOptions : undefined
});
resource.addMethod('GET', new apigateway.LambdaIntegration(githubSearchFunction));

こちらはローカル環境とほぼ同じです。Lambda関数とAPI Gatewayリソースを作成しています。
ただ、リモート環境(ここではAWS)の場合は、ChatGPTからのアクセスはサーバーサイドからになるようです。このためCORS対応のためのPreflightリクエストは外しました。

続いて、プラグインマニフェストやOpenAPI仕様等の静的リソースを格納するS3バケットを用意します。
なお、ここからはAWS環境でのみ作成するリソースになります。

const bucket = new s3.Bucket(this, 'StaticBucket', {
  bucketName: `${this.stackName}-static-resource`,
  accessControl: s3.BucketAccessControl.PRIVATE,
  objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_ENFORCED,
  autoDeleteObjects: true,
  removalPolicy: RemovalPolicy.DESTROY,
});
new s3deploy.BucketDeployment(this, 'DeployWebsite', {
  sources: [s3deploy.Source.asset('../static')],
  destinationBucket: bucket
});

S3バケットを用意して、先程用意したstaticディレクトリ配下をアップロードします。

続いて、ChatGPTからのリクエストを受付けるCloudFrontディストリビューション(CDN)です。
ここでは、先程S3にアップロードした静的リソースに加えて、プラグインAPIを提供するAPI Gatewayそれぞれにリクエスト振り向けます。

const oai = new cloudfront.OriginAccessIdentity(this, 'StaticBucketOriginAccessIdentity');
const apiCachePolicy = new cloudfront.CachePolicy(this, 'ChatGPTGitHubSearchCachePolicy', {
  cachePolicyName: `${this.stackName}-api-policy`,
  queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(),
  headerBehavior: cloudfront.CacheHeaderBehavior.none(),
  cookieBehavior: cloudfront.CacheCookieBehavior.none()
});
const domainName = this.node.getContext('domain');
const certificateArn = this.node.getContext('acm-arn');
const certificate = acm.Certificate.fromCertificateArn(this, 'PluginCert', certificateArn);
const distribution = new cloudfront.Distribution(this, 'ChatGPTDistribution', {
  certificate,
  domainNames: [domainName],
  // デフォルトキャッシュビヘイビア -> 静的リソースバケット(opneapi.yaml, ai-plugin.json...)
  defaultBehavior: {
    cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
    allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
    viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
    origin: new origins.S3Origin(bucket, {
      originAccessIdentity: oai
    })
  },
  additionalBehaviors: {
    // /api配下のアクセスはAPI Gateway(Lambda)にルーティング
    'api/*': {
      origin: new origins.RestApiOrigin(api),
      cachePolicy: apiCachePolicy,
      allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
      viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS
    }
  }
});
// 静的リソースはCloudFront経由のみアクセス可能
bucket.addToResourcePolicy(new iam.PolicyStatement({
  effect: iam.Effect.ALLOW,
  principals: [new iam.CanonicalUserPrincipal(
    oai.cloudFrontOriginAccessIdentityS3CanonicalUserId)],
  actions: ['s3:GetObject'],
  resources: [bucket.arnForObjects('*')]
}));

/api/*パスはAPI Gateway(Lambda関数)の方に、それ以外(デフォルト)は静的リソースを格納したS3バケットにルーティングしています。静的リソースの方はCDNとしてのキャッシュを効かせます。
また、カスタムドメインや証明書はAWS CDKのコンテキストより取得するようにしました。

最後はDNSの設定です。
Route53のAレコードを追加し、プラグインのカスタムドメインをCloudFrontディストリビューションに紐付けます。

// ここではHostedZoneは既存のものを使用
const zone = route53.HostedZone.fromHostedZoneAttributes(this, 'HostedZone', {
  zoneName: domainName.substring(domainName.indexOf('.') + 1),
  hostedZoneId: this.node.getContext('zone-id')
});
new route53.ARecord(this, 'DNSRecord', {
  recordName: domainName,
  zone,
  target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(distribution))
});

少し長くなりましたが、これで終わりです。

AWS環境にデプロイする

#

AWS環境にデプロイします。

事前にACM(Amazon Certificate Manager)で、HTTPS証明書を作成しておきます。
なお、CloudFrontにつけるものですのでバージニア北部(us-east-1)リージョンで作成する必要があります。

デプロイはAWS CDK CLIのdeployコマンドで実施します。

# 作成した証明書のARN
export ACM_ARN=arn:aws:acm:us-east-1:xxxxxxxxxxxx:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Route53ホステッドゾーン(事前に作成済み)
export HOSTED_ZONE_ID=XXXXXXXXXXXXXXXXXXXXX
# ChatGPTプラグインのドメイン
export PLUGIN_DOMAIN=chatgpt.mamezou-tech.com

cdk deploy --context github-token=${GITHUB_TOKEN} \
  --context stage=aws \
  --context acm-arn=${ACM_ARN} \
  --context zone-id=${HOSTED_ZONE_ID} \
  --context domain=${PLUGIN_DOMAIN}

デプロイ終了後はマネジメントコンソールより確認しました。

プラグインAPI(Lambda関数/API Gateway)

#
  • Lambda関数
    aws lambda
  • API Gateway
    aws apigateway

静的リソース(S3)

#

s3 bucket

CDN(CloudFront Distribution)

#
  • オリジン
    cloudfront distribution origin
  • キャッシュビヘイビア
    cloudfront distribution cache

最後にローカル環境同様にcurlで各リソースにアクセスできることを確認します。

# プラグインAPI
curl "https://${PLUGIN_DOMAIN}/api/search?q=language:javascript"
# プラグインマニフェスト
curl https://${PLUGIN_DOMAIN}/.well-known/ai-plugin.json
# OAS
curl https://${PLUGIN_DOMAIN}/openapi.yaml

今回はカスタムドメインを使っていますので、DNSレコードの伝播時間が発生します。正常にアクセスできるまでは少し時間がかかります。

ChatGPTプラグイン登録をする

#

curlで疎通確認ができたら、ChatGPTプラグインとして登録します[2]

リモート環境にデプロイした場合は、少し手順が増えますが基本的なインストールの手順はローカル環境と変わりません。

  1. 「GPT-4」 -> 「Plugins」をクリック
  2. プラグイン選択で「Plugin store」をクリック
  3. 「Develop your own plugin」をクリック
  4. Domainにプラグインドメイン(この場合はchatgpt.mamezou-tech.com)を入力して、「Find manifest file」をクリック
  5. 「Next」をクリック
  6. 「Install for me」をクリック
  7. 「Continue」をクリック
  8. 「Install plugin」をクリック

これでインストールが完了しました。以下のようにプラグインが確認できました。

ローカル環境ではロゴファイルをアップロードしていなかったので、ログがリンク切れしていましたが今回は大丈夫です。

リモート環境にデプロイしたプラグインで動作した結果です。

ローカル環境と同様に、ChatGPTがプロンプトに応じてプラグインAPIを実行してくれているのが分かります(アイコンが突然「T」に変わってしまうのはなぜだろう...)。
見た目はローカル環境と同じですが、デベロッパーツールを見ていてリモート環境の場合はプラグインAPIの実行はサーバーサイドになるようです。そのためリモート環境の場合はCORS対応は不要でした。

今回は検証できませんでしたが、公式FAQによるとこの状態で15名までのユーザーがこのプラグインを動作させられるようです。

Can I invite people to try my plugin?
Yes, all unverified plugins can be installed by up to 15 other developers who have plugin access. If your plugin is available in the plugin store, it will be accessible to all ChatGPT plus customers.

引用元: https://platform.openai.com/docs/plugins/production/can-i-invite-people-to-try-my-plugin

この場合は「Install an unverified plugin」からドメインを入力すれば良さそうです。
ただし、現時点ではこのリンクはプラグイン開発が許可されているユーザーにしか表示されないようです。

最後に

#

今回は実際にAWS上にプラグインをデプロイして、ChatGPTから使う様子を見てみました。
やってみるとプラグイン開発自体は通常のWeb開発と大きな差はないのかなと思いました。

なお、今回未実施ですがChatGPTプラグインとして実際に公開する場合は、OpenAIのBotからレビュー申請を行うようです。

ちょうどこの記事を書いていたところに、こんなニュースも目にしました。

プラグイン開発に習熟してれば、MicrosoftのCopilot製品群のプラグインにもそのスキルは流用できそうです。

まだ、開発スタイルが定着していると言えない状況ですが、ウォッチしておいて損はなさそうなスキルですね。


  1. 今回はChatGPTにご指導(?)いただきながら作成しました。実際に公開する際は本物の専門家と相談するのが良いかと思います。とはいえ、これをチェックされるのは公開申請の時ですので今回の作業ではなくても構いません。 ↩︎

  2. 前回ローカル環境でインストールしたプラグインは、事前にプラグインストアからアンインストールしておきます。 ↩︎

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。