【AWS】ReactをCodepipeineでS3にデプロイ

1. 使用するサービス

(AWS)

  • CodePipeline
  • CodeBuild
  • S3


2.概要

GitHubで管理しているReactソースを指定したブランチをプッシュするとそのブランチの中のReactソースをビルドしてS3にデプロイしてくれる流れを説明します。
前提としてS3は作成済で、GitHubなどソース管理のツールは導入済として特に説明はしません。
CodePipelineの説明は過去記事参照。
buffalokusojima.hatenablog.com

3. 実装

GitHubやCodeCommitのブランチのプッシュに対するCodePipelineのキックは上記の過去記事参考。
CodeBuildから説明します。

3-1. CodeBuildの設定

基本的に過去記事通りで大丈夫です。buildspec.ymlの中は以下のようにします。

Reactソースはappフォルダにある事にして書いています。適宜編集お願いします。

version: 0.2
phases:
  install:
    commands:
      - n stable
      - npm update -g npm
      - node -v
      - npm -v
  pre_build:
    commands:
      - echo Entring app directory
      - cd app
      - npm install
      - npm run lint
  build:
    commands:
      - echo Build started on `date`
      - npm run build
      
  post_build:
    commands:
      #環境変数にCloudfrontのディストリビューションを設定しておく
      - aws cloudfront create-invalidation --distribution-id $DISTRIBUTION --paths "/*"
  
artifacts:
  files:
    - '**/*'
  #S3に入れる中身を指定
  #buildするとbuildフォルダが出来るのでそれを指定
  base-directory: app/build

3-2. Deploy

CodeBuildの作成が終わったら次にS3へのデプロイの設定です。
下記のように入力し、S3のバケットを指定すれば完了です。

deploy_to_s3
S3へのデプロイ

4.おわりに

Reactに関しては2年前に少し勉強した程度で全くわかりませんが、いずれ勉強はするつもりです。
今回、ReactのBuildについて触れ、ソースのbuildとデプロイをする機会があったので備忘録的な感じで載せました。
意外と簡単でした。ローカルだとReact死ぬ程分からなかったですが、S3へのデプロイとなると何故かすんなり行きました。

【AWS】Cloudfront使用時のedge Lambdaを使ったindex.html省略

1. 使用するサービス

(AWS)

  • Cloudformation
  • Cloudfront
  • edge Lambda

2. 概要

CloudfrontとS3を用いたWebサイトのURLがパスを区切った時にindex.htmlを指定しないと、見たいパスが表示されない現象を、Cloudfrontへのアクセスをedge Lambdaで受け取り、パスを変換してくれる構成を説明します。

3. 実装

3-1. Cloudfrontの設定

Cloudformationのテンプレート載せます。パラメータとかは適宜調整してください。
LambdaFunctionAssociationsの部分が今回の対象です。

Cloudfrontnet:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Aliases:
          - !Sub "${S3DomainName}.${HostedZoneName}"
        Enabled: true
        PriceClass: PriceClass_All
        DefaultCacheBehavior:
          TargetOriginId: !Sub "S3-${WebContentsBucketBaseName}-${Project}-${Stage}/*"
          ViewerProtocolPolicy: redirect-to-https
          MinTTL: 0
          AllowedMethods:
            - HEAD
            - GET
            - OPTIONS
          CachedMethods:
            - HEAD
            - GET
          ForwardedValues:
            Headers:
              - Authorization
              - Origin
              - Access-Control-Request-Method
              - Access-Control-Request-Headers
            Cookies:
              Forward: none
            QueryString: false
          LambdaFunctionAssociations:
            - EventType: origin-request
    #3-2で作成するLambdaのArn:version
              LambdaFunctionARN: !Sub "arn:aws:lambda:us-east-1:${AWS::AccountId}:function:urlReplaceFunction:2"

3-2. Lambdaの実装

まず、リージョンをus-east-1にします。Lambdaを通常通りに作成し、triggerにCloudfrontを選択します。すると、下記のようにedge@Lambdaにするか聞かれるので、Deployを選択します。

edge_lambda_deploy
edge@Lambdaデプロイ

実装内容としてはnodejsで以下のようになります。

exports.handler = (event, context, callback) => 
{
   const request = event.Records[0].cf.request;
   const olduri = request.uri;

   const newuri = olduri.replace(/\$/, '\/index.html');

   request.uri = newuri;

   return callback(null, request);
}

4. 以上で、URLのパスだけでアクセス出来るようになります。index.htmlを打ち込む必要がなくなります。

【AWS】CloudformationでAPI GatewayとLambdaでHello World

1. 使用するサービス

(AWS)

  • Cloudformation
  • Lambda

2. 概要

API GatewayとLambdaを使った構成を簡単にCloudformationで実装出来るテンプレートを載せます。CORSにも対応しているのでS3の静的ウェブサイトからの通信にも対応してます。

3. 実装

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

Parameters:

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

  Stage:
    Description: "Environment stage"
    Default: dev
    Type: String
    AllowedValues: [dev, staging, prod]
Resources:

  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub "${Project}-${Stage}-HelloWorldFunction"
      Handler: index.handler
      Runtime: python3.8
      CodeUri: ./hello
      Role: !GetAtt HelloWorldRole.Arn

  HelloWorldRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${Project}-${Stage}-HelloWorldRole" 
      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
        
      MaxSessionDuration: 3600
      Path: "/"

  HelloWorldFunctionApiPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      Principal: apigateway.amazonaws.com
      FunctionName: !Ref HelloWorldFunction

  RestApi:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Body:
        info:
          version: '1.0'
          title: !Sub "${Project}-${Stage}-API"
        paths:
          /hello:
            get:
              produces:
              - "application/json"
              responses:
                "200":
                  description: "200 response"
                  schema:
                    $ref: "#/definitions/Empty"
                  headers:
                    Access-Control-Allow-Origin:
                      type: "string"
                      
              x-amazon-apigateway-integration:
                uri: !Sub >-
                  arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${HelloWorldFunction.Arn}/invocations
                responses:
                  default:
                    statusCode: "200"
                    responseParameters:
                      method.response.header.Access-Control-Allow-Origin: "'*'"
                passthroughBehavior: "when_no_match"
                httpMethod: "POST"
                contentHandling: "CONVERT_TO_TEXT"
                type: "aws"
            options:
              consumes:
              - "application/json"
              produces:
              - "application/json"
              responses:
                "200":
                  description: "200 response"
                  schema:
                    $ref: "#/definitions/Empty"
                  headers:
                    Access-Control-Allow-Origin:
                      type: "string"
                    Access-Control-Allow-Methods:
                      type: "string"
                    Access-Control-Allow-Headers:
                      type: "string"
              x-amazon-apigateway-integration:
                responses:
                  default:
                    statusCode: "200"
                    responseTemplates:
                      application/json: |
                        {}
                    responseParameters:
                      method.response.header.Access-Control-Allow-Methods: "'GET,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: "'*'"
                requestTemplates:
                  application/json: "{\"statusCode\": 200}"
                passthroughBehavior: "when_no_match"
                type: "mock"
              
        definitions:
          Empty:
            type: object
            title: Empty Schema
        x-amazon-apigateway-gateway-responses:
          UNAUTHORIZED:
            statusCode: 401
            responseParameters:
              gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
            responseTemplates:
              application/json: "{\"message\":$context.error.messageString}" 
              
        swagger: '2.0'


  ApiGatewayDeploy:
    Type: AWS::ApiGateway::Deployment
    Properties:
      RestApiId: !Ref RestApi
      Description: "apigateway deployment you need to change Title when you modify Apigateway to deploy"
      StageName: !Ref Stage

4. おわりに

上記のテンプレートで簡単なAPI GatewayとLambdaの実装ができます。CORSの元URLの記載など、細かい部分の修正は必須ですが、とりあえず実装して試してみたいって場合には十分かと思います。

【AWS】CloudformationでSSH接続可能なEC2作成

1. 使用するサービス

(AWS)

  • Cloudformation
  • InternetGateway
  • SecurityGroup
  • EC2


2. 概要

今回は改めて基本に立ち帰り、SSHで外部接続出来るEC2の作成をCloudformationのテンプレートで説明しようかと思います。
構成は単純で、パブリックサブネットに対してVPCにアタッチされたインターネットゲートウェイにルートテーブルが設定され、かつEC2をパブリックサブネットに配置し、セキュリティグループに22番ポートを空けた物を設定して終わりです。
秘密鍵は作成済の物を使用する設定です。

3. 実装

以下、テンプレートコピペでいけるはずです。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Outputs EC2
Parameters:

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

  Stage:
    Description: "Environment stage"
    Type: String
    Default: dev

Resources:

  VPC:
    Type: AWS::EC2::VPC
    Properties: 
      CidrBlock: 10.0.0.0/16
      Tags:
      - Key: "Name"
        Value: !Sub "${Project}-${Stage}-Vpc"

  InternetSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.0.0/28
      AvailabilityZone: "ap-northeast-1c"
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${Project}-${Stage}-internetgateway-subnet"

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${Project}-${Stage}-internetgateway"
          
  InternetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${Project}-${Stage}-internetgateway-route"
          
  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway
      
  InternetSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref InternetRouteTable
      SubnetId: !Ref InternetSubnet
      
  InternetRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      RouteTableId: !Ref InternetRouteTable
      GatewayId: !Ref InternetGateway
    DependsOn: VPCGatewayAttachment

  EC2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: EC2 SecurityGroup
      VpcId: !Ref VPC
      Tags: 
        - Key: "Name"
          Value: !Sub "${Project}-${Stage}-workbench"
      
  EC2SecurityGroupIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref EC2SecurityGroup
      IpProtocol: tcp
      FromPort: '22'
      ToPort: '22'
      CidrIp: 0.0.0.0/0

  EC2:
    Type: AWS::EC2::Instance
    Properties:
      DisableApiTermination: 'false'
      InstanceInitiatedShutdownBehavior: stop
      ImageId: ami-01748a72bed07727c
      InstanceType: t2.micro
      KeyName: testKey #作成済のキーペア名を入力
      Monitoring: 'false'
      Tags:
        - Key: Name
          Value: !Sub "${Project}-${Stage}-workbench" 
      NetworkInterfaces:
        - DeleteOnTermination: 'true'
          Description: Primary network interface
          DeviceIndex: 0
          SubnetId: !Ref InternetSubnet
          GroupSet:
            - !Ref EC2SecurityGroup
            
            
  EC2EIP:
    Type: AWS::EC2::EIP
    Properties:
      InstanceId: !Ref EC2

4. おわりに

以上で、インターネットから接続出来るEC2の作成完了です。
何かインストールしたければ、ユーザデータにパッケージインストールの設定をすればいいと思います。
今回の用途的には外部からの踏み台として使用していて、EC2のセキュリティグループからのMYSQL接続を許可してRDSの接続等に使用していました。

【AWS】CodeBuildをVPCに入れる

1. 使用するサービス

(AWS)

  • CodeBuild
  • InternetGateway
  • NatGateway


2.概要

CodeBuild内でRDSに接続する必要がある場合、VPC内に設置する必要があります。今回はCodeBuildのVPC内設置の仕方について説明します。

大まかな図としては以下になります。

whole_architecture
全体図

①Internetgateway
インターネット接続のゲートウェイVPC自体にアタッチする。

②Nat Gateway
プライベートサブネット内の端末からインターネット接続出来るようにする為のゲートウェイ

③CodeBuild
AWSサービスのビルドツールです。

④RDS Proxy
今回はRDSProxy->RDSの形をとっていて、実際にCodeBuildが接続する先はRDSProxyになります。

⑤RDS
接続先のデータベースサービス

3. 実装

前提として、VPCとCodebuild、各種サブネット、RDSProxy RDSは作成済とします。

3-1. InternetGatewayの作成

ネットに落ちている通りに作成して、作成済のVPCにアタッチしておわりです。

3-2. Nat Gatewayの作成

Subnetはパブリックにしたいサブネットを選択します。Elastic IPAllocate Eastic IPで新規作成します。

NATGateway_create
NATGateway作成

3-3. サブネットのルートテーブル作成

サブネットそれぞれでルートテーブルを作成する。
VPCはどちらも作成済のVPCを洗濯します。

RouteTable_create
ルートテーブル作成

3-4. ルートテーブルの編集をする。
作成したルートテーブルをそれぞれ編集します。
下記のRoutesからedit routesを選択します。

route_table_edit
ルートテーブル編集


①パブリックサブネット側

0.0.0.0/0の転送先を作成したInternetGatewayにします。

public_subnet_route_table
パブリックサブネットルートテーブル編集


②プライベートサブネット側

0.0.0.0/0の転送先を作成したNAT Gatewayにします。

private_subnet_route_table_create
プライベートサブネットルートテーブル作成

3-5 ルートテーブルをサブネットに紐付ける

Subnet Associationsを選択し、Edit subnet associationsを開いてサブネット紐付け画面を開き、パブリック、プライベートそれぞれのサブネットに紐付けをします。

3-6. CodeBuildのVPC設定

CodeBuildの画面を開いて、editを選択してドロップダウンを表示し、environmentを選択肢、環境設定画面を開きます。

CodeBuild_setting
CodeBuild環境設定

VPCの蘭があるので、作成済のVPCを選択し、サブネットにはプライベートサブネットを選択、セキュリティグループにはRDSProxyに接続可能なものを選択します。

4. おわりに

以上で、CodeBuildのVPC内設置は完了です。
CodeBuildの環境設定画面にあるValidate VPC Configボタンを押して成功すれば疎通確認完了となります。

【AWS】Cloud9上でITテスト

1. 使用するサービス

(AWS)

  • Cloud9

2. 使用する言語

3.概要

まず、ここで言う結合テストの目的としては、Cloud9上の関数をAWSのサービス、Lambdaとしてデプロイした後の疎通確認として位置付けます。テストの内容も単体テストと同じです。違いとしては仮想的なLambdaか実際にデプロイしたLambdaに対してテストをするかになります。
実装面に関しては違いのある箇所のみ記載します。
後は前回参照です。
buffalokusojima.hatenablog.com


4. 実装

import unittest
import json
import csv
import logging
import os

import boto3
lambda_client = boto3.client('lambda')

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def readTestData(csvFile):
    
    test_list = []

    with open(csvFile, encoding="UTF-8") as f:
        for line in csv.DictReader(f):
            del line["備考"]
            test_list.append(line)

    return test_line

class TestHandlerClass(unittest.TestCase):

    def test_response(self):
        logger.info("Hello Lambda Test")
        test_file_path = "../Data/hello.csv"
        test_file_path = os.path.join(os.path.dirname(__file__), test_file_path)
        test_list = readTestData(test_file_path)
        logger.info(test_list)
        for test in test_list:
            result = lambda_client.invoke(FunctionName=Project+"-"+Stage+"-HelloWorldFunction")
            self.assertEqual(result['statusCode'], 200)
            result = result['Payload'].read()
            result = json.loads(result)
            self.assertEqual(result['statusCode'], 200)
            self.assertEqual(json.loads(result['body']), test)
            self.assertEqual(result['headers'], {"Content-type": "application/json"})

if __name__ == '__mian__':
    unittest.main()

5. おわりに

これでデプロイ後のLambdaに対してテストが自動で行えます。
主に、単体テストでロジックの確認で、結合テストではRoleやタイムアウト時間などの確認になります。

【AWS】Cloud9上でUnitテスト

1. 使用するサービス

(AWS)

  • Cloud9

2. 使用する言語

3.概要

Cloud9上でPythonのモジュール、unittestを用いたUnitテストに関して記載します。主にテストの対象はLambdaでテストの内容は基本的に与えられたテストデータとそれに対して期待通りのリターンをするかのブラックボックステストになります。

4. 実装

4-1. フォルダ構成

下記のようにhello Lambdaとそれをテストするtest_hello.py、及びテストデータとなるhello.csvを用意します。

demo----hello
| |
| |--index.py
|
|--tests
|
|--Data
| |
| |--hello.csv
|
|--Unit
|
|-test_hello.py

4-2. テスト対象のLambda

下記のようにコードを用意しておきます。

import json

def handler(event, context):
    
    data = {
         "data": "Hello World"
     }
     print("hello world")

     return {
          "statusCode": 200,
          "body": json.dump(data),
          "headers": {"Content-type": "application/json"}
     }

4-3. テストスクリプト

か機能ようにtest_hello.pyのコードを用意しておきます。テストスクリプトのファイル名の最初には必ずtestをつけてください。

import unittest
import json
import csv
import logging
import os
from hello import index

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def readTestData(csvFile):
    
    test_list = []

    with open(csvFile, encoding="UTF-8") as f:
        for line in csv.DictReader(f):
            del line["備考"]
            test_list.append(line)

    return test_line

class TestHandlerClass(unittest.TestCase):

    def test_response(self):
        logger.info("Hello Lambda Test")
        test_file_path = "../Data/hello.csv"
        test_file_path = os.path.join(os.path.dirname(__file__), test_file_path)
        test_list = readTestData(test_file_path)
        logger.info(test_list)
        for test in test_list:
            result = index.handler(None, None)
            self.assertEqual(result['statusCode'], 200)
            self.assertEqual(json.loads(result['body']), test)
            self.assertEqual(result['headers'], {"Content-type": "application/json"})

if __name__ == '__mian__':
    unittest.main()

4-4. テストデータ

下記のようにテストデータとその説明を備考として書いてあります。プログラム内でJSONに変換し、備考の部分は削除します。あくまでレビュー時に見やすくする為に存在します。

"data","備考"
"Hello World", "正常値テスト"

5. 実行

demoフォルダの上のフォルダから実行します。

5-1. 環境変数の更新

下記コマンドでPythonの実行環境を変更します。

export PYTHONPATH=/home/ec2-user/environment/demo

5-2. 下記コマンドでテストを指定して実行します。

python -m unittest demo.tests.Unit.test_hello

5-3 下記コマンドでフォルダ内のtestと名のつくファイルを実行します。

python -m unittest discover demo.tests.Unit


6. おわりに

これで、テストデータを増やすことでテストを自動的に複数実行されるようになります。テストデータもcsvで比較的見やすく、lambdaのコードと一緒にレビューすることで漏れも少なくなるかと思います。また、レビューしてもらう前にはしっかりと事前に実行してテストが完了してから改めてレビューしてもらいましょう。