• yundream
  • 2018-06-22 16:19:42
  • 2018-06-14 05:00:10
  • 98695

Contents

 AWS SAM

AWS Serverless Application Model (AWS SAM)

AWS는 서버리스 환경에서의 애플리케이션 구현 모델인 AWS SAM을 개발하고 있다. 여기에는 SAM 사양과 SAM 템플릿을 AWS CloudFormation으로 변환하는 코드, 프로그래밍 예제 등을 포함하고 있다. SAM Local은 SAM의 구현체다.

서비리스 응용 프로그램을 만들려면, 람다함수에 대한 사양을 저장하는 JSON이나 YAML 형식의 SAM 템플릿을 만든다. 그리고 SAM 구현체 중 하나인 SAM Local의 CLI를 이용해서 응용 프로그램을 테스트 하고 업로드 및 배포를 한다. SAM은 응용 프로그램의 사양을 클라우드포메이션 구문으로 변환하면서, 지정되지 않은 속성들을 기본 값으로 채우고 호출 권한등을 결정해서 람다를 배포 한다.

SAM LOCAL 설치

개인 리눅스 데스크탑에 SAM LOCAL 개발 환경을 설정했다.
  • 우분투 리눅스 17.10
  • Docker version : 17.12.0-ce
  • Python 2.7.14 : 2.7 혹은 3.6이어야 한다.
  • go1.9.2
pip로 설치했다(보통 nodejs 기반인데, javascript가 주력이 아니라서 그나마? 익숙한 python 기반으로).
$ sudo pip install aws-sam-cli

Sample 테스트

aws-sam-golang example로 테스트 하기로 했다. 셈플패키지를 다운로드한다.
$ go get -u github.com/cpliakas/aws-sam-golang-example
셈플 패키지 디렉토리를 보면 run.sh가 있다. 내용을 보자.
GOOS=linux go build -o main
sam local start-api

main 이름으로 빌드하고 sam local start-api를 실행한다.
# sam local start-api
2018-06-14 13:54:14 Mounting ExampleAPI at http://127.0.0.1:3000/hello [GET]
2018-06-14 13:54:14 Mounting ExampleAPI at http://127.0.0.1:3000/goodbye [GET]
2018-06-14 13:54:14 Mounting ExampleAPI at http://127.0.0.1:3000/ [GET]
2018-06-14 13:54:14 You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2018-06-14 13:54:14  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
2018-06-14 13:54:27 Invoking main (go1.x)
2018-06-14 13:54:27 Found credentials in shared credentials file: ~/.aws/credentials

Fetching lambci/lambda:go1.x Docker container image..............
2018-06-14 13:55:04 Mounting /home/yundream/golang/src/github.com/cpliakas/aws-sam-golang-example as /var/task:ro inside runtime container
START RequestId: 857bf496-779d-18c1-a151-8d00a7c4db3f Version: $LATEST
END RequestId: 857bf496-779d-18c1-a151-8d00a7c4db3f
REPORT RequestId: 857bf496-779d-18c1-a151-8d00a7c4db3f	Duration: 1.12 ms	Billed Duration: 100 ms	Memory Size: 128 MB	Max Memory Used: 6 MB	
sam local 은 docker를 기반으로 하는 개발환경을 제공한다. 처음 실행 할 경우 lambci/lambda:go1.x 도커이미지를 설치한다. 이 후에는 현재 디렉토리를 컨테이너 볼륨으로 해서 컨테이너를 실행하고 main 파일을 실행한다.

테스트를 해보자.
$ curl http://127.0.0.1:3000/hello
{"message":"Hello, world!"}

테스트 API

위의 테스트로 대략적인 작동은 확인은 했지만 셈플코드가 별로 마음에 들지 않아서(셈플 코드를 보면 람다코드가 아닌 http 웹서버를 띄우고 있음을 알 수 있다. 적절한 예제 같지가 않았다.) 실제 코드를 만들어보기로 했다. 테스트에 사용할 코드다.
package main

import (
    "context"
    "encoding/json"
    "fmt"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
)

type Info struct {
    Name  string `json:"name"`
    Age   int    `json:"age"`
    Email string `json:"email"`
}

func Handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    fmt.Println(request.Path)
    body, _ := json.Marshal(Info{"yundream", 5, "yundream@gmail.com"})
    return events.APIGatewayProxyResponse{
        Body:       string(body),
        StatusCode: 200,
    }, nil

}

func main() {
    lambda.Start(Handler)
}

SAM Local를 위한 API-Gateway 설정을 해보자.
AWSTemplateFormatVersion : '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Description: 유저 정보를 반환한다.
Resources:
  ExampleAPI:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: go1.x
      Handler: main
      Events:
        RootHandler:
          Type: Api
          Properties:
            Path: '/'
            Method: get
        HelloHandler:
          Type: Api
          Properties:
            Path: '/hello'
            Method: get
        GoodbyeHandler:
          Type: Api 
          Properties:
            Path: '/goodbye'
            Method: get
테스트를 해보자.
$ curl localhost:3000/hello -i
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 52
Server: Werkzeug/0.11.15 Python/2.7.14
Date: Thu, 21 Jun 2018 06:35:13 GMT

{"name":"yundream","age":5,"email":"yundream@gmail.com"}

쿼리 파라메터

/hello/{name}을 처리하도록 코드를 수정했다.
func Handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    name := request.PathParameters["name"]
    body, _ := json.Marshal(Info{name, 5, "yundream@gmail.com"})
    return events.APIGatewayProxyResponse{
        Body:       string(body),
        StatusCode: 200,
    }, nil  
}
request.PathParameters로 쿼리 파라메터의 값을 가져올 수 있다. template.yml 파일을 여기에 맞게 수정했다.
AWSTemplateFormatVersion : '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Description: 유저 정보를 반환한다.
Resources:
  ExampleAPI:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: go1.x
      Handler: main
      Events:
        RootHandler:
          Type: Api
          Properties:
            Path: '/'
            Method: get
        HelloHandler:
          Type: Api
          Properties:
            Path: '/hello/{name}'
            Method: get
        GoodbyeHandler:
          Type: Api 
          Properties:
            Path: '/goodbye/{name}'
            Method: get
테스트 결과
$ curl localhost:3000/hello/john -i
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 52
Server: Werkzeug/0.11.15 Python/2.7.14
Date: Thu, 21 Jun 2018 06:35:13 GMT

{"name":"john","age":5,"email":"yundream@gmail.com"}

DynamoDB 연동

DynamoDB Local 준비

DynamoDB는 로컬에 설치 해서 사용 할 수 있으며, 도커 이미지도 제공한다. DynamoDB 도커 버전을 이용해서 테스트하기로 했다.
$ docker run --name dynamo -p 8000:8000 dwmkerr/dynamodb  -sharedDb
Initializing DynamoDB Local with the following configuration:
Port:	8000
InMemory:	false
DbPath:	null
SharedDb:	true
shouldDelayTransientStatuses:	false
CorsParams:	*
Web Shell에 접근해 보자.

User 테이블을 만들었다.
$ aws dynamodb create-table \
  --table-name User \
  --attribute-definitions \
    AttributeName=Email,AttributeType=S \
  --key-schema AttributeName=Email,KeyType=HASH \
  --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 \
  --endpoint-url http://localhost:8000
$ aws dynamodb list-tables --endpoint-url http://localhost:8000
{
    "TableNames": [
        "User"
    ]
}

테스트용 아이템을 몇개 넣었다.
$ aws dynamodb put-item \
        --table-name User \
        --item '{
"Name": {"S": "yundream"},
"Email": {"S": "yundream@gmail.com"} ,
"Age": {"N": "45"} }' \
        --return-consumed-capacity TOTAL \
        --endpoint-url http://localhost:8000


$ aws dynamodb put-item \
        --table-name User \
        --item '{
"Name": {"S": "sangbae.yun"},
"Email": {"S": "sangbae.yungjoinc.co.kr"} ,
"Age": {"N": "40"} }' \
        --return-consumed-capacity TOTAL \
        --endpoint-url http://localhost:8000

데이터를 가져와보자.
$ aws dynamodb get-item --table-name User --key '{"Email":{"S": "yundream@gmail.com"}}' --endpoint-url http://localhost:8000
{
    "Item": {
        "Age": {
            "N": "45"
        }, 
        "Name": {
            "S": "yundream"
        }, 
        "Email": {
            "S": "yundream@gmail.com"
        }
    }
}

DynamoDB 준비를 끝냈다.

API 개발

DynamoDB에 연결하기 위한 aws 세션 설정을 한다. Endpoint를 DynamoDB 컨테이너의 IP로 설정했다. CI툴에 통합하기 위해서는 IP설정이 아닌 도메인설정으로 바꿔야 겠으나 일단은 하드코딩 했다.
awsConfig := &aws.Config{
    Region:   aws.String("ap-northeast-2"),   
    Endpoint: aws.String("http://172.17.0.2:8000"),
}

sess, err := session.NewSession(awsConfig)

DynamoDB 세션을 만들었다.
sess, err := session.NewSession(awsConfig)
if err != nil {
    fmt.Println("ERROR ", err.Error())
    return
}

DynamoDB를 읽도록 핸들러를 수정했다.
result, err := dbSvc.GetItem(&dynamodb.GetItemInput{
    TableName: aws.String("User"),  
    Key: map[string]*dynamodb.AttributeValue{
        "Email": {S: aws.String(name)}, 
    },
})

item := Item{}
err = dynamodbattribute.UnmarshalMap(result.Item, &item)
if err != nil {
    return events.APIGatewayProxyResponse{ 
        StatusCode: 500,
    }, nil

}

작동하는 완전한 코드다.
package main

import (
    "context"
    "encoding/json"
    "fmt"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/dynamodb"
    "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)

var (
    dbSvc *dynamodb.DynamoDB
)

type Item struct {
    Name  string `json:"name"` 
    Age   int    `json:"age"`
    Email string `json:"email"`
} 
  
// Handler는 람다함수함수를 호출 할 때 실행되는 코드다.
// AWS API Gateway의 요청과 응답 처리 매커니즘은 aws-lambda-go/envents 패키지가 제공한다.
func Handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {                              
    name := request.PathParameters["name"]
  
    fmt.Println("Find User ", name) 
    result, err := dbSvc.GetItem(&dynamodb.GetItemInput{
        TableName: aws.String("User"),  
        Key: map[string]*dynamodb.AttributeValue{
            "Email": {S: aws.String(name)}, 
        },
    })

    item := Item{}
    err = dynamodbattribute.UnmarshalMap(result.Item, &item)
    if err != nil {
        return events.APIGatewayProxyResponse{ 
            StatusCode: 500,
        }, nil

    }

    body, _ := json.Marshal(item)   

    return events.APIGatewayProxyResponse{ 
        Body:       string(body),       
        StatusCode: 200,
    }, nil
}
func main() {
    awsConfig := &aws.Config{
        Region:   aws.String("ap-northeast-2"),
        Endpoint: aws.String("http://172.17.0.2:8000"),
    }

    sess, err := session.NewSession(awsConfig)
    if err != nil {
        fmt.Println("ERROR ", err.Error())
        return
    }
    dbSvc = dynamodb.New(sess)
    fmt.Println("Server Start ........")
    lambda.Start(Handler)
}

테스트
$ curl localhost:3000/hello/yundream@gmail.com
{"name":"yundream","age":45,"email":"yundream@gmail.com"}

테스트와 배포