목차

  1. Serverless란?
  2. Serverless Framework는?
  3. 관심 갖게된 계기
  4. 공식 지원 runtime
  5. 자주 사용하는 명령어 목록
  6. serverless.yml
  7. 실습

1. Serverless란?

위키피디아에는 Function as a Service 라고... 진짜로 서버가 없다는 뜻은 아니고, AWS 등을 사용하는 사용자가 Elastic Cache나 RDS 와 같은 Managed Service의 한 종류로 관리수준을 최고 수준으로 끌어올려서 사용자는 함수(function)만 업로드하면 나머지(보안패치, scale out(in, up, down))는 클라우드 사업자가 대신 관리해줘서 serveless.... 사용자가 관리해야할 서버가 없다는 뜻... 제대로 동작하는 함수만 올리면 그 뒤부턴 신경쓸게 하나도 없다!

2. Serverless Framework

  • 2015년에 소개되었고 최초엔 이름이 JAWS 였다고 합니다.
  • nodejs로 만들어졌고,
  • 현재는 AWS 기반의 serverless 환경 구축에 맞춰져있으나 추후 다른 클라우드(MS Azure 등)으로 확장할꺼 같다.
  • 대단한건 아니고 AWS Console이나 상세 옵션 등이 생소한 상태에선 조금 쉽게 구축하게 추상화 해주는 도구

3. 관심 갖게된 계기

  • 비용최적화 관점에서 접근
    • 최소 과금단위가 100ms!
  • Managed!
  • Apex와 함께 비교하다가 Serverless framework를 선택하게됨.
    • Serverless Framework는 Inc!(알고보니 Apex도 회사)
    • 제가 처음에 조사할 당시에는 투자도 받았다고 하고 개발자도 뽑는다하고, 뭔가 그냥 오픈소스(?)를 넘어 투자 많이 받은 회사에서 주도적으로 개발하는 느낌을 폴폴 풍겼음...
    • 이때는 TJ가 그렇게 대단한 사람(Express, Mocha 등을 만든사람이라고...)인지도 몰랐고...
    • 가장 마음에 들었던건 api gateway와 lambda를 함께 추상화 해준다는 점이 마음에 들었음
    • 물론 Apex로도 api gateway와 lambda 모두 가능하다.

4. 공식 지원 runtimes

AWS Lambda가 공식지원하는것만 역시 같이 지원함.

  • nodejs
  • python2.7
    • 3.x는 왜 지원하지 않는걸까...? 의문
  • java
  • csharp
  • 기타 등등(scala, gradle, maven)

5. 자주 사용하는 명령어 목록

$ serverless --help

$ sls --help

$ sls deploy -v # v는 verbose

$ sls logs

6. serverless.yml

핵심 설정 파일!

  • 가장 기본형
service: my-project  
provider:  
  name: aws
  runtime: python2.7

functions:  
  hello:
    handler: handler.hello
    events:
      - http:
          path: hello
          method: get
          integration: lambda
  • 확장 cli opt
# serverless.yml
service: my-project  
provider:  
  name: aws
  runtime: python2.7
  memorySize: 512
  timeout: 20

functions:  
  hello:
    handler: handler.hello
    environment:
      someValue: hahah
      otherValue: 1234
      optValue: ${opt:infra} # --infra option을 통해서 입력
    events:
      - http:
          path: hello
          method: get
          integration: lambda
# handler.py
import os

def hello(event, context):  
    # your code
    return {
      "test": "test",
      "tset": 123,
      "opt": os.environ['optValue']
    }

$ sls deploy --infra fighting!

  • 확장 self-reference
# serverless.yml
service: my-project  
provider:  
  name: aws
  runtime: python2.7
  memorySize: 512 # <<-- 이부분을 참조함.
  timeout: 20

functions:  
  hello:
    handler: handler.hello
    environment:
      someValue: ${self:provider.memorySize} # 위의 memorySize
    events:
      - http:
          path: hello
          method: get
          integration: lambda
# handler.py
import os

def hello(event, context):  
    # your code
    return {
      "test": "test",
      "tset": 123,
      "memSize": os.environ['someValue']
    }

$ sls deploy

  • 확장 overwriting variables
# serverless.yml
service: my-project  
provider:  
  name: aws
  runtime: python2.7
  memorySize: 512 # <- provider.memorySize
  timeout: 20

functions:  
  hello:
    handler: handler.hello
    environment:
      memSize: ${opt:mem, self:provider.memorySize} # opt가 있으면 opt의 mem 값으로 없으면 provider.memorySize가 들어감.
    events:
      - http:
          path: hello
          method: get
          integration: lambda
# handler.py
import os

def hello(event, context):  
    # your code
    return {
      "test": "test",
      "tset": 123,
      "memSize": os.environ['memSize']
    }

$ sls deploy --mem 1024

$ sls deploy

7. 실습

  • slack incoming webhook
# serverless.yml
service: my-project  
provider:  
  name: aws
  runtime: python2.7
  memorySize: 512 # <- provider.memorySize
  timeout: 20

functions:  
  hello:
    handler: handler.hello
    events:
      - http:
          path: hello
          method: get
          integration: lambda
# handler.py
import os, sys

here = os.path.dirname(os.path.realpath(__file__))  
sys.path.append(os.path.join(here, "./libararies"))

import requests

def hello(event, context):  
    requests.post(
    'https://hooks.slack.com/services/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',
    json = {"text": event['query'].get('msg', "Hello infra study!")})
    return {
        "event": event
    }

$ docker run -it --rm -v $(pwd):/host python:2 bash -c "pip install -r /host/requirements.txt -t /host/libararies"

참고: https://serverless.com/framework/docs/providers/aws/guide/packaging/

  • s3 image resizer
# serverless.yml
service: my-project  
provider:  
  name: aws
  runtime: python2.7
  memorySize: 512 # <- provider.memorySize
  timeout: 20
  iamRoleStatements:
    - Effect: Allow
      Action:
        - s3:*
      Resource: "*"

functions:  
  hello:
    handler: handler.hello
    events:
      - http:
          path: hello
          method: get
          integration: lambda
  resizer:
    handler: node/resize.main
    memorySize: 1024
    timeout: 30
    runtime: nodejs4.3 # Overrides the default set above.
    events:
      - s3:
          bucket: infra-han-photos
          event: s3:ObjectCreated:*
          rules:
            - prefix: uploads/
            - suffix: .jpg
// resize.js
// http://docs.aws.amazon.com/lambda/latest/dg/with-s3-example-deployment-pkg.html#with-s3-example-deployment-pkg-nodejs
// dependencies
var async = require('async');  
var AWS = require('aws-sdk');  
var gm = require('gm')  
            .subClass({ imageMagick: true }); // Enable ImageMagick integration.
var util = require('util');

// constants
var MAX_WIDTH  = 100;  
var MAX_HEIGHT = 100;

// get reference to S3 client
var s3 = new AWS.S3();

exports.main = function(event, context, callback) {  
    // Read options from the event.
    console.log("Reading options from event:\n", util.inspect(event, {depth: 5}));
    var srcBucket = event.Records[0].s3.bucket.name;
    // Object key may have spaces or unicode non-ASCII characters.
    var srcKey    =
    decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " "));  
    var dstBucket = srcBucket + "-resized";
    var dstKey    = "resized-" + srcKey;

    // Sanity check: validate that source and destination are different buckets.
    if (srcBucket == dstBucket) {
        callback("Source and destination buckets are the same.");
        return;
    }

    // Infer the image type.
    var typeMatch = srcKey.match(/\.([^.]*)$/);
    if (!typeMatch) {
        callback("Could not determine the image type.");
        return;
    }
    var imageType = typeMatch[1];
    if (imageType != "jpg" && imageType != "png") {
        callback('Unsupported image type: ${imageType}');
        return;
    }

    // Download the image from S3, transform, and upload to a different S3 bucket.
    async.waterfall([
        function download(next) {
            // Download the image from S3 into a buffer.
            s3.getObject({
                    Bucket: srcBucket,
                    Key: srcKey
                },
                next);
            },
        function transform(response, next) {
            gm(response.Body).size(function(err, size) {
                // Infer the scaling factor to avoid stretching the image unnaturally.
                var scalingFactor = Math.min(
                    MAX_WIDTH / size.width,
                    MAX_HEIGHT / size.height
                );
                var width  = scalingFactor * size.width;
                var height = scalingFactor * size.height;

                // Transform the image buffer in memory.
                this.resize(width, height)
                    .toBuffer(imageType, function(err, buffer) {
                        if (err) {
                            next(err);
                        } else {
                            next(null, response.ContentType, buffer);
                        }
                    });
            });
        },
        function upload(contentType, data, next) {
            // Stream the transformed image to a different S3 bucket.
            s3.putObject({
                    Bucket: dstBucket,
                    Key: dstKey,
                    Body: data,
                    ContentType: contentType
                },
                next);
            }
        ], function (err) {
            if (err) {
                console.error(
                    'Unable to resize ' + srcBucket + '/' + srcKey +
                    ' and upload to ' + dstBucket + '/' + dstKey +
                    ' due to an error: ' + err
                );
            } else {
                console.log(
                    'Successfully resized ' + srcBucket + '/' + srcKey +
                    ' and uploaded to ' + dstBucket + '/' + dstKey
                );
            }

            callback(null, "message");
        }
    );
};

$ docker run --rm -v $(pwd):/host node:4.3 bash -c "cd /host && npm install"