【AWS】CodepipelineのSlack通知

1. 使用するサービス

(AWS)

  • Codepipeline

(外部サービス)

  • Slack

2. 概要

開発でCodepieplineを使用していますが、githubからのプルリクの通知はSlackに対して行ってましたが、そういえばCodepipelineからの通知をやって無いなってことで備忘録として残しておきます。

3. 実装

3-1. IAMポリシー作成

下記のようにchatbot用のポリシーを作成し、自身のIAMユーザにアタッチします。

chatbot_policy
chatbotのポリシー

3-2. chatbotを開く

ポリシーがアタッチされたユーザでchatbotをコンソール上で開き、slackを選択します。

chatbot_console
chatbotコンソール上

すると、Slackでログインをしていると、連携許可画面になるので許可します。
後は、通知先のチャンネルを設定して終わりです。

3-3. Codepipelineとの連携

Codepipelineの通知設定をしたいパイプラインからCreate Notificationを選択し、通知設定を作成します。
Events that trigger notificationsで通知したい状態を選択します。基本はSuccessとFaildでいいかと思います。

Codepipeline_notification
Codepipeline通知

4. 終わり
以上で、簡単にCodepipelineのSlack通知が完了します。やってみるとわかりますが、連続でプッシュとかすると鬼のようにうるさいです。開発メンバーのプッシュ頻度とか、通知するチャンネルを考慮して実装して見てください。

【AWS】CloudformationでCognito作成

1. 使用するサービス

(AWS)

  • Cloudformation
  • Cognito

2.概要

Cloudformationを使用してログイン後にトークンを渡し、以降のLambdaとのやり取りをトークンの認証を用いて実装するようなCognitoを作成します。
大体デフォルト設定ですが、注意点コメント入れておきます。

  UserPool:
    Type: "AWS::Cognito::UserPool"
    Properties:
      UserPoolName: !Sub "${Project}-${Stage}-UserPool"
      AdminCreateUserConfig:
          AllowAdminCreateUserOnly: false
          UnusedAccountValidityDays: 7
      AliasAttributes:
        - email
      AutoVerifiedAttributes:
        - email
      UsernameConfiguration:
        CaseSensitive: True
      EmailVerificationMessage: "Your verification code is {####}."
      EmailVerificationSubject: "Your verification code"
      #ログイン後に処理をしたい場合はここにLambda追加
      LambdaConfig:
        PostConfirmation: !GetAtt PostConfirmationFunction.Arn
      MfaConfiguration: 'OFF'
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: true
          RequireUppercase: true
      Schema:
        - AttributeDataType: "String"
          DeveloperOnlyAttribute: false
          Mutable: true
          Name: "email"
          StringAttributeConstraints:
            MaxLength: "2048"
            MinLength: "0"
          Required: true
      SmsAuthenticationMessage:  "Your authentication code is {####}."
      SmsVerificationMessage: "Your verification code is {####}."
  
  
  UserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: !Sub "${Project}-${Stage}-UserPollClient"
      
      #トークン承認の場合は、基本的にimplicitとopenidで大丈夫なはず
   AllowedOAuthFlows:
        - implicit
      AllowedOAuthScopes:
        - openid
      AllowedOAuthFlowsUserPoolClient: true
      GenerateSecret: false
   #ここをCOGNITOにしないとログイン画面でエラーになる
   #必要に応じてFacebookなどプロバイダー追加
      SupportedIdentityProviders:
        - COGNITO
      #ログイン後の画面URLを記載
      CallbackURLs:
       - !Sub "https://${S3DomainName}-${Project}-${Stage}.${HostedZoneName}/index.html"
      #ログアウト後の画面URLを記載
      LogoutURLs:
       - !Sub "https://${S3DomainName}-${Project}-${Stage}.${HostedZoneName}/index.html"
      UserPoolId:
        Ref: UserPool
  
  # Not use custom domain because of this https://github.com/aws-cloudformation/aws-cloudformation-coverage-roadmap/issues/241      
  UserPoolClientDomain:    
    Type: AWS::Cognito::UserPoolDomain
    Properties: 
      Domain: !Sub "${SinginDomain}-${Project}-${Stage}"
      UserPoolId: !Ref UserPool

ログイン画面のドメインはデフォルトの使用します。CLoudformationで一気に独自ドメインまで作成するのは現時点では簡単に実装できません。恐らく、ドメインの紐付けか何かに時間がかかる為?
GUIだと確かに時間はかかるけど簡単にできます。

【AWS】CloudformationでAPI GatewayをHttps化

1. 使用するサービス

(AWS)

  • Cloudformation
  • Cloudfront
  • Route53


2. 概要

CLoudformationでAPI GateawyのHttps化を行います。正確には作成済のAPI Gawewayに対してドメインを作成し、HTTPSで配信可能にします。その際、デプロイするAPIとそのステージをマッピングします。

3. 実装

API Gatewayと証明書、ドメインは作成済とします。

Parameters:

  Project:
    Description: "Project Name"
    Default: "demo"
    Type: String

  Stage:
    Description: "Environment stage"
    Default: dev
    Type: String
    AllowedValues: [dev, staging, prod]
    
  APIDomainName:
    Description: "DomainName, format of which is api-Project-Stage"
    Default: api
    Type: String
    
  WebContentsBucketBaseName:
    Description: "WebContents S3 Bucket Name, fomat of which is Project-Stage"
    Default: webcontents
    Type: String
    
  HostedZoneName:
    Description: "DomainName"
    Default: example.com
    Type: String
   

Resources:

#API Gatewayのドメインを作成します。
#事前に作成済のドメインに合うようなドメインにします。
  ApiGatewayDomainName:
    Type: 'AWS::ApiGateway::DomainName'
    Properties:
      #証明書のArn
      CertificateArn: !Sub "arn:aws:acm:us-east-1:${AWS::AccountId}:certificate/111111111111111111"
      DomainName: !Sub "${Project}-${Stage}-${APIDomainName}.${HostedZoneName}"
      
   #API Gatewayのマッピング
  ApiGatewayDomainMapping:
    Type: 'AWS::ApiGateway::BasePathMapping'
    Properties:
      DomainName: !Ref ApiGatewayDomainName
      Stage: !Ref Stage
      RestApiId: !Ref RestApi

  #ドメインレコードを登録して、名前解決出来るようにする
  Route53RecordSetForAPI:
    Type: 'AWS::Route53::RecordSetGroup'
    Properties:
      HostedZoneName: !Sub "${HostedZoneName}."
      RecordSets:
        - Name: !Sub "${Project}-${Stage}-${APIDomainName}.${HostedZoneName}."
          Type: A
          AliasTarget:
            HostedZoneId: Z2FDTNDATAQYW2
            DNSName: !GetAtt ApiGatewayDomainName.DistributionDomainName

 
以上で、指定したドメインを叩くことでAPI Gatewayからレスポンスが返って来るようになります。

【AWS】CloudformationでRDSと

1. 使用するサービス

(AWS)

  • Cloudformation
  • Lambda
  • Subnet
  • Security Group
  • RDS
  • RDS Proxy

2.概要

仕事でCloudformationを触る機会が増えたので備忘録として残します。最近グローバルリリースされたRDS Proxyを検索しても中々これといったCloudformationで作成した記事が無いので色々、調べた結果作成されたテンプレートを載せます。

3. 実装

3-1. 実装イメージ

各種セキュリティグループでアクセス制御します。RDSのパスワード等はSystem Managerに保管します。

architecture_image
実装イメージ

3-2. テンプレート

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Outputs the time

Parameters:
    
  DBName:
    Description: "DB Name"
    Default: test
    Type: String

Resources:
          
  TestFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.handler
      Runtime: python3.8
      CodeUri: ./test
      Role: !GetAtt TestRole.Arn
      VpcConfig:
        SecurityGroupIds:
          - !Ref TestLambdaSecurityGroup
        SubnetIds:
          - !Ref TestPubSubnet1a
          - !Ref TestPubSubnet1c
      Environment: 
        Variables:
          RDSEndpoint: !GetAtt RDSProxyTest.Endpoint
          MasterUsername: !Sub '{{resolve:secretsmanager:${RDSTestInstanceRotationSecret}:SecretString:username}}'
          MasterUserPassword: !Sub '{{resolve:secretsmanager:${RDSTestInstanceRotationSecret}:SecretString:password}}'
          DBName: !Ref DBName
              
  TestRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "TestRole"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/AmazonRDSFullAccess
        - arn:aws:iam::aws:policy/AmazonEC2FullAccess
        
      MaxSessionDuration: 3600
      Path: "/"
      
  TestVPC:
    Type: AWS::EC2::VPC
    Properties: 
      CidrBlock: 10.0.0.0/16
      Tags:
      - Key: "Name"
        Value: "TestVpc"
        
  TestPubSubnet1a:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: 
        Ref: TestVPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: "ap-northeast-1a"
      Tags:
      - Key: "Name"
        Value: "TestPubSubnet1a"
  
  TestPubSubnet1c:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: 
        Ref: TestVPC
      CidrBlock: 10.0.2.0/24
      AvailabilityZone: "ap-northeast-1c"
      Tags:
      - Key: "Name"
        Value: "TestPubSubnet1c"
  
  TestPriSubnet1a:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: 
        Ref: TestVPC
      CidrBlock: 10.0.3.0/24
      AvailabilityZone: "ap-northeast-1a"
      Tags:
      - Key: "Name"
        Value: "TestPriSubnet1a"
        
  TestPriSubnet1c:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: 
        Ref: TestVPC
      CidrBlock: 10.0.4.0/24
      AvailabilityZone: "ap-northeast-1c"
      Tags:
      - Key: "Name"
        Value: "TestPriSubnet1c"
  
  TestLambdaSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
        GroupDescription: Nothing
        VpcId: 
          Ref: TestVPC
        Tags: 
        - Key: "Name"
          Value: "TestLambdaSecurityGroup"
          
  TestRDSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
        GroupDescription: Allow mysql to client host
        VpcId: 
          Ref: TestVPC
        Tags: 
        - Key: "Name"
          Value: "TestRDSSecurityGroup"
          
  TestRDSSecurityIngress:
   Type: 'AWS::EC2::SecurityGroupIngress'
   Properties:
      GroupId: !Ref TestRDSSecurityGroup
      IpProtocol: tcp
      FromPort: 3306
      ToPort: 3306
      SourceSecurityGroupId: !GetAtt TestRDSProxySecurityGroup.GroupId
  
  
  TestRDSProxySecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
        GroupDescription: Allow mysql to client host
        VpcId: 
          Ref: TestVPC
        Tags: 
        - Key: "Name"
          Value: "TestRDSProxySecurityGroup"
          
  TestRDSProxySecurityIngress:
   Type: 'AWS::EC2::SecurityGroupIngress'
   Properties:
      GroupId: !Ref TestRDSProxySecurityGroup
      IpProtocol: tcp
      FromPort: 3306
      ToPort: 3306
      SourceSecurityGroupId: !GetAtt TestLambdaSecurityGroup.GroupId
      
  
  TestDBInstance:
    Type: 'AWS::RDS::DBInstance'
    Properties:
      AllocatedStorage: 20
      DBInstanceClass: db.t2.micro
      DBInstanceIdentifier: -rds-test
      DBName: !Ref DBName
      DBSubnetGroupName: !Ref TestDBSubnetGroup
      Engine: mysql
      EngineVersion: 5.7.22
      MasterUsername: !Sub '{{resolve:secretsmanager:${RDSTestInstanceRotationSecret}:SecretString:username}}'
      MasterUserPassword: !Sub '{{resolve:secretsmanager:${RDSTestInstanceRotationSecret}:SecretString:password}}'
      PubliclyAccessible: false
      VPCSecurityGroups: 
        - !GetAtt TestRDSSecurityGroup.GroupId
      
  TestDBSubnetGroup:
    Type: 'AWS::RDS::DBSubnetGroup'
    Properties:
      DBSubnetGroupDescription: TestDBSubnetGroup
      DBSubnetGroupName: TestDBSubnetGroup
      SubnetIds:
        - !Ref TestPriSubnet1a
        - !Ref TestPriSubnet1c
        
  RDSProxyTest:
    Type: 'AWS::RDS::DBProxy'
    Properties:
      Auth:
        - AuthScheme: SECRETS
          IAMAuth: DISABLED
          SecretArn: !Ref RDSTestInstanceRotationSecret
      EngineFamily: MYSQL
      DBProxyName: -proxy-test
      IdleClientTimeout: 120
      RequireTLS: false
      RoleArn: !GetAtt SecretsManagerRole.Arn
      VpcSecurityGroupIds: 
        - !Ref TestRDSProxySecurityGroup
      VpcSubnetIds: 
        - !Ref TestPriSubnet1a
        - !Ref TestPriSubnet1c
      
  RDSProxyTargetGroupTest:
    Type: 'AWS::RDS::DBProxyTargetGroup'
    Properties:
      DBProxyName: !Ref RDSProxyTest
      DBInstanceIdentifiers: 
        - !Ref TestDBInstance
      ConnectionPoolConfigurationInfo:
          ConnectionBorrowTimeout: 120
          MaxConnectionsPercent: 100
          MaxIdleConnectionsPercent: 100
      TargetGroupName: default
      
     
  RDSTestInstanceRotationSecret:
    Type: 'AWS::SecretsManager::Secret'
    Properties:
      Description: 'This is my rds instance secret'
      GenerateSecretString:
        SecretStringTemplate: '{"username": "master"}'
        GenerateStringKey: "password"
        PasswordLength: 16
        ExcludeCharacters: '"@/\'
              
  SecretRDSInstanceAttachment:
    Type: 'AWS::SecretsManager::SecretTargetAttachment'
    Properties:
      SecretId: !Ref RDSTestInstanceRotationSecret
      TargetId: !Ref TestDBInstance
      TargetType: 'AWS::RDS::DBInstance'
  
    
  SecretsManagerManagedPolicy:
    Type: 'AWS::IAM::ManagedPolicy'
    Properties:
      Description: "Get values from Secrets Manager"
      Path: /
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action:
              - "secretsmanager:GetResourcePolicy"
              - "secretsmanager:GetSecretValue"
              - "secretsmanager:DescribeSecret"
              - "secretsmanager:ListSecretVersionIds"
            Resource: !Ref RDSTestInstanceRotationSecret
          
          - Effect: Allow
            Action:
              - "kms:Decrypt"
            Resource: !Sub "*"
            Condition:
              StringEquals:
                kms:ViaService: !Sub "secretsmanager.${AWS::Region}.amazonaws.com"
  
  SecretsManagerRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - "rds.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Description: "Use for RDS Proxy"
      ManagedPolicyArns:
        - !Ref SecretsManagerManagedPolicy
      Path: /

何も問題なければこのテンプレートで一から作成されるはずです。
10〜15分くらいデプロイに時間かかると思います。

【AWS】bitbank Stop注文実装

1. 使用するサービス

(AWS)

  • Lambda
  • IAM
  • SSM
  • DynamoDB

 
(外部サービス)

  • Bitbank API 


2.概要

bitbank APIにはデフォルトで逆指値注文など特殊な注文方法がありあません。今回はDynamoDBを使用して事前に設定した価格を超えるまたは下がった場合にMarket注文を行う仕組みを実装します。

3. 実装

3-1. DynamoDBの中身

テーブルを事前に作成しておきます。PrimaryKeyをcoin_pair、SortKeyをPriceにします。bitflyer 自動注文などで作成したテーブルと基本内容は同じです。

buffalokusojima.hatenablog.com

DynamoDB_contents
テーブルの中身

3-2. Lambdaの実装

const AWS = require('aws-sdk');
const ssm = new (require('aws-sdk/clients/ssm'))();
const request = require('request');

const momentTimezone = require('moment-timezone');

const lambda = new AWS.Lambda();

AWS.config.update({region: 'ap-northeast-1'});

const ddb = new AWS.DynamoDB({apiVersion: '2012-08-10'});

exports.handler = (event, context, callback) => {
    
    var XRP_JPY = process.env['XRP_JPY'];
    var dbData;
    getDatafromDynamoDB('stop_check_bitbank', callback)
    .then(function(data){
        
        if(data.Items.length == 0){
          console.log("stop check data not set");
          callback(null, {
              statusCode: 200,
              body: JSON.stringify({message: "price check data not set"}),
              headers: {"Content-type": "application/json"}
            });
          return;
        }
      
        dbData = data.Items;
        
        console.log(dbData);
        
        const method = "GET"
        
        const path = "/xrp_jpy/depth" 
        
        
        var option = {
          url: 'https://public.bitbank.cc' + path,
          method: method,
          headers: {
            'Content-Type': 'application/json'
            }
        }
        
    sendRequest(option,callback)
    .then(function(data){
        
        if(data.response.statusCode != 200){
          console.error("Error:",data.response);
          callback(null, {
            statusCode: data.response.statusCode,
            body: JSON.stringify({message: data.response}),
            headers: {"Content-type": "application/json"}
          });
          return;
        }
        
        
        var price_data = JSON.parse(data.body);
        
        if(price_data == null){
          console.log("price data not Found");
          callback(null, {
            statusCode: 403,
            body: JSON.stringify({message: 'No Data Found'}),
            headers: {"Content-type": "application/json"}
          });
          return;
        }
        
        price_data = price_data.data;
        console.log(price_data);
        
        dbData.forEach(function(item){
          
          if(item.side.S == "buy"){
            if(item.price.N < Number(price_data.asks[0][0])){
              executeOrder(item,callback);
            }
          }else if(item.side.S == "sell"){
            if(item.price.N > Number(price_data.bids[0][0])){
              executeOrder(item,callback);
            }
          }
        });
        
        callback(null, {
            statusCode: 200,
            body: JSON.stringify({message: 'No Order Executed'}),
            headers: {"Content-type": "application/json"}
        });
        return;
    });
    
    });
    function executeOrder(item,callback){
      var param = {
          "coin_pair": item.coin_pair.S,
          "price": item.price.N,
          "size": item.size.N,
          "side": item.side.S,
          "type": "market"
      };
      
      console.log("order will be executed:", param);
                
      var payload = param;
      
      payload = JSON.stringify({body:JSON.stringify(payload)});
      callLambda(payload, callback);
      deleteDataFromDynamoDB('stop_check_bitbank', param, callback);
    }
      
    function getDatafromDynamoDB(table_name, callback){
      
      return new Promise(function (resolve) {
          var params = {
        ExpressionAttributeValues: {
          ':c': {S: 'xrp_jpy'}
        },
        KeyConditionExpression: 'coin_pair = :c',
        ProjectionExpression: 'coin_pair, price, side, size',
        TableName: table_name
      };
    
      // Call DynamoDB to add the item to the table
      ddb.query(params, function(err, data) {
          if (err) {
            console.log("Error", err);
            callback(null, {
                statusCode: 401,
                body: JSON.stringify({message: err.toString()}),
                headers: {"Content-type": "application/json"}
              });
            resolve(null);
            return;
          }
          resolve(data);
        });
      });
    }
    
    function deleteDataFromDynamoDB(table_name, params, callback){
        var params = {
        TableName: table_name,
        Key: {
                'coin_pair' : {S: params.coin_pair},
                'price' : {N: params.price}
             }
      };
      ddb.deleteItem(params, function(err, data){
        if (err) {
          console.log("Error", err);
          callback(null, {
            statusCode: 400,
            body: JSON.stringify({message: err}),
            headers: {
              "Content-type": "application/json"
            }
          });
          return;
        }
        console.log("Success");
        
        params = params.Key;
        
        callback(null,{
          statusCode: 200,
          body: JSON.stringify({message: 'DeleteItem Success: {coin_pair: ' + params.coin_pair.S
                              + ' price: ' + params.price.N
           }),
          headers: {
            "Content-type": "application/json"
          }
        });
        return;
      });
    }
    
    function getParameterFromSystemManager(apikey_name, callback) {
    
        return new Promise(function (resolve) {
            var apikey = process.env[apikey_name];
            
            if(!apikey || typeof apikey == undefined){
            
                // Fetches a parameter called REPO_NAME from SSM parameter store.
                // Requires a policy for SSM:GetParameter on the parameter being read.
                var params = {
                    Name: apikey_name,
                    /* required */
                    WithDecryption:true
                };
                
                ssm.getParameter(params, function(err, apikey) {
                    if (err){
                        console.error(err.stack);
                        callback(null,{
                            statusCode: 500,
                            body: JSON.stringify({message: err.toString()}),
                            headers: {"Content-type": "application/json"}
                        });
                        resolve(null);
                        return;
                    }
                    process.env[apikey_name] = apikey.Parameter.Value;
                    resolve(apikey.Parameter.Value);
                });
            }else resolve(apikey);
        });
    }
    
    function callLambda(payload, callback){
        return new Promise(function (resolve) {
            
            var params = {
                FunctionName: "bitbank-project-pipeline-stac-ControlOrderFunction-114U1VFTL0EE0",
                InvocationType: "RequestResponse",
                Payload: payload
            }
            console.log(params);
            lambda.invoke(params, function(error, res){
                if(error){
                        console.error(error);
                        callback(null,{
                            statusCode: 500,
                            body: JSON.stringify({message: error.toString()}),
                            headers: {"Content-type": "application/json"}
                        });
                        resolve(null);
                    }
                resolve(res);
            }); 
        });          
    }
    
    function sendRequest(option, callback){
        
        return new Promise(function (resolve) {
            request(option, function(error, response, body){
                    
                    if(error){
                        console.error(error);
                        callback(null,{
                            statusCode: 500,
                            body: JSON.stringify({message: error.toString()}),
                            headers: {"Content-type": "application/json"}
                        });
                        resolve(null);
                    }
                    var data = {response, body}
                    resolve(data);
            });
            });
    }
};

CloudWatchで定期的にキックすることでSTOP注文を実現します。

【AWS】bitbank 新規注文

1. 使用するサービス

(AWS)

  • Lambda
  • IAM
  • SSM

 
(外部サービス)

  • bitbank API 


2. 概要

bitbank Apiを使用して、自作の注文サイトを作成します。
基本的な仕組みはbitflyer Lightning Apiで実装したものと同じでURLを叩くのが最終的なゴールです。
buffalokusojima.hatenablog.com


3. 実装

今回はバックエンド側を実装します。フロントは特にbitflyerと違いはなく、渡すパラメータが異なるのみなので省略します。

大まかな違いとしては、ヘッダの中身とパラメータくらいです。

const ssm = new (require('aws-sdk/clients/ssm'))();
const request = require('request');
const crypto = require('crypto');

exports.handler = function(event, context, callback) {
    
    var apikey;
    var sercretKey;
    
    var body = JSON.parse(event.body);
    
    if(!body){
        console.log("body empty");
          callback(null, {
              statusCode: 400,
              body: JSON.stringify({message: "body empty"}),
              headers: {"Content-type": "application/json"}
            });
        return;
    }
    
    const COIN_PAIR = body.coin_pair;
    
    const PRICE = body.price;
    
    const SIDE = body.side;
    
    const SIZE = body.size;
    
    const TYPE = body.type;
    
    const PARAMETERS = body.parameters;

    const ORDER_METHOD = body.order_method;
    
    if(!TYPE && !PARAMETERS){
        console.log("invalid order:",body);
          callback(null, {
              statusCode: 400,
              body: JSON.stringify({message: "invalid order"}),
              headers: {"Content-type": "application/json"}
            });
        return;
    }
    
    if(COIN_PAIR != 'xrp_jpy'){
        console.log("invalid coin pair");
          callback(null, {
              statusCode: 400,
              body: JSON.stringify({message: "invalid order"}),
              headers: {"Content-type": "application/json"}
            });
        return;
    }
    
    if(!checkElement(body)){
            console.log("Bad body:", body);
            callback(null, {
              statusCode: 400,
              body: JSON.stringify({message: "invalid order"}),
              headers: {"Content-type": "application/json"}
            });
            return;
    }
    
    body = {
        pair: body.coin_pair,
        amount: body.size,
        price: body.price,
        side: body.side,
        type: body.type
    }
    
    getParameterFromSystemManager('bitbank-keys',callback)
    .then(function(data){
       
        apikey = data.split(",")[0];
        sercretKey = data.split(",")[1];
        
        var timestamp = Date.now().toString();
        var method = 'POST';
        var path = '/v1/user/spot/order';
        
        body = JSON.stringify(body);
        console.log(body)
        var text = timestamp + body;
        var sign = crypto.createHmac('sha256', sercretKey).update(text).digest('hex');
        
        var option = {
          url: 'https://api.bitbank.cc' + path,
          method: method,
          headers: {
            'ACCESS-KEY': apikey,
            'ACCESS-NONCE': timestamp,
            'ACCESS-SIGNATURE': sign,
            'Content-Type': 'application/json'
            },
          body: body
        }
        
        return sendRequest(option, callback);
    }).then(function(data){
        data = JSON.parse(data.body)
        console.log(data)
        callback(null, {
              statusCode: 200,
              body: JSON.stringify({message: data.data}),
              headers: {"Content-type": "application/json"}
            });
        return;
    });
    
    
    function checkElement(element){
        if(element.type == 'market' || element.type == 'limit'
        || element.type == 'stop'){
                
            if(element.side != 'buy' && element.side != 'sell'){
                console.log("invalid side");
                return false;
            }
            
            
            element.price = Number(element.price);
            element.size = Number(element.size);
            if(isNaN(element.price) && element.type != 'market'){
                console.log("number invalid:", element.price);
                return false;
            }
            if(isNaN(element.size)){
                console.log("number invalid:", element.size);
                return false;
            }
            
            return true;
        }
        return false;
    }
    
    function getParameterFromSystemManager(apikey_name, callback) {
    
        return new Promise(function (resolve) {
            var apikey = process.env[apikey_name];
            
            if(!apikey || typeof apikey == undefined){
            
                // Fetches a parameter called REPO_NAME from SSM parameter store.
                // Requires a policy for SSM:GetParameter on the parameter being read.
                var params = {
                    Name: apikey_name,
                    /* required */
                    WithDecryption:true
                };
                
                ssm.getParameter(params, function(err, apikey) {
                    if (err){
                        console.error(err.stack);
                        callback(null,{
                            statusCode: 500,
                            body: JSON.stringify({message: err.toString()}),
                            headers: {"Content-type": "application/json"}
                        });
                        resolve(null);
                        return;
                    }
                    process.env[apikey_name] = apikey.Parameter.Value;
                    resolve(apikey.Parameter.Value);
                });
            }else resolve(apikey);
        });
    }
    
    
    function sendRequest(option, callback){
        
        return new Promise(function (resolve) {
            request(option, function(error, response, body){
                    
                    if(error){
                        console.error(error);
                        callback(null,{
                            statusCode: 500,
                            body: JSON.stringify({message: error.toString()}),
                            headers: {"Content-type": "application/json"}
                        });
                        resolve(null);
                    }
                    var data = {response, body}
                    resolve(data);
            });
            });
    }
}

4. おわりに

AWS開発再開しているのでまた何か発見があれば載せます。

【AWS】Cognito でLambdaアクセス認可(Cors対応)

1. 使用するサービス

(AWS)

  • Lambda
  • Cognito

2. 概要

ApiGatewayの認可機能の1つとしてCognitoを選択できます。その設定の仕方とcorsを適用している場合のレスポンスの留意点を説明します。
前提としてCognitoで既にユーザプールは作成済とします。

3. 実装

3-1. Authorizer作成

ApiGatewayから認可機能をつけるApiを選択し、Authroizer項目を選択します。

Authorizer_setting
Authorizer設定

Cognito User Poolには事前に作成したUser Poolが選択できるはずなので、それを選んでください。


3-2. メソッドに対して認証をつける

認可機能を付与したいメソッドのリクエストを選択し、Authorizationに作成したAuthorizerを選択します。

authorizer_attached
メソッドにAuthorizer付与


3-3. 認可確認

以上の設定で、クライアントの通信するヘッダにAuthorizationを付与し、値に事前にCognitoへの認証で得たidTokenを入れます。
認可が成功すると、ApiGatewayで設定した通りにレスポンスが返ってきます。主にCorsの部分が気になると思います。
認可が正しくできた場合はCorsが上手く機能しますが、認可が失敗した場合は恐らく403のCorsエラーが出るかと思います。

No 'Access-Control-Allow-Origin' header is present on the requested resource.

よく見るエラーかと思います。
原因としては認可失敗の場合、Access-Control-Allow-Origin Headerが無いからである。

その設定はApiGatewayのGateway Responseから行います。
レスポンスヘッダに該当のヘッダを付与することで、Unauthoriedを返す場合にレスポンスヘッダに追加したヘッダを付与できます。

edit_unauthorized_response
Unauthorizedのレスポンス編集

これにより、認可失敗の場合でも正しい形でレスポンスを遅れます。これが無いとクライアント側は何故エラーがあったのか(Unauthorized)がわかりません。