Closed39

Serverless FrameworkでLambda + Apollo Serverの環境を作りたい

qmotasqmotas
  • 題材として、Apollo Odyssey のAPIの一部の仕様を再現する
  • TypeScriptで書きたい
  • スキーマは別ファイルに出したい
node -v
v18.14.0
pnpm --version
7.27.0

スキーマ

type Query {
  tracksForHome: [Track!]!
}

type Track {
  id: ID!
  title: String!
  author: Author!
  thumbnail: String
  length: Int
  modulesCount: Int
}

type Author {
  id: ID!
  name: String!
  photo: String
}

参考資料

qmotasqmotas

ハマったとことか

  • Serverless FrameworkのドキュメントのGetting Startedで、公式のテンプレートより先にコミュニティのテンプレート(Example)が案内されていて、最終的に採用したaws-nodejs-typescriptに辿り着けなかった
  • esbuildを最新化するとserverless-esbuildがこけるので、esbuildのバージョンは0.16.xにしておく必要がある(2023-02-17時点、serverless-esbuildは1.37.3
  • GraphQL/Apollo Serverの勉強に使ったApollo OdysseyはApollo Server v3を使っていたので、v4へのマイグレーションガイドをちゃんと確認しないとダメだった
    • v3はDataSourcesApolloServerのコンストラクタに渡してたけどv4はサーバ起動時にContextを作ってこの中で初期化する
  • graphql-code-generator initがJSONのパースエラーでこけていて、これが解消できなかった
    • 詳細なログが出せなかったので原因箇所は不明(package.jsonくらいしかJSONのエラーが出そうな要素がないのでこれ?)
    • codegen.tsは生成できていたので、それ以外(package.jsonの編集)は手作業で解決した
  • Resolver Chainで親Resolverの型がスキーマからgraphql-codegenした型(子Resolverも解決済みの最終的な形)になっているとtype errorが出るので、mappersというオプションでResolverの処理中に扱う中間的な型を指定する必要がある
    • 今回のスキーマだとTrack.authorgetTracksForHomeのResolverの結果時点ではauthorIdなので、TrackauthorauthorIdに置き換えた型が必要
      • TrackModel型を作った
        • べた書きするとcodegenの意味がないのでgraphql-codegenで生成したTrackをベースにした
        • Omit<Track, "author"> & { authorId: string }
        • ドキュメントの例に倣ってTrackModelと命名したけどあまり適切だと思っていない
          • IntermediateTrackModelとかTrackUnderResolutionとか?
          • TrackResolverResultItemとか
  • 今回はシンプルなスキーマでも苦労したので、複雑なスキーマになるとResolverの型をちゃんと付けるのかなり大変なんじゃないかと思う
qmotasqmotas

体験がよかったとことか

  • aws-nodejs-typescriptテンプレートを使ったらout-of-the-boxでTypeScriptが書けた
    • serverless-esbuildプラグインのおかげっぽい
  • Serverless Frameworkそのものもserverless-offlineプラグインも特に問題なくサッと動いてくれた
    • 特にデプロイの手順が簡素で体験がよかった(AppSyncの構築で使ったときも感じた)
  • AppSyncに比べるとローカルで実装/検証できるぶん(実装部分はControllableなので)トラブルシュートしやすかった
    • ただこれくらいシンプルなスキーマだと慣れちゃえばAppSyncの方がサッと作れると思う
qmotasqmotas

検証できてないこと

  • Mutation
  • 認証
    • Apollo Serverで実装する場合はAppSyncより細かい制御が行えるのでは
    • ProxyになってるAPI Gatewayで認証がかけられるか(IAM、Cognito)
    • またはLambdaとIAMで制御する?
    • など
qmotasqmotas

初期化

pnpm dlx serverless create -t aws-nodejs-typescript
pnpm install
qmotasqmotas

ライブラリをアップデート

ncu -u
Upgrading /Users/qmotas/workspaces/odyssey-ts-serverless/package.json
[====================] 12/12 100%

 @middy/core                      ^3.6.2  →    ^4.2.4
 @middy/http-json-body-parser     ^3.6.2  →    ^4.2.4
 @types/node                   ^14.18.36  →  ^18.13.0
 esbuild                        ^0.14.54  →   ^0.17.8
 json-schema-to-ts                ^1.6.5  →    ^2.6.2
 tsconfig-paths                  ^3.14.1  →    ^4.1.2

Run pnpm install to install new versions.
pnpm install
qmotasqmotas

動作を確認したいのでserverless-offlineを入れる

pnpm add -D serverless-offline
serverless.ts
- plugins: ['serverless-esbuild'],
+ plugins: ['serverless-esbuild', 'serverless-offline'],
qmotasqmotas

こけた

pnpm serverless offline
(node:50290) NOTE: The AWS SDK for JavaScript (v2) will be put into maintenance mode in 2023.

Please migrate your code to use AWS SDK for JavaScript (v3).
For more information, check the migration guide at https://a.co/7PzMCcy
(Use `node --trace-warnings ...` to show where the warning was created)[ERROR] Invalid option in build() call: "incremental"

    /Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/esbuild@0.17.8/node_modules/esbuild/lib/main.js:255:12:
      255 │       throw new Error(`Invalid option ${where}: ${quote(key)}`);
          ╵             ^

    at checkForInvalidFlags (/Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/esbuild@0.17.8/node_modules/esbuild/lib/main.js:255:13)
    at flagsForBuildOptions (/Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/esbuild@0.17.8/node_modules/esbuild/lib/main.js:457:3)
    at buildOrContextContinue (/Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/esbuild@0.17.8/node_modules/esbuild/lib/main.js:1009:9)
    at buildOrContextImpl (/Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/esbuild@0.17.8/node_modules/esbuild/lib/main.js:993:5)
    at Object.buildOrContext (/Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/esbuild@0.17.8/node_modules/esbuild/lib/main.js:776:5)
    at /Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/esbuild@0.17.8/node_modules/esbuild/lib/main.js:2163:15
    at new Promise (<anonymous>)
    at Object.build (/Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/esbuild@0.17.8/node_modules/esbuild/lib/main.js:2162:25)
    at build (/Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/esbuild@0.17.8/node_modules/esbuild/lib/main.js:2011:51)
    at bundleMapper (/Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/serverless-esbuild@1.37.3_esbuild@0.17.8/node_modules/serverless-esbuild/src/bundle.ts:98:31)
    at /Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/p-map@4.0.0/node_modules/p-map/index.js:57:28

Environment: darwin, node 18.14.0, framework 3.27.0 (local), plugin 6.2.3, SDK 4.3.2
Docs:        docs.serverless.com
Support:     forum.serverless.com
Bugs:        github.com/serverless/serverless/issues

Error:
Error: Build failed with 1 error:
/Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/esbuild@0.17.8/node_modules/esbuild/lib/main.js:255:12: ERROR: Invalid option in build() call: "incremental"
    at failureErrorWithLog (/Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/esbuild@0.17.8/node_modules/esbuild/lib/main.js:1636:15)
    at /Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/esbuild@0.17.8/node_modules/esbuild/lib/main.js:953:16
    at responseCallbacks.<computed> (/Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/esbuild@0.17.8/node_modules/esbuild/lib/main.js:697:9)
    at handleIncomingPacket (/Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/esbuild@0.17.8/node_modules/esbuild/lib/main.js:752:9)
    at Socket.readFromStdout (/Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/esbuild@0.17.8/node_modules/esbuild/lib/main.js:673:7)
    at Socket.emit (node:events:513:28)
    at Socket.emit (node:domain:489:12)
    at addChunk (node:internal/streams/readable:324:12)
    at readableAddChunk (node:internal/streams/readable:297:9)
    at Socket.Readable.push (node:internal/streams/readable:234:10)
    at Pipe.onStreamRead (node:internal/stream_base_commons:190:23)
qmotasqmotas
pnpm serverless offline
(node:50632) NOTE: The AWS SDK for JavaScript (v2) will be put into maintenance mode in 2023.

Please migrate your code to use AWS SDK for JavaScript (v3).
For more information, check the migration guide at https://a.co/7PzMCcy
(Use `node --trace-warnings ...` to show where the warning was created)

Starting Offline at stage dev (us-east-1)

Offline [http for lambda] listening on http://localhost:3002
Function names exposed for local invocation by aws-sdk:
           * hello: odyssey-ts-serverless-dev-hello

   ┌─────────────────────────────────────────────────────────────────────────┐
   │                                                                         │
   │   POST | http://localhost:3000/dev/hello                                │
   │   POST | http://localhost:3000/2015-03-31/functions/hello/invocations   │
   │                                                                         │
   └─────────────────────────────────────────────────────────────────────────┘

Server ready: http://localhost:3000 🚀

ローカルで起動できた

qmotasqmotas

Apollo Serverを動かす

Apolloの関連ライブラリを追加

pnpm add @apollo/server graphql @as-integrations/aws-lambda
qmotasqmotas

src/配下のファイルを削除してserver.tsを追加

src/server.ts
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateLambdaHandler, handlers } from '@as-integrations/aws-lambda';

const typeDefs = `#graphql
  type Query {
    hello: String
  }
`;

const resolvers = {
  Query: {
    hello: () => 'world',
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

// This final export is important!
export const graphqlHandler = startServerAndCreateLambdaHandler(
  server,
  // We will be using the Proxy V2 handler
  handlers.createAPIGatewayProxyEventV2RequestHandler(),
);

https://www.apollographql.com/docs/apollo-server/deployment/lambda/

qmotasqmotas
serverless.ts
  functions: {
    graphql: {
      handler: 'src/server.graphqlHandler',
      events: [
        {
          httpApi: {
            path: '/',
            method: 'POST',
          },
        },
        {
          httpApi: {
            path: '/',
            method: 'GET',
          },
        },
      ],
    },
  },
qmotasqmotas
pnpm serverless offline
(node:51840) NOTE: The AWS SDK for JavaScript (v2) will be put into maintenance mode in 2023.

Please migrate your code to use AWS SDK for JavaScript (v3).
For more information, check the migration guide at https://a.co/7PzMCcy
(Use `node --trace-warnings ...` to show where the warning was created)

Starting Offline at stage dev (us-east-1)

Offline [http for lambda] listening on http://localhost:3002
Function names exposed for local invocation by aws-sdk:
           * graphql: odyssey-ts-serverless-dev-graphql

   ┌───────────────────────────────────────────────────────────────────────────┐
   │                                                                           │
   │   POST | http://localhost:3000/                                           │
   │   POST | http://localhost:3000/2015-03-31/functions/graphql/invocations   │
   │   GET  | http://localhost:3000/                                           │
   │   POST | http://localhost:3000/2015-03-31/functions/graphql/invocations   │
   │                                                                           │
   └───────────────────────────────────────────────────────────────────────────┘

Server ready: http://localhost:3000 🚀

動いた
ブラウザからアクセスしたらApollo Sandboxが使えて、クエリも確認できた

qmotasqmotas

API開発の準備

eslintとprettierを入れておく

pnpm add -D eslint prettier
.prettierrc
{
  "trailingComma": "es5",
  "tabWidth": 2,
  "semi": true,
  "singleQuote": false
}
qmotasqmotas

スキーマを外部ファイル化

必要なライブラリを追加

pnpm add @graphql-tools/graphql-file-loader @graphql-tools/load @graphql-tools/schema

schema.graphqlを作成

schema.graphql
type Query {
  hello: String
}

src/server.tsを修正

src/server.ts
import {
  handlers,
  startServerAndCreateLambdaHandler,
} from "@as-integrations/aws-lambda";
import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader";
import { loadSchemaSync } from "@graphql-tools/load";
import { addResolversToSchema } from "@graphql-tools/schema";
import { join } from "path";

const schema = loadSchemaSync(join(__dirname, "../schema.graphql"), {
  loaders: [new GraphQLFileLoader()],
});

const resolvers = {
  Query: {
    hello: () => "world",
  },
};

const schemaWithResolvers = addResolversToSchema({ schema, resolvers });

const server = new ApolloServer({
  schema: schemaWithResolvers,
});

// This final export is important!
export const graphqlHandler = startServerAndCreateLambdaHandler(
  server,
  // We will be using the Proxy V2 handler
  handlers.createAPIGatewayProxyEventV2RequestHandler()
);

https://zenn.dev/eringiv3/books/a85174531fd56a/viewer/a8fab6

qmotasqmotas

pnpm serverless offlineしてブラウザでアクセスしたらこけた

GET / (λ: graphql)
✖ Unhandled exception in handler 'graphql'.
✖ Error:
        Unable to find any GraphQL type definitions for the following pointers:

            - /Users/qmotas/workspaces/odyssey-ts-serverless/.esbuild/.build/schema.graphql

      at prepareResult (/Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/@graphql-tools+load@7.8.12_graphql@16.6.0/node_modules/@graphql-tools/load/esm/load-typedefs.js:88:15)
      at loadTypedefsSync (/Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/@graphql-tools+load@7.8.12_graphql@16.6.0/node_modules/@graphql-tools/load/esm/load-typedefs.js:75:20)
      at loadSchemaSync (/Users/qmotas/workspaces/odyssey-ts-serverless/node_modules/.pnpm/@graphql-tools+load@7.8.12_graphql@16.6.0/node_modules/@graphql-tools/load/esm/schema.js:24:21)
      at Object.<anonymous> (/Users/qmotas/workspaces/odyssey-ts-serverless/src/server.ts:11:16)
      at Module._compile (node:internal/modules/cjs/loader:1226:14)
      at Module._extensions..js (node:internal/modules/cjs/loader:1280:10)
      at Module.load (node:internal/modules/cjs/loader:1089:32)
      at Module._load (node:internal/modules/cjs/loader:930:12)
      at Module.require (node:internal/modules/cjs/loader:1113:19)
      at require (node:internal/modules/cjs/helpers:103:18)
✖
        Unable to find any GraphQL type definitions for the following pointers:

            - /Users/qmotas/workspaces/odyssey-ts-serverless/.esbuild/.build/schema.graphql
qmotasqmotas

スキーマを置き換える

サンプルとして用意されていたスキーマをこれから作りたいAPIのスキーマに置き換える

schema.graphql
type Query {
  tracksForHome: [Track!]!
}

type Track {
  id: ID!
  title: String!
  author: Author!
  thumbnail: String
  length: Int
  modulesCount: Int
}

type Author {
  id: ID!
  name: String!
  photo: String
}
qmotasqmotas

型定義を生成する

GraphQL Code Generatorを入れる

pnpm add -D @graphql-codegen/cli

初期化

pnpm graphql-code-generator init

    Welcome to GraphQL Code Generator!
    Answer few questions and we will setup everything for you.

? What type of application are you building? Backend - API or server
? Where is your schema?: (path or url) ./schema.graphql
? Pick plugins: TypeScript (required by other typescript plugins), TypeScript Resolvers (strongly typed resolve functions)
? Where to write the output: src/generated/graphql.ts
? Do you want to generate an introspection file? No
? How to name the config file? codegen.ts
? What script in package.json should run the codegen? codegen
Fetching latest versions of selected plugins...
Unexpected end of JSON input

こけた 😭
codegen.tsは生成されている

codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  overwrite: true,
  schema: "./schema.graphql",
  generates: {
    "src/generated/graphql.ts": {
      plugins: ["typescript", "typescript-resolvers"],
    },
  },
};

export default config;
qmotasqmotas

エラーログが出ないので原因がわからないけど、package.jsonの更新に失敗したとかかなという決め打ちで手動セットアップを試みる

pnpm add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers
pnpm graphql-codegen --config codegen.ts
✔ Parse Configuration
✔ Generate outputs

動いた

npm scriptも設定しておく

package.json
  "scripts": {
    "codegen": "graphql-codegen --config codegen.ts"
  },
pnpm codegen

> odyssey-ts-serverless@1.0.0 codegen /Users/qmotas/workspaces/odyssey-ts-serverless
> graphql-codegen --config codegen.ts

✔ Parse Configuration
✔ Generate outputs

https://www.apollographql.com/docs/apollo-server/workflow/generate-types/

qmotasqmotas

GraphQL Code Generatorの設定

  • 出力先ディレクトリ名を明確に区別できるようにしておく(src/__generated__
  • 設定はデフォルトのままにしておく
codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  overwrite: true,
  schema: "./schema.graphql",
  generates: {
    "src/__generated__/graphql.ts": {
      plugins: ["typescript", "typescript-resolvers"],
    },
  },
};

export default config;
qmotasqmotas

実装

DataSource(RESTDataSource )とResolverを実装していく

Apollo OdysseyだとResolverは1ファイルで実装していたけど、typeごと分割する

qmotasqmotas

データソースの実装

pnpm add @apollo/datasource-rest
src/datasources/track-api.ts
import { RESTDataSource } from "@apollo/datasource-rest";

export class TrackAPI extends RESTDataSource {
  constructor() {
    super();
    this.baseURL = "https://odyssey-lift-off-rest-api.herokuapp.com/";
  }

  async getTracksForHome() {
    return this.get("tracks");
  }

  async getAuthor(authorId) {
    return this.get(`author/${authorId}`);
  }
}
qmotasqmotas

Resolverの実装

Query

src/resolvers/query.ts
import { QueryResolvers } from "./../__generated__/graphql";

export const queryResolver: QueryResolvers = {
  tracksForHome: (_, __, { dataSources }) => {
    return dataSources.trackAPI.getTracksForHome();
  },
};

dataSourcesanyになってしまうので、Contextに型を付けたい

Contextの型を追加

src/types/context.ts
import { TrackAPI } from "src/datasources/track-api";

export type Context = {
  dataSources: {
    trackAPI: TrackAPI;
  };
};

graphql-codegenの設定を追加

codegen.ts
  const config: CodegenConfig = {
    overwrite: true,
    schema: "./schema.graphql",
    generates: {
      "src/__generated__/graphql.ts": {
        plugins: ["typescript", "typescript-resolvers"],
      },
    },
+   config: {
+     useIndexSignature: true,
+     contextType: "src/types/context#Context",
+   },
  };

型定義を生成

pnpm codegen

型がついた

https://www.apollographql.com/docs/apollo-server/workflow/generate-types/#context-typing-for-resolvers

qmotasqmotas

Track

src/resolvers/track.ts
import { TrackResolvers } from "src/__generated__/graphql";

export const trackResolver: TrackResolvers = {
  author: ({ authorId }, _, { dataSources }) => {
    return dataSources.trackAPI.getAuthor(authorId);
  },
};
  • ここでparentはTrackになるため、authorIdが取り出せない
  • TrackAPI#getTracksForHome()authorIdをもつオブジェクトを戻したい
  • QueryのResolver(tracksForHome())はauthorをもつオブジェクトを戻したい
  • チェイン先のTrackのResolver(author())はauthorIdを持つオブジェクトを受け取りたい
  • どうすればいい?
<--Track[]-- Query.tracksForHome <--- TrackAPI#getTracksForHome(): TrackWithAuthorId[]
                  │
          <TrackWithAuthorId>
                  │
                  └─ Track.author <--- TrackAPI#getAuthor(authorId): Author

qmotasqmotas

typescript-resolversプラグインのmappersというオプションでResolverが内部的に使う型を明示的に指定できる

https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-resolvers#use-your-model-types-mappers

src/types/trackModel.ts
import { Track } from "src/__generated__/graphql";

export type TrackModel = {
  id: string;
  length?: number;
  modulesCount?: number;
  thumbnail?: string;
  title: string;
  authorId: string;
};
codegen.ts
  const config: CodegenConfig = {
    overwrite: true,
    schema: "./schema.graphql",
    generates: {
      "src/__generated__/graphql.ts": {
        plugins: ["typescript", "typescript-resolvers"],
      },
    },
    config: {
      useIndexSignature: true,
      contextType: "src/types/context#Context",
+     mappers: { Track: "src/types/trackModel#TrackModel" },
    },
  };
pnpm codegen
qmotasqmotas

ただ、これだと自分でスキーマに対応した型を実装しないといけない
生成された型を使って一部だけ変更する

src/types/trackModel.ts
import { Track } from "src/__generated__/graphql";

export type TrackModel = Omit<Track, "author"> & {
  authorId: string;
};
src/resolvers/track.ts
import { TrackResolvers } from "src/__generated__/graphql";

export const trackResolver: TrackResolvers = {
  author: ({ authorId }, _, { dataSources }) => {
    return dataSources.trackAPI.getAuthor(authorId);
  },
};

いけた

qmotasqmotas

Resolverをまとめてexport

src/resolvers/index.ts
import { trackResolver } from "./track";
import { Resolvers } from "src/__generated__/graphql";
import { queryResolver } from "./query";

export const resolvers: Resolvers = {
  Query: queryResolver,
  Track: trackResolver,
};
qmotasqmotas

サーバの実装

ここまでApollo Server v3のやり方で実装してたけど、v4でけっこう変わっていたので対応する

https://www.apollographql.com/docs/apollo-server/migration

apollo-datasource-rest -> @apollo/datasource-restに置き換える

pnpm remove apollo-datasource-rest
pnpm add @apollo/datasource-rest

データソースのimportを修正
ついでに型もつけておく

src/datasources/track-api.ts
import { RESTDataSource } from "@apollo/datasource-rest";
import { TrackModel } from "src/types/trackModel";
import { Author } from "src/__generated__/graphql";

export class TrackAPI extends RESTDataSource {
  constructor() {
    super();
    this.baseURL = "https://odyssey-lift-off-rest-api.herokuapp.com/";
  }

  async getTracksForHome(): Promise<Array<TrackModel>> {
    return this.get<Array<TrackModel>>("tracks");
  }

  async getAuthor(authorId): Promise<Author> {
    return this.get<Author>(`author/${authorId}`);
  }
}

v4はデータソースの指定方法が変わって、ApolloServerのコンストラクタではなく起動処理の引数でContextを初期化する

src/server.ts
import { ApolloServer } from "@apollo/server";
import {
  handlers,
  startServerAndCreateLambdaHandler
} from "@as-integrations/aws-lambda";
import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader";
import { loadSchemaSync } from "@graphql-tools/load";
import { addResolversToSchema } from "@graphql-tools/schema";
import { join } from "path";
import { TrackAPI } from "./datasources/track-api";
import { resolvers } from "./resolvers";
import { Context } from "./types/context";

const schema = loadSchemaSync(join(__dirname, "../schema.graphql"), {
  loaders: [new GraphQLFileLoader()],
});

const schemaWithResolvers = addResolversToSchema({ schema, resolvers });

const server = new ApolloServer<Context>({
  schema: schemaWithResolvers,
});

export const graphqlHandler = startServerAndCreateLambdaHandler(
  server,
  handlers.createAPIGatewayProxyEventV2RequestHandler(),
  {
    context: async () => {
      return {
        dataSources: {
          trackAPI: new TrackAPI(),
        },
      };
    },
  }
);
qmotasqmotas

開発用のnpm scriptを追加する

動くようになったので、serverless offlineで起動しつつgraphql-codegen --watchしてスキーマの変更を反映できるようにしたい

複数コマンドの同時実行を行うため、concurrentlyを入れる

pnpm add -D concurrently

スクリプトを追加する
"で囲ったコマンドをconcurrentlyの引数に指定する

package.json
  "scripts": {
+   "dev": "concurrently \"serverless offline\" \"graphql-codegen --config codegen.ts --watch\"",
    "codegen": "graphql-codegen --config codegen.ts"
  },

pnpm devするとServerlessのローカル環境が起動してschema.graphqlの監視が始まる

pnpm dev

> odyssey-ts-serverless@1.0.0 dev /Users/qmotas/workspaces/odyssey-ts-serverless
> concurrently "serverless offline" "graphql-codegen --config codegen.ts --watch"

[0] (node:69347) NOTE: The AWS SDK for JavaScript (v2) will be put into maintenance mode in 2023.
[0]
[0] Please migrate your code to use AWS SDK for JavaScript (v3).
[0] For more information, check the migration guide at https://a.co/7PzMCcy
[0] (Use `node --trace-warnings ...` to show where the warning was created)
[1] [STARTED] Parse Configuration
[1] [SUCCESS] Parse Configuration
[1] [STARTED] Generate outputs
[1] [STARTED] Generate to src/__generated__/graphql.ts
[1] [STARTED] Load GraphQL schemas
[1] [SUCCESS] Load GraphQL schemas
[1] [STARTED] Load GraphQL documents
[1] [SUCCESS] Load GraphQL documents
[1] [STARTED] Generate
[1] [SUCCESS] Generate
[1] [SUCCESS] Generate to src/__generated__/graphql.ts
[1] [SUCCESS] Generate outputs
[1]   ℹ Watching for changes...
[0]
[0] Starting Offline at stage dev (us-east-1)
[0]
[0] Offline [http for lambda] listening on http://localhost:3002
[0] Function names exposed for local invocation by aws-sdk:
[0]            * graphql: odyssey-ts-serverless-dev-graphql
[0]
[0]  ┌───────────────────────────────────────────────────────────────────────────┐
[0]  │                                                                           │
[0]  │   POST | http://localhost:3000/                                           │
[0]  │   POST | http://localhost:3000/2015-03-31/functions/graphql/invocations   │
[0]  │   GET  | http://localhost:3000/                                           │
[0]  │   POST | http://localhost:3000/2015-03-31/functions/graphql/invocations   │
[0]  │                                                                           │
[0]  └───────────────────────────────────────────────────────────────────────────┘
[0]
[0] Server ready: http://localhost:3000 🚀
qmotasqmotas

AWS Lambdaにデプロイする

AWSの認証情報が未設定の場合はaws configureまたはserverless config credentialsで設定する

https://www.serverless.com/framework/docs/providers/aws/guide/credentials/

serverless.tsprovider等の設定が初期化時のままになっているので修正

  • regionap-northeast-1を指定
  • runtimeのバージョンをnodejs18.xに変更
    • esbuildのtargetnode18に変更
  • CORSの設定を追加
  • テンプレートにあったAPI Gatewayの設定を削除
serverless.ts
import type { AWS } from "@serverless/typescript";

const serverlessConfiguration: AWS = {
  service: "odyssey-ts-serverless",
  frameworkVersion: "3",
  plugins: ["serverless-esbuild", "serverless-offline"],
  provider: {
    name: "aws",
    region: "ap-northeast-1",
    runtime: "nodejs18.x",
    httpApi: {
      cors: true,
    },
    environment: {
      AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1",
      NODE_OPTIONS: "--enable-source-maps --stack-trace-limit=1000",
    },
  },
  // import the function via paths
  functions: {
    graphql: {
      handler: "src/server.graphqlHandler",
      events: [
        {
          httpApi: {
            path: "/",
            method: "POST",
          },
        },
        {
          httpApi: {
            path: "/",
            method: "GET",
          },
        },
      ],
    },
  },
  package: { individually: true, include: ["./schema.graphql"] },
  custom: {
    esbuild: {
      bundle: true,
      minify: false,
      sourcemap: true,
      exclude: ["aws-sdk"],
      target: "node18",
      define: { "require.resolve": undefined },
      platform: "node",
      concurrency: 10,
    },
  },
};

module.exports = serverlessConfiguration;
qmotasqmotas

デプロイ

pnpm serverless deploy
(node:71768) NOTE: The AWS SDK for JavaScript (v2) will be put into maintenance mode in 2023.

Please migrate your code to use AWS SDK for JavaScript (v3).
For more information, check the migration guide at https://a.co/7PzMCcy
(Use `node --trace-warnings ...` to show where the warning was created)

Deploying odyssey-ts-serverless to stage dev (ap-northeast-1)

✔ Service deployed to stack odyssey-ts-serverless-dev (127s)

endpoints:
  POST - https://**********.execute-api.ap-northeast-1.amazonaws.com/
  GET - https://**********.execute-api.ap-northeast-1.amazonaws.com/
functions:
  graphql: odyssey-ts-serverless-dev-graphql (1.7 MB)

1 deprecation found: run 'serverless doctor' for more details

Need a better logging experience than CloudWatch? Try our Dev Mode in console: run "serverless --console"

動いた 🎉

qmotasqmotas

1 deprecation foundとのことなので言われた通りserverless doctorで確認する

pnpm serverless doctor
(node:71916) NOTE: The AWS SDK for JavaScript (v2) will be put into maintenance mode in 2023.

Please migrate your code to use AWS SDK for JavaScript (v3).
For more information, check the migration guide at https://a.co/7PzMCcy
(Use `node --trace-warnings ...` to show where the warning was created)
1 deprecation triggered in the last command:

Support for "package.include" and "package.exclude" will be removed in the next major release. Please use "package.patterns" instead
More info: https://serverless.com/framework/docs/deprecations/#PACKAGE_PATTERNS

serverless.tsでスキーマファイルを指定している場所の書き方が非推奨らしいのでpatternsで書き換える

https://www.serverless.com/framework/docs/providers/aws/guide/packaging

serverless.ts
- package: { individually: true, include: ["./schema.graphql"] },
+ package: { individually: true, patterns: ["./schema.graphql"] },
qmotasqmotas

AWS Lambdaのコンソールを見てみる

API Gatewayがプロキシになって、ここへのリクエストをトリガとして関数を実行するという感じだろうか(AWS Lambda何もわかってない)

src/server.ts
export const graphqlHandler = startServerAndCreateLambdaHandler(
  server,
  handlers.createAPIGatewayProxyEventV2RequestHandler(),
  {
    context: async () => {
      return {
        dataSources: {
          trackAPI: new TrackAPI(),
        },
      };
    },
  }
);

handlers.createAPIGatewayProxyEventV2RequestHandler()の部分に対応している

qmotasqmotas

不要なパッケージを除去

テンプレートで使われていたパッケージで不要なもの(消し忘れ)があったので除去した

pnpm remove @middy/core @middy/http-json-body-parser json-schema-to-ts
このスクラップは2023/02/17にクローズされました