목차

목차

해당 문서는 CDK 1.x 버전에 맞추어진 버전으로 2022년에는 정상 동작하지 않습니다.

2020-06-01

  • Docker 이미지를 ECR에 등록시 에러 나는 부분 수정
  • 스크린샷 이미지 교체
  • URL cdk.awsdemokr.com 에서 cdk.awsdemo.kr 로 수정 배포

2019-11-14

  • 짧은 URL을 만들고, 통계 분석으로 미연에 장애를 인지할 수 있는 환경을 CDK 기반으로 구축 (Custom Domain 배포 되는 부분 삭제)

문서 URL: https://cdk.awsdemo.kr/hol-short-url


본 실습은 애플리케이션을 개발하기 위하여 CDK를 이용하여 개발하고 인프라 및 개발 소스를 배포하는 방법을 살펴 봅니다.
또한, 배포한 서비스의 성능을 측정하기 위하여 부하를 발생시키고, 부하에 따른 성능을 모니터링 하는 방법을 살펴 봅니다.

실습 설명 참고 영상

Infrastructure is Code with the AWS CDK - AWS Online Tech Talks


아키텍처 다이어그램 소개

본 실습은 2개의 스택으로 구성되어져 있습니다.

  • 첫 번째는 CDK 서비스를 이용한 짧은 URL 생성 애플리케이션인 UrlShortStack입니다.
  • 두 번째는 부하 테스트를 위한 TrafficStack입니다. 부하에 의해서 생성되는 트래픽은 모니터링 대쉬보드 작성을 위하여 CloudWatch의 Dashboard를 사용합니다.


미션 1. CDK 서비스를 이용한 짧은 URL 생성 애플리케이션 구축하기

요구사항 전달

보스의 명령이 하달 되었습니다.

다음주까지 ASAP(아삽)으로 이메일 캠페인 마케팅을 위한 짧은 URL 생성 어플리케이션이 필요합니다!

당신은 DevOps 엔지니어  이며, 능력을 또 한 번 발휘할 기회입니다.
AWS CDK를 이용해서 bit.ly와 같은 기능을 하는 사이트를 만들어 봅시다.


Demo URL

해당 앱의 결과물은 다음과 같은 링크를 가지는 것입니다. 여기서는 API를 위한 Custom domain name(cdk.awsdemo.kr처럼)을 설정하는 방법이 있지만, Route 53에 Domain이 등록되어 있지 않는 것을 가정하고 진행합니다. 미션2는 생략할 수 있습니다.
Demo 테스트 방법: https://cdk.awsdemo.kr?targetUrl=https://www.amazon.com 과 같이 URL을 호출하면 짧은 URL을 반환해 주는 App입니다. 아래와 같은 결과물이 화면에 출력됩니다.

Created URL: https://cdk.awsdemo.kr/19e23fa8

해당 데모는 bit.ly 사이트처럼 Front-end 페이지를 구축하지는 않습니다.(CDK를 이용하여 Back-end를 구축하는 방법에 집중합니다.)


아키텍처 소개

아래와 같이 API를 호출할 때, 쿼리 파라미터 또는 경로에 따라서 short url을 만들거나 short url을 통해 원래 url로 리다이렉트 하는 기능을 구현합니다.
API Gateway로 API를 받고, Lambda가 로직을 처리하고, DynamoDB에 관련 정보를 저장합니다.


개발환경 구성

개발통합환경은 AWS Cloud9을 이용합니다. 개발을 위해서 별도의 개발 서버를 할당 받거나 서버에 런타임 설정을 할 필요가 없습니다.
서비스를 구성하는 리소스는 AWS Cloud Development Kit을 이용해서 개발합니다. 해당 실습은 Python 언어를 이용해서 인프라와 개발을 동시에 진행합니다.

리전 선택

이 애플리케이션은 Seoul  리전에서 개발합니다. AWS 관리 콘솔 우측 하단에 Seoul 를 선택합니다. 화면에 표시되는 언어는 English 를 기본으로 진행합니다.

Elastic IP 생성

VPC의 NAT Gateway가 사용할 EIP를 생성합니다. AWS 관리콘솔에서 VPC 서비스로 이동한 후, Elastic IPs 메뉴를 선택하고, 상단의 Allocate new address 버튼을 클릭합니다.

다음 화면에서 Allocate 버튼을 클릭하면 고정 IP를 한 개 할당 받을 수 있습니다.

VPC 생성하기

부하 테스트를 위해서, Docker 이미지를 ECR에 올리고, Fargate를 통해 서버리스 형태로 배포하기 위해서 VPC와 Private subnet 이 필요합니다.
VPC 서비스의 Dashboard 화면에서 Launch VPC Wizard 버튼을 클릭하여 Public subnet과 Private subnet을 포함하고, NAT Gateway를 포함하는 VPC를 생성합니다.

AWS 관리콘솔에서 VPC 서비스로 이동한 후, VPC Dashboard 화면에서,  Launch VPC Wizard 버튼을 클릭합니다.

VPC 설정 Step 1에서 2번째에 있는 VPC with Public and Private Subnets 을 선택 후, Select 버튼을 클릭합니다.

VPC 설정 Step 2에서 VPC name 에는 CDK-TEST 라는 이름을 입력하고, NAT Gateway 설정의 Elastic IP Allocation ID 는 위 단계에서 할당 받은 EIP를 클릭하고 하단의 Create VPC 버튼을 클릭합니다.

VPC 생성 작업이 진행되고, 아래와 같이 새로운 VPC가 생성됩니다. 해당 VPC ID 를 사용하므로 메모장에 복사 해 놓습니다.

AWS Cloud9 시작하기

만약 기존 실습에서 사용중인 Cloud9 환경이 있을 경우에는 새로 생성할 필요 없이 전 환경에서 진행합니다.

서비스에서 AWS Cloud9으로 이동하여 새로운 AWS Cloud9 환경을 생성합니다. Create Environment 버튼을 클릭합니다.

AWS Cloud9의 Name cdkenv 로 주고, 우측 하단의 Next step 버튼을 클릭합니다.

기본 설정 그대로 Next step 버튼을 클릭합니다.

Review에서 설정된 상태(EC2의 t2.micro 타입)를 확인하고 하단의 Create environment 버튼을 클릭합니다.

Cloud9 환경이 준비되는 것을 볼 수 있습니다. Cloud9이 가용해질 때까지 잠시 기다립니다. (약 1분 내외면 웹 상에서 개발을 할 수 있는 환경을 구축할 수 있습니다.)

AWS-CDK 설치

Cloud 9 최신 환경에는 AWS CDK가 설치되어 있습니다.

cdk --version

만약 설치가 안되어 있다면 아래와 같이 설치하세요.

CDK 개발을 위해서 환경을 설치합니다. Cloud9 하단의 Terminal 에서 아래와 같이 코드를 입력하고 실행합니다.

npm을 이용한 aws-cdk 설치 명령
npm install -g aws-cdk



프로젝트 시작

프로젝트 생성

프로젝트 디렉토리를 생성하고 이동합니다.

프로젝트 폴더 생성
mkdir url-short
cd url-short/

CDK 개발 언어 설정 및 초기화

python을 개발 언어로 설정합니다.

CDK 개발 언어 설정
cdk init --language python


CDK 개발 환경 확인

URL Short Demo 환경이 설치되었습니다. Cloud9의 좌측 폴더 구조를 확인합니다.

virtualenv 환경 진입

virtualenv 환경에서 개발을 합니다. 다음과 같이 virtualenv를 활성화합니다.

virtualenv 진입
source .venv/bin/activate

패키지 설정

setup.py 파일을 열어 데모에 필요한 패키지를 아래와 같이 추가하고 저장 합니다. 저장은 단축키 (Ctrl+S)로도 가능합니다.

setup.py cdk 의존성 패키지 지정
        install_requires=[
            "aws-cdk.core==1.97.0",
            "aws-cdk.aws-dynamodb",
            "aws-cdk.aws-events",
            "aws-cdk.aws-events-targets",
            "aws-cdk.aws-lambda",
            "aws-cdk.aws-s3",
            "aws-cdk.aws-ec2",
            "aws-cdk.aws-ecs-patterns",
            "aws-cdk.aws-apigateway",
            "aws-cdk.aws-cloudwatch",
            "cdk-watchful",
            "boto3"
        ],


패키지 설치

virtualenv 안에서 의존성에 맞추어 패키지를 설치합니다.

의존 패키지 설치
pip install -e .

CDK bootstrap

CDK Toolkit Stack을 만들기 위하여 bootstrap을 합니다.  123456789012 본인 AWS 계정 12자리 숫자로 대체하면 됩니다. AWS CDK로 클라우드 애플리케이션을 관리하는 데 필요한 리소스를 관리하는 S3 버킷도 생성합니다.

CDK Bootstrap
cdk bootstrap aws://123456789012/ap-northeast-2

프로젝트 환경 설정

프로젝트 시작점인 app.py를 열어 봅니다. 직접 Cloud 9 UI의 네비게이션에 있는 파일을 직접 클릭 해서 열 수 있고,  npm install -g c9 과 같이 패키지를 설치한 후에 c9 open app.py 와 같은 형태로 terminal에서도 바로 열 수 있습니다.

소스 코드 중에 UrlShortStack 클래스 정의로 커서를 이동하고, 함수 정의로 이동을 위해서 F3를 누릅니다.

새로운 탭에 url_short_stack.py 파일이 열립니다. 이제 이 스택에 리소스를 하나씩 만들어 나가겠습니다.

리소스 작성

DynamoDB 테이블 리소스 정의

아키텍처 다이어그램에 나와 있던 인프라를 하나씩 만들어 보겠습니다.
DynamoDB부터 만듭니다. CDK API Reference에 들어가면 다양한 리소스를 다룰 수 있는 것을 알 수 있습니다. 

DynamoDB를 생성할 것이므로 아래 aws-dynamodb의 Overview를 들어갑니다. 
https://docs.aws.amazon.com/cdk/api/latest/docs/aws-dynamodb-readme.html
Python에서는 aws_cdk에 aws_dynamodb 패키지를 활용할 수 있는 것을 볼 수 있습니다.

CDK 각 객체에 있는 함수 또는 속성 값들은 대소문자를 구분합니다. 대소문자가 틀리지 않도록 주의합니다.

url_short_stack.py 파일의 첫 줄을 아래와 같이 변경합니다. (, aws_dynamodb 문자 추가)

url_shortener_stack.py에 import
from aws_cdk import core, aws_dynamodb

"# The code that defines your stack goes here" 주석 하단 에 DynamoDB 테이블 추가를 위해 아래에 코드를 추가합니다.
URL Short에서 생성되는 URL을 담아 둘 DynamoDB 테이블의 이름은 'mapping-table'이라 만들고 id에는 Short URL을 위한 코드를 UUID 값으로 넣을 겁니다.

url_shortener_stack.py에서 CDK를 이용한 DynamoDB Table 생성 코드
        table = aws_dynamodb.Table(self, "mapping",
            partition_key = aws_dynamodb.Attribute(name="id",type=aws_dynamodb.AttributeType.STRING))

코드 작성이 완료 되면 아래와 같이 코드가 만들어졌을 것이고 저장합니다.
image2020-6-1_14-45-54.png

CDK deploy로 DynamoDB 테이블 배포

DynamoDB 테이블 추가를 위한 설정이 완료되었으므로 리소스를 배포해야 합니다. 하단의 Terminal에서 CDK 명령을 통해서 Deploy를 합니다.

cdk로 리소스 배포
cdk deploy

Lambda 함수 코드 작성

Lambda 함수를 개발하기 위하여 폴더를 하나 생성합니다. UI에서 lambda라는 폴더를 만들고, 폴더 하단에 handler.py 파일을 생성합니다. (terminal에서 cmd로 만드셔도 됩니다.)

handler.py 파일은 Short URL을 생성하거나, Short URL이 들어올 경우 원래 URL로 Redirect 하는 기능을 제공하는 함수입니다.
다음과 같은 코드를 함수에 넣습니다. 코드를 한 번 읽어보면서 동작되는 원리를 살펴 봅니다.

lambda에서 사용할 handler.py 코드
import json
import os
import uuid
import logging

import boto3

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


def main(event, context):
    LOG.info("EVENT: " + json.dumps(event))

    query_string_params = event["queryStringParameters"]
    if query_string_params is not None:
        target_url = query_string_params['targetUrl']
        if target_url is not None:
            return create_short_url(event)

    path_parameters = event['pathParameters']
    if path_parameters is not None:
        if path_parameters['proxy'] is not None:
            return read_short_url(event)

    return {
        'statusCode': 200,
        'body': 'usage: ?targetUrl=URL'
    }


def create_short_url(event):
    # Pull out the DynamoDB table name from environment
    table_name = os.environ.get('TABLE_NAME')

    # Parse targetUrl
    target_url = event["queryStringParameters"]['targetUrl']

    # Create a unique id (take first 8 chars)
    id = str(uuid.uuid4())[0:8]

    # Create item in DynamoDB
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table(table_name)
    table.put_item(Item={
        'id': id,
        'target_url': target_url
    })

    # Create the redirect URL
    url = "https://" \
        + event["requestContext"]["domainName"] \
        + event["requestContext"]["path"]
    url += '/' if url[-1] != '/' else ''
    url += id

    return {
        'statusCode': 200,
        'headers': {'Content-Type': 'text/plain'},
        'body': "Created URL: %s" % url
    }


def read_short_url(event):
    # Parse redirect ID from path
    id = event['pathParameters']['proxy']

    # Pull out the DynamoDB table name from the environment
    table_name = os.environ.get('TABLE_NAME')

    # Load redirect target from DynamoDB
    ddb = boto3.resource('dynamodb')
    table = ddb.Table(table_name)
    response = table.get_item(Key={'id': id})
    LOG.debug("RESPONSE: " + json.dumps(response))

    item = response.get('Item', None)
    if item is None:
        return {
            'statusCode': 400,
            'headers': {'Content-Type': 'text/plain'},
            'body': 'No redirect found for ' + id
        }

    # Respond with a redirect
    return {
        'statusCode': 301,
        'headers': {
            'Location': item.get('target_url')
        }
    }

Lambda 함수 리소스 정의

Lambda 함수 코드를 만들었으니 Lambad 함수가 리소스로 배포되어야 합니다.
Lambda 함수를 만들기 위해서 AWS CDK를 참고하겠습니다.  https://docs.aws.amazon.com/cdk/api/latest/docs/aws-lambda-readme.html

url_short_stack.py 파일에 aws_lambda를 사용하기 위해서 import에 추가합니다. (, aws_lambda 코드 추가)

url_shortener_stack.py 파일에 CDK Lambda 정보 import
from aws_cdk import core, aws_dynamodb, aws_lambda

Lambda 함수를 선언합니다.

url_shortener_stack.py 파일에 Lambda 함수에 선언
        function = aws_lambda.Function(self, "backend",
            runtime=aws_lambda.Runtime.PYTHON_3_7,
            handler="handler.main",
            code=aws_lambda.Code.asset("./lambda"))

Lambda 함수 리소스 배포

배포 Lambda 함수 확인

배포한 Lambda 함수 콘솔에 들어가서 확인이 가능합니다.

Lambda 함수에 DynamoDB Table로의 접근 권한 부여

테이블과 람다 함수를 만들었지만 아직 서로 동작하지는 않습니다.
2가지 문제가 남아 있습니다. 테이블을 환경 변수로 설정했지만, 아직 변수 값을 넣어주지 않았습니다. 또한 테이블에 접근하기 위한 권한이 없습니다.

backend Lambda 함수가 mapping-table DynamoDB 테이블로 접근할 수 있는 권한 부여

모든 DynamoDB 테이블에 대한 접근 권한이 아닌 이 테이블에 대해서만 권한이 주어집니다.

Lambda 함수에 DynamoDB 접근 권한 부여
        table.grant_read_write_data(function)

람다 함수에 환경 변수 값 추가

Lambda 함수 코드 중간에 os.environ.get('TABLE_NAME')  과 같이 외부 환경 변수로 설정된 값을 이용할 수 있습니다.
해당 테이블의 이름이 필요하므로 환경 변수 값을 아래와 같이 코드에 추가할 수 있습니다.

        function.add_environment("TABLE_NAME", table.table_name)

다음과 같은 형태의 코드가 작성이 완료되었습니다.

CDK diff 기능

새로운 CDK 명령인 diff 기능입니다. 리소스에 대한 변경 사항을 확인할 수 있습니다.

cdk diff 실행
cdk diff

 
IAM 설정이 변경된 것을 볼 수 있으며, DDB mapping에 대한 Lambda 함수에 정책이 추가된 것을 볼 수 있습니다. backend라는 Lambda 함수가 이 정책을 쓰는 주체가 되는 것을 알 수 있습니다.
Lambda 함수가 수정된 것을 볼 수 있으며, 환경 변수 값이 추가되었고 새롭게 만들어진 정책이 적용된 것을 볼 수 있습니다.

Lambda 함수의 DynamoDB 테이블 접근 권한 부여 배포

cdk를 deploy 하면 아래와 같이 변경 사항을 적용할지를 물어보고, y를 선택하면 배포 되는 것을 확인할 수 있습니다.

cdk deploy command
cdk deploy

API Gateway 리소스 정의

이번에는 API를 연결하기 위해서 API Gateway를 추가 하겠습니다.
먼저 API Gateway를 Import 해야 합니다. 사용 방법은 CDK API 레퍼런스를 참조합니다.
https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigateway-readme.html


해당 패키지를 추가합니다.

API Gateway를 위한 import
from aws_cdk import core, aws_dynamodb, aws_lambda, aws_apigateway


API Gateway를 선언하겠습니다.

API Gateway 선언
        api = aws_apigateway.LambdaRestApi(self, "api", handler=function)


전체 소스 코드는 다음과 같습니다.

API Gateway 리소스 배포

API Gateway를 배포합니다.

cdk deploy

다음과 같이 API가 배포되는 것을 볼 수 있습니다. Endpoint URL이 생성됩니다.

테스트

배포된 API Endpoint URL이 정상 동작이 되는지 확인해 봅시다. 배포된 URL을 복사해서 웹 브라우저 주소창에 넣고 확인합니다. (본인의 프로젝트에서 생성된 URL을 이용하여 테스트 하세요.)

https://vp8tpwdmd4.execute-api.ap-northeast-2.amazonaws.com/prod/

호출에 대한 정당한 200 OK 메시지를 받는 것을 알 수 있습니다. 사용 방법에 맞춰서 한 번 호출해 봅시다.


아마존닷컴에 접속할 수 있는 URL을 만들어 보겠습니다. (본인의 프로젝트에서 생성된 URL을 이용하여 테스트 하세요.)
https://vp8tpwdmd4.execute-api.ap-northeast-2.amazonaws.com/prod/?targetUrl=https://www.amazon.com

생성된 URL을 입력할 경우, 정상적으로 Redirect 되는지 확인해 봅시다. (본인의 프로젝트에서 생성된 URL을 이용하여 테스트 하세요.)
https://vp8tpwdmd4.execute-api.ap-northeast-2.amazonaws.com/prod/7c9b02b8
잘 동작하는 것을 볼 수 있습니다.


미션 2. 부하 테스트 및 모니터링 대시보드 작성

보스는 당신의 실력을 절대적으로 신임합니다. 하지만 성공적인 마케팅을 위해서는 성능을 보장할 수 있는 지표를 알아야 합니다.

이를 증명할 수 있는 부하 테스트 및 모니터링 대쉬보드를 만드는 방법을 살펴 봅니다.

아키텍처 소개

부하 테스트는 Docker 이미지를 만들고 Fargate를 이용해서 Task를 배포하여 테스트 합니다.

트래픽 생성용 Docker 이미지 작성

트래픽 생성용 Docker 이미지를 작성하기 위하여 load-test 폴더를 하나 만듭니다.

트랙픽 생성용 폴더 생성
mkdir pinger
cd pinger

다음과 같이 ping.sh 파일을 만들고 코드를 넣습니다. 1초 간격으로 URL을 호출합니다.

URL 호출용 1 TPS 스크립트 작성
#!/bin/sh
while true; do
  curl -i $URL
  sleep 1
done

해당 파일에 대한 실행 권한을 부여합니다.

스크립트 실행 권한 생성
chmod +x ./ping.sh

다음과 같이 CLI 상에서 URL 정보를 입력하고 실행합니다. 아래의 URL은 본인이 작성한 테스트 결과 수집한 Short URL을 사용합니다.

1 TPS 테스트를 위한 실행
URL=https://vp8tpwdmd4.execute-api.ap-northeast-2.amazonaws.com/prod/7c9b02b8 ./ping.sh

만약 서브도메인 기반으로 api를 만들었다면 아래와 같은 형태로 만들 수 있습니다. (아래의 URL을 사용하지 않고, 본인이 생성한 Custom domain url을 사용합니다.)

1 TPS 테스트 예(도메인 있을 경우)
URL=https://cdk.awsdemo.kr/7c9b02b8 ./ping.sh

테스트를 하면 아래와 같이 1초마다 갱신되는 화면을 URL 정보가 리다이렉트 되는 것을 볼 수 있습니다.

트래픽 생성용 Docker 이미지 작성

이 작업을 이제 직접 하지 않고 도커를 이용해서 작업하게 할 겁니다. 그러기 위해서 Fargate를 이용할 것입니다.
Docker 이미지를 생성하는 스크립트 파일인 Dockerfile을 아래와 같은 코드로 작성하여 만듭니다.

Docker 이미지 생성 스크립트
FROM adoptopenjdk/openjdk11:alpine

RUN apk add curl
ADD ping.sh /ping.sh

CMD [ "/bin/sh", "/ping.sh" ]

Docker 이미지를 빌드하여 이미지를 생성합니다.

Docker 이미지 생성
docker build -t pinger .

이미지를 실행합니다. 이 작은 컨테이너는 매 초마다 단일 트랜잭션을 수행합니다.

Dorker 이미지 실행
docker run -it -e URL=https://vp8tpwdmd4.execute-api.ap-northeast-2.amazonaws.com/prod/7c9b02b8 pinger

CDK를 이용한 Fargate 배포 설정

이제 cdk를 이용해서 Fargate에 배포해 봅시다. 새로운 파일을 url_short 폴더에 traffic.py  파일을 생성하고 아래와 같이 코드를 작성합니다.

traffic.py
from aws_cdk.core import Construct
from aws_cdk import aws_ecs, aws_ec2


# a user-defined construct
# just a class the inherits from the core.Construct base class
class Traffic(Construct):
    """
    An HTTP traffic generator.
    Hits a specified URL at some TPS.
    """

    def __init__(self, scope: Construct, id: str, *, vpc: aws_ec2.IVpc, url: str, tps: int):
        """
        Defines an instance of the traffic generator.
        :param scope: construct scope
        :param id:    construct id
        :param vpc:   the VPC in which to host the traffic generator
        :param url:   the URL to hit
        :param tps:   the number of transactions per second
        """
        super().__init__(scope, id)

        # define an ECS cluster hosted within the requested VPC
        cluster = aws_ecs.Cluster(self, 'cluster', vpc=vpc)

        # define our task definition with a single container
        # the image is built & published from a local asset directory
        task_definition = aws_ecs.FargateTaskDefinition(self, 'PingTask')
        task_definition.add_container('Pinger', image=aws_ecs.ContainerImage.from_asset("pinger"), environment={'URL': url})

        # define our fargate service. TPS determines how many instances we
        # want from our task (each task produces a single TPS)
        aws_ecs.FargateService(self, 'service', cluster=cluster, task_definition=task_definition, desired_count=tps)


기존 url_short_stack.py 파일을 아래와 같이 수정합니다.

기존 소스에서 변경되는 부분이 있으니 주의해 주세요:  super().__init__(scope, id, env=AWS_ENV, **kwargs)

url_short_stack.py
from aws_cdk import core, aws_dynamodb, aws_lambda, aws_apigateway, aws_ec2
import os
from traffic import Traffic

# we need default values here since aws-cdk-examples build synthesizes the app
ACCOUNT= os.environ['CDK_DEFAULT_ACCOUNT']
REGION = os.environ['CDK_DEFAULT_REGION']
VPC_ID = os.environ.get('TESTENV_VPC_ID', 'vpc-07aa21d32b2df894f')
AWS_ENV = core.Environment(account=ACCOUNT, region=REGION)

class UrlShortStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, env=AWS_ENV, **kwargs)

        # The code that defines your stack goes here
        table = aws_dynamodb.Table(self, "mapping",
            partition_key = aws_dynamodb.Attribute(name="id",type=aws_dynamodb.AttributeType.STRING))
            
        function = aws_lambda.Function(self, "backend",
            runtime=aws_lambda.Runtime.PYTHON_3_7,
            handler="handler.main",
            code=aws_lambda.Code.asset("./lambda"))

        table.grant_read_write_data(function)
        function.add_environment("TABLE_NAME", table.table_name)
        
        api = aws_apigateway.LambdaRestApi(self, "api", handler=function)
        
        
class TrafficStack(core.Stack):
    
    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, env=AWS_ENV, **kwargs)
        
        # lookup our pre-created VPC by ID
        vpc_env = aws_ec2.Vpc.from_lookup(self, "vpc",
            vpc_id=VPC_ID)

        Traffic(self, 'TestTraffic',
            vpc= vpc_env,
            url="https://vp8tpwdmd4.execute-api.ap-northeast-2.amazonaws.com/prod/7c9b02b8",
            tps=10)

* VPC_ID는 Private subnet을 포함하고 있어야 합니다. 만약 없다면, VPC Wizard를 통해서 생성하고 해당 VPC를 연결합니다.


app.py 파일을 수정합니다.

app.py 파일 수정
#!/usr/bin/env python3

from aws_cdk import core

from url_short.url_short_stack import UrlShortStack, TrafficStack

app = core.App()
UrlShortStack(app, "url-short")
TrafficStack(app,  "test-ping")

app.synth()


이제 배포를 합니다.

cdk 트래픽 테스트 배포
cd ..
cdk deploy "*"

ECS를 들어가면, 현재 동작중인 Fargate Task 숫자 및 상태를 확인할 수 있습니다. 총 10개의태스크가 Fargate로 동작하는 것을 확인할 수 있습니다.

Fargate로 생성된 태스크 10개가 동작하는 것을 확인할 수 있습니다.

Docker 이미지가 ECR에 리포지토리에 저장되어 있는 것을 확인할 수 있습니다.


미션 3. CloudWatch 대시보드를 이용한 리소스 모니터링

Dynamodb, API Gateway, Lambda가 배포되어 있는 Backend를 모니터링 하기 위해서 CloudWatch 메트릭을 이용할 수 있습니다. 해당 데이터를 모아 보기 위해서는 CloudWatch에서 Dashboard를 만들면 됩니다.
이 작업 역시 CDK를 이용하면 쉽게 구성할 수 있습니다.

아키텍처 소개


pypi.org에서 모니터링 관련 cdk를 검색합니다: https://pypi.org/search/?q=aws+cdk+monitoring

이중 cdk-watchful을 사용합니다: https://pypi.org/project/cdk-watchful/

cdk-watchful 설치

새 터미널을 하나 더 열어주고 환경으로 진입하고 나서 다음과 같이 해당 모듈을 설치합니다.

cdk-watchful 패키지 설치
sudo pip install cdk-watchful



cdk-watchful 사용을 위한 파일 수정

기존 url_short_stack.py 파일을 수정합니다.

 url_short_stack.py for cdk-watchful
import os
from aws_cdk import core, aws_dynamodb, aws_lambda, aws_apigateway, aws_ecs, aws_ec2
from traffic import Traffic
from cdk_watchful import Watchful

# we need default values here since aws-cdk-examples build synthesizes the app
ACCOUNT= os.environ['CDK_DEFAULT_ACCOUNT']
REGION = os.environ['CDK_DEFAULT_REGION']
VPC_ID = os.environ.get('TESTENV_VPC_ID', 'vpc-07aa21d32b2df894f')
AWS_ENV = core.Environment(account=ACCOUNT, region=REGION)

class UrlShortStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, env=AWS_ENV, **kwargs)

        # The code that defines your stack goes here
        table = aws_dynamodb.Table(self, "mapping",
            partition_key = aws_dynamodb.Attribute(name="id",type=aws_dynamodb.AttributeType.STRING))
            
        function = aws_lambda.Function(self, "backend",
            runtime=aws_lambda.Runtime.PYTHON_3_7,
            handler="handler.main",
            code=aws_lambda.Code.asset("./lambda"))

        table.grant_read_write_data(function)
        function.add_environment("TABLE_NAME", table.table_name)
        
        api = aws_apigateway.LambdaRestApi(self, "api", handler=function)
        
        wf = Watchful(self, 'monitoring', alarm_email='scv@studydev.com')
        wf.watch_scope(self)
        
        
class TrafficStack(core.Stack):
    
    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, env=AWS_ENV, **kwargs)
        
        # lookup our pre-created VPC by ID
        vpc_env = aws_ec2.Vpc.from_lookup(self, "vpc",
            vpc_id=VPC_ID)

        Traffic(self, 'TestTraffic',
            vpc= vpc_env,
            url="https://vp8tpwdmd4.execute-api.ap-northeast-2.amazonaws.com/prod/7c9b02b8",
            tps=10)


cdk-watchful 배포

모든 cdk 스택 배포
cdk deploy "*"

하단의 모니터링을 위한 CloudWatch 대쉬보드 URL이 생성되는 것을 볼 수 있습니다. 해당 링크를 클릭합니다.

대시보드를 통한 트래픽 확인

다음과 같은 대시보드를 통해서 상태를 확인할 수 있습니다.

트래픽을 증가시켜서 DynamoDB의 RCU를 넘기도록 설정합니다. TPS를 10에서 15로 변경하여 트래픽을 절반 더 늘리고, 5분 정도 기다립니다.

 url_short_stack.py for cdk-watchful
import os
from aws_cdk import core, aws_dynamodb, aws_lambda, aws_apigateway, aws_ecs, aws_ec2
from traffic import Traffic
from cdk_watchful import Watchful

# we need default values here since aws-cdk-examples build synthesizes the app
ACCOUNT= os.environ['CDK_DEFAULT_ACCOUNT']
REGION = os.environ['CDK_DEFAULT_REGION']
VPC_ID = os.environ.get('TESTENV_VPC_ID', 'vpc-07aa21d32b2df894f')
AWS_ENV = core.Environment(account=ACCOUNT, region=REGION)

class UrlShortStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, env=AWS_ENV, **kwargs)

        # The code that defines your stack goes here
        table = aws_dynamodb.Table(self, "mapping",
            partition_key = aws_dynamodb.Attribute(name="id",type=aws_dynamodb.AttributeType.STRING))
            
        function = aws_lambda.Function(self, "backend",
            runtime=aws_lambda.Runtime.PYTHON_3_7,
            handler="handler.main",
            code=aws_lambda.Code.asset("./lambda"))

        table.grant_read_write_data(function)
        function.add_environment("TABLE_NAME", table.table_name)
        
        api = aws_apigateway.LambdaRestApi(self, "api", handler=function)
        
        wf = Watchful(self, 'monitoring', alarm_email='scv@studydev.com')
        wf.watch_scope(self)
        
        
class TrafficStack(core.Stack):
    
    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, env=AWS_ENV, **kwargs)
        
        # lookup our pre-created VPC by ID
        vpc_env = aws_ec2.Vpc.from_lookup(self, "vpc",
            vpc_id=VPC_ID)

        Traffic(self, 'TestTraffic',
            vpc= vpc_env,
            url="https://vp8tpwdmd4.execute-api.ap-northeast-2.amazonaws.com/prod/7c9b02b8",
            tps=15)
모든 cdk 스택 배포
cdk deploy "*"


DynamoDB Console에서도 동일한 메트릭 정보를 확인할 수 있습니다. RCU가 부족한 현상이 발생합니다.

구독한 이메일에 대해서 승인을 했다면 아래와 같이 이메일을 통해서 RCU가 부족하다는 경고 메일이 도착하는 것을 볼 수 있습니다.


부록 1. Custom Domain을 설정하고 싶다면...

위 Test URL은 API Gateway에서 생성하여 제공하는 URL입니다. 따라서, URL 호출을 하기 위해서 URL이 길어집니다.
따라서, 만약 Route 53에 도메인이 등록되어 있고, 해당 도메인을 이용하여 API Gateway에 Custom Domain을 설정할 수 있습니다.

예를 들어, https://vp8tpwdmd4.execute-api.ap-northeast-2.amazonaws.com/prod/7c9b02b8 라는 URL 대신, https://cdk.awsdemo.kr/7c9b02b8 변경할 수 있습니다.
또한, Lambda 함수를 수정하거나(쿼리스트링의 매개변수를 추가로 받거나), DynamoDB에 들어있는 내용을 추가하여 https://cdk.awsdemo.kr/hol-short-url 이름으로 현재 wiki 페이지로 접속하도록 설정할 수 있습니다.

먼저 Certificate Manager에서 Custom Domain으로 사용할 인증서를 생성하고, 인증서와 Route 53에 등록된 Zone 정보를 기반으로 아래 코드에 반영합니다.
성능 테스트는 종료되었으므로 tps 값은 0으로 하거나 또는 해당 스택을 destory 하여 운용할 수 있습니다.

stack for custom domain
from aws_cdk import core, aws_dynamodb, aws_lambda, aws_apigateway, aws_ec2, aws_route53, aws_route53_targets, aws_certificatemanager
import os
from traffic import Traffic
from cdk_watchful import Watchful

# we need default values here since aws-cdk-examples build synthesizes the app
ACCOUNT= os.environ['CDK_DEFAULT_ACCOUNT']
REGION = os.environ['CDK_DEFAULT_REGION']
VPC_ID = os.environ.get('TESTENV_VPC_ID', 'vpc-07aa21d32b2df894f')
ZONE_NAME = os.environ.get('TESTENV_ZONE_NAME', 'awsdemo.kr')
ZONE_ID = os.environ.get('TESTENV_ZONE_ID', 'Z2XXXXXXXXXXYO')
ZONE_CERT = os.environ.get('TESTENV_ZONE_CERT', 'arn:aws:acm:ap-northeast-2:123456789012:certificate/8d2adfd2-xxxx-xxxx-abcd-2c50292xxxx')

AWS_ENV = core.Environment(account=ACCOUNT, region=REGION)

class UrlShortStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, env=AWS_ENV, **kwargs)

        # The code that defines your stack goes here
        table = aws_dynamodb.Table(self, "mapping",
            partition_key = aws_dynamodb.Attribute(name="id",type=aws_dynamodb.AttributeType.STRING))
            
        function = aws_lambda.Function(self, "backend",
            runtime=aws_lambda.Runtime.PYTHON_3_7,
            handler="handler.main",
            code=aws_lambda.Code.asset("./lambda"))

        table.grant_read_write_data(function)
        function.add_environment("TABLE_NAME", table.table_name)
        
        api = aws_apigateway.LambdaRestApi(self, "api", handler=function)
        
        wf = Watchful(self, 'monitoring', alarm_email='scv@studydev.com')
        wf.watch_scope(self)
        
        # Custom Domain
        self.map_subdomain("cdk", api)
        
    def map_subdomain(self, subdomain: str, api: aws_apigateway.RestApi) -> str:
        """
        Maps a sub-domain of waltersco.co to an API gateway
        :param subdomain: The sub-domain (e.g. "www")
        :param api: The API gateway endpoint
        :return: The base url (e.g. "https://www.waltersco.co")
        """
        domain_name = subdomain + '.' + ZONE_NAME
        url = 'https://' + domain_name
 
        cert = aws_certificatemanager.Certificate.from_certificate_arn(self, 'DomainCertificate', ZONE_CERT)
        hosted_zone = aws_route53.HostedZone.from_hosted_zone_attributes(self, 'HostedZone', hosted_zone_id=ZONE_ID, zone_name=ZONE_NAME)
 
        # add the domain name to the api and the A record to our hosted zone
        domain = api.add_domain_name('Domain', certificate=cert, domain_name=domain_name)
 
        aws_route53.ARecord(
            self, 'ShortUrlDomain',
            record_name=subdomain,
            zone=hosted_zone,
            target=aws_route53.RecordTarget.from_alias(aws_route53_targets.ApiGatewayDomain(domain)))
 
        return url
        

class TrafficStack(core.Stack):
    
    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, env=AWS_ENV, **kwargs)
        
        # lookup our pre-created VPC by ID
        vpc_env = aws_ec2.Vpc.from_lookup(self, "vpc", vpc_id=VPC_ID)

        Traffic(self, 'TestTraffic',
            vpc= vpc_env,
            url="https://vp8tpwdmd4.execute-api.ap-northeast-2.amazonaws.com/prod/7c9b02b8",
            tps=0)
        

리소스 삭제

모든 실습이 완료되었다면 다음과 같이 3개의 작업을 통해서 리소스를 삭제해야 합니다.

1. CDK에 의해 배포된 Stack을 제거 합니다. 

cdk 리소스 삭제
cdk destroy "*"

2. CDK 배포를 위해서 생성된 S3 버킷을 찾아서 파일들을 모두 삭제합니다. Cloud9 Terminal에서 작업하면 편합니다. (DynomoDB와 S3는 데이터가 있으면 지워지지 않을 수 있습니다. 지워진 것을 최종 확인합니다.)

aws cloudformation describe-stacks --stack-name CDKToolkit --query "Stacks[0].Outputs[0].OutputValue"
aws s3 rm s3://cdktoolkit-stagingbucket-yrlacd8hy29p --recursive
aws cloudformation delete-stack --stack-name CDKToolkit

3. 테스트를 위해 생성한 VPC에서 Nat Gateway를 삭제하고, Elastic IP를 반납한 후, CDK-TEST VPC를 삭제합니다.

4. Cloud9 환경을 삭제합니다.


관련 정보 모음

해당 HoL은 아래 GitHub과 영상을 기반으로 작성되어졌습니다. Github과 소스 구성은 다를 수 있으며, Youtube 발표 정보를 기반으로 재작성되었습니다.


Infrastructure is Code with the AWS CDK - AWS Online Tech Talks


Lambda에서 받는 event['pathParameters']['proxy'] 처리 부분: https://docs.aws.amazon.com/ko_kr/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html


  • 레이블 없음