LoginSignup
3
2

AWS Copilot を使用して API Gateway × Fargate の構成で API サーバを構築する

Last updated at Posted at 2023-05-31

はじめに

よくある構成として API Gateway × Lambda がありますが,
RDS を使いたいがコネクション問題を解決しにくい場合などで Lambda の採用を見送るケースがあります.
そこで今回, AWS Copilot を使用して API Gateway × Fargate の構成で API サーバを構築します.

なお, ドメインを example.com としている箇所がありますが, お使いのドメインに置き換えてください.

構成図

プレゼンテーション1.png

サンプルコード

全体像

.
├── copilot
│   ├── environments
│   │   └── test
│   │       └── manifest.yml
│   └── services-example-api
│       ├── addons
│       │   ├── addons.parameters.yml
│       │   └── api-gateway.yml
│       └── manifest.yml
└── services
    └── example-api
        ├── src
        │   └── index.ts
        ├── Dockerfile
        ├── package.json
        └── tsconfig.json

copilot

copilot/environments/test/manifest.yml
name: test
type: Environment

observability:
  container_insights: false
copilot/services-example-api/addons/addons.parameters.yml
Parameters:
  DiscoveryServiceArn: !GetAtt DiscoveryService.Arn
copilot/services-example-api/addons/api-gateway.yml
Parameters:
  App:
    Type: String
  Env:
    Type: String
  Name:
    Type: String
  DiscoveryServiceArn:
    Type: String
  HostedZoneId:
    Type: AWS::Route53::HostedZone::Id
    Default: # ホストゾーン ID を指定してください

Mappings:
  test:
    ApiGateway:
      DomainName: example-api-test.example.com
    LogGroup:
      RetentionInDays: 1

Resources:
  VpcLink:
    Type: AWS::ApiGatewayV2::VpcLink
    Properties:
      Name: !Sub ${App}-${Env}-${Name}-VpcLink
      SubnetIds:
        !Split [",", Fn::ImportValue: !Sub "${App}-${Env}-PublicSubnets" ]
      SecurityGroupIds:
        - Fn::ImportValue: !Sub ${App}-${Env}-EnvironmentSecurityGroup

  HttpApi:
    Type: AWS::ApiGatewayV2::Api
    Properties:
      Name: !Sub ${App}-${Env}-${Name}
      ProtocolType: HTTP
      DisableExecuteApiEndpoint: true

  Stage:
    Type: AWS::ApiGatewayV2::Stage
    Properties:
      ApiId: !Ref HttpApi
      StageName: $default
      AutoDeploy: true
      AccessLogSettings:
        DestinationArn: !GetAtt AccessLogGroup.Arn
        Format: '{
                   "request-id": "$context.requestId",
                   "extended-request-id": "$context.extendedRequestId",
                   "ip": "$context.identity.sourceIp",
                   "request-time": "$context.requestTime",
                   "http-method": "$context.httpMethod",
                   "resource-path": "$context.resourcePath",
                   "status": "$context.status",
                   "protocol": "$context.protocol",
                   "response-length": "$context.responseLength"
                 }'

  Integration:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId: !Ref HttpApi
      ConnectionId: !Ref VpcLink
      ConnectionType: VPC_LINK
      IntegrationMethod: ANY
      IntegrationType: HTTP_PROXY
      IntegrationUri: !Sub ${DiscoveryServiceArn}
      PayloadFormatVersion: "1.0"

  PreflightRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId: !Ref HttpApi
      RouteKey: OPTIONS /
      Target: !Join
        - /
        - - integrations
          - !Ref Integration

  PostRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId: !Ref HttpApi
      RouteKey: POST /
#      API Gateway のオーソライザーに Cognito を使用する場合は下記を参考にしてください
#      AuthorizationType: JWT
#      AuthorizerId: !Ref Authorizer
      Target: !Join
        - /
        - - integrations
          - !Ref Integration

#  API Gateway のオーソライザーに Cognito を使用する場合は下記を参考にしてください
#  Authorizer:
#    Type: AWS::ApiGatewayV2::Authorizer
#    Properties:
#      ApiId: !Ref HttpApi
#      AuthorizerType: JWT
#      IdentitySource:
#        - $request.header.Authorization
#      JwtConfiguration:
#        Audience:
#          - # Cognito ユーザプールのクライアント ID を指定してください
#        Issuer: !Sub
#          - "https://cognito-idp.ap-northeast-1.amazonaws.com/${UserPoolId}"
#          - UserPoolId: # Cognito ユーザプール ID を指定してください
#      Name: !Sub ${App}-${Env}-${Name}-authorizer

  Certificate:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName: !FindInMap [ !Ref Env, ApiGateway, DomainName ]
      DomainValidationOptions:
        - DomainName: !FindInMap [ !Ref Env, ApiGateway, DomainName ]
          HostedZoneId: !Ref HostedZoneId
      ValidationMethod: DNS

  CustomDomainName:
    Type: AWS::ApiGatewayV2::DomainName
    Properties:
      DomainName: !FindInMap [ !Ref Env, ApiGateway, DomainName ]
      DomainNameConfigurations:
        - CertificateArn: !Ref Certificate
          EndpointType: REGIONAL

  ApiMapping:
    Type: AWS::ApiGatewayV2::ApiMapping
    DependsOn: CustomDomainName
    Properties:
      DomainName: !FindInMap [ !Ref Env, ApiGateway, DomainName ]
      ApiId: !Ref HttpApi
      Stage: !Ref Stage

  CustomDomainRecord:
    Type: AWS::Route53::RecordSet
    Properties:
      Name: !Ref CustomDomainName
      Type: A
      HostedZoneId: !Ref HostedZoneId
      AliasTarget:
        DNSName: !GetAtt CustomDomainName.RegionalDomainName
        HostedZoneId: !GetAtt CustomDomainName.RegionalHostedZoneId

  AccessLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/apigateway/${App}-${Env}-${Name}"
      RetentionInDays: !FindInMap [ !Ref Env, LogGroup, RetentionInDays ]
copilot/services-example-api/manifest.yml
name: services-example-api
type: Backend Service

image:
  build: ./services/example-api/Dockerfile
  port: 3000

cpu: 256
memory: 512
count: 1

environments:
  test:
    logging:
      retention: 1

services/example-api

今回は GraphQL で API サーバを実装します.

services/example-api/src/index.ts
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";

const typeDefs = `#graphql
type User {
    id: Int
    name: String
}

type Query {
    users: [User]
}
`;

const users = [
    {
        id: 1,
        name: "Alice",
    },
    {
        id: 2,
        name: "Bob",
    },
];

const resolvers = {
    Query: {
        users: () => users,
    }
};

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

    const { url } = await startStandaloneServer(server, {
        listen: { port: 3000 },
    });

    console.log(`🚀  Server ready at: ${url}`);
};

bootstrap().catch(console.error);
services/example-api/Dockerfile
FROM node:18-slim AS deps

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci


FROM node:18-slim AS builder

WORKDIR /app

COPY . .
COPY --from=deps /app/node_modules ./node_modules

RUN npm run build


FROM node:18-slim AS runner

WORKDIR /app

COPY --from=builder /app/dist/index.js .

EXPOSE 3000

CMD ["index.js"]
services/example-api/package.json
{
  "name": "example-api",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc --noEmit && esbuild --platform=node --format=cjs --bundle --minify --outdir=dist src/index.ts",
    "start": "npm run build && node ./dist/index.js"
  },
  "dependencies": {
    "@apollo/server": "^4.7.1",
    "graphql": "^16.6.0"
  },
  "devDependencies": {
    "@types/node": "^20.2.5",
    "esbuild": "^0.17.19",
    "typescript": "^5.0.4"
  }
}
services/example-api/tsconfig.json
{
  "compilerOptions": {
    "rootDirs": ["src"],
    "outDir": "dist",
    "lib": ["es2022"],
    "target": "es2022",
    "module": "commonjs",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "types": ["node"]
  }
}

動作確認

localhost
$ cd services/example-api
$ npm i
$ npm start
$ curl --request POST \
    --header 'content-type: application/json' \
    --url http://localhost:3000/ \
    --data '{"query":"query Users {\n  users {\n    id\n    name\n  }\n}"}'
{"data":{"users":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]}}
example-api-test.example.com
$ copilot init
# 割愛
$ curl --request POST \
    --header 'content-type: application/json' \
    --url 'https://example-api-test.example.com/' \
    --data '{"query":"query Users {\n  users {\n    id\n    name\n  }\n}"}'
{"data":{"users":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]}}

問題なくデプロイできていますね.

最後に

AWS Copilot を使用して API Gateway × Fargate の構成で API サーバを構築しました.
API Gateway のバックエンドとして Lambda の採用が難しい場合の選択肢の一つになれば.
※ 試した後は $ copilot app delete を行ってリソースを削除してください

備考

動作確認環境
$ copilot -v
copilot version: v1.28.0
$ node -v
v18.16.0
3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2