( 참고: https://amazon-dynamodb-labs.com/hands-on-labs.html )

실습편

 

CloudFormation을 통한 환경 구성

퍼블릭 서브넷 1개 / 프라이빗 서브넷 3개 / 퍼블릭 서브넷에 배포된 AWS Cloud 9 환경

더보기
AWSTemplateFormatVersion: "2010-09-09"

# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
# software and associated documentation files (the "Software"), to deal in the Software
# without restriction, including without limitation the rights to use, copy, modify,
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Description: >
  This template builds a VPC with 1 public and 3 private subnets.

Parameters:
  vpccidr:
    Type: String
    MinLength: 9
    MaxLength: 18
    AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
    ConstraintDescription: Must be a valid CIDR range in the form x.x.x.x/16
    Default: 10.20.0.0/16
  AppPublicCIDRA:
    Type: String
    MinLength: 9
    MaxLength: 18
    AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
    ConstraintDescription: Must be a valid CIDR range in the form x.x.x.x/22
    Default: 10.20.1.0/24
  AppPrivateCIDRA:
    Type: String
    MinLength: 9
    MaxLength: 18
    AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
    ConstraintDescription: Must be a valid CIDR range in the form x.x.x.x/22
    Default: 10.20.2.0/24
  AppPrivateCIDRB:
    Type: String
    MinLength: 9
    MaxLength: 18
    AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
    ConstraintDescription: Must be a valid CIDR range in the form x.x.x.x/22
    Default: 10.20.3.0/24
  AppPrivateCIDRC:
    Type: String
    MinLength: 9
    MaxLength: 18
    AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})"
    ConstraintDescription: Must be a valid CIDR range in the form x.x.x.x/22
    Default: 10.20.4.0/24
  IdeType:
    Type: String
    Default: "t3.medium"
  ProjectTag:
    Type: String
    Default: "dynamodb-labs"

Resources:
  VPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: !Ref vpccidr
      EnableDnsHostnames: 'true'
      EnableDnsSupport: 'true'
      Tags:
        -
          Key: Project
          Value: !Ref ProjectTag
        -
          Key: Name
          Value: !Join ["", [!Ref ProjectTag, "-VPC"]]
  IGW:
    Type: "AWS::EC2::InternetGateway"
    Properties:
      Tags:
        -
          Key: Project
          Value: !Ref ProjectTag
        -
          Key: Name
          Value: !Join ["", [!Ref ProjectTag, "-IGW"]]
  GatewayAttach:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      InternetGatewayId: !Ref IGW
      VpcId: !Ref VPC
  SubnetPublicA:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: !Select [0, !GetAZs ]
      CidrBlock: !Ref AppPublicCIDRA
      MapPublicIpOnLaunch: true
      VpcId: !Ref VPC
      Tags:
        -
          Key: Project
          Value: !Ref ProjectTag
        -
          Key: Name
          Value: !Join ["", [!Ref ProjectTag, "-Subnet-PublicA"]]
  SubnetPrivateA:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: !Select [0, !GetAZs ]
      CidrBlock: !Ref AppPrivateCIDRA
      MapPublicIpOnLaunch: false
      VpcId: !Ref VPC
      Tags:
        -
          Key: Project
          Value: !Ref ProjectTag
        -
          Key: Name
          Value: !Join ["", [!Ref ProjectTag, "-Subnet-PrivateA"]]
  SubnetPrivateB:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: !Select [1, !GetAZs ]
      CidrBlock: !Ref AppPrivateCIDRB
      MapPublicIpOnLaunch: false
      VpcId: !Ref VPC
      Tags:
        -
          Key: Project
          Value: !Ref ProjectTag
        -
          Key: Name
          Value: !Join ["", [!Ref ProjectTag, "-Subnet-PrivateB"]]
  SubnetPrivateC:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: !Select [2, !GetAZs ]
      CidrBlock: !Ref AppPrivateCIDRC
      MapPublicIpOnLaunch: false
      VpcId: !Ref VPC
      Tags:
        -
          Key: Project
          Value: !Ref ProjectTag
        -
          Key: Name
          Value: !Join ["", [!Ref ProjectTag, "-Subnet-PrivateC"]]
  SubnetRouteTableAssociatePublicA: # Associates the subnet with a route table - passed via import
    DependsOn: SubnetPublicA
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      RouteTableId: !Ref RouteTablePublic
      SubnetId: !Ref SubnetPublicA
  SubnetRouteTableAssociatePrivateA: # Associates the subnet with a route table - passed via parameter
    DependsOn: SubnetPrivateA
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      RouteTableId: !Ref RouteTablePrivateA
      SubnetId: !Ref SubnetPrivateA # Associates the subnet with a route table - passed via parameter
  SubnetRouteTableAssociatePrivateB: # Associates the subnet with a route table - passed via parameter
    DependsOn: SubnetPrivateB
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      RouteTableId: !Ref RouteTablePrivateB
      SubnetId: !Ref SubnetPrivateB # Associates the subnet with a route table - passed via parameter
  SubnetRouteTableAssociatePrivateC: # Associates the subnet with a route table - passed via parameter
    DependsOn: SubnetPrivateC
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      RouteTableId: !Ref RouteTablePrivateC
      SubnetId: !Ref SubnetPrivateC # Associates the subnet with a route table - passed via parameter
  RouteDefaultPublic:
    Type: "AWS::EC2::Route"
    DependsOn: GatewayAttach
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref IGW
      RouteTableId: !Ref RouteTablePublic
  RouteTablePublic:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
  RouteDefaultPrivateA:
    Type: "AWS::EC2::Route"
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGatewayA
      RouteTableId: !Ref RouteTablePrivateA
  RouteDefaultPrivateB:
    Type: "AWS::EC2::Route"
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGatewayA
      RouteTableId: !Ref RouteTablePrivateB
  RouteDefaultPrivateC:
    Type: "AWS::EC2::Route"
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGatewayA
      RouteTableId: !Ref RouteTablePrivateC
  RouteTablePrivateA:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
  RouteTablePrivateB:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
  RouteTablePrivateC:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref VPC
  EIPNatGWA:
    DependsOn: GatewayAttach
    Type: "AWS::EC2::EIP"
    Properties:
      Domain: vpc
  NatGatewayA:
    Type: "AWS::EC2::NatGateway"
    Properties:
      AllocationId: !GetAtt EIPNatGWA.AllocationId
      SubnetId: !Ref SubnetPublicA
      Tags:
        -
          Key: Project
          Value: !Ref ProjectTag
        -
          Key: Name
          Value: !Join ["", [!Ref ProjectTag, "-NatGWA"]]

  DynamoDBLabsIDE:
    Type: AWS::Cloud9::EnvironmentEC2
    Properties:
      Description: "Cloud 9 IDE"
      InstanceType: !Ref IdeType
      SubnetId: !Ref SubnetPublicA
      Tags:
        -
          Key: Project
          Value: !Ref ProjectTag
        -
          Key: ProjectName
          Value: !Join ["", [!Ref ProjectTag, "-Ide"]]

Outputs:
  VpcId:
    Description: VPC ID
    Value: !Ref VPC
  SubnetIdPublicA:
    Description: Subnet ID for first public subnet
    Value: !Ref SubnetPublicA
  SubnetIdPrivateA:
    Description: Subnet ID for first private subnet
    Value: !Ref SubnetPrivateA
  SubnetIdPrivateB:
    Description: Subnet ID for second private subnet
    Value: !Ref SubnetPrivateB
  SubnetIdPrivateC:
    Description: Subnet ID for third private subnet
    Value: !Ref SubnetPrivateC
  RouteTableIdPrivateC:
    Value: !Ref RouteTablePrivateC
  RouteTableIdPrivateB:
    Value: !Ref RouteTablePrivateB
  RouteTableIdPrivateA:
    Value: !Ref RouteTablePrivateA

 

Cloud9 콘솔

aws sts get-caller-identity

위의 명령어로 AWS 자격 증명이 올바르게 구성되었는지 확인

 

aws dynamodb create-table \
    --table-name ProductCatalog \
    --attribute-definitions \
        AttributeName=Id,AttributeType=N \
    --key-schema \
        AttributeName=Id,KeyType=HASH \
    --provisioned-throughput \
        ReadCapacityUnits=10,WriteCapacityUnits=5

aws dynamodb create-table \
    --table-name Forum \
    --attribute-definitions \
        AttributeName=Name,AttributeType=S \
    --key-schema \
        AttributeName=Name,KeyType=HASH \
    --provisioned-throughput \
        ReadCapacityUnits=10,WriteCapacityUnits=5

aws dynamodb create-table \
    --table-name Thread \
    --attribute-definitions \
        AttributeName=ForumName,AttributeType=S \
        AttributeName=Subject,AttributeType=S \
    --key-schema \
        AttributeName=ForumName,KeyType=HASH \
        AttributeName=Subject,KeyType=RANGE \
    --provisioned-throughput \
        ReadCapacityUnits=10,WriteCapacityUnits=5

aws dynamodb create-table \
    --table-name Reply \
    --attribute-definitions \
        AttributeName=Id,AttributeType=S \
        AttributeName=ReplyDateTime,AttributeType=S \
    --key-schema \
        AttributeName=Id,KeyType=HASH \
        AttributeName=ReplyDateTime,KeyType=RANGE \
    --provisioned-throughput \
        ReadCapacityUnits=10,WriteCapacityUnits=5

aws dynamodb wait table-exists --table-name ProductCatalog && \
aws dynamodb wait table-exists --table-name Reply && \
aws dynamodb wait table-exists --table-name Forum && \
aws dynamodb wait table-exists --table-name Thread

create-table 명령어를 통해 테이블을 생성하고 wait 명령어를 활용해 테이블을 하나씩 생성하도록...

https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/SampleData.html

 

wget https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/samples/sampledata.zip

unzip sampledata.zip

샘플 데이터를 다운로드하고 압축을 푼다.

 

aws dynamodb batch-write-item --request-items file://ProductCatalog.json

aws dynamodb batch-write-item --request-items file://Forum.json

aws dynamodb batch-write-item --request-items file://Thread.json

aws dynamodb batch-write-item --request-items file://Reply.json

batch-write-item CLI를 사용해 샘플 데이터 로드

 

CLI로 DynamoDB 탐색

aws dynamodb scan --table-name ProductCatalog

scan api 를 사용해 테이블을 스캔할 수 있다. 하지만 DynamoDB에서 데이터를 가져올 때 가장 느리고 비싼 방법!

현재는 아이템이 몇 개 없으니까 테스트 가능하다.

(CLI의 데이터 입력 및 출력은 JSON 형식을 활용한다.)

 

aws dynamodb get-item \
    --table-name ProductCatalog \
    --key '{"Id":{"N":"101"}}'

하나의 아이템을 가져오기 위해선 GetItem api 활용! DynamoDB에서 데이터를 가져오는 가장 빠르고 저렴한 방법이다.

 

aws dynamodb get-item \
    --table-name ProductCatalog \
    --key '{"Id":{"N":"101"}}' \
    --consistent-read \
    --projection-expression "ProductCategory, Price, Title" \
    --return-consumed-capacity TOTAL

읽기 일관성에 관한 옵션에는 

--consistent-read: 강력한 일관된 읽기를 원함(쓰기 작업 결과가 다 반영이 된 후에 읽음)

--projection-expression: 요청에서 특정 속성만 반환되도록 지정

--return-consume-capacity: 요청에 의해 소비된 용량 알려줌

위의 결과는 --consistent-read 옵션을 사용했을 때, 1.0 RCU를 사용(항목이 4KB 미만)

해당 옵션을 제거하면 최종적으로 일관된 읽기가 절반의 용량을 사용함!!(0.5 RCU)


이제부턴 쿼리를 사용해 아이템을 읽어올 것!

 

Item Collections은 partition key를 공유하는 collection group.

DynamoDB에서 "query"는 Item collection의 전체 또는 일부를 읽는 특정 의미이다.(RDBMS와 다름)

aws dynamodb scan --table-name Reply

 

Replay 테이블에 데이터에는 Thread 테이블의 항목을 참조하는 Id Attribute가 있음

쿼리 CLI를 통해 Thread 1의 아이템만 가져오자.

aws dynamodb query \
    --table-name Reply \
    --key-condition-expression 'Id = :Id' \
    --expression-attribute-values '{
        ":Id" : {"S": "Amazon DynamoDB#DynamoDB Thread 1"}
    }' \
    --return-consumed-capacity TOTAL

 

Replay 테이블의 정렬 키는 타임스탬프다. 정렬 키 조건을 추가해 특정 시간 이후에 게시된 스레드의 응답만 가져오자.

aws dynamodb query \
    --table-name Reply \
    --key-condition-expression 'Id = :Id and ReplyDateTime > :ts' \
    --expression-attribute-values '{
        ":Id" : {"S": "Amazon DynamoDB#DynamoDB Thread 1"},
        ":ts" : {"S": "2015-09-21"}
    }' \
    --return-consumed-capacity TOTAL

 

키가 아닌 attribute의 기반으로 결과를 제한하기 위한 필터 표현식을 사용하자.

aws dynamodb query \
    --table-name Reply \
    --key-condition-expression 'Id = :Id' \
    --filter-expression 'PostedBy = :user' \
    --expression-attribute-values '{
        ":Id" : {"S": "Amazon DynamoDB#DynamoDB Thread 1"},
        ":user" : {"S": "User B"}
    }' \
    --return-consumed-capacity TOTAL

위와 같은 결과를 얻게 되는데 키 조건 표현식이 2개 아이템(ScannedCount)과 일치하고 필터 표현식이 1개 아이템으로 줄였다는 것을 볼 수 있음..

 

이 밖에도 여러 옵션으로 쿼리 작성 가능하다!


이제는 테이블 스캔 관련 작업을 해보자.

 

scan api는 단일 아이템 컬렉션이 아닌 전체 테이블을 스캔하려고 하기 때문에 스캔에 대한 필터 표현식을 걸어주는게 좋다.

예) User A가 게시한 답장에서 모든 답글 찾기

aws dynamodb scan \
    --table-name Reply \
    --filter-expression 'PostedBy = :user' \
    --expression-attribute-values '{
        ":user" : {"S": "User A"}
    }' \
    --return-consumed-capacity TOTAL

 

데이터를 스캔할 때 서버 측의 1MB 제한에 도달하거나 지정된 --max-items 매개변수 보다 더 많은 아이템이 남아 있는 경우가 있을 수 있다. 이 경우엔 스캔 응답에 NextToken이 포함되며 이를 후속 스캔 호출에 넣어 중단한 위치에서 선택이 가능하다.

예) 위의 예시에서 --max-items를 2로 걸어보자(위의 응답은 3개 였음..)

aws dynamodb scan \
    --table-name Reply \
    --filter-expression 'PostedBy = :user' \
    --expression-attribute-values '{
        ":user" : {"S": "User A"}
    }' \
    --max-items 2 \
    --return-consumed-capacity TOTAL

위의 NextToken을 --starting-token 옵션에 포함해 명령하자.

aws dynamodb scan \
    --table-name Reply \
    --filter-expression 'PostedBy = :user' \
    --expression-attribute-values '{
        ":user" : {"S": "User A"}
    }' \
    --max-items 2 \
    --starting-token eyJFeGNsdXNpdmVTdGFydEtleSI6IG51bGwsICJib3RvX3RydW5jYXRlX2Ftb3VudCI6IDJ9 \
    --return-consumed-capacity TOTAL

데이터 삽입 관련 (put-item)

aws dynamodb put-item \
    --table-name Reply \
    --item '{
        "Id" : {"S": "Amazon DynamoDB#DynamoDB Thread 2"},
        "ReplyDateTime" : {"S": "2021-04-27T17:47:30Z"},
        "Message" : {"S": "DynamoDB Thread 2 Reply 3 text"},
        "PostedBy" : {"S": "User C"}
    }' \
    --return-consumed-capacity TOTAL

 

데이터 업데이트 (update-item)

--condition-expression이 충족되는 경우에만 업데이트 가능!

aws dynamodb update-item \
    --table-name Forum \
    --key '{
        "Name" : {"S": "Amazon DynamoDB"}
    }' \
    --update-expression "SET Messages = :newMessages" \
    --condition-expression "Messages = :oldMessages" \
    --expression-attribute-values '{
        ":oldMessages" : {"N": "4"},
        ":newMessages" : {"N": "5"}
    }' \
    --return-consumed-capacity TOTAL

 

데이터 삭제 (delete-item)

aws dynamodb delete-item \
    --table-name Reply \
    --key '{
        "Id" : {"S": "Amazon DynamoDB#DynamoDB Thread 2"},
        "ReplyDateTime" : {"S": "2021-04-27T17:47:30Z"}
    }'

 

Replay 테이블에서 항목을 제거했으니 Forum 관련 메시지 개수 줄여야함!

aws dynamodb update-item \
    --table-name Forum \
    --key '{
        "Name" : {"S": "Amazon DynamoDB"}
    }' \
    --update-expression "SET Messages = :newMessages" \
    --condition-expression "Messages = :oldMessages" \
    --expression-attribute-values '{
        ":oldMessages" : {"N": "5"},
        ":newMessages" : {"N": "4"}
    }' \
    --return-consumed-capacity TOTAL

이번엔 GSI를 다뤄보자.

 

GSI는 테이블에 이미 데이터가 있더라도 언제든 만들고 제거가 가능하다.

예) PostedBy 속성을 파티션(HASH) 키로 사용, ReplyDateTime으로 정렬된 메시지를 정렬(RANGE) 키로 유지하는 PostedBy-ReplyDateTime-gsi 라는 이름의 GSI 생성

aws dynamodb update-table \
    --table-name Reply \
    --attribute-definitions AttributeName=PostedBy,AttributeType=S AttributeName=ReplyDateTime,AttributeType=S \
    --global-secondary-index-updates '[{
        "Create":{
            "IndexName": "PostedBy-ReplyDateTime-gsi",
            "KeySchema": [
                {
                    "AttributeName" : "PostedBy",
                    "KeyType": "HASH"
                },
                {
                    "AttributeName" : "ReplyDateTime",
                    "KeyType" : "RANGE"
                }
            ],
            "ProvisionedThroughput": {
                "ReadCapacityUnits": 5, "WriteCapacityUnits": 5
            },
            "Projection": {
                "ProjectionType": "ALL"
            }
        }
    }
]'

 

DynamoDB가 GSI를 생성하고 테이블의 데이터를 인덱스로 채우는 데 시간이 걸릴 수 있음.

IndexStatus가 Active가 되어야 함!!

aws dynamodb describe-table --table-name Reply | grep IndexStatus

 

어떤 식으로 사용하게 되냐면...

--index-name query 옵션과 사용하게 됨!

aws dynamodb query \
    --table-name Reply \
    --key-condition-expression 'PostedBy = :pb' \
    --expression-attribute-values '{
        ":pb" : {"S": "User A"}
    }' \
    --index-name PostedBy-ReplyDateTime-gsi \
    --return-consumed-capacity TOTAL

현재는 아이템 수가 적어서 괜찮지만 예를 들어 십만개 이상의 아이템이 있더라도 반환하려는 정확한 아이템을 읽는데만 비용이 든다!

 

GSI 삭제

aws dynamodb update-table \
    --table-name Reply \
    --global-secondary-index-updates '[{
        "Delete":{
            "IndexName": "PostedBy-ReplyDateTime-gsi"
        }
    }
]'

DynamoDB 콘솔 살펴보기

테이블로 들어가 표 항목 탐색을 클릭하면 아이템들을 확인할 수 있다.

아이템들 중 101을 클릭해보면 편집기를 확인할 수 있고, 모든 속성을 보고 수정할 수 있다!


쿼리를 사용해 아이템 컬렉션을 읽어보자.

 

위와 같이 쿼리를 간단하게 사용 가능하다.

필터 또한 추가 가능하다!

 

위와 같이 스캔도 가능!


이번엔 GSI 생성 해보자.

인덱스 생성을 클릭하면,

위와 같이 생성해주면 된다.

생성이 끝나고 다시 해당 테이블에 대해 쿼리를 해주려 들어가면..

Table / index 선택 항목이 생겨 있다!

 

 

 

❗ 여까지 간단하게 DynamoDB 다루는 방법에 대해 알아봤고..

다음엔 백업 + RDBMS에서 마이그레이션 해오는 방법을 알아보도록 하자!! ❗

'Cloud > AWS' 카테고리의 다른 글

[AWS] Windows Server RDP 접속  (0) 2022.12.09
[AWS] Serverless Service - DynamoDB 편(4)  (0) 2022.12.08
[AWS] Serverless Service - DynamoDB 편(2)  (0) 2022.12.05
[AWS] Serverless Service - DynamoDB 편(1)  (0) 2022.12.02
[AWS] AppStream 2.0  (0) 2022.11.26

( 참고: https://www.youtube.com/watch?v=I7zcRxHbo98 )

 

DynamoDB concept 정리

  • (Primary key = Partition key + Sort key), 해당 PK로만 쿼리 가능!!
  • 제약 조건
    • Scaling
      • 처리량(throughput):
        • RCU: 4KB per second / WCU: 1KB per second (독립적)
        • Eventually Consistent는 Strongly Consistent 보다 RCU를 절반만 사용: 8KB per second
      • 사이즈(size): 
        • Item 최대 크기는 400KB(모두 사용하는 건 비권장)
        • 하나의 item에서 한글자만 바뀌어도 전체 item 다시 씀 → item 한 개의 크기를 작게, 여러개의 item으로 넣자!
      • 파티셔닝(partitioning):
        • 각 파티션은 1000 WCU/sec, 3000 RCU/sec (둘 중 하나가 초과되면 파티션 늘어남)
          • 테이블에 10만 RCU 설정 시 파티션의 구조가 100개로 늘어남
        • 데이터 용량이 10GB 초과 시 파티션 늘어남

 

DynamoDB에서 데이터 읽는 방법 (REST API)

  • GetItem
    • Partition key의 정확한 값 지정
    • 정확히 0 또는 1개의 아이템을 반환
    • 아이템 크기에 따라 RCU 사용
  • Query
    • Partition key의 정확한 값 지정
    • 선택적으로 non-key 어트리뷰트에 필터링 조건 추가
    • 일치하는 아이템 반환(여러 개 가능)
    • Key 조건과 일치하는 아이템의 크기에 따라 RCU를 소비하여 단일 결과 반환
  • Scan
    • 키가 아닌 어트리뷰트에 대한 필터 조건 지정(필터 표현식과 일치하는 모든 아이템 반환)
    • 1MB 단위로 스캔 가능(리턴 메시지의 토큰 값으로 다음 1MB API 호출 반복)
    • OLTP(On-Line Transaction Processing) 환경에선 사용 못함
      • OLTP: 여러 과정의 연산이 하나의 트랜잭션으로 실행하는 프로세스(정규화 쪽..)
    • On-Line 마이그레이션 때 고민하게 될거다
      • 하나씩 다 옮겨버리면 되는거니까... (단순 작업)
  • 대표적인 API 제약 조건
    • Query & Scan: 단일 호출로 최대 1MB 반환, 응답 메시지가 1MB 이상이면 LastEvaluatedKey을 이용해 pagination 가능
    • BatchGetItem: 단일 호출로 최대 100개 아이템 혹은 최대 16MB 반환
      • (API 서버와 DynamoDB 사이에 RoundTrip Time을 줄여 한번에 GetItem을 묶어 실행)
    • BatchWriteItem: 단일 호출로 최대 25 PutItem 혹은 DeleteItem 수행하며 최대 16MB 쓰기

 

LSI vs GSI

LSI(Local Secondary Index) GSI(Global Secondary Index)
테이블 생성 시에만 가능 언제든 생성 및 삭제 가능
베이스 테이블과 WCU/RCU 공유 베이스 테이블과 별도의 WCU/RCU
Collection size <= 10GB No size limits
Limit = 5 Limit = 10
Strong Consistency Eventual Consistency

 

DynamoDB 키 디자인 패턴

Tenet

  • 애플리케이션의 usecase가 DynamoDB와 맞는지
    • DynamoDB는 무한대에 가까운 item들 중에서 하나 또는 몇 개의 item을 primary key로 빠르게 찾는걸 잘함
    • 대량의 range 쿼리, fullstacks search, 집계 쿼리 못함
  • 테이블 안에 파티션의 개수가 많을수록 애플리케이션의 데이터 액세스가 여러 개의 파티션에서 일어날 확률이 높아짐
    • Hot Partition의 확률이 낮아짐
    • size가 작은 여러 개의 테이블 보단, 하나의 큰 테이블이 장점이 더 많음!
  • OLTP여야하고 OLAP의 분석이 필요할 경우 DynamoDB web으로 분석 파이프라인 생성해 워크로드 수행 

 

디자인 패턴 및 비정규화

  • 비정규화
    •  " DynamoDB는 무한하게 많은 아이템 중에 하나 혹은 몇 개를 일정한 시간 안에 찾아낸다"
  • RDBMS의 경우 데이터 모델 정규화에 대한 노력을 하는데 DynamoDB는 모든 데이터 액세스 패턴을 알고 시작한다.
    • RDBMS: 데이터 중복을 최소화하여 정규화된 테이블 간의 조인을 이용해 Ad-hoc하게 런타임의 디스크에서 데이터를 읽어 CPU 연산을 이용해 조인을 수행(과거엔 디스크가 더 비싸... 디스크에 최소의 데이터를 저장하고 런타임의 cpu 리소스를 많이 사용하게끔..)
    • NoSQL: 디스크 가격이 싸져 상황이 역전됨. 조인과 같은 복잡한 연산 불가(결과 셋 형태로 입력이 필요하다...?)

  • 간단한 PK 유일 키: 사용자 프로파일, 세션 스토어, 상품 정보

 

status와 date 어트리뷰트를 하나의 복합키로 만들어 검색 조건으로 사용
PK + SK 복합키 예제 2

  • PK + SK 복합키: 
    • SK(Sort key):
      • 많이 사용되는 값: 알파벳, 숫자, 타임스탬프, ULID/KSUID
      • 오름차순, 내림차순 조회 가능
    • 예제: IoT 로그, 소셜 네트워크의 포스트 리스트, 주문 상세정보 혹은 이력 등

PK + SK 복합키 예제 2

  • 노래의 상세 정보만 조회 가능
  • begins_with 연산자를 통해 Did로 시작하는 다운로드 이력만 조회 가능
  • 쿼리 API를 이용해 PK id 1에 모든 아이템을 한번에 조회 가능
Key 디자인 시 지속적으로 생각할 포인트: 내가 보여줄 UI에 있는 데이터를 그대로 저장하는 것

싱글 테이블 디자인

  • 모든 엔티티를 하나의 테이블로 설계하는 방법
  • 장점: 적은 운영 부담, 높은 테이블 최대 성능 및 쓰로틀링 줄어들 수 있음
  • 단점: 높은 러닝 커브, 시계열이나 다른 액세스 패턴의 엔티티에는 적합하지 않음
  • 안티 패턴
    • PK를 UserID로 고정하고 시작하는 습관
      • 일반 사용자와 VIP를 같은 키 디자인으로 해결하려는 습관
      • 대량 트래픽을 유발하는 heavy user를 항상 고민해야함
    • 엔티티 별로 테이블을 만들려는 습관
    • GSI를 많이 사용하려는 습관
  • 예시: 이커머스 애플리케이션 디자인
    • 키 디자인 full-cycle
      • 비즈니스 유즈 케이스 이해 → ER 다이어그램 그리기 → 모든 데이터 액세스 패턴 정리 → 키 디자인 시작
    • 비즈니스 유즈 케이스(엔티티 설정)
      • 고객(customer)은 온라인 샵에 방문하고 여러개의 온라인 상품을 둘러보다 여러개의 상품(products) 주문(order)
      • 주문과 연결된 인보이스(invoice)를 기반해 여러 결제수단을 결합해 지불(해당 영상에선 결제 관련 엔티티 다루지 않음)
      • 구매된 상품들은 하나 혹은 여러 곳의 물류 창고(warehouses)에서 픽업되어 고객의 주소로 배송됨(shipped)
    • ERD 그리기(엔티티 연결 관계)
      •  고객(customer)은 여러개 주문(order) 가능 / 주문(order)은 하나의 (invoice) / 주문(order) 안에 여러개의 상품(product) / 상품(product)은 여러개의 주문(order)에 속함
    • 모든 액세스 패턴 정리
      • 어떤 값으로 어떤 데이터를 조회하겠다. 
      • 실제 워크로드의 경우 읽기/쓰기 모두 정리되어야 함
    • 키 디자인 시작하기
    • 참고: https://github.com/aws-samples/amazon-dynamodb-design-patterns/tree/master/examples/an-online-shop

 

 

 

❗ 솔직히 겁네 어렵네...?! ❗

(출처:  https://www.youtube.com/watch?v=U_GJYMUjiwA&t=2s )

DynamoDB

  • Key-Value 형식의 NoSQL
  • 다수의 서비스들과 컴퓨팅이 결합된 분산 시스템 구조
  • 3 AZ - 3 copy / 수백만의 요청에 한자리 ms 지연시간(모든 규모에서 10ms 미만)
  • Data types: String, Number, Binary, Bool, Null, List, Set of String/Number, Map

 

동작 방식

  • Item: 테이블에 저장되는 레코드
  • 각 Item들은 RDBMS에서 Column 이라 부르는 Attributes로 구성
  • 하나의 테이블은 Partition Key를 기준으로 분산됨
    • Partition Key 필수(사용자 ID와 같은 unique한 데이터)
    • 결국 Partition Key는 물리적인 공간인 파티션을 특정하는 키
  • Partition Key + Sort Key = PK(Primary Key)
    • PK는 유일 조건을 만족해야 함
    • Sort Key: 파티션 내에서 정렬하는 기준 값
  • DynamoDB Secondary Index:
    • LSI(Local Secondary Indexes): 원본 테이블과 파티션 키를 같이 가져감 → 테이블에 속한 인덱스
    • GSI(Global Secondary Indexes):  원본 테이블로부터 복사본을 갖는 별도의 테이블(원본 테이블과 다른 파티션 키를 생성 가능) → Item을 조회하기 위한 또 다른 (PK + SK)를 추가할 수 있음!!
  • DynamoDB의 성능: RCU / WCU

 

SQL / NoSQL

  • RDBMS와 다르게 정규화 과정이 없으며 액세스 패턴에 따라 Item의 Attribute들이 달라질 수 있음
  • 기존에 테이블들로 분리했던 데이터들은 Document 형식으로 데이터 처리 가능

 

 DynamoDB 테이블

  • partition key에 의해 분산되며 같은 노드에서 sort key로 정렬됨
  • 각 Item의 Attribute들은 schemaless 방식으로 순서 및 개수에 상관 없이 저장 가능

 

Partitioning

  • OrderId가 Partition Key, 해당 OrderId는 해시함수에 의해 물리적으로 Partition된 노드로 전환
    • 요청 받은 파티션 키를 파라미터로 Hash Value를 도출하여 파티션이 결정되기 때문에 테이블이 아무리 커져도 Hash Value를 도출해 빠른 속도로 해당 데이터에 접근하기 때문에 일관된 응답 시간을 제공할 수 있음!!
    • Partition Key 연산은 "Equal" 만 가능!!
  • 설정된 용량은 각 노드에 균등하게 배분되어 리소스로 사용됨
    • 1 Partition 의 크기 = 10GB / 1 Item = Max 400 KB / 1 Partition에는 25000개의 Item이 들어감
    • 1 Partition의 최고 RCU = 3000 / 최고 WCU = 1000

  • Sort Key는 동일 파티션 키에 대해서 정렬 기준으로 사용 됨
    • 연산: 선택, 범위 연산 가능(>, >=, <, <=, begin_withs, between)
  • Customer#가 같은 애들은 같은 파티션에 저장되며 Order#에 대해 정렬됨

 

LSI(Local secondary index)

  • Table과 같은 파티션 키를 가지고 sort key를 별도로 구성이 가능하다
  • 용량은 10GB(테이블당 5개)
  • Projection: 인덱스에 어떤 Attribute를 추가할지 결정하는 것
    • KEYS_ONLY / INCLUDE / ALL
  • 최대한 안쓰는 걸 추천!
    • LSI는 파티션 키를 테이블의 파티션 키와 동일하게 설정해야하기 떄문에 두 파티션 키가 같은 데이터를 바라보고 연산한다는 점에서 별루.. 또, 테이블 생성 시에만 생성 가능하고, 삭제 불가능

 

GSI(Global secondary index)

  • Table과 다른 파티션 키를 가질 수 있음 → Item을 조회하기 위한 또다른 (PK + SK) 추가
  • 원본 Table로 부터 새로운 테이블을 만드는 방식(원본 테이블 변경되면 GSI의 테이블도 동기화 되는 구조)
  • 별도의 저장공간 필요(용량 제한 없음)하기 때문에 RCUs/WCUs 설정해야함
    • 원본 Table의 저장 성능을 보장하기 위해 같은 Capacity설정 권장
  • 중요한게... Eventual Consistent Read / Strong Consistent Read 가 있는데 GSI는 Strong Consistent Read 불가!!
    • Eventual Consistent Read: 응답 속도는 빠르지만 최근 완료된 쓰기 작업 결과가 반영되지 않아 최종적인 데이터를 가져오지 않음(3 AZ에서 젤 빠른거 가져옴)
    • Strong Consistent Read: 응답 속도는 느리지만 최근 완료된 쓰기 작업 결과가 반영됨(3 AZ 변경 다 하고 나서 가져옴, GSI 사용 불가)

 

Burst capacity

  • Partition당 사용되지 않는 용량을 최대 5분동안 저장
  • 설정된 용량 이상의 요청이 들어올 경우 저장된 용량을 사용할 수 있음

  • Partition 1,2,3에 미사용 용량이 Partition 4로 넘어가 초과되는 용량을 커버쳐줌
  • 커버를 치지 못할 경우 Throttling 발생 → "Hot Partition"
    • 500 Error 리턴 후 write 요청 실패
    • 특정 파티션에 지속적으로 예기치 않은 요청이 오는 것을 막을 수 있도록 파티션 키의 변경이 필요함
    • 만약 워크로드가 예상된다면, provisioned 모드에서 on-demand 모드로 변경

 

테이블 설계 예시

Hierarchical data structures as items

  • ProductID 2에 대해 type을 보면, albumID의 track에 대해 Attributes가 존재하도록 구성 가능

as documents (JSON)

  • 하나의 ProductID에 대해서 Attribute에 document 형식으로 데이터 관리 가능

 

 

 

❗ 아래의 링크는 DynamoDB 데이터 설계할 때 도움이 많이 될만한 블로그 이다.. ❗

https://zuminternet.github.io/DynamoDB/ 

'Cloud > AWS' 카테고리의 다른 글

[AWS] Serverless Service - DynamoDB 편(3)  (0) 2022.12.07
[AWS] Serverless Service - DynamoDB 편(2)  (0) 2022.12.05
[AWS] AppStream 2.0  (0) 2022.11.26
[AWS] Amazon QuickSight 개념 및 실습  (0) 2022.11.20
[AWS] Amazon Athena 사용법 -3  (0) 2022.11.20

간단하게(?) WebSocket API, Lambda, DynamoDB를 사용해 서버리스 채팅 앱을 구축해보자..

https://docs.aws.amazon.com/ko_kr/apigateway/latest/developerguide/websocket-api-chat-app.html

해당 링크에서 cloudformation 템플릿으로 작업 후에 하나씩 살펴보자..


  • CloudFormation 생성

CloudFormation 템플릿

 

  • WebSocket API 생성

라우팅 선택 표현식 값으로 표현된 속성은 클라이언트가 API 호출할 때마다 전송되는 모든 메시지에 포함되어야 한다.

$request.body.action 의 경우, 클라이언트 요청의 body 내에 "action" 키에 mapping 된 값으로 route 한다는 의미!

 

다음으로 넘어가 $connect / $disconnect / $default 다 추가하기.

(요청과 일치하는 다른 경로가 없을 때 $default 경로를 호출)

통합 연결에서 Lambda 선택 후 각 각 맞는 함수를 넣어준다.

 

  • Test the chat API

wscat 유틸을 사용해 테스트 해보자

 

터미널 환경을 위해 cloud9을 열어주자... 2개를 만들 것!(메시지를 던지는 서버 1 / 메시지를 받는 서버 2)

wscat을 사용하기 위해선 npm이 필요!

npm -v
npm install -g wscat

publish 된 API endpoint에 연결

wscat -c wss://{YOUR-API-ID}.execute-api.{YOUR-REGION}.amazonaws.com/{STAGE}

api gateway의 대시보드를 보면 websocket url 알 수 있음..

위의 명령어 입력 시 connected 가 뜨면 연결 된 것..

DynamoDB에 connect Id 가 들어간 것을 볼 수 있음

sendMessage 함수 테스트를 위해 아래와 같은 JSON 메시지 전달 (cloud9 -1)

던진 메시지를 보기 위한 것..(cloud9 -2)

두 개의 터미널(cloud9 -1,2)에서 연결을 종료 시킨 뒤 다시 dynamoDB를 확인해보면...

 

이제 하나씩 다시 살펴보자!


일단 Lambda 함수들 먼저 보면

Connect / Disconnect / Default / sendMessage 이렇게 4개의 함수가 생성되어 있다.

(물론 모든 함수들은 트리거로 위에서 생성했던 API Gateway 잡혀있음!!)

  • ConnectHandler
const AWS = require('aws-sdk');
      const ddb = new AWS.DynamoDB.DocumentClient();
      exports.handler = async function (event, context) {
        try {
          await ddb
            .put({
              TableName: process.env.table,
              Item: {
                connectionId: event.requestContext.connectionId,
              },
            })
            .promise();
        } catch (err) {
          return {
            statusCode: 500,
          };
        }
        return {
          statusCode: 200,
        };
      };

DynamoDB 테이블에 connectId 값을 넣는 작업

 

  • DisconnectHandler
const AWS = require('aws-sdk');
      const ddb = new AWS.DynamoDB.DocumentClient();
      
      exports.handler = async function (event, context) {
        await ddb
          .delete({
            TableName: process.env.table,
            Key: {
              connectionId: event.requestContext.connectionId,
            },
          })
          .promise();
        return {
          statusCode: 200,
        };
      };

DynamoDB 테이블에 connectId 값에 해당되는 record 삭제

(따라서 위 두 함수의 역할에는 기본적인 Lambda + DynamoDB 에 접근할 수 있는 권한이 필요!)

 

  • DefaultHandler
const AWS = require('aws-sdk');

      exports.handler = async function (event, context) {
        let connectionInfo;
        let connectionId = event.requestContext.connectionId;
      
        const callbackAPI = new AWS.ApiGatewayManagementApi({
          apiVersion: '2018-11-29',
          endpoint:
            event.requestContext.domainName + '/' + event.requestContext.stage,
        });
      
        try {
          connectionInfo = await callbackAPI
            .getConnection({ ConnectionId: event.requestContext.connectionId })
            .promise();
        } catch (e) {
          console.log(e);
        }
      
        connectionInfo.connectionID = connectionId;
      
        await callbackAPI
          .postToConnection({
            ConnectionId: event.requestContext.connectionId,
            Data:
              'Use the sendmessage route to send a message. Your info:' +
              JSON.stringify(connectionInfo),
          })
          .promise();
      
        return {
          statusCode: 200,
        };
      };

잘못된 경로(?)로 들어오면... 들어오는 방법 설명!

 

  • sendMessageHandler
const AWS = require('aws-sdk');
      const ddb = new AWS.DynamoDB.DocumentClient();
      
      exports.handler = async function (event, context) {
        let connections;
        try {
          connections = await ddb.scan({ TableName: process.env.table }).promise();
        } catch (err) {
          return {
            statusCode: 500,
          };
        }
        const callbackAPI = new AWS.ApiGatewayManagementApi({
          apiVersion: '2018-11-29',
          endpoint:
            event.requestContext.domainName + '/' + event.requestContext.stage,
        });
      
        const message = JSON.parse(event.body).message;
      
        const sendMessages = connections.Items.map(async ({ connectionId }) => {
          if (connectionId !== event.requestContext.connectionId) {
            try {
              await callbackAPI
                .postToConnection({ ConnectionId: connectionId, Data: message })
                .promise();
            } catch (e) {
              console.log(e);
            }
          }
        });
      
        try {
          await Promise.all(sendMessages);
        } catch (e) {
          console.log(e);
          return {
            statusCode: 500,
          };
        }
      
        return { statusCode: 200 };
      };

메시지 주고 받게...

(위 두 함수들은 연결된 클라이언트들 호출을 위한 "execute-api:ManageConnections" 권한이 필요하다.)

 

❗ 근데.. 위는 정말 간단하게 메시지 주고 받는 거고... 실제로 사용할 땐 어떤 식으로 사용하는지 궁금하다... ❗

그리고 웹소켓 연결 후 주고받는 메시지들은 어떻게 관리가 되는지, 유실은 없는지 등의 궁금증이 생기네...?

'Cloud > AWS' 카테고리의 다른 글

[AWS] Serverless Service - Lambda 편 (2)  (0) 2022.11.05
[AWS] Serverless Service - Lambda 편 (1)  (0) 2022.11.05
[AWS] Lambda Warm Start / Cold Start  (0) 2022.10.28
[AWS] Fan-Out 시나리오 -1(easy)  (0) 2022.10.11
[AWS] Event-Driven Architecture  (0) 2022.10.11

1편에서 장치 설치 하고 오기!

https://realyun99.tistory.com/153

이어서 진행해보자..


Kinesis Data Streams

  • 스트림 생성

 

DynamoDB

  • DB 생성

 

Lambda

  • IAM 역할 생성 - Lambda

  • IAM 역할 생성 - IoT

권한의 경우 알아서 추가해줌.. 그대로 진행하면 됨

 

  • Lambda 함수 생성 및 트리거 추가

역할의 경우 위에서 만들었던 IAM Lambda 역할을 지정해주자.

 

다음으로 트리거 추가를 하자. 먼저 Kinesis!

함수에 코드를 추가해주자.. 추가 후에 꼭 Deploy 버튼 누르기!!!!!!

더보기
from __future__ import print_function
import base64
import boto3
from boto3.dynamodb.conditions import Key, Attr
import json
import os
import traceback

#-----Dynamo Info change here------
TABLE_NAME = os.environ.get('TABLE_NAME', "default")
DDB_PRIMARY_KEY = "deviceid"
DDB_SORT_KEY = "timestamp"
DDB_ATTR = "temp"
#-----Dynamo Info change here------

dynamodb = boto3.resource('dynamodb')
table  = dynamodb.Table(TABLE_NAME)

'''
This Kinesis data(Json Image) is below
{
    "DEVICE_NAME": $device_name,
    "TIMESTAMP": $TimeStamp(yyyy-mm-ddThh:MM:SS),
    "HUMIDITY" : int,
    "TEMPERATURE" : int
}
'''
def checkItem(str_data):
    try:
        #String to Json object
        json_data = json.loads(str_data)
        # adjust your data format
        resDict = {
            DDB_PRIMARY_KEY:json_data['DEVICE_NAME'],
            DDB_SORT_KEY:json_data['TIMESTAMP'],
            "HUMIDITY": json_data['HUMIDITY'],
            "TEMPERATURE": json_data['TEMPERATURE']

        }
        print("resDict:{}".format(resDict))
        return resDict

    except Exception as e:
        print(traceback.format_exc())
        return None

def writeItemInfo(datas):
    ItemInfoDictList = []
    try:
        for data in datas:
            itemDict = checkItem(data)
            if None != itemDict:
                ItemInfoDictList.append(itemDict)
            # if data does not have key info, just pass
            else:
                print("Error data found:{}".format(data))
                pass

    except Exception as e:
        print(traceback.format_exc())
        print("Error on writeItemInfo")

    return ItemInfoDictList

def DynamoBulkPut(datas):
    try:
        putItemDictList = writeItemInfo(datas)
        with table.batch_writer() as batch:
            for putItemDict in putItemDictList:
                batch.put_item(Item = putItemDict)
        return

    except Exception as e:
        print("Error on DynamoBulkPut()")
        raise e

def decodeKinesisData(dataList):
    decodedList = []
    try:
        for data in dataList:
            payload =  base64.b64decode(data['kinesis']['data'])
            print("payload={}".format(payload))
            decodedList.append(payload)

        return decodedList

    except Exception as e:
        print("Error on decodeKinesisData()")
        raise e

#------------------------------------------------------------------------
# call by Lambda here.
#------------------------------------------------------------------------
def lambda_handler(event, context):
    print("lambda_handler start")

    try:
        print("---------------json inside----------------")
        print(json.dumps(event))
        encodeKinesisList = event['Records']
        print(encodeKinesisList)
        decodedKinesisList = decodeKinesisData(encodeKinesisList)
        # Dynamo Put
        if 0 < len(decodedKinesisList):
            DynamoBulkPut(decodedKinesisList)
        else:
            print("there is no valid data in Kinesis stream, all data passed")

        return

    except Exception as e:
        print(traceback.format_exc())
        # This is sample source. When error occur this return success and ignore the error.
        raise e

 

환경 변수 지정을 위해 설정을 들어가자 (구성 → 환경변수)

DynamoDB 테이블을 넣어주자

 

IoT Core 확인

  • 메시지 라우팅 규칙 생성

(메시지 라우팅 → 규칙)

from 뒤에는 아까 생성했던 디바이스의 topic을 넣어준다.

 

IoT와 Kinesis를 연결해준다. IoT 디바이스에서 데이터를 받아와 Kinesis로 흐르게

Role도 아까 생성했던 iot 역할로 지정해준다.

 

현재 iot 디바이스 연결도 잘 되어 있는 것을 볼 수 있음.

 

  • Kinesis Stream 모니터링

또, 위에서 연결했던 Kinesis의 모니터링을 보면 데이터가 잘 들어오는 것을 볼 수 있다.

(지표들 중에서 GetRecords - 합계 | PutRecords - 합계 값이 0이 아닌지 확인)

시간이 좀 지나야 해당 그래프를 볼 수 있음...!

  • DynamoDB 모니터링

라이브 항목 수 가져오기 클릭

항목 탐색을 확인해보자.

 

  • Lambda 모니터링

(Invocations 그래프가 끝에 떨어지는 모양은 내가 cloud9 실행을 멈췄기 때문/ 스크립트 계속 돌리면 그래프 유지됨.)

 

API 용 Lambda 구성

  • Lambda 함수 만들기

해당 코드를 넣어준다.(Deploy 클릭)

더보기
from __future__ import print_function
import boto3
from boto3.dynamodb.conditions import Key
import datetime
import json
import traceback
import os

#-----Dynamo Info change here------
TABLE_NAME = os.environ.get('TABLE_NAME', "default")
DDB_PRIMARY_KEY = "deviceid"
DDB_SORT_KEY = "timestamp"
#-----Dynamo Info change here------

dynamodb = boto3.resource('dynamodb')
table  = dynamodb.Table(TABLE_NAME)

#------------------------------------------------------------------------
def dynamoQuery(deviceid, requestTime):
    print("dynamoQuery start")
    valList = []
    res = table.query(
        KeyConditionExpression=
            Key(DDB_PRIMARY_KEY).eq(deviceid) &
            Key(DDB_SORT_KEY).lt(requestTime),
            ScanIndexForward = False,
            Limit = 30
        )

    for row in res['Items']:
        val = row['TEMPERATURE']
        itemDict = {
            "timestamp":row['timestamp'],
            "value":int(val)
        }
        valList.append(itemDict)

    return valList

#------------------------------------------------------------------------
# call by Lambda here.
#  Event structure : API-Gateway Lambda proxy post
#------------------------------------------------------------------------
def lambda_handler(event, context):
    #Lambda Proxy response back template
    HttpRes = {
        "statusCode": 200,
        "headers": {"Access-Control-Allow-Origin" : "*"},
        "body": "",
        "isBase64Encoded": False
    }

    try:
        print("lambda_handler start")
        print(json.dumps(event))

        # get Parameters
        pathParameters = event.get('pathParameters')
        deviceid = pathParameters["deviceid"]
        requestTime = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S')

        resItemDict = { deviceid : ""}
        resItemDict[deviceid] = dynamoQuery(deviceid, requestTime)
        HttpRes['body'] = json.dumps(resItemDict)

    except Exception as e:
        print(traceback.format_exc())
        HttpRes["statusCode"] = 500
        HttpRes["body"] = "Lambda error. check lambda log"

    print("response:{}".format(json.dumps(HttpRes)))
    return HttpRes

위에서 했던 것과 같이 DynamoDB 환경변수를 넣어준다.

 

API Gateway

  • REST API 생성

(작업 → 리소스 생성)

(/datas 클릭 후 리소스 생성)

리소스 경로 괄호에 주의하라

(/deviceid에서 매서드 생성)

권한 추가는 확인을 클릭해준다.

위와 같은 화면이 구성되면 완료.

(작업 → CORS 활성화)

/{deviceid} 에서 cors 활성화!

(작업 → API 배포)

/{deviceid} 에서 api 배포를 진행하자.

생성된 url을 복사해두자.

 

DrawGraph.zip을 받고

(js/createGraph.js 파일)

첫번째 줄 device_name에 만들었던 디바이스 입력

두번째 줄 hosturl에 api url 입력

 

다운받았던 폴더에서 html 브라우저에서 실행하면

다음과 같은 그래프가 나오면 성공이다.(해당 그래프 나올 때까지의 시간이 걸릴 수 있음!)

위의 값은 DynamoDB에 저장된 디바이스의 Temperature값을 표시한다.

 

 

❗ 다음엔 실시간 시각화 OpenSearch를 사용해보자. ❗

+ Recent posts