728x90

API Gateway의 권한 부여자(Authorizer)를 이용한 권한 부여(P139)

API Gateway의 권한 부여자를 이용해서 JWT 토큰을 검증해서 유효한 토큰인 경우에만 사용자 프로필을 조회하는 람다 함수를 실행할 수 있는 권한을 부여

 

참고 ⇒ https://auth0.com/docs/tokens/json-web-tokens/validate-json-web-tokens



https://docs.aws.amazon.com/ko_kr/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html





#1 JWT 토큰을 검증하는 custom-authorizer 람다 함수를 생성

#1-1 람다 함수 생성



#1-2 작업 디렉터리 생성 및 필요 모듈 설치

PS C:\Users\i> cd C:\serverless\

PS C:\serverless> mkdir custom-authorizer

PS C:\serverless> cd .\custom-authorizer\

PS C:\serverless\custom-authorizer> npm init -y

PS C:\serverless\custom-authorizer> npm install jsonwebtoken



#1-3 package.json 파일에 predeploy, deploy 스크립트를 추가

{

  "name": "custom-authorizer",

  "version": "1.0.0",

  "description": "",

  "main": "index.js",

  "scripts": {

    "predeploy": "del Lambda-Deployment.zip & zip -r Lambda-Deployment.zip * -x *.zip *.log node_modules/aws-sdk/*", 

    "deploy": "aws lambda update-function-code --function-name custom-authorizer-람다함수ARN --zip-file fileb://Lambda-Deployment.zip"

  },

  "keywords": [],

  "author": "",

  "license": "ISC",

  "dependencies": {

    "jsonwebtoken": "^8.5.1"

  }

}

 

#1-4 람다 함수 구현 ⇒ c:\serverless\custom-authorizer\index.js

// P141

'use strict';



var jwt = require('jsonwebtoken');



// generatePolicy 함수를 정의

/* 정책 문서 형식을 생성

    {

        "principalId": "...", 

        "policyDocument": {

            "Version": "2012-10-17",

            "Statement": [

                {

                    "Effect": "Allow",

                    "Action": "*",

                    "Resource": "*"

                }

            ]

        }

    }

*/

var generatePolicy = function (principalId, effect, resource) {

    var authResponse = {};  // authResponse 객체 선언

    authResponse.principalId = principalId;

    if (effect && resource) {

        var policyDocument = {};

        policyDocument.Version = '2012-10-17'; // 정책 문서 형식 버전

        policyDocument.Statement = []; 



        var statementOne = {};

        statementOne.Action = 'execute-api:Invoke'; 

        statementOne.Effect = effect;

        statementOne.Resource = resource;



        policyDocument.Statement[0] = statementOne;

        authResponse.policyDocument = policyDocument;

    }

    return authResponse;

};



// 핸들러 함수를 정의

exports.handler = function(event, context, callback) {



    if (!event.authorizationToken) {

        callback('Could not find authorizationToken');

        return;

    }



    // JWT 토큰의 앞부분(Bearer)을 제거

    // "authorizationToken": "Bearer eyJhbGciOiJ~~~cCI6IkpXVCJ9.eyJnaXZlbl~~~pKNDQuZSJ9.mioxKcb1~~~W1LTk5_anGo"

    var token = event.authorizationToken.split(' ')[1];



    // auth0.com에서 제공한 Client Secret을 환경변수로부터 읽어와서 변수에 할당

    var secretBuffer = new Buffer(process.env.AUTH0_SECRET);

    // JWT 토큰을 검증

    jwt.verify(token, secretBuffer, function(err, decoded) {

        if (err) {

            console.log('Failed jwt verification: ', err, 'auth:', event.authorizationToken);

            callback('Authorization Failed');

        } else {

            var policy = generatePolicy('user', 'allow', event.methodArn);

            console.log(policy);

            callback(null, policy);

        }

    });

};

 



#1-5 람다 함수 배포

PS C:\serverless\custom-authorizer> npm run deploy



#1-6 람다 함수 실행에 필요한 환경 변수를 설정

 

#1-7 람다 함수 테스트

로컬 스토리지에 저장된 idToken 값에 "Bearer "를 추가한 값을 "authorizationToken" 값으로 설정

 

{

   "authorizationToken": "Bearer xxxxxxxxxxxxxxxxxxxxx.yyyyyyyyyyyyyyyyyyyy.zzzzzzzzzzzzzzzzzzz"

}

 

 

#1-8 https://jwt.io 사이트를 통해서 JWT 토큰을 수작업으로 검증



#1-9 auth0.com 서명에서 사용하는 알고리즘을 변경

:

 

Specify the algorithm used to sign the JsonWebToken: 

  • HS256: JWT will be signed with your client secret. ⇐ 비밀키 암호화 방식
  • RS256: JWT will be signed with your private signing key and they can be verified using your public signing key. ⇐ 공개키 암호화 방식 



#1-10 다시 로그인해서 발급받은 idToken을 추출해서 테스트

 

#1-11 API Gateway에 권한 부여자를 생성

 

#1-12 사용자 정의 권한 부여자를 /user-profile - GET 메서드에 연결

주의: API Gateway 대시보드로 이동했다가 리소스 메뉴로 이동

 



#1-13 로그인 후 프로필 버튼을 클릭했을 때 사용자 정보가 콘솔에 출력되는지 확인

요청/응답 헤더

 

브라우저 콘솔 로그

 

user-profile 로그 그룹이 생성 ⇒ user-profile 람다 함수가 호출(실행)되었음

 

custom-authorizer 로그

 

user-profile 로그



#1-14 개발자 도구를 이용해서 idToken 값을 수정(조작)했을 때 오류가 출력되는 것을 확인

요청/응답 헤더 ⇒ 4xx번 대 오류와 5xx번 대 오류가 발생하는 경우 CORS 관련 헤더를 설정하지 않음

 

브라우저 콘솔 로그

 

CloudWatch에 user-profile 로그 그룹이 생성되지 않음 ⇒ user-profile 람다 함수가 호출(실행)되지 않음

 

customer-authorizer 람다 함수의 로그




#1-15 4xx번 대 오류와 5xx번 대 오류가 발생해다 CORS 관련 헤더를 설정하도록 CORS 설정



 

S3 버킷에 저장된 파일 목록을 반환하는 람다 함수를 생성, 배포 (P166)

CLI 환경에서 람다 함수를 생성하고, 배포 ⇒ CreateFunction 권한이 필요

 

#1 Lambda-DevOps 그룹에 CreateFunction 권한을 추가



#2 프로젝트 환경 구성

#2-1 작업 디렉터리 생성 및 프로젝트 초기화

PS C:\serverless\custom-authorizer> cd C:\serverless\

PS C:\serverless> mkdir get-video-list

PS C:\serverless> cd .\get-video-list\

PS C:\serverless\get-video-list> npm init -y

 

#2-2 필요 모듈 추가

PS C:\serverless\get-video-list> npm install aws-sdk

PS C:\serverless\get-video-list> npm install async

 

참고 ⇒ https://www.npmjs.com/package/async



#2-3 package.json 파일에 create, precreate 스크립트를 추가

{

  "name": "get-video-list",

  "version": "1.0.0",

  "description": "",

  "main": "index.js",

  "scripts": {

    "create": "aws lambda create-function --function-name get-video-list --handler index.handler --memory-size 128 --runtime nodejs4.3 --role 본인의_lambda-s3-execution-role_ARN --timeout 3 --publish --zip-file fileb://Lambda-Deployment.zip", 

    "precreate": "zip -r Lambda-Deployment.zip * -x *.zip *.log node_modules/aws-sdk/*"

  },

  "keywords": [],

  "author": "",

  "license": "ISC", 

  "dependencies": {

    "async": "^3.2.0",

    "aws-sdk": "^2.865.0"

  }

}

--publish ⇒ 생성과 동시에 배포

 

참고 ⇒ PS C:\serverless\get-video-list> aws lambda create-function help



#3 람다 함수 작성

async 모듈의 waterfall() 함수 사용법

// https://caolan.github.io/async/v3/docs.html#waterfall



var async = require("async");

 

async.waterfall([

    function(firstcallbackfunc) {

        console.log(`첫번째 함수`);

        firstcallbackfunc(null, "Peter", "Sam");

    }, 

    function(a1, a2, secondcallbackfunc) {

        console.log(`두번째 함수 ${a1}, ${a2}`);

        secondcallbackfunc(null, "Serverless");

    }, 

    function(a3, thirdcallbackfunc) {

        console.log(`세번째 함수 ${a3}`);

        thirdcallbackfunc(null, "Done")

    }

], function(err, result) {

    console.log(`최종 콜백 ${err}, ${result}`);

});

 

C:\serverless\get-video-list\index.js 

// P172

'use strict';



// 필요 모듈 추가 및 S3 객체 생성

var AWS = require('aws-sdk');

var async = require('async');

var s3 = new AWS.S3();



// next : callback 함수

// next(ERROR, DATAS, ...)

// next(null, ...) ==> 오류가 발생하지 않았으며, 어떤 값을 반환

// next(어떤값)    ==> callback 함수로 오류를 반환



// S3.listObjects 함수 호출에 사용할 입력 포맷을 생성

function createBucketParams(next) {

    var params = {

        Bucket: process.env.BUCKET,

        EncodingType: 'url'

    };

    next(null, params); // #1 함수가 호출

}



// #1 버킷의 객체(파일) 목록을 조회

function getVideosFromBucket(params, next) {

    // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#listObjects-property

    s3.listObjects(params, function(err, data) {

        if (err) {

            next(err);

        } else {

            next(null, data); // #2 함수가 호출

        }

    });

}



// #2 버킷의 객체 목록 조회 결과를 반환 형식에 맞춰서 변형

function createList(data, next) {

    console.log(data); 



    // 버킷의 객체 이름(폴더명과 확장자를 포함)을 저장할 배열

    var urls = [];

    for (var i = 0; i < data.Contents.length; i ++) {

        var file = data.Contents[i];

        // https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/String/substr

        // 키(객체 이름)의 마지막 세글자가 mp4인 경우 

        if (file.Key && file.Key.substr(-3, 3) === 'mp4') {

            urls.push(file);

        }

    }



    var result = {

        baseUrl: process.env.BASE_URL,  // 버킷 접근 URL

        bucket: process.env.BUCKET,

        urls: urls

    };



    next(null, result); // #3 함수로 전달

}



exports.handler = function(event, context, callback) {

    async.waterfall([ 

        createBucketParams, 

        getVideosFromBucket, 

        createList

    ], 

    // #3 함수 : [ ... ]에 정의된 함수가 모두 정상 수행 또는 오류가 발생한 경우에 호출

    function(err, result) {

        if (err) {

            callback(err);

        } else {

            callback(null, result); // 버킷에 저장된 객체 목록을 버킷 접속 주소, 버킷 ARN과 함께 반환

        }

    });

};

 

 

#4 람다 함수 생성 및 배포

PS C:\serverless\get-video-list> npm run create

:

An error occurred (AccessDeniedException) when calling the CreateFunction operation: User: arn:aws:iam::199503606661:user/lambda-upload is not authorized to perform: iam:PassRole on resource: arn:aws:iam::199503606661:role/lambda-s3-execution-role ⇒ iam:PassRole을 추가

npm ERR! code ELIFECYCLE

npm ERR! errno 254

npm ERR! get-video-list@1.0.0 create: `aws lambda create-function --function-name get-video-list --handler index.handler --memory-size 128 --runtime nodejs4.3 --role arn:aws:iam::199503606661:role/lambda-s3-execution-role --timeout 3 --publish --zip-file fileb://Lambda-Deployment.zip`

npm ERR! Exit status 254

npm ERR!

npm ERR! Failed at the get-video-list@1.0.0 create script.

npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

 

npm ERR! A complete log of this run can be found in:

npm ERR!     C:\Users\i\AppData\Roaming\npm-cache\_logs\2021-03-17T06_28_26_758Z-debug.log



#5 Lambda-DevOps 그룹에 권한을 추가

 



#6 다시 람다 함수 생성 및 배포

PS C:\serverless\get-video-list> npm run create

:

An error occurred (InvalidParameterValueException) when calling the CreateFunction operation: The runtime parameter of nodejs4.3 is no longer supported for creating or updating AWS Lambda functions. We recommend you use the new runtime (nodejs12.x) while creating or updating functions. ⇒ 런타임 버전이 낮아서 함수를 생성할 수 없음

npm ERR! code ELIFECYCLE

npm ERR! errno 254

npm ERR! get-video-list@1.0.0 create: `aws lambda create-function --function-name get-video-list --handler index.handler --memory-size 128 --runtime nodejs4.3 --role arn:aws:iam::199503606661:role/lambda-s3-execution-role --timeout 3 --publish --zip-file fileb://Lambda-Deployment.zip`

npm ERR! Exit status 254

npm ERR!

npm ERR! Failed at the get-video-list@1.0.0 create script.

npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

 

npm ERR! A complete log of this run can be found in:

npm ERR!     C:\Users\i\AppData\Roaming\npm-cache\_logs\2021-03-17T06_42_58_746Z-debug.log



#7 런타임 버전 변경 후 다시 람다 함수 생성 및 배포

c:\serverless\get-video-list\package.json

{

  "name": "get-video-list",

  "version": "1.0.0",

  "description": "",

  "main": "index.js",

  "scripts": {

    "create": "aws lambda create-function --function-name get-video-list --handler index.handler --memory-size 128 --runtime nodejs12.x --role arn:aws:iam::199503606661:role/lambda-s3-execution-role --timeout 3 --publish --zip-file fileb://Lambda-Deployment.zip",

    "precreate": "zip -r Lambda-Deployment.zip * -x *.zip *.log node_modules/aws-sdk/*"

  },

  "keywords": [],

  "author": "",

  "license": "ISC",

  "dependencies": {

    "async": "^3.2.0",

    "aws-sdk": "^2.865.0"

  }

}

 

 

PS C:\serverless\get-video-list> npm run create

:

> get-video-list@1.0.0 create C:\serverless\get-video-list

> aws lambda create-function --function-name get-video-list --handler index.handler --memory-size 128 --runtime nodejs12.x --role arn:aws:iam::199503606661:role/lambda-s3-execution-role --timeout 3 --publish --zip-file fileb://Lambda-Deployment.zip

 

{

    "FunctionName": "get-video-list",

    "FunctionArn": "arn:aws:lambda:us-east-1:199503606661:function:get-video-list",

    "Runtime": "nodejs12.x",

    "Handler": "index.handler",

    "CodeSize": 557793,

    "Description": "",

    "Timeout": 3,

    "MemorySize": 128,

    "LastModified": "2021-03-17T06:47:31.507+0000",

    "CodeSha256": "NyFR3MY462w8GWw2xysqgP13albNUfi4K+gstWh+rNI=",

    "Version": "1",

    "TracingConfig": {

        "Mode": "PassThrough"

    },

    "RevisionId": "1ae981a6-5d03-4ffd-999f-5622b53780e2",

    "State": "Active",

    "LastUpdateStatus": "Successful",

    "PackageType": "Zip"

}



#8 함수 실행에 필요한 환경 변수 설정

 

BUCKET 환경 변수 ⇒ 트랜스 코딩된 결과가 저장된 버킷 이름

 

BASE_URL 환경 변수 ⇒ 트랜스 코딩된 객체에 객체 URL에서 키를 제외한 부분 ⇒ https://버킷이름.s3.amazonaws.com 

 

#9 람다 함수 테스트

해당 함수는 외부(함수를 호출하는 곳)에서 전달하는 값을 사용하는 부분이 없음 ⇒ event 객체를 사용하는 부분이 없음

⇒ 테스트를 수행할 때 별도의 값 설정이 필요 없음

 

 

동영상 파일 목록을 반환하는 것을 확인 (모두 세 개를 반환)




API Gateway에 get-video-list API를 추가 (P193)

#1 get-video-list API 생성

 

#2 리소스 생성

 

 

#3 메소드 생성



#4 메소드 테스트 

 

#5 CORS 활성화 및 API 배포







웹 사이트에 비디오 목록을 가져와서 재생하는 기능을 추가 (P209. 참고로 책 내용과는 상이)

#1 config.js 파일에 get-file-list API 호출 URL을 추가

var configConstants = {

    auth0: {

        domain: 'naanjini.us.auth0.com',

        clientId: 'BAdEeZTkFlAdyC1a7nWENB3sRydpByXR'

    },

    // user-profile API 호출 URL

    apiBaseUrl: 'https://vfhp67njsk.execute-api.us-east-1.amazonaws.com/dev', 



    // get-file-list API 호출 URL

    getFileListApiUrl: 'https://6ln109dh77.execute-api.us-east-1.amazonaws.com/dev'

};

 

#2 video-controller.js 파일을 추가

 

c:\serverless\24-hour-video\js\video-controller.js

// P209

var videoController = {

    data: {

        config: null

    },

    uiElements: {

        videoCardTemplate: null, 

        videoList: null

    }, 

    init: function(config) {

        // index.html 문서에 id 속성이 video-template, video-list인 요소를 참조

        this.uiElements.videoCardTemplate = $('#video-template');

        this.uiElements.videoList = $('#video-list');

 

        // config.js 파일에 있는 내용을 참조

        this.data.config = config;

 

        this.getVideoList();

    }, 

    // get-video-list API를 호출

    getVideoList: function() {

        var that = this;

 

        // get-video-list API 호출 URL + 리소스 이름

        // videos 리소스를 GET 방식으로 호출 --> get-video-list 람다 함수를 실행하고 결과를 반환 받음

        var url = this.data.config.getFileListApiUrl + '/videos';

        $.get(url, function(data, status) {

            that.updateVideoFrontPage(data);

        });

    }, 

    // get-video-list 람다 함수의 실행 결과를 목록으로 화면에 출력

    updateVideoFrontPage: function(data) {

        console.log(data);

        // TODO

    }

};

 



#3 main.js 파일에 videoController 객체의 초기화 함수 호출을 추가 

c:\serverless\24-hour-video\js\main.js

// 즉시 실행 함수

(function() {

    console.log("### 1");

    // 해당 웹 페이지 문서가 로딩되면 설정 정보를 가져와서 설정

    $(document).ready(function() {

        console.log("### 2");

        // user-controller.js에 선언되어 있는 userController 객체의 init 메소드를 호출

        // coonfig.js에 선언되어 있는 configConstants 객체를 인자로 전달

        userController.init(configConstants);

        videoController.init(configConstants);  /* 추가 */

    });

})();



#4 index.html 파일에 동영상 목록과 출력 부분을 추가

c:\serverless\24-hour-video\index.html

 

 <!-- (생략) -->

    <!-- Main jumbotron for a primary marketing message or call to action -->

    <div class="jumbotron">

      <!-- 대략 66번 라인

      <div class="container">

        <h1>Hello, world!</h1>

        <p>This is a template for a simple marketing or informational website. It includes a large callout called a jumbotron and three supporting pieces of content. Use it as a starting point to create something more unique.</p>

        <p><a class="btn btn-primary btn-lg" href="#" role="button">Learn more &raquo;</a></p>

      </div>

      -->

      <!-- 추가 시작: P209 코드 참조 -->

      <div class="container" id="video-list-container">

        <div id="video-template" class="col-md-6 col">

          <div class="video-card">

            <!-- 동영상을 재생해 주는 요소(태그) -->

            <video width="100%" height="100%" controls>

              <!-- video 태그에 사용될 동영상의 소스 -->

              <!-- src 속성에 동영상의 주소를 추가하면 재생이 가능 -->

              <source type="video/mp4">

              지원하지 않는 타입

            </video>

          </div>

        </div>

        <div calss="row">

          <!-- S3 버킷에 저장된 동영상의 목록을 제시하고 

               동영상 이름을 클릭하면 video 태그를 통해서 동영상 재생 -->

          <ul id="video-list">

 

          </ul>

        </div>

      </div>

      <!-- 추가 끝 -->

    </div>

    <!-- (생략) -->

        <script src="https://cdn.auth0.com/js/lock/11.27/lock.min.js"></script>

        <script src="js/config.js"></script>

        <script src="js/user-controller.js"></script>

        <script src="js/video-controller.js"></script>  <!-- 추가 -->

        <script src="js/main.js"></script>

        <!-- (생략) -->

 

#5 웹 페이지 테스트  

#6 S3 버킷 목록을 화면에 출력

c:\serverless\24-hour-video\js\video-controller.js

 

  updateVideoFrontPage: function(data) {

        console.log(data);

        // <ul id="video-list">

        //

동영상 파일명
        //          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~  ~~~~~~~~~~~~

        //          baseUrl                                    urls.Key                     urls.Key에서 파일명만 추출

        var baseUrl = data.baseUrl;

        var urls = data.urls;

        /*

        for (var i = 0; i < urls.length; i ++) {

            var url = urls[i];

            var key = url.Key;

            var filename = key.split('/')[1];

            var litag = '<li url="' + baseUrl + '/' + key + '">' + filename + '</li>';

            $('#video-list').append(litag);

        }

        */



        urls.forEach(url => {

            var key = url.Key;

            var filename = key.split('/')[1];

            var litag = `<li url="${baseUrl}/${key}">${filename}</li>`; 

            $('#video-list').append(litag);

        });

    }

 

 

#7 목록을 클릭했을 때 비디오가 재생

c:\serverless\24-hour-video\js\video-controller.js

// P209

var videoController = {

    data: {

        config: null

    },

    uiElements: {

        videoCardTemplate: null, 

        videoList: null

    }, 

    init: function(config) {

        // index.html 문서에 id 속성이 video-template, video-list인 요소를 참조

        this.uiElements.videoCardTemplate = $('#video-template');

        this.uiElements.videoList = $('#video-list');

 

        // config.js 파일에 있는 내용을 참조

        this.data.config = config;

 

        this.getVideoList();

        this.wireEvents();   // 이벤트 핸들러를 추가

    }, 

    // get-video-list API를 호출

    getVideoList: function() {

        var that = this;

 

        // get-video-list API 호출 URL + 리소스 이름

        // videos 리소스를 GET 방식으로 호출 --> get-video-list 람다 함수를 실행하고 결과를 반환 받음

        var url = this.data.config.getFileListApiUrl + '/videos';

        $.get(url, function(data, status) {

            that.updateVideoFrontPage(data);

        });

    }, 

    // get-video-list 람다 함수의 실행 결과를 목록으로 화면에 출력

    updateVideoFrontPage: function(data) {

        console.log(data);

        // <ul id="video-list">

        //

동영상 파일명
        //          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~  ~~~~~~~~~~~~

        //          baseUrl                                                      urls.Key                     urls.Key에서 파일명만 추출

        var baseUrl = data.baseUrl;

        var urls = data.urls;



        /*

        for (var i = 0; i < urls.length; i ++) {

            var url = urls[i];

            var key = url.Key;

            var filename = key.split('/')[1];

            var litag = '<li url="' + baseUrl + '/' + key + '">' + filename + '</li>';

            $('#video-list').append(litag);

        }

        */



        urls.forEach(url => {

            var key = url.Key;

            var filename = key.split('/')[1];

            var litag = `<li url="${baseUrl}/${key}">${filename}</li>`; 

            $('#video-list').append(litag);

        });

    }, 



    // 추가된 부분 시작

    // 이벤트를 처리하는 함수(이벤트 핸들러)를 정의

    // https://developer.mozilla.org/ko/docs/Web/HTML/Element/Video

    wireEvents: function() {

        // id 속성(attibute) 값이 video-list인 요소(element, tag) 아래에서 

        // li 요소에 click 이벤트가 발생했을 때 수행할 동작을 정의

        $('#video-list').on('click', 'li', function() {

            // 클릭한 li 태그(요소)의 url 속성의 값을 가져와서 url 변수에 할당

            var url = $(this).attr('url');

            /*

            <video width="100%" height="100%" controls>

              <!-- video 태그에 사용될 동영상의 소스 -->

              <!-- src 속성에 동영상의 주소를 추가하면 재생이 가능 -->

              <source type="video/mp4">

              지원하지 않는 타입

            </video>

            */

            // source 태그(요소)의 src 속성의 값으로 url 변수의 값을 설정

            $('source').attr('src', url);

            // video 태그(요소)에 설정된 동영상 파일을 읽어들림

            $('video').load();

        });

    }

    // 추가된 부분 끝

};

728x90

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

3/19 - AWS 마무리 정리  (0) 2021.03.19
3/19 - AWS 15차시  (0) 2021.03.19
3/16 - AWS 13차시  (0) 2021.03.16
3/15 - AWS 12차시  (0) 2021.03.15
3/12 - AWS 11차시  (0) 2021.03.12

+ Recent posts