Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
0
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

AWS SAMとCircleCIでアプリのバックエンドをつくった話

AWS SAMってなーに?

AWS サーバーレスアプリケーションモデル (SAM、Serverless Application Model) とは、

サーバーレスアプリケーション構築用のオープンソースフレームワークです。迅速に記述可能な構文で関数、API、データベース、イベントソースマッピングを表現できます。リソースごとにわずか数行で、任意のアプリケーションを定義して YAML を使用してモデリングできます。デプロイ中、SAM が SAM 構文を AWS CloudFormation 構文に変換および拡張することで、サーバーレスアプリケーションの構築を高速化することができます。
引用: AWS サーバーレスアプリケーションモデル

つまり、サーバレスなアプリケーションを割と簡単に構築可能にします。
サーバレスに特化したCloudFormation(というか拡張版CloudFormation)

やりたいこと

  • モバイルアプリのバックエンドをAWSサービスで実現したい!(APIとかDBとか)
    • API Gateway + Lambda + DynamoDB
  • できるだけコードベースで実現したい!(Infrastructure as Code)
    • AWS SAM
  • 単体テスト自動化とかやってみたい!
    • Jest
  • CI/CDとかもやってみたい!
    • CircleCI

つくったもの

:muscle:実際にAWS SAMで構築したアーキテクチャ全体

aws-Page-2.png

SAM Templete(クリックで展開)
templete.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: An AWS Serverless Specification template for KameraYohou.

Globals:
  Function:
    Runtime: nodejs10.x
    Timeout: 3
    MemorySize: 128
    Description: Functions Created by SAM template yaml

Resources:
  # API Gateway
  kameraApi:
    Type: AWS::Serverless::Api
    Properties:
      # Auth: ApiAuth
      Name: KameraYohouAPI
      StageName: develop
      EndpointConfiguration: REGIONAL
      Auth:
        ApiKeyRequired: true

  # Lambda
  getSubjects:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.handler
      CodeUri: getSubjects
      Policies:
        - DynamoDBReadPolicy:
            TableName: subject
      Events:
        putSubjectApi:
            Type: Api
            Properties:
                Path: /subjects
                Method: GET
                RestApiId: !Ref kameraApi
  registerSubject:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.handler
      CodeUri: registerSubject
      Policies:
        - DynamoDBCrudPolicy:
            TableName: subject
      Events:
        putSubjectApi:
            Type: Api
            Properties:
                Path: /subjects
                Method: POST
                RestApiId: !Ref kameraApi
  disableSubject:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: index.handler
      CodeUri: disableSubject
      Policies:
        - DynamoDBCrudPolicy:
            TableName: subject
      Events:
        putSubjectApi:
            Type: Api
            Properties:
                Path: /subjects/{subject_id}
                Method: DELETE
                RestApiId: !Ref kameraApi
  getSpots:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.handler
      CodeUri: getSpots
      Policies:
        - DynamoDBReadPolicy:
            TableName: spot
      Events:
        getSpotApi:
            Type: Api
            Properties:
                Path: /spots
                Method: GET
                RestApiId: !Ref kameraApi

  # DynamoDB
  spot:
    Type: AWS::Serverless::SimpleTable
    Properties:
      TableName: spot
      PrimaryKey:
        Name: spot_id
        Type: String
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5
  subject:
    Type: AWS::Serverless::SimpleTable
    Properties:
      TableName: subject
      PrimaryKey:
        Name: subject_id
        Type: String
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5

  # API Key
  FlutterApiKey:
    Type: AWS::ApiGateway::ApiKey
    DependsOn: kameraApi
    Properties:
      Name: flutter-api-key
      Enabled: true
      StageKeys:
        - RestApiId: !Ref kameraApi
          StageName: !Ref kameraApidevelopStage
  FlutterApiUsagePlan:
    Type: AWS::ApiGateway::UsagePlan
    DependsOn: kameraApi
    Properties:
      ApiStages:
      - ApiId: !Ref kameraApi
        Stage: !Ref kameraApidevelopStage
      Throttle:
        BurstLimit: 20
        RateLimit: 10
      UsagePlanName: flutter-api-usage-plan
  FlutterApiUsagePlanKey:
    Type: AWS::ApiGateway::UsagePlanKey
    DependsOn:
      - FlutterApiKey
      - FlutterApiUsagePlan
    Properties :
      KeyId: !Ref FlutterApiKey
      KeyType: API_KEY
      UsagePlanId: !Ref FlutterApiUsagePlan

Outputs:
  ApiGatewayID:
    Description: The API Gateway ID
    Value: !Ref kameraApi
  ApiKeyID:
    Description: The API Key ID
    Value: !Ref FlutterApiKey

:muscle:テストやCI/CDを実行するフロー

aws-Page-3.png

CircleCI Config(クリックで展開)
config.yml
version: 2.1
orbs:
  node: circleci/node@1.1.6

jobs:
  unit-test:
    executor: node/default
    steps:
      - checkout
      - node/with-cache:
          steps:
            - run: npm install
      - run: npm test
      - store_test_results:
          path: coverage

  build-test:
    docker:
      - image: cleartone1216/aws-sam-cli:0.0.2
    steps:
      - checkout
      - run: sam validate
      - run: sam build
      - run: sam deploy

  post-output:
    docker:
      - image: cleartone1216/aws-sam-cli:0.0.2
    steps:
      - checkout
      - run: chmod +x ./.circleci/getResult.sh
      - run: ./.circleci/getResult.sh

workflows:
  version: 2
  pull-request:
    jobs:
      - unit-test
      - build-test:
          requires:
            - unit-test
      - post-output:
          requires:
            - build-test

むずかしかったこと

:confounded:CircleCI上でAWS SAM CLIを実行する

CircleCI上でどうしてもaws-sam-cliが使いたかったので、実行可能なCircleCI Orbを利用してみる。
https://circleci.com/orbs/registry/orb/circleci/aws-serverless

すると、テスト実行ごとにいろんなツールがインストールされて時間がめちゃくちゃかかる :clock1:
(自分の環境では、全体で2分。そのうちツール導入で1分半)

ほかに手法がないか調べたが、疲れちゃったので自分でコンテナを作ることに :whale2:

Dockerfile
FROM python:3.8.1

RUN pip install awscli aws-sam-cli
RUN apt update && apt install -y nodejs npm jq && apt clean -y
LABEL com.circleci.preserve-entrypoint=true

ビルドした内容をDocker Hubにあげて、CircleCIから参照することで解決 :white_check_mark:
無いなら作る、エンジニアの基本かも。

:confounded:SAM実行ユーザのIAM Policy

導入ガイドでは「管理者ユーザを用意します」とあるが、最小権限の原則に反するため必要最低限のユーザを作成。
情報収集不足のせいか、どのポリシーが必要なのかが全く分からず。。。
仕方がないので、実行➡権限エラー:boom:➡権限付与➡実行➡権限エラー:boom:...を繰り返してみた。

結果以下のようなポリシーを作成してIAMユーザに付与。
ざっくりまとめると、

  • S3 (Get,Put)
  • Lambda (CRUD,Configuration)
  • IAM (RoleのCRUD, PolicyのCRUD, アタッチデタッチ)
  • DynamoDB (Create, Delete, Describe)
  • CloudFormation (ChangeSet, Template, Stackまわり)
  • API Gateway (method)

SAM CLI用IAM Policy(クリックで展開)
AWSSAM-EnableDeploy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "lambda:UpdateFunctionConfiguration",
                "lambda:UpdateFunctionCode",
                "lambda:GetFunctionConfiguration",
                "lambda:GetFunction",
                "lambda:DeleteFunction",
                "lambda:CreateFunction",
                "lambda:AddPermission",
                "iam:PutRolePolicy",
                "iam:PassRole",
                "iam:ListPolicies",
                "iam:GetRolePolicy",
                "iam:GetRole",
                "iam:DetachRolePolicy",
                "iam:DeleteRolePolicy",
                "iam:DeleteRole",
                "iam:CreateRole",
                "iam:AttachRolePolicy",
                "dynamodb:DescribeTable",
                "dynamodb:DeleteTable",
                "dynamodb:CreateTable",
                "cloudformation:ValidateTemplate",
                "cloudformation:GetTemplateSummary",
                "cloudformation:ExecuteChangeSet",
                "cloudformation:DescribeStacks",
                "cloudformation:DescribeStackEvents",
                "cloudformation:DescribeChangeSet",
                "cloudformation:CreateChangeSet",
                "apigateway:PUT",
                "apigateway:POST",
                "apigateway:PATCH",
                "apigateway:GET",
                "apigateway:DELETE"
            ],
            "Resource": "*"
        }
    ]
}

:confounded:APIエンドポイントとAPI Keyの共有

エンドポイントのURLやAPI Keyが変更されるたびに、開発チーム内で共有するのが大変だったのでいい方法ないかと思案 :thinking:

そこで、CircleCI上でデプロイ成功したタイミングで、Slackに通知するようにした。
(セキュリティ的な観点は一旦考慮しない)

流れとしては、

  • AWS SAMのtemplete.yamlにOutputsとして、「通知したい内容」を記載
  • aws-cliのaws cloudformation describe-stacksコマンドから「通知したい内容」を取得
  • Slack Incoming Webhookを叩く

問題点としては、template.yaml内でAPI GatewayのAPI Keyを取得する仕様がないこと :construction:
そこで、「通知したい内容」にAPI KeyのIDを含めてから、
aws-cliのaws apigateway get-api-keyでKeyの中身を取得することにした。

これら一連の流れをシェルスクリプトにまとめて、CircleCI内で実行。

get-api-info.sh
#!/bin/bash
ApiKeyID=`aws cloudformation describe-stacks --stack-name KameraLambda | jq -r '.Stacks[].Outputs | map(select(.OutputKey == "ApiKeyID")) | .[].OutputValue'`
ApiGatewayID=`aws cloudformation describe-stacks --stack-name KameraLambda | jq -r '.Stacks[].Outputs | map(select(.OutputKey == "ApiGatewayID")) | .[].OutputValue'`
ApiKey=`aws apigateway get-api-key --api-key $ApiKeyID --include-value | jq -r .value`

curl -X POST --data-urlencode "payload={\"text\": \"API_BASE_URL=https://${ApiGatewayID}.execute-api.ap-northeast-1.amazonaws.com/develop
\n API_KEY=${ApiKey}\"}" $Slack_URL

※$Slack_URLはCircleCI上の環境変数で定義

これからやること

:runner: CircleCI上でAWS LambdaのInvokeテスト

現状だと、テストはJestのみのUnitテストなので、aws-sam-cliのsam local invokeでLambdaの機能テストも実施したい。
sam localの実行にはDocker環境が必要なので、CircleCI上でDockerコンテナが動くようにする、ということが課題になりそう。

参考文献

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
0
Help us understand the problem. What are the problem?