AWS CloudFormation StackSets使ってみる

AWS CloudFormation StackSetsとは

AWS CloudFormation StackSets は、複数のアカウントおよびリージョンのスタックを 1 度のオペレーションで、作成、更新、削除できるようにすることで、スタックの機能を拡張します。

使用するケース

1つのAWSアカウントに複数のアプリケーションを作成してしまっている場合にアカウントを分けて管理する
CloudTrailの有効化等一律設定が必要なものを1発で設定する場合

準備

アカウントを複数用意する

ルートアカウント

すべてのサブドメインの親アカウントにする

サブドメイン用アカウント

アカウント名
メールアドレス

※.com等なしのものを用意する

aws-cliのアップデート

pip install --upgrade awscli

.bash_profile

export AdminAccountId=
export AdminAccountBucket=
export TargetAccount=
export TargetAccountBucket=

管理者アカウントでAWSCloudFormationStackSetAdministrationRoleの作成(例)

aws configure --profile ${TargetAccount}
aws s3 mb s3://${TargetAccountBucket} --region ap-northeast-1 --profile ${TargetAccount}
aws s3 cp ./yaml/target/AWSCloudFormationStackSetExecutionRole.yml s3://${TargetAccountBucket}/yaml/target/ --profile ${TargetAccount}

aws cloudformation create-stack --stack-name target-stack-sets \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameters ParameterKey=AdministratorAccountId,ParameterValue=${AdminAccountId} \
  --template-url https://s3-ap-northeast-1.amazonaws.com/${TargetAccountBucket}/yaml/target/AWSCloudFormationStackSetExecutionRole.yml \
  --profile ${TargetAccount}

ターゲットアカウントAWSCloudFormationStackSetExecutionRoleを作成(例)

aws configure --profile ${TargetAccount}
aws s3 mb s3://${TargetAccountBucket} --region ap-northeast-1 --profile ${TargetAccount}
aws s3 cp ./yaml/target/AWSCloudFormationStackSetExecutionRole.yml s3://${TargetAccountBucket}/yaml/target/ --profile ${TargetAccount}

aws cloudformation create-stack --stack-name target-stack-sets \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameters ParameterKey=AdministratorAccountId,ParameterValue=${AdminAccountId} \
  --template-url https://s3-ap-northeast-1.amazonaws.com/${TargetAccountBucket}/yaml/target/AWSCloudFormationStackSetExecutionRole.yml \
  --profile ${TargetAccount}

stack-setsの作成

aws cloudformation create-stack-set --stack-set-name my-awsconfig-stackset \
  --capabilities CAPABILITY_IAM \
  --template-url https://s3.amazonaws.com/cloudformation-stackset-sample-templates-us-east-1/EnableAWSConfig.yml

stack-instancesの作成

aws cloudformation create-stack-instances --stack-set-name my-awsconfig-stackset \
  --accounts '["1111111111111"]' \
  --regions '["ap-northeast-1"]' \
  --operation-preferences FailureToleranceCount=0,MaxConcurrentCount=1

stack-instancesの削除

aws cloudformation delete-stack-instances --stack-set-name my-awsconfig-stackset \
  --accounts '["1111111111111"]' \
  --regions '["ap-northeast-1"]' \
  --operation-preferences FailureToleranceCount=0,MaxConcurrentCount=1 \
  --no-retain-stacks

stack-setsの削除

aws cloudformation delete-stack-set --stack-set-name my-awsconfig-stackset

textlintで日本語のチェックを自動化してみる

インストール

npm i -g textlint textlint-rule-preset-japanese textlint-rule-preset-jtf-style

設定

  • ~/.textlintrc
{
    "rules" : {
        "preset-japanese": true,
        "preset-jtf-style": true
    }
}

チェック

textlint XXXX.txt

以下のようになりました。

    3:18  error    Line 3 exceeds the maximum line length of 100             preset-japanese/sentence-length
    3:21  ✓ error  %E => %E                                                 preset-japanese/spellcheck-tech-word
    5:11  error    一文に二回以上利用されている助詞 "ば" がみつかりました。  preset-japanese/no-doubled-joshi
   14:5   ✓ error  数値の範囲を示す場合には全角の〜を使用します。            preset-jtf-style/4.2.5.波線(〜)
   27:10  error    一文に二回以上利用されている助詞 "に" がみつかりました。  preset-japanese/no-doubled-joshi
   27:55  error    一つの文で"、"を3つ以上使用しています                     preset-japanese/max-ten
   40:10  error    一文に二回以上利用されている助詞 "に" がみつかりました。  preset-japanese/no-doubled-joshi
   40:42  error    一文に二回以上利用されている助詞 "が" がみつかりました。  preset-japanese/no-doubled-joshi
   44:15  error    一文に二回以上利用されている助詞 "が" がみつかりました。  preset-japanese/no-doubled-joshi
   84:53  error    一つの文で"、"を3つ以上使用しています                     preset-japanese/max-ten
  106:18  error    一文に二回以上利用されている助詞 "が" がみつかりました。  preset-japanese/no-doubled-joshi

✖ 11 problems (11 errors, 0 warnings)
✓ 2 fixable problems.

修正

textlint --fix XXXX.txt
   3:21  ✔   %E => %E                                       preset-japanese/spellcheck-tech-word
  14:5   ✔   数値の範囲を示す場合には全角の〜を使用します。  preset-jtf-style/4.2.5.波線(〜)

✔ Fixed 2 problems

自動修正は難しいようです。

いい文章の要素をまとめれば、文章の品質も自動的にチェックできそうな気もしますね。

参考

GitHub - textlint/textlint: The pluggable natural language linter for text and markdown.

GitHub - textlint-ja/textlint-rule-preset-JTF-style: JTF日本語標準スタイルガイド for textlint.

GitHub - textlint-ja/textlint-rule-preset-japanese: textlint rule preset for Japanese.

頭を良くするには

最近はてなでランキング1位になっていたブログを見てみます。

blog.hatenablog.com

脳を鍛えるには運動しかないとハーバードの教授が言っているようです。

https://www.amazon.co.jp/%E8%84%B3%E3%82%92%E9%8D%9B%E3%81%88%E3%82%8B%E3%81%AB%E3%81%AF%E9%81%8B%E5%8B%95%E3%81%97%E3%81%8B%E3%81%AA%E3%81%84-%E6%9C%80%E6%96%B0%E7%A7%91%E5%AD%A6%E3%81%A7%E3%82%8F%E3%81%8B%E3%81%A3%E3%81%9F%E8%84%B3%E7%B4%B0%E8%83%9E%E3%81%AE%E5%A2%97%E3%82%84%E3%81%97%E6%96%B9-%E3%82%B8%E3%83%A7%E3%83%B3-J-%E3%83%AC%E3%82%A4%E3%83%86%E3%82%A3/dp/4140813539

生まれつき運動しないと気持ち悪い・やってられない性格の人間は脳力が高くなりやすい気はしています。

脳力を鍛える脳トレは以下がよいと思います。

朝ラン

朝ランをするとしないでは、脳力に雲泥の差が出てくると思います。
効果も2~3日続くような気がします。

以下が具体的な効果です

仕事の生産性向上
器の大きさ/ストレス耐性力向上
ストレス低減
ミス削減
脳の容量の増加
記憶力増加
新しい知識を付ける力の向上

朝になんらかの運動になるものをしていない人は生産性が低く、器が小さく、ストレスフルでミスが多く、記憶力が低く、新しい知識を身につける力が低く、保守的になりやすい気はしています。

脳はランニングで鍛えろ!「脳力」を鍛えるとっておきの走り方|Career Supli

ランニングによる脳への5つの効果!走って脳を活性化させよう!

運動と頭を使うことを交互に行う

運動で脳力が上がった後に頭を使うことで脳力向上につながる気がします。

タイミングがよければ、脳が超回復していくのでしょうか。

朝になんらかの運動になるものをすれば、仕事での脳の超回復を発生させることができる気がします。

仕事だけだと、肝心な脳力は下がる一方でストレスは貯まるだけで、脳力は低い人間になってしまいます。

労働時間が長い人に頭がいい人がいないのはそのためかと。

オフィスとジムを一緒にしてみるのもよいかもしれません。

http://jp.wsj.com/articles/SB12616845268056034052504582158340471478120

新しい刺激

新しい刺激になるのは転職や現場を変えたりフリーランスになったり、住む場所を変えたりことです。

周りの環境や人が変わることは大きな刺激になります。

人によっては大きなストレスとなるかもしれませんが、新しい刺激の超回復で脳力は高くなると思います。

ストレスが大きすぎると逆に脳は萎縮して、脳力は下がると思いますが。

勤続XX年×マイホーム×会社員が最も脳力を低くしやすいような気がします。

物事のネガティブな面に執着したり、重箱の隅をつついてばかりになってしまった人はかなり脳が退化しています。

回避よりも新規追求が高い人間は生まれつき、脳力が高くなりやすいと思います。

その点、年を取ると回避が高くなり、新規追求が低くなりやすいので、脳力は上げにくくなっていきやすいかと思います。

オーバーワークを避ける

オーバーワークを避けることで脳の萎縮を防ぎます。

脳を休め、脳が成長する機会を与えます。

マインドフルネス等が流行っているのですが、基本的には脳に休養を与えれば瞑想である必要はないと思います。

インプットに偏らない

よくあるのがインプットに偏ったビジネスマン・管理職です。

インプットは脳としては単に情報を蓄えているだけです。

日本でGoogleAmazonのような企業が出てこないのはここに起因していると思われます。

人間らしさである肝心な生み出す脳力が低くなってしまいます。

情報をインプットして、それ通り意思決定や作業・業務をするのは日本の得意分野ですが、それは機械がすることで、AIに代替されるものです。

生み出す脳力によいインプットとしては日常から距離のあるもの、普段触れない情報です。

日常・自身の業界の情報には脳が慣れきっているため、よい刺激にはなりません。

メシ

脳の原料は基本的にメシです。

何を食べるかは脳の品質を決めます。

ジャンキーなものをよく食べれば、脳も劣化していきます。

ディスプレイを見る時間を少なくする

長くエンジニアをしている人には人間としての基本的な機能が低下している人が多い印象です。

滑舌が悪くなってしまった人、コミュニケーション力が低下してしまった人、顔色が暗くなってしまった人、覇気がなくなってしまった人、イライラしやすい人、意欲が低くなってしまった人

これらは基本的にディスプレイを見ることで脳が萎縮してしまっていることが原因の1つと想定されます。

スマホやPCよりもVRの方がより脳が萎縮する気はしていますので、拡張現実は今後想定されているよりも成長しない分野になると思います。

20年前に比べて、各種統計に大きな変化があったものはもしかしたらスマホ・PCを見る時間が増えたことに起因するかもしれません。

AWS Serverless Application Model (AWS SAM)使ってみる: 変換後のリソースのyamlを見てみる

AWS SAMとはAWSが提供しているサーバーレスフレームワークです。

APIGateway, Lambda, DynamoDBをすべてCloudFormationで作成するのは難易度が高いため、
簡素化したyamlAWS側でCloudFormation標準のものに変換して各リソースを作成してくれます。

なお、作成されるのは標準化されたものですので、自由に柔軟なリソース管理をしたい場合はSAMではなく、
CloudFormationで作成した方がよいと思います。
誰でも、簡単にという点ではSAMの標準にならって作成するのが一番よいと思われます。

詳細をAWS SAMのリソースの指定とAWS側での変換後のリソースで比較してみます。

Transform: AWS::Serverless-2016-10-31

これを指定することでyamlがAWS側でCloudFormation標準の書き方に変更されます
aws cloudformation create-stackコマンドのyamlでは使えません
aws cloudformation deployコマンドのyamlに使用できます

AWS::Serverless::SimpleTable

このぐらいなら、AWS::DynamoDB::Tableで問題ないですね。

To use the more advanced functionality of DynamoDB, use an AWS::DynamoDB::Table resource instead.

だそうですので、最初からAWS::DynamoDB::Tableを使えばいい気がします。

ユーザーが指定するリソース

  Table:
    Type: AWS::Serverless::SimpleTable

AWSで変換後のリソース

  Table:
    Type: AWS::DynamoDB::Table
    Properties:
      ProvisionedThroughput:
        WriteCapacityUnits: 5
        ReadCapacityUnits: 5
      AttributeDefinitions:
      - AttributeName: id
        AttributeType: S
      KeySchema:
      - KeyType: HASH
        AttributeName: id

AWS::Serverless::Function

以下を作成してくれるようです。
これなら、最初からCloudFormationで作成した方が自由で柔軟性がありそうです。
指定したいけど、SAM非対応な指定項目が出そうな気がします。

  • AmazonDynamoDBReadOnlyAccessとAWSLambdaBasicExecutionRoleのロール
  • DeploymentとProdStage
  • swaggerでのRestApi
  • test用とprod用のLambda::Permission
  • Lambda関数

ユーザーが指定するもの

  GetFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.get
      Runtime: nodejs6.10
      CodeUri: s3://keiwt-hogeless-api/api.zip
      Policies: AmazonDynamoDBReadOnlyAccess
      Environment:
        Variables:
          TABLE_NAME: !Ref Table
      Events:
        GetResource:
          Type: Api
          Properties:
            Path: /resource/{resourceId}
            Method: get

AWSで変換後のリソース

IAM

  GetFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Action:
          - sts:AssumeRole
          Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com

ApiGateway

  ServerlessRestApiProdStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      DeploymentId:
        Ref: ServerlessRestApiDeployment77d5a05be2
      RestApiId:
        Ref: ServerlessRestApi
      StageName: Prod

  ServerlessRestApiDeployment77d5a05be2:
    Type: AWS::ApiGateway::Deployment
    Properties:
      RestApiId:
        Ref: ServerlessRestApi
      Description: 'RestApi deployment id: 77d5a05be2514b45c5df870702eac8162155cb25'
      StageName: Stage

  ServerlessRestApi:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Body:
        info:
          version: '1.0'
          title:
            Ref: AWS::StackName
        paths:
          "/resource/{resourceId}":
            put:
              x-amazon-apigateway-integration:
                httpMethod: POST
                type: aws_proxy
                uri:
                  Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PutFunction.Arn}/invocations
              responses: {}
            get:
              x-amazon-apigateway-integration:
                httpMethod: POST
                type: aws_proxy
                uri:
                  Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetFunction.Arn}/invocations
              responses: {}
            delete:
              x-amazon-apigateway-integration:
                httpMethod: POST
                type: aws_proxy
                uri:
                  Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${DeleteFunction.Arn}/invocations
              responses: {}
        swagger: '2.0'

Lambda

  GetFunctionGetResourcePermissionTest:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:invokeFunction
      Principal: apigateway.amazonaws.com
      FunctionName:
        Ref: GetFunction
      SourceArn:
        Fn::Sub:
        - arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/resource/{resourceId}
        - __Stage__: "*"
          __ApiId__:
            Ref: ServerlessRestApi

  GetFunctionGetResourcePermissionProd:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:invokeFunction
      Principal: apigateway.amazonaws.com
      FunctionName:
        Ref: GetFunction
      SourceArn:
        Fn::Sub:
        - arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/resource/{resourceId}
        - __Stage__: Prod
          __ApiId__:
            Ref: ServerlessRestApi

  GetFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: keiwt-hogeless-api
        S3Key: api.zip
      Tags:
      - Value: SAM
        Key: lambda:createdBy
      Environment:
        Variables:
          TABLE_NAME:
            Ref: Table
      Handler: index.get
      Role:
        Fn::GetAtt:
        - GetFunctionRole
        - Arn
      Runtime: nodejs6.10

以下はyaml全体のbeforeとafterです。


example-before.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
  GetFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.get
      Runtime: nodejs6.10
      CodeUri: s3://keiwt-hogeless-api/api.zip
      Policies: AmazonDynamoDBReadOnlyAccess
      Environment:
        Variables:
          TABLE_NAME: !Ref Table
      Events:
        GetResource:
          Type: Api
          Properties:
            Path: /resource/{resourceId}
            Method: get

  PutFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.put
      Runtime: nodejs6.10
      CodeUri: s3://keiwt-hogeless-api/api.zip
      Policies: AmazonDynamoDBFullAccess
      Environment:
        Variables:
          TABLE_NAME: !Ref Table
      Events:
        PutResource:
          Type: Api
          Properties:
            Path: /resource/{resourceId}
            Method: put

  DeleteFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.delete
      Runtime: nodejs6.10
      CodeUri: s3://keiwt-hogeless-api/api.zip
      Policies: AmazonDynamoDBFullAccess
      Environment:
        Variables:
          TABLE_NAME: !Ref Table
      Events:
        DeleteResource:
          Type: Api
          Properties:
            Path: /resource/{resourceId}
            Method: delete

  Table:
    Type: AWS::Serverless::SimpleTable

example-after.yaml

---
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  GetFunctionGetResourcePermissionTest:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:invokeFunction
      Principal: apigateway.amazonaws.com
      FunctionName:
        Ref: GetFunction
      SourceArn:
        Fn::Sub:
        - arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/resource/{resourceId}
        - __Stage__: "*"
          __ApiId__:
            Ref: ServerlessRestApi
  GetFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Action:
          - sts:AssumeRole
          Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
  DeleteFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: keiwt-hogeless-api
        S3Key: api.zip
      Tags:
      - Value: SAM
        Key: lambda:createdBy
      Environment:
        Variables:
          TABLE_NAME:
            Ref: Table
      Handler: index.delete
      Role:
        Fn::GetAtt:
        - DeleteFunctionRole
        - Arn
      Runtime: nodejs6.10
  PutFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: keiwt-hogeless-api
        S3Key: api.zip
      Tags:
      - Value: SAM
        Key: lambda:createdBy
      Environment:
        Variables:
          TABLE_NAME:
            Ref: Table
      Handler: index.put
      Role:
        Fn::GetAtt:
        - PutFunctionRole
        - Arn
      Runtime: nodejs6.10
  DeleteFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Action:
          - sts:AssumeRole
          Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
  ServerlessRestApiProdStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      DeploymentId:
        Ref: ServerlessRestApiDeployment77d5a05be2
      RestApiId:
        Ref: ServerlessRestApi
      StageName: Prod
  ServerlessRestApi:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Body:
        info:
          version: '1.0'
          title:
            Ref: AWS::StackName
        paths:
          "/resource/{resourceId}":
            put:
              x-amazon-apigateway-integration:
                httpMethod: POST
                type: aws_proxy
                uri:
                  Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PutFunction.Arn}/invocations
              responses: {}
            get:
              x-amazon-apigateway-integration:
                httpMethod: POST
                type: aws_proxy
                uri:
                  Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetFunction.Arn}/invocations
              responses: {}
            delete:
              x-amazon-apigateway-integration:
                httpMethod: POST
                type: aws_proxy
                uri:
                  Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${DeleteFunction.Arn}/invocations
              responses: {}
        swagger: '2.0'
  PutFunctionPutResourcePermissionTest:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:invokeFunction
      Principal: apigateway.amazonaws.com
      FunctionName:
        Ref: PutFunction
      SourceArn:
        Fn::Sub:
        - arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/PUT/resource/{resourceId}
        - __Stage__: "*"
          __ApiId__:
            Ref: ServerlessRestApi
  GetFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: keiwt-hogeless-api
        S3Key: api.zip
      Tags:
      - Value: SAM
        Key: lambda:createdBy
      Environment:
        Variables:
          TABLE_NAME:
            Ref: Table
      Handler: index.get
      Role:
        Fn::GetAtt:
        - GetFunctionRole
        - Arn
      Runtime: nodejs6.10
  GetFunctionGetResourcePermissionProd:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:invokeFunction
      Principal: apigateway.amazonaws.com
      FunctionName:
        Ref: GetFunction
      SourceArn:
        Fn::Sub:
        - arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/resource/{resourceId}
        - __Stage__: Prod
          __ApiId__:
            Ref: ServerlessRestApi
  ServerlessRestApiDeployment77d5a05be2:
    Type: AWS::ApiGateway::Deployment
    Properties:
      RestApiId:
        Ref: ServerlessRestApi
      Description: 'RestApi deployment id: 77d5a05be2514b45c5df870702eac8162155cb25'
      StageName: Stage
  DeleteFunctionDeleteResourcePermissionProd:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:invokeFunction
      Principal: apigateway.amazonaws.com
      FunctionName:
        Ref: DeleteFunction
      SourceArn:
        Fn::Sub:
        - arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/DELETE/resource/{resourceId}
        - __Stage__: Prod
          __ApiId__:
            Ref: ServerlessRestApi
  DeleteFunctionDeleteResourcePermissionTest:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:invokeFunction
      Principal: apigateway.amazonaws.com
      FunctionName:
        Ref: DeleteFunction
      SourceArn:
        Fn::Sub:
        - arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/DELETE/resource/{resourceId}
        - __Stage__: "*"
          __ApiId__:
            Ref: ServerlessRestApi
  Table:
    Type: AWS::DynamoDB::Table
    Properties:
      ProvisionedThroughput:
        WriteCapacityUnits: 5
        ReadCapacityUnits: 5
      AttributeDefinitions:
      - AttributeName: id
        AttributeType: S
      KeySchema:
      - KeyType: HASH
        AttributeName: id
  PutFunctionPutResourcePermissionProd:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:invokeFunction
      Principal: apigateway.amazonaws.com
      FunctionName:
        Ref: PutFunction
      SourceArn:
        Fn::Sub:
        - arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/PUT/resource/{resourceId}
        - __Stage__: Prod
          __ApiId__:
            Ref: ServerlessRestApi
  PutFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Action:
          - sts:AssumeRole
          Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com

DRYにするか分けるかについて

DRYにするかどうかはよく誤って判断されることが多いと思います。 スタートアップ時には先のことを想定していないケースが多く、サービスの規模が拡大してから問題が発生することが多いかと思います。

そこで、どういうものはDRYにすべきで、どういう場合にきちんと分けるべきかまとめてみます。

アプリケーション内の処理

言うまでもなく、1処理1メソッドにすべきです

同じような処理が複数見られる場合は1箇所に集約します。 ただし、DRYにするのはあくまで処理であって、種類が別のものまでDRYにしてしまうと特定の種類の場合しか不要な処理が他の種類の場合でも無駄に動いてしまうでしょう。 種類毎にアプリケーションの受け口は分けて、各種類の受け口から使用するものは共通にしておくと、後で柔軟に対応できると思います。

アプリケーション(リポジトリ)をまたいでの共通の処理はライブラリとしてアプリケーションとは別のリポジトリにすると良いと思います。 そうするとマイクロサービス化しやすいと思います。

汎用的な処理は自作よりもgem、標準のライブラリを使用するのが良いと思います。

サービスの対象の種類

例えば、不動産であれば、賃貸マンション、新築マンション、新築一戸建て、中古マンション、中古一戸建てなどのことで、飲食業界ではランチや宴会などのことです。

これは基本的にはサービス毎明確に分けて、マイクロサービス化すべきです。

最もやってはいけないのは種類をパラメーターにしたり、フラグで分けることです。
そうしてしまうと、いずれサービスは崩壊するでしょう。
サービスの裏側は種類を見るif分等でぐちゃぐちゃになるでしょう。
また、世の中にリノベーションマンション、部ランチ等の新しい種類が追加された場合にはそれだけ別に分けてしまったり、既存の種類を消したい場合には工数が膨大になり、リスクが高くなります。 つまり、世の中の変化に脆弱なアプリケーションになるでしょう。

インフラ

各サービスで使用する標準のインフラのソースを作成すべきでしょう。 そうすれば、各サービスのインフラ構築が数分で済みます。 また、各サービスで共通の変更が必要な場合はソースを修正して、各サービスに適応するだけで済みます。

それぞれのサービスがそれぞれインフラを構築してしまっている場合には各サービスで調査が必要になることもあるかもしれません。

一番望ましいのはサーバーレス構成のインフラのソースを作成するのみで、AmazonGoogleにインフラはお任せすることだと思います。

ドメイン

ブランド戦略によりますが、基本的に各サービスは企業orサービスドメインサブドメインになると思います。 マイクロサービスの場合も同様ですがサービスの実態に合わせるのが良いかと思います。 ドメインをすべて同じにしてしまうとロードバランサの障害で全サービスが停止しないように不必要に良いインスタンスにして、余分なコストが発生するでしょう。

サービス名称

例えば、WantedlyがサービスのWantedlyをWantedly Visitに変更したように、スタートアップ時には会社名がサービス名になっていることが多いと思います。

また、Wantedlyというブランドは人材系のサービスに自身を限定してしまっている印象があります。

ブランドやサービス名の変更は大きな影響があるため、最初から先を想定する方が良いかと思います。

ブランドの戦略にもよるのですが、ブランド名を分けて、ブランド価値低下の影響範囲を少なくするのか、既存の確立されたブランドのカチを新規サービスでも享受するのかは判断が分かれるところだと思います。

どちらにしろ、企業名やサービス名はサービスの領域を限定しない抽象的な方がよいと思います。

その点、AmazonGoogleはよく考えられたものです。 AmazonならIT分野に進出してAmazonWebServices、Googleなら、ホームデバイスGoogle Home。企業名が抽象的なためにどの分野にも進出しやすいと思います。

The parameter Origin DomainName does not refer to a valid S3 bucket.

CloudFormationでサーバーレスアプリケーションを作る際にCloudFrontからWebsiteの設定をしたS3にトラフィックを流すと思います。 その際にタイトルのエラーが発生することがあります。

以下のような設定の場合です

        Origins:
          - DomainName: !Sub
              - ${Domain}.s3-website-${AWS::Region}.amazonaws.com
              - { Domain: !Ref Domain }
             Id: !Ref OriginId
             S3OriginConfig:
               OriginAccessIdentity: !Ref OriginAccessIdentity

結論としてはWebsiteのドメイン名はS3OriginConfigの対象ではないので、以下のようにCustomOriginConfigを使用すれば解決します。

CustomOrigin is a property of the Amazon CloudFront Origin property that describes an HTTP server.

docs.aws.amazon.com

        Origins:
          - DomainName: !Sub
              - ${Domain}.s3-website-${AWS::Region}.amazonaws.com
              - { Domain: !Ref Domain }
            Id: !Ref OriginId
            CustomOriginConfig:
              OriginProtocolPolicy: http-only
              HTTPPort: 80

間違えやすい原因としてはCustomOriginConfigのキー名かと。 S3のWebsite用ドメインなので、ついS3OriginConfigに設定しそうになってしまいます。

AWSにはCustomOriginConfigではなく、以下の3通りのいずれかにして欲しいものです。

ドキュメント通りにHttpServerOriginConfigにする
CustomOriginConfigとは別にS3WebsiteOriginConfigを作る
CustomOriginConfigではなく、WebsiteOriginConfigを作る

WEBにおいて明確しておいた方がよい方針・指針

WEBにおいては方針や指針をちゃんと決めておけば、負の遺産の発生を防ぐことができます。

方針・指針が特に明確に決まっていない、または曖昧なまま事業を開始するといずれ負の遺産が膨大になったり、スピードが出ない開発になったり、開発プロセスが増えていってしまいます。

結果として作り直した方がよい状況が発生してしまいます。

設計等に関しては基本的に最初からきちんと分けて、疎にしておくことが重要です。

特に開発初期の中心メンバーが方針・指針を明確に決めるべきだと思います。

そこで、こうしておいた方がよい方針・指針をご紹介します。

開発標準

静的解析

明確にしない場合

様々な書き方が散乱
後の開発者の混乱の元となるコードが多数
XXX派論争による無駄な時間の発生
ソースの複雑度が高くなり、膨大な負の遺産が発生する
1コード1機能で明確にディレクトリを分けて、実装では使うだけにすることができなくなる
似たようなソースが散乱するようになる
ソースのコピペが多発する
ソースの量が膨大になる
パフォーマンス劣化
メモリを無駄に消費
DDOSに対して、脆弱になる
単体のspecが巨大になりやすく、テスト工数が増大する

保存時に動かせるもの

行末のスペースを削除
余分なタブやスペースの削除
ファイルの最終行の改行

保存時に何も動かしていない場合

無駄な大量の空白(余分なタブやスペース)が散乱
行末の改行がないwarning
保存時に自動的にスクリプトを動かすと空白の大量のdiff
無駄にリソースを使う
リポジトリが大きくなればなるほど、余分なタブやスペースの総バイト数が増加

技術要素の選定・採用基準

先を見る

先を見ていない場合

流行っているからという理由だけで飛びつく
後にオワコンになり、学習コストが高く、デメリットしかなくなる
その技術要素を採用する理由があるかをゼロベースで考えることができる人がいない

ツール・フレームワーク等をできるだけ自作しない

不要に自作する場合

オペレーションに問題がある
変えるべきはほとんどの場合、自社・自分自身であり、ツール側ではないことを認識していない
膨大で保守性が低い、自作デプロイスクリプト・ツール
各種バージョンアップをしにくくなる(自作フレームワークの場合)
バージョンアップができないことによる脆弱性・情報漏洩リスクの上昇
変にカスタマイズした結果作者の意図とは異なる使い方をされるリスクの増大
秘伝のXXXXシリーズで後の開発者のコストが高くなる
個人やコミュニティが開発したものを使用している場合メンテされなくなる
プロビジョニング用サーバーの保守運用が発生する
CI用サーバーの保守運用が発生する
保守運用コストの増大

用語・名称マスター

英語にする

英語にしない場合

マルチバイト起因の脆弱性
日本語や英語での似たような表現が散乱する

省略語

明確にしていない場合

様々な省略語が散乱する

連続した数値に意味を持たせない

0012→A
0013→B

連続した数値に意味を持たせる場合

直感的なソースの理解を妨げてしまう

同じ対象の名称を統一する

同じ対象の名称を統一しない場合

同じもので、異なった名称が散乱する

※明確に用語・名称の定義をしていないことが原因です


ディレクトリ分けの方針・各ディレクトリの役割の方針

実装時にどれを使うか

実装時にどれを使うかの方針が明確ではない場合

毎回機能の使用ではなく、実装が必要になる
工数が多くなる
人数が必要になる
バグが発生しやすくなる
テスト工数の増大
似たような処理が散乱するようになっていく
※リポジトリのサイズが大きくなる
レビュー時間が長くなる
出戻りが発生する

キャッシュの方針

明確ではない場合

キャッシュに接続できない場合にアプリケーションが機能しない
いろんな所でキャッシュされてしまう

リポジトリを分ける方針

明確ではない場合

リポジトリが巨大になる
デプロイ時間が長くなる
インフラがどんぶり勘定になる

各サーバーの役割

1サーバー1機能ではない場合(機能が共存している場合)

※フロント、管理画面、バッチ、API、BFF API

フロントの問題で他の機能に影響が出る
管理画面の問題で他の機能に影響が出る
バッチの問題で他の機能に影響が出る
APIの問題で他の機能に影響が出る
BFF APIの問題で他の機能に影響が出る

パラメーターの複数対応

複数対応していない場合

APIに必要以上にアクセスしてしまうリスクが高くなる
※複数必要になった段階で改修しない場合

RDB設計

カラム設計

アプリケーション側での処理を考慮できていない場合

SQLでできることをアプリケーション側で計算することになる

データの特性を考えていない場合





汎用的にするか目的に特化したものにするか

明確にしていない場合

似たような処理が散乱する
他の目的に他の目的が混ざってスパゲティコードになる

どこをDRYでどこを疎にするか

明確ではない場合

モノリシックでDRYなインフラができる

バリデーションの方針

明確ではない場合

同じ値を複数回バリデーションして無駄に処理をしてしまう

データの存在の担保の方針

明確ではない場合

キーの存在確認が散乱して、無駄に処理をしてしまう

DB(SQL等)/API/アプリケーションでやることの方針

明確ではない場合

アプリケーションでデータを取得して表示するだけにならない
アプリケーション側のコードが複雑になる

DynamoDBのベスト・プラクティス

事前に必要な知識

リレーショナルではない

アプリケーション側でデータの整合性を担保する

キャパシティーユニット

4KBを1秒あたり1回の強力な整合性のある読み込み
or
4KBを1秒あたり2回の結果整合性のある読み込み

制限

http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Limits.html

パーティション

10GB
3000ReadCapacityUnits
1000WriteCapacityUnits

項目サイズ

400KB

項目数

制限なし

裏側の動作

データ

複数のサーバーに配置する

パーティション

  • 作成時
( readCapacityUnits / 3,000 ) + ( writeCapacityUnits / 1,000 ) = initialPartitions (rounded up)
read capacity units / partitions = read capacity units per partition
write capacity units / partitions = write capacity units per partition

パーティションが増えるほどスループットが落ちる

  • 10GBオーバー時
超えたパーティションのみを2つに分割する

※分割されたパーティションは半分のスループットになる

スループットの変更時

  • 対応できない場合
パーティションを2倍にする

パーティションが増えるほどスループットが落ちる

  • 減らした場合
パーティションの数を減らさない

※最初から大きめにしない

スループットを超えた場合

ProvisionedThroughputExceeded例外が発生する

※再試行 or UpdateTable(スループットを増やす)する

http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Programming.Errors.html#Programming.Errors.RetryAndBackoff

クエリ

http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/QueryAndScan.html

Query

プライマリキー or セカンダリインデックスを使用

Scan

テーブル or セカンダリインデックスのすべての項目の読み込み
※一度に 1 つのパーティションしか読み込めない

結果整合性のある読み込みがデフォルトで実行
1024KB / 4KB / 2 = 256の読み込みオペレーション

並列スキャン

テーブル or セカンダリインデックスを複数のセグメントに論理的に分割して、
複数のアプリケーションワーカーがセグメントに対して並列スキャン
  • Segments
特定のワーカーがスキャンするセグメント
  • TotalSegments
並列スキャンの対象となるセグメントの合計数

※アプリケーションで使用されるワーカーの数と同じにする

ローカルセカンダリインデックス

http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/LSI.html

テーブルと同じパーティションキーと異なるソートキーで構成されるインデックス
local secondary indexを作成してQuery or Scan
非キー属性に頻繁にアクセスする場合に設定する
書き込みや更新が多数になる場合はKEYS_ONLYを射影することでサイズを最小限にする

※インデックスに射影されていない属性も取り出せる
※テーブルの一部またはすべての属性がコピーされる
※リクエストした属性が射影されていない場合はテーブルから項目全体を読み込む ※テーブル毎に最大5つ
※最大10GB
※ストレージとプロビジョニング済みのスループットを消費する

グローバルセカンダリインデックス

http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/GSI.html

テーブルのプライマリキーとは異なるプライマリキーによって構成されるインデックス
クエリではベーステーブルから属性をフェッチできません
非キー属性にアクセスすることはできません
キー値は一意である必要がありません
テーブルのパーティションキーとソートキーは必ずインデックスに射影されます

テーブルのベストプラクティス

パーティションキー

個別の値が多数含まれ、できるだけランダムかつ均一に値がリクエストされるようにする
2014-07-09.1
...
2014-07-09.200

急激に増大するキャパシティーは控えめに使用する

データアップロード時に書き込みアクティビティを分散する

パーティションキーとレンジキーの組み合わせ毎にアップロードして分散する

パーティションキーAとレンジキーAのアップロード
パーティションキーAとレンジキーBのアップロード
パーティションキーBとレンジキーAのアップロード
パーティションキーBとレンジキーBのアップロード

※できるだけ多くのサーバーに分散したアップロードをする

時系列データへのアクセスパターンを理解する

すべての項目を単一のテーブルに格納せずに月単位または週単位のテーブルにする
古い項目はS3にバックアップして、テーブル毎削除する

人気の高い項目をキャッシュに格納する

まずキャッシュを見て、なければ初めてDynanoDBに問い合わせる

プロビジョニングされたスループットを調整するときにワークロードの均一性を考慮する

バルクロード時に必要なスループットと通常のスループットは合わせる。

※バルクロード時だけ、スループットを上げて、後で下げるとパーティションが2倍に増えて、スループットが悪化する可能性大

大規模環境でのアプリケーションのテスト

データが増えて、パーティションが増え、スループットが悪化したときのことを考えておく
高いスループット設定をしてから、下げると同じ状況を作り出せる

スループットに対するストレージの比率は大規模と同じようにする
※そもそもパーティション当たり10GBを超えないようにする


項目のベストプラクティス

大規模な設定属性の代わりに 1 対多のテーブルを使用する

なんでも1つのテーブルに格納しない

パーティションキーはID等にする

複数テーブルの使用による多様なアクセスパターンのサポート

表示頻度・アクセス頻度によってテーブルを分ける

パーティションキーはID等にする

大量の属性値を圧縮する

長い文字列は圧縮してから格納する

※圧縮しても400KB超える可能性がある場合には最初からS3に格納する

Amazon S3 に大量の属性値を格納する

画像(DynamoDBにはURL)
長い文字列(DynamoDBにはS3のオブジェクトID)

※S3のオブジェクトIDは最大1024バイト
※&等非推奨の文字はS3のオブジェクトIDに使用しない

http://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/UsingMetadata.html

大量の属性を複数の項目に分割する

XxxxChunksテーブルの作成
親テーブルにはChunkCount,ChunkVersionを置いておく
XxxxChunksテーブルのIDは${親ID}#${ChunkVersion}#${ChunkSequenceNo}

※BatchGetItem・BatchWriteItemを使用する


クエリとスキャンのベストプラクティス

読み込みアクティビティの急激な増大の回避

Scan(Query)時にLimitパラメータを指定して読み込みオペレーションの数を減らす

重要なトラフィック用と記録用にテーブルを分けて1時間毎に分担する
※重要なものをそうでないものを分ける

ミッションクリティカルなテーブルとシャドウテーブルに同時に書き込んでおく
※重要なリクエストと重要でないリクエストをテーブルを分けて対応する

並列スキャンの利用

並列スキャンで高速化

※あくまでデータサイズが大きく、読み込みスループットが十分にある場合で高速化したい場合です
スループットが多量に消費されるので要注意

並列スキャンが適している場合

テーブルのサイズが20GB以上
プロビジョニングされている読み込みスループットが完全に使用されていない
シーケンシャルScanオペレーションでは遅すぎる

TotalSegments の選択

1セグメント/2GBにしてみる

※アプリケーションのワーカー数とセグメント数は合わせる

  • スループットを消費しないうちにScanリクエストが制限される場合
TotalSegmentsの値を増やす
  • Scan リクエストによって、プロビジョニングされたスループットが必要以上に消費される場合
TotalSegmentsの値を減らします

ローカルセカンダリインデックスのベストプラクティス

インデックスの使用は控えめにする

頻繁にクエリを行わない属性では、local secondary index を作成しない

※使用されていないインデックスは、ストレージおよび I/O コストを増大させる

頻繁に更新されず、多数の属性でクエリが行われるテーブルは複数のインデックスを作成する

※readが多く行われ、書き込みは頻繁には発生しないテーブル

多量の書き込みアクティビティが発生するテーブルにはインデックスを設定しない

インデックスが頻繁に変わるため、高コストになってしまう

※インデックスを設定したいものはデータを別のテーブルにコピーする
※インデックスを設定するなら、KEYS_ONLYでもよい

インデックスのサイズは可能な限り小さくする

インデックスが小さいほど高パフォーマンス
頻繁にリクエストを行う属性だけを射影する

ソートキーが少数のデータにだけ存在する項目にインデックスを設定する

writeがreadより多い場合

  • インデックスのエントリーサイズを1KB以下にする
KEYS_ONLYにする

※追加コストが発生しない

  • ALL
異なるソートキーによってテーブルを並べ替えるようにする場合のみ指定する

項目コレクションの拡張の監視

テーブル and インデックスで同じパーティションキーを持つすべての項目が10GBを超えないか

※10GB超えるとItemCollectionSizeLimitExceededExceptionが発生してしまう

http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/LSI.html#LSI.ItemCollections.SizeLimit

グローバルセカンダリインデックス のベストプラクティス

ワークロードが均一になるようにキーを選択する

値の数が多いパーティションキーとソートキーを選択します

グローバルセカンダリインデックス を使用したすばやい検索

小さいグローバルセカンダリインデックスにする

※インデックスに射影するテーブル属性の数が少ないものにする

結果整合性のある読み込みレプリカを作成する

テーブルに書き込んでからそのデータがインデックスに反映されるまでの間に短い伝達遅延がある
スループットの高低で、複数のグローバルセカンダリインデックスを作成する

http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/specifying-conditions.html

参考

docs.aws.amazon.com

http://aws.typepad.com/sajp/2016/12/should-your-dynamodb-table-be-normalized-or-denormalized.html

RDS(RDBMS)とDynamoDB(NoSQL)の使い分けについて

日本ではRDBMSが多用されている件についてですが、よくあるアンチパターンが以下です。

DynamoDB(NoSQL)が最適の場合にRDS(RDBMS)を使っている

何も考えずにRDS(RDBMS)を使用
※モバイル, ウェブ, ゲーム, アドテク, IoTでもRDSを使用
↓
サービスがスケール
↓
アーキテクチャ上の課題が発生する。
※パフォーマンスのボトルネック
※スケーラビリティ
※メンテナンス困難な壮大なSQL
※チューニング工数の増大
※障害が多発
※DBを単一障害点になる
※DBインスタンスが死亡
※スケールアップによる費用増大

DBインスタンスを分けていない

複数アプリケーションのテーブルが同じDBインスタンスに同居しているため、
コネクションプール枯渇の問題、タイムアウトリスクの向上、パフォーマンスの問題が発生する

lambdaからRDSの使用を試みている

リクエストが増える
↓
コネクションが増える
↓
レイテンシの増大

RDBMSの厳密なスキーマ定義の利点を使えていない

テーブルのデータにビジネスルールを適用できておらず、現実の世界でおかしいデータがDBに存在することになる

きちんとAWSの資料に目を通している人が少ないのが上記事象が発生する原因かと思います。

AWSの資料を見て、どういう場合に何を使うのかを見てみます。

まず、それぞれの特徴を見ます。

特徴

項目 RDBMS NoSQL
データモデル テーブル、列、index、テーブル間の関係などはスキーマによって厳密に定義 値、列セット、半構造化 JSON等柔軟。データ取得にはパーティションキー
ACIDプロパティ ACIDをサポートする。アーキテクチャ上の課題が生じやすい。 完全には保証しないが、代わりにRDBMSアーキテクチャ上の課題が生じた場合に、パフォーマンスのボトルネック、スケーラビリティ、複雑な運用、管理コストとサポートコストの上昇といった問題を解消
パフォーマンス クエリ、インデックス、テーブル構造の最適化が必要 チューニング不要
拡張性 スケールアップ(AWS使用料が爆増する) スケールアウト(AWS使用料は変わらない)
API SQL(ORM使用する際はオブジェクトベース) オブジェクトベースの API

ACID

トランザクションが完全に実行されるか一切実行されないか
トランザクションが実行されたら、データが必ずデータベーススキーマに従う
同時発生したトランザクションが相互に独立して実行される
異常発生前の最後の状態まで復旧できる

※ACID(不可分性、一貫性、独立性、永続性)

NoSQLの種類

列指向

データ行ではなく、データ列の読み書きに最適化
必要な総ディスク I/O と、ディスクからロードする必要のあるデータ量が大幅に減少
分析クエリに向いている

Redshift, EMR

ドキュメント指向

JSON等で保存
データを柔軟に整理、保存でき、他の機能を使用する際に必要なストレージを減らせる

パフォーマンス、スケーラビリティを求めるものに向いている
モバイル、ウェブ、ゲーム、アドテク、IOTやその他多くのアプリケーションに向いている

グラフ指向

頂点と辺と呼ばれる有向リンクが格納
SQLでも、NoSQLでも
Amazon DynamoDB and Titan

https://aws.amazon.com/jp/blogs/big-data/building-a-graph-database-on-aws-using-amazon-dynamodb-and-titan/

メモリ内キー値ストア

読み取りの負荷が大きいアプリケーションワークロード
(SNS、ゲーム、メディアの共有、Q&A ポータル
や計算量の多いワークロード(レコメンデーションエンジン))に向いている

キャッシュ, セッション, pub/sub, leaderboards(redis)

Amazon ElastiCache

RDSのユースケース

NoSQLのユースケース以外のもの
パフォーマンスを無視しても、データの整合性を求めるもの
銀行の口座等
※RDBMSのスキーマ定義をビジネス・ルールと完全に一致させることができる場合

データの整合性について

RDBMSではアプリケーションでもバリデーションとDB側でのバリデーションが2重に発生します。
スキーマの定義が完璧でないとRDBMSを使用している利点はなくなります。

余分なオブジェクトについて

RDBMSでORMを使用する場合はオブジェクトを別途作成するため、オーバーヘッドが大きいです
また、最終的にはハッシュとなり、viewに渡されるので、NoSQLが代替になる可能性が高いです

allocated memory by gem
-----------------------------------
    685900  activemodel-5.0.1
    580514  activerecord-5.0.1
     71540  activesupport-5.0.1
     12288  mysql2-0.4.5
      8832  2.3.3/lib
      8192  ruby-memory/app
      3633  arel-7.1.4
      2432  concurrent-ruby-1.0.4

参考

http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/SQLtoNoSQL.html

https://aws.amazon.com/jp/nosql/?nc1=h_ls

http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Introduction.html

https://aws.amazon.com/jp/relational-database/

https://aws.amazon.com/jp/nosql/columnar/

https://aws.amazon.com/jp/nosql/document/

https://aws.amazon.com/jp/nosql/graph/

https://aws.amazon.com/jp/nosql/key-value/

Ruby2.3.3での省メモリ化をやってみる

rubyはよく、メモリリークが発生するとか、メモリを爆食いするとか言われています。
明示的にnilを入れて、メモリを開放したり、GC.startしたりして対応する人が多いようです。 そこで、実際どうなのかruby2.3.3で確認してみます。

個人的にはrubyは書きやすくて好きですが、あまり使うべきではないかと思います。
使うならgoですね〜

https://www.techempower.com/benchmarks/#section=data-r13&hw=cl&test=query

環境

$ ruby -v
ruby 2.3.3p222 (2016-11-21 revision 56859) [x86_64-darwin16]
$ rails -v
Rails 5.0.1

準備

$ rails new ruby-memory -T -B -d mysql
$ bundle install --path=vendor/bundle
$ bundle exec rails g scaffold user a:string b:string c:string d: string e: string f: string g: string h: string i: string j: string k: string l: string m: string n: string o: string p: string q: string r: string s: string t: string u: string v: string w: string x: string y: string z: string
$ mysql.server start
※database.ymlのuserとpasswordを調整
$ bundle exec rails db:create
$ bundle exec rails db:migrate
※seeds作成
$ bundle exec rails db:seed 

db/seeds.rb

User.create([
  {
    a: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
    b: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
    c: 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
    d: 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
    e: 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
    f: 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
    g: 'gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg',
    h: 'hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh',
    i: 'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii',
    j: 'jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj',
    k: 'kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk',
    l: 'llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll',
    m: 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
    n: 'nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn',
    o: 'oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo',
    p: 'pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp',
    q: 'qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq',
    r: 'rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr',
    s: 'ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss',
    t: 'tttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt',
    u: 'uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu',
    v: 'vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv',
    w: 'wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww',
    x: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
    y: 'yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy',
    z: 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz',
  },
  ....
])

ソース1

require 'objspace'

class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]

  # GET /users
  # GET /users.json
  def index
    p ObjectSpace.memsize_of_all
    p `ps -o rss= -p #{Process.pid}`.to_i

    @users = User.all
    @users.each do |u|
      p ObjectSpace.memsize_of_all
      p `ps -o rss= -p #{Process.pid}`.to_i
    end
    p ObjectSpace.memsize_of_all
    p `ps -o rss= -p #{Process.pid}`.to_i
  end
  ...
end

結果(1回アクセス)

76413584
107100
  User Load (1.1ms)  SELECT `users`.* FROM `users`
77939342
108876
77942258
108876
...
78041722
108936
78044638
108936

結果(f5長押し)

133268
99497606
133268
99518745
133280
99587787
133300

大量アクセスするとrubyのメモリ解放が追いつかないのか。

ソース2

require 'objspace'

class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]

  # GET /users
  # GET /users.json
  def index
    p ObjectSpace.memsize_of_all
    p `ps -o rss= -p #{Process.pid}`.to_i

    @users = User.all
    @users.each do |u|
      u = nil
      p ObjectSpace.memsize_of_all
      p `ps -o rss= -p #{Process.pid}`.to_i
    end
    p ObjectSpace.memsize_of_all
    p `ps -o rss= -p #{Process.pid}`.to_i
  end
  ...
end

結果(1回アクセス)

69259461
110160
  User Load (0.8ms)  SELECT `users`.* FROM `users`
70789587
110332
70792503
110332
70795419
110332
...
70891687
110340
70894603
110340

結果(f5長押し)

89480104
136112
89483020
136112
89485936
136112
89488852
136112

何もしないよりかは、ましなのかもしれません。

ソース3

require 'objspace'

class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]

  # GET /users
  # GET /users.json
  def index
    p ObjectSpace.memsize_of_all
    p `ps -o rss= -p #{Process.pid}`.to_i

    @users = User.all
    @users.each do |u|
      GC.start
      p ObjectSpace.memsize_of_all
      p `ps -o rss= -p #{Process.pid}`.to_i
    end
    p ObjectSpace.memsize_of_all
    p `ps -o rss= -p #{Process.pid}`.to_i
  end
  ...
end

結果(1回アクセス)

74074939
110832
  User Load (0.7ms)  SELECT `users`.* FROM `users`
66737741
110880
66737141
110880
66737141
110880
66737141
110880
...
66737141
110880
66740057
110880

結果(f5長押し)

69000597
135268
69000597
135268
69000597
135268
69003833
135268

結果としては、GC.startすると一番メモリ使用量は少なくなりました。

rubyGCのことまで考えないといけないので、もはや欠陥スクリプト言語ですかね。

memory関連のgem

https://github.com/SamSaffron/memory_profiler

https://github.com/tagomoris/ruby-memory-usage-profiler

参考

http://blog.inouetakuya.info/entry/2015/07/26/204320

http://2012.rubyworld-conf.org/files/slides/rwc2012_A-5.pdf

rbenvの使い方をまとめてみる

rbenvの使い方で以下をすべてまとめたサイトがないのでまとめてみる。

初回にすべきこと
バージョン切替時
rbenv install -lで最新のバージョン(2.3.3)が表示されるようにする

rbenv

初回

brew install rbenv
rbenv init
echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
source ~/.bash_profile

upgrade

brew upgrade rbenv ruby-build
cd ~/.rbenv
git pull
cd ~/.rbenv/plugins/ruby-build
git pull

install

rbenv install -l
rbenv install 2.3.3
rbenv global 2.3.3
rbenv rehash
gem update --system
gem install bundler
gem install rails

参考

https://github.com/rbenv/rbenv

dockerでローカルにAmazonLinuxの環境構築してみる

手順

  • Authenticate your Docker client to the Amazon Linux container image Amazon ECR registry.
aws ecr get-login --region ap-northeast-1 --registry-ids 137112412989
  • Authenticate your Docker CLI to the registry
docker login -u AWS -p password -e none https://137112412989.dkr.ecr.ap-northeast-1.amazonaws.com
パスワード入力
  • You can list the images within the Amazon Linux repository
aws ecr list-images --region ap-northeast-1 --registry-id 137112412989 --repository-name amazonlinux
  • Pull the Amazon Linux container image using the docker pull command
docker pull 137112412989.dkr.ecr.ap-northeast-1.amazonaws.com/amazonlinux:latest
  • pullしたイメージの確認
docker images
  • Run the container locally.
docker run -it 137112412989.dkr.ecr.ap-northeast-1.amazonaws.com/amazonlinux:latest /bin/bash

bash-4.2# cat /etc/os-release 
NAME="Amazon Linux AMI"
VERSION="2016.09"

exit

※docker execだとexitしても停止されません

  • もう一回ログインしたい場合
docker ps
docker ps -a
docker start ${CONTAINER_ID}
docker attach ${CONTAINER_ID}

いろいろコマンドがないようですが、AWSとしてはミニマムで用意されたようなので、自分でもろもろ入れる必要があります。

参考

http://docs.aws.amazon.com/AmazonECR/latest/userguide/amazon_linux_container_image.html

https://forums.aws.amazon.com/thread.jspa?threadID=242790

CloudFormationで複数アプリケーションでExport名を一意にする

For each AWS account, Export names must be unique within a region.

ということなので、Export名は動的にしないと2つ目のアプリケーションで同じテンプレートを使う場合にエラーになってしまいます。

そこで、Export名を動的にする方法をご紹介致します。

Export名を動的にする

  • スタック生成
  IAMStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub
        - ${S3TemplateURL}/IAM.yaml
        - { S3TemplateURL: !Ref S3TemplateURL }
      TimeoutInMinutes: 5
      Parameters:
        AppName: !Ref AppName
  • Output
Parameters:
  AppName:
    Type: String
....
Outputs:
  FlowLogsRoleArn:
    Value: !GetAtt FlowLogsRole.Arn
    Export:
      Name: !Sub
        - ${AppName}FlowLogsRoleArn
        - { AppName: !Ref AppName }
  • Import
  VPCFlowLog:
    Type: AWS::EC2::FlowLog
    Properties:
      DeliverLogsPermissionArn:
        Fn::ImportValue: !Sub
        - ${AppName}FlowLogsRoleArn
        - { AppName: !Ref AppName }
      LogGroupName: !Sub VPCFlowLogsGroup-${AWS::StackName}
      ResourceId: !Ref VPC
      ResourceType: VPC
      TrafficType: ALL

これでアプリケーション毎にExport名がuniqueになります。

コツとしては一番親のcreate-stack時に渡したパラメーターはExportせずにパラメーターとして子スタックに渡すことです。
そして、各スタックで生成されたものはExportします。

配列に対して、指定する場合はインデントを注意してください。以下のように指定すれば成功します。

      Certificates:
        - CertificateArn:
            Fn::ImportValue: !Sub
              - ${AppName}AcmCertificateArn
              - { AppName: !Ref AppName }

Outputs - AWS CloudFormation

CloudFormationでAPIGatewayのCORSを有効にするには

サーバーレスアーキテクチャの実現にはEC2やECSを使用せずに、S3上のJSからApiGatewayにアクセスできるないといけません。 するとおのずと、ApiGatewayに作成したAPIにCORSを有効にする必要があります。

AWSを使うなら、ちゃんとCloudFormationを使ってインフラをソースで管理するのがよいと思います。
本番のアプリケーションのソースをリビジョン管理せずに直接修正しないのと同じです。
似たような仕組みを作るのに数ヶ月ではなく、10分程度で作れるので、手間がかからず、効率的です。
インフラのリファクタや刷新も楽にできます。

ApiGatewayにCORSを設定していきます。

CORSを有効にする際に必要な設定

コンソールから設定する場合

コンソールから設定する場合に表示される内容

Create OPTIONS method
Add 200 Method Response with Empty Response Model to OPTIONS method
Add Mock Integration to OPTIONS method
Add 200 Integration Response to OPTIONS method
Add Access-Control-Allow-Headers, Access-Control-Allow-Methods, Access-Control-Allow-Origin Method Response Headers to OPTIONS method
Add Access-Control-Allow-Headers, Access-Control-Allow-Methods, Access-Control-Allow-Origin Integration Response Header Mappings to OPTIONS method
Add Access-Control-Allow-Origin Method Response Header to POST method
Add Access-Control-Allow-Origin Integration Response Header Mapping to POST method
Add Access-Control-Allow-Origin Method Response Header to GET method
Add Access-Control-Allow-Origin Integration Response Header Mapping to GET method

Access-Control-Expose-Headers(カンマ区切りの許可するブラウザ), Access-Control-Max-Age(preflightリクエストのキャッシュ秒), Access-Control-Allow-Credentials(credentialsでのアクセス許可をする場合はtrue)はオプションです。

設定される内容

  • OPTIONSメソッド
Mock Endpoint
  • Method Request
  • Integration Request
Type: MOCK

Request body paththrough
→When no template matches the request Content-Type header

Content-Type: application/json
  • Integration Response
Method response status: 200
Default mapping: true

Header Mappings
Access-Control-Allow-Headers: 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'
Access-Control-Allow-Methods: 'POST,GET,OPTIONS'
Access-Control-Allow-Origin: '*'

Content-Type: application/json

Output passthrough: Yes
  • Method Response
HTTP Status: 200

Response Headers for 200
Access-Control-Allow-Headers
Access-Control-Allow-Methods
Access-Control-Allow-Origin

Response Body for 200
Content type: application/json
Models: Empty

上記を実現するyaml

  ${path}OptionsMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      AuthorizationType: NONE
      HttpMethod: OPTIONS
      ResourceId:
        !Ref ${path}Resource
      RestApiId:
        !ImportValue RestApi
      Integration:
        Type: MOCK
        PassthroughBehavior: WHEN_NO_TEMPLATES
        RequestTemplates:
          application/json: "{\"statusCode\" : 200}"
        IntegrationResponses:
          - StatusCode: 200
            ResponseTemplates:
              application/json: "$input.body"
            ResponseParameters:
              method.response.header.Access-Control-Allow-Methods: "'GET,POST,OPTIONS'"
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
              method.response.header.Access-Control-Allow-Origin: !Sub
                - "'http://${Domain}'"
                - { Domain: !ImportValue Domain }
      MethodResponses:
        - ResponseParameters:
            method.response.header.Access-Control-Allow-Methods: true
            method.response.header.Access-Control-Allow-Headers: true
            method.response.header.Access-Control-Allow-Origin: true
          StatusCode: 200

※${path}はAPIのOPTIONSメソッドを追加する対象のパスに変換します
※RestApiはAWS::ApiGateway::RestApiを事前に作成して、Exportしておきます
※${path}ResourceはAWS::ApiGateway::Resourceを事前に作成して、Exportしておきます

実行結果

OPTIONSメソッド

  • Response Body
{}
  • Response Headers
{
  "Access-Control-Allow-Origin":"http://${Domain}",
  "Access-Control-Allow-Methods":"GET,POST,OPTIONS",
  "Access-Control-Allow-Headers":
    "Content-Type,
    X-Amz-Date,
    Authorization,
    X-Api-Key,X-Amz-Security-Token",
  "Content-Type":"application/json"
}

GETメソッド

  • Response Headers
{
  "Access-Control-Allow-Origin":"http://${Domain}",
  "Access-Control-Allow-Methods":"GET,OPTIONS",
  "X-Amzn-Trace-Id":"Root=1-111111111111111111",
  "Access-Control-Allow-Headers":
    "Content-Type,
    X-Amz-Date,
    Authorization,
    X-Api-Key,
    X-Amz-Security-Token",
  "Content-Type":"application/json"
}

Rails5のAPIモードを試してみる

準備

$ ruby -v
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin13]
$ rails -v
Rails 5.0.0.1

new && bundle install

rails new hoge --api -T -B -d mysql
bundle install --path=vendor/bundle

middleware

$ rails middleware
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
run XXXX::Application.routes

※削りたければconfig.middleware.delete ::Rack::Sendfile等で削れるようです。

database.ymlのuser, passwordを設定

CREATE USER 'hoge'@'localhost' IDENTIFIED WITH mysql_native_password BY 'hoge';
UPDATE MYSQL.USER SET xxxx_priv = 'y' WHERE USER = 'hoge';

各種作成

rails g scaffold user
rails db:create

参考

http://edgeguides.rubyonrails.org/api_app.html