LoginSignup
1
1

サーバレスwebアプリをLambdaでホストする

Posted at

はじめに

今更ながらGPTを使っている。GPTを使うとLambda(Node.js)をつかって、簡単にフロントエンド部分が書ける。フロントよくわからないので非常に良い。今回はLambda、APIGateway、DynamoDBを使ってCloudFormationでテンプレート化してみる。

今回やること

  • CloudFormationでテンプレート化する(SAMは使わない)
  • 画像はS3からロードさせる
  • WebページDynamoDBの表示、DBへの入力欄を持つ

作ったもの

テンプレート化の対象

  • テンプレート化の対象。閉域用途のためVPCエンドポイント経由で。
    image.png

  • Lambdaに乗せるindex.mjs
    (画像付き+DynamoDB機能付き・・・基礎はGTP、画像はdale3で作った)
    image.png

Clodformationテンプレート

vpcエンドポイント経由でAPI Gatewayへアクセスさせる部分はコメントアウトした。

template.yaml
AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  UUID:
    Description: UUID of stack items
    Type: String
  APIPath:
    Type: String
    Default: TrialPath
  PartitionKey:
    Type: String
    Default: pipelineID
  # vpcEndpoint:
  #   Description: vpce
  #   Type: String
  #   Default: vpce-xxxxxxxx

Resources:
  MyTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub GitReuslt-${UUID}
      AttributeDefinitions:
        - AttributeName: !Ref PartitionKey
          AttributeType: S
      KeySchema:
        - AttributeName: !Ref PartitionKey
          KeyType: HASH
      BillingMode: PROVISIONED
      ProvisionedThroughput:
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1
  RestAPI:
    Type: AWS::ApiGateway::RestApi
    Properties:
      EndpointConfiguration:
        Types:
          - REGIONAL
          # - PRIVAETE
      Name: !Sub "frontend-${UUID}-API"
      # Policy: {
      #   "Version": "2012-10-17",
      #   "Statement": [
      #       {
      #           "Effect": "Allow",
      #           "Principal": "*",
      #           "Action": "execute-api:Invoke",
      #           "Resource": [
      #               "execute-api:/*"
      #           ]
      #       },
      #       {
      #           "Effect": "Deny",
      #           "Principal": "*",
      #           "Action": "execute-api:Invoke",
      #           "Resource": [
      #               "execute-api:/*"
      #           ],
      #           "Condition" : {
      #               "StringNotEquals": {
      #                   "aws:SourceVpce": !Sub "${vpcEndpoint}"
      #               }
      #           }
      #       }
      #   ]
      # }

  APIResource:
    Type: "AWS::ApiGateway::Resource"
    Properties:
      RestApiId: !Ref RestAPI
      ParentId: !GetAtt RestAPI.RootResourceId  
      PathPart: !Ref APIPath
  APIPermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      FunctionName:  !Ref MyLambdaFunction
      Action: "lambda:InvokeFunction"
      Principal: apigateway.amazonaws.com
  APIMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref RestAPI
      ResourceId: !Ref APIResource
      HttpMethod: ANY
      AuthorizationType: NONE
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: "POST"
        Uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyLambdaFunction.Arn}/invocations"
    DependsOn: APIPermission
  APIDeployment:
    Type: AWS::ApiGateway::Deployment
    Properties:
      RestApiId: !Ref RestAPI
    DependsOn: APIMethod
  APIStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      RestApiId: !Ref RestAPI
      StageName: prod
      Description: prod stage
      DeploymentId: !Ref APIDeployment
  LambdaServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service: lambda.amazonaws.com
          Action: sts:AssumeRole
      Policies:
        - PolicyName: InlinePolicy
          PolicyDocument: 
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: iam:PassRole
                Resource: '*'
                
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      - arn:aws:iam::aws:policy/AmazonS3FullAccess
      RoleName: !Sub "LambdaServiceRole-${UUID}"

  MyLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub "App-Lambda-${UUID}"
      Handler: index.handler
      Role: !GetAtt LambdaServiceRole.Arn
      Runtime: nodejs18.x
      Timeout: 300
      Environment:
        Variables:
          APIPATH: !Sub prod/${APIPath}
          APIURL: !Sub https://${RestAPI}.execute-api.${AWS::Region}.amazonaws.com/
          BUCKETNAME01: images-for-serverless-page
          DYNAMODB_TALBENAME: !Ref MyTable
          IMAGE_KEY01: background.png
          PARTITION_KEY: !Ref PartitionKey

      Code:
        ZipFile: |
          const { DynamoDBClient, PutItemCommand, ScanCommand } = require("@aws-sdk/client-dynamodb")
          const { GetObjectCommand, S3Client } = require ("@aws-sdk/client-s3");
          const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");

          const client = new DynamoDBClient({ region: "ap-northeast-1" });
          const s3Client = new S3Client({ region: "ap-northeast-1" });
          const TABLE_NAME = process.env.DYNAMODB_TALBENAME
          const URL = process.env.APIURL
          const apiPath = process.env.APIPATH
          const bucketName = process.env.BUCKETNAME01
          const imageKey = process.env.IMAGE_KEY01
          const partitionKey = process.env.PARTITION_KEY

          exports.handler = async (event) => {
              switch (event.httpMethod) {
                  case "GET":
                      return getItems();
                  case "POST":
                      return addItem(event);
                  default:
                      return {
                          statusCode: 400,
                          headers: {
                              "Content-Type": "text/html",
                          },
                          body: "<p>Invalid method</p>",
                      };
              }
          };

          const getItems = async () => {
              const command = new ScanCommand({ TableName: TABLE_NAME });
              const data = await client.send(command);

              // 署名付きURLを取得
              const s3command = new GetObjectCommand({
                  Bucket: bucketName,
                  Key: imageKey
              });
              const imageUrl = await getSignedUrl(s3Client, s3command, { expiresIn: 3600 });

              let body = `<form action="${URL}${apiPath}" method="post" enctype="application/x-www-form-urlencoded">
                  <input type="text" name="data" placeholder="Enter data" required />
                  <button type="submit">Add</button>
                  </form>
                  <img src="${imageUrl}" alt="S3 Image" /><br><br>
                  <table border="1"><tr><th>ID</th><th>Data</th></tr>`;

              
              data.Items.forEach(item => {
                  body += `<tr><td>${item[partitionKey]["S"]}</td><td>${item.data.S}</td></tr>`;
              });
              body += "</table>";

              return {
                  statusCode: 200,
                  headers: {
                      "Content-Type": "text/html",
                  },
                  body: body,
              };
          };

          const addItem = async (event) => {
              var Item_ = {}
              Item_[partitionKey] = { S: Date.now().toString() }
              Item_["data"] = { S: unescape(event.body.split('=')[1]) }
              const command = new PutItemCommand({
                  TableName: TABLE_NAME,
                  Item: Item_,
              });

              await client.send(command);

              return {
                  statusCode: 303,
                  headers: {
                      "Location": "/" + apiPath,
                      "Content-Type": "text/html",
                  },
                  body: "",
              };
          };

Outputs:
  APIENdppointURL: # 渡す値を出力する
    Value: !Sub https://${RestAPI}.execute-api.${AWS::Region}.amazonaws.com/prod/${APIPath}

最後に

  • はじめPowershellでテンプレートからcreate stackしていたら、ダブルクオーテーションをうまく読み取れなかったため、bashからスタック作成するようにした。
  • API Gatewayで定義すべきリソースが結構多かった。SAM使った方が簡単そう…
  • テンプレートにLambdaコードをベタ書きするのもいい加減やめたい。
1
1
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
1
1