はじめに

これは頭の中を整理するための試行錯誤的なメモです。
ちゃんとしたオチまでたどり着くかは不明です。
ブラウザバックするなら今のうちです。

もし先に進まれる方は、固有名そのままのところは、薄目で見てやってください。

達成したいこと

2つのプロジェクトがあります。

  • Angularでできたフロントエンド → S3にアップロードしています。
  • AWS Lambdaで動かすバックエンド

2つのプロジェクトをいっぺんにビルドしてデプロイしたいです。

統合前の各プロジェクトの構成

各プロジェクトのディレクトリ構成と、ビルド&デプロイ方法はこのようになっています。

ディレクトリ構成

Angular

.
├── angular.json
├── browserslist
├── dist/              # ビルド結果はここ
├── e2e/
├── karma.conf.js
├── node_modules/
├── package-lock.json
├── package.json
├── src/
│   ├── app/
│   ├── assets/
│   ├── environments/
│   ├── favicon.ico
│   ├── index.html
│   ├── main.ts
│   ├── polyfills.ts
│   ├── styles.scss
│   └── test.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── tslint.json

AWS Lambda

TypeScriptを使っています。

.
├── .aws-sam/
│   └── build/  # ビルド結果はここ。CloudFormationのテンプレートも作成される
├── coverage/
├── event.json
├── karma.conf.js
├── node_modules/
├── package-lock.json
├── package.json
├── src/
│   └── google-suggest-proxy/  # プログラムはここ 
├── template.yaml              # CloudFormationテンプレートのテンプレート
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── webpack.config.js

ビルド&デプロイ方法

Angular

ビルドはngコマンドを使います。

ng build --prod

デプロイはawsコマンドを使います。S3へアップロードしています。

aws s3 sync dist/hokanchan s3://hokanchan/

AWS Lambda

ビルドはwebpackです。

./node_modules/.bin/webpack-cli

デプロイはsamコマンドを使います。パッケージングと実際のデプロイの2段階です。

sam package --template-file ./aws-sam/build/template.yml --s3-bucket ラムダ置き場 --output-template-file /tmp/packaged.yml
sam deploy --template-file /tmp/packages.yml --stack-name hokanchan --capabilities CAPABILITY_IAM

まずビルドについて

いっぺんにビルドしたいので、2つのプロジェクトが一緒のディレクトリに入っていると嬉しいような気がします。
(難しいこと考えないで、シェルスクリプトかMakefileでまとめたほうが簡単かもしれないけど、あえてやります)。

悩みどころ

各プロジェクトのディレクトリ構成でコンフリクトしているものがあります。

  • srcディレクトリ
  • tsconfig.json
  • tsconfig.app.json
  • tsconfig.spec.json
  • karma.conf.js

これらをどう統合、あるいは分けて使えばよいか。。。

解決案

方針

こんなふうに考えました。

設定ファイルを統合するのはまたにして、まずは分けた状態でまとめよう。
ngコマンドの設定に手を出すと道のりが長そうなので、AWS LambdaをAngular側に寄せよう。
Angularはgenerate applicationしてprojectsディレクトリへ移そう。

考えたディレクトリ構成

.
├── .aws-sam/          # AWS Lambdaビルド結果
├── dist/              # Angularビルド結果
├── prjects/           
│   ├── frontend-hokanchan/    # Angularのもの
│   │   ├── src/               # Angularのプログラムソース
│   │   ├── karma.conf.json
│   │   ├── tsconfig.app.json
│   │   └── tsconfig.spec.json
│   └── backend-google-suggest-proxy/  # AWS Lambdaのもの 
│   │   ├── src/                       # AWS Lambdaのプログラムソース
│   │   ├── karma.conf.json
│   │   ├── tsconfig.app.json
│   │   └── tsconfig.spec.json
├── template.yaml              # モトイキ
├── tsconfig.json              # Angular用
├── tsconfig.sam.json          # AWS Lambda用
└── webpack.config.js          # モトイキ

この構成に合わせて、webpack.config.js angular.js tsconfig.*.json karma.conf.json を書き換えました。

期待通りにビルドやテストができるようになりました。

さてデプロイについて

ビルドができるようになったので、次はデプロイです。

悩みどころ

  • Lambdaへのデプロイと、S3への静的コンテンツアップロードを同時にやりたい
    • Lambdaへのデプロイは2段階
      • 1段階目は、パッケージングとS3へのアップロード
      • 2段階目は、実際にLambdaへのデプロイ
    • 2段階目と同時に、静的コンテンツのアップロードはできる?
  • SAM(Serveless-applicatin-model)でLambdaを作ると、API Gatewayも作られる
    • このAPI Gatewayに独自ドメインを割り当てたい
    • このAPI GatewayからS3に入れた静的コンテンツも配信させたい

解決(あるいは諦め)案

  • CloundFormationによる静的コンテンツのアップロードは諦める。
    • 静的コンテンツをアップロードする素直なやり方はなさそう。
    • 静的コンテンツとLambdaは別々にデプロイする。
  • API Gatewayのルートに、S3 WebサイトをGETするメソッドを追加する。

CloudFormationテンプレートに追加するリソース

samコマンドで生成したテンプレートには、あらかじめ AWS::Serverless::Function が入っています。

これ以外に追加するリソースです。

S3バケットとポリシー

公開WebサイトとしてS3バケットを作ります。
AWSのテンプレートスニペットからコピペして作りました。

S3バケット

  StaticContentBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref bucketName
      AccessControl: PublicRead
      WebsiteConfiguration:
        IndexDocument: index.html
        ErrorDocument: error.html

ポリシー

  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    DependsOn: StaticContentBucket
    Properties:
      Bucket: !Ref StaticContentBucket
      PolicyDocument:
        Id: StaticContentBucketPolicy
        Version: '2012-10-17'
        Statement:
          - Sid: PublicReadForGetBucketObjects
            Effect: Allow
            Principal: '*'
            Action: 's3:GetObject'
            Resource: !Join
              - ''
              - - 'arn:aws:s3:::'
                - !Ref StaticContentBucket
                - /*

API Gatewayドメイン名とマッピング

独自ドメイン名を使えるようにします。

ドメイン名

    Type: AWS::ApiGateway::DomainName
    Properties:
      CertificateArn: !Ref certificateArn
      DomainName: !Ref domainName
      EndpointConfiguration:
        - REGIONAL

マッピング

ServerlessRestSApiは、AWS::Serverless::Functionの中で作成されるAPI Gatewayの論理名です。

  DomainNameMapping:
    DependsOn:
      - RestApi
      - CustomDomainName
    Type: AWS::ApiGateway::BasePathMapping
    Properties:
      DomainName: !Ref CustomDomainName
      RestApiId: !Ref ServerlessRestApi
      BasePath: ''

静的コンテンツのリソースとメソッド

API Gatewayのルート直下にHTTP Proxyを設定してS3バケットへ向けます。

API Gatewayリソース

  StaticContentResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      ParentId: !GetAtt RestApi.RootResourceId
      RestApiId: !Ref ServerlessRestApi
      PathPart: '{path}'

API Gatewayメソッド

  StaticContentResourceMethod:
    DependsOn: StaticContentResource
    Type: AWS::ApiGateway::Method
    Properties:
      HttpMethod: GET
      ResourceId: !Ref StaticContentResource
      RestApiId: RestApi
      AuthorizationType: NONE
      RequestParameters:
        method.request.path.path: true
      Integration:
        IntegrationHttpMethod: GET
        Type: HTTP_PROXY
        Uri: !Join
          - ''
          - - !GetAtt StaticContentBucket.WebsiteURL
            - '/{path}'
        PassthroughBehavior: WHEN_NO_MATCH
        IntegrationResponses:
          - StatusCode: 200
        CacheKeyParameters:
          - 'method.request.path.path'
        RequestParameters:
          integration.request.path.path: 'method.request.path.path'

コマンドをpackage.jsonにまとめる

よく使ったコマンドはpackage.jsonにまとめました。
これで忘れても安心。

AWS S3へビルドしたAngularをアップロード


    "s3:sync": "aws s3 sync dist/frontend-hokanchan  s3://hokanchan.uart.dev",

AWS S3コンテナの中身を削除

    "s3:delete": "aws s3 rm s3://hokanchan.uart.dev --recursive",

Lambdaをビルド

    "sam:build": "webpack-cli",

Lambdaをテスト

    "sam:test": "karma start projects/backend-google-suggest-proxy/karma.conf.js",

Lambdaをパッケージング

    "sam:package": "npm run sam:build;sam package --template-file .aws-sam/build/template.yaml --s3-bucket lambdastash-ap1 --output-template-file /tmp/packaged.yaml",

Lambda等をデプロイ

    "sam:deploy": "npm run sam:package;sam deploy --template-file /tmp/packaged.yaml --stack-name hokanchan --capabilities CAPABILITY_IAM",

Cloudformationの失敗内容を出力

    "sam:failed": "aws cloudformation describe-stack-events --stack-name hokanchan --query 'StackEvents[?ResourceStatus==`CREATE_FAILED`]'",

できあがったAPI GatewayのURLを出力

    "sam:apiurl": "aws cloudformation describe-stacks --stack-name hokanchan --query 'Stacks[].Outputs[?OutputKey==`GoogleSuggestProxyApi`]' --output table",

Cloudformationスタックを削除

    "sam:undeploy": "aws cloudformation delete-stack --stack-name hokanchan;aws cloudformation wait stack-delete-complete --stack-name hokanchan"

参考リンク

もしかしたらダメかも?

長々と書いてきましたが、成功していません。

AWS::Gateway::RestApiリソースを作成したら、API::Gateway::Deploymentリソースも作成しないと、実際に使えるようにはなりません。
API::Gateway::Deploymentは、AWS::Serverless::Functionの中で暗黙的に作成されます。
したがって、API::Serverless::Functionが作成された後にAWS::Gateway::ResourceAWS::Gateway::Methodを追加しても使えません。

AWS::Serverless::*をやめて、すべて手書きするしかないかなー?