CDK를 이용한 AWS Lambda 함수 생성
AWS CDK(Cloud Development Kit)는 프로그래밍 언어를 이용해 클라우드 인프라스트럭처를 정의할 수 있게 해 주는 도구다. CloudFormation 스크립트를 사용하는 것 보다 훨씬 쉽게 인프라스트럭처 컴포넌트를 정의하고 관계를 설정할 수 있다.
데이터베이스에 저장되어 있는 텍스트를 읽어 트위터에 주기적으로 트윗하는 간단한 앱을 만들려 한다. 데이터베이스로 DynamoDB를 사용할 것이고 트윗 기능은 Lambda 함수로 정의할 것이다. DynamoDB와 Lambda 정의, 이벤트 및 기타 환경 설정에 CDK를 이용할 것이다.
CDK 설치
CDK는 npm
으로 간단히 설치할 수 있다.
$ npm install -g aws-cdk
프로젝트 생성
다음과 같이 프로젝트 루트로 사용할 디렉터리를 만들고 cdk init
명령으로 디렉터리를 초기화한다.
$ mkdir tweetbook
$ cd tweetbook
$ cdk init --app Tweetbook --language javascript
cdk init
명령은 해당 디렉터리를 Git 저장소로 만든다. Github에서 tweetbook-lambda-cdk.git
저장소를 생성하고, 연결하려면 다음과 같이 한다.
$ git remote add origin git@github.com:ntalbs/tweetbook-lambda-cdk.git
$ git push -u origin master
lib
디렉터리 안의 tweetbook-stack.js
에 인프라스트럭처 정의 코드를 넣을 것이다. 처음에는 다음과 같이 아무것도 안 하는 템플릿 코드만 있다.
const cdk = require('@aws-cdk/core');
class TweetbookStack extends cdk.Stack {
/**
*
* @param {cdk.Construct} scope
* @param {string} id
* @param {cdk.StackProps=} props
*/
constructor(scope, id, props) {
super(scope, id, props);
// The code that defines your stack goes here
}
}
module.exports = { TweetbookStack }
스택 이름이 TweetbookStack
으로 되어 있다. 스택 이름은 디렉터리 이름을 참조해 생성된다.
DynamoDB 테이블 생성
DynamoDB 테이블을 만들기 위해 다음과 같이 @aws-cdk/aws-dynamodb
모듈을 설치한다.
$ npm install @aws-cdk/aws-dynamodb
그리고 lib/tweetbook-stack.js
파일을 열어 코드를 다음과 같이 수정한다.
const cdk = require('@aws-cdk/core')
const dynamodb = require('@aws-cdk/aws-dynamodb')
class TweetbookStack extends cdk.Stack {
constructor(scope, id, props) {
super(scope, id, props)
const quotesTable = new dynamodb.Table(this, 'Tweetbook-Quotes', {
tableName: 'Tweetbook-Quotes',
partitionKey: { name: '_id', type: dynamodb.AttributeType.NUMBER },
})
}
}
module.exports = { TweetbookStack }
이제 콘솔에서 cdk deploy
명령을 실행한다. 실행이 끝난 후 AWS 콘솔에 들어가보면 Tweetbook-Quotes
테이블이 생성된 것을 확인할 수 있다.
Lambda 함수 정의
Lambda 함수를 만들기 위해서는 @aws-cdk/aws-lambda
모듈이 필요하다.
$ npm install @aws-cdk/aws-lambda
Lambda 함수의 소스 코드를 저장할 디레터리를 다음과 같이 만든다.
$ mkdir -p lambdas/tweet
그리고 위 디렉터리에 다음 내용으로 index.js
파일을 만든다.
exports.handler = async (event) => {
const response = {
statusCode: 200,
body: 'Hello Lambda'
}
return response
}
일단은 간단한 JSON을 리턴하도록 했다. 나중에 좀더 의미있는 일을 하도록 수정할 것이다.
이제 lib/tweetbook-stack.js
파일을 열어 다음 코드를 추가한다.
// ...
const lambda = require('@aws-cdk/aws-lambda');
class TweetbookStack extends cdk.Stack {
constructor(scope, id, props) {
// ...
const tweetFn = new lambda.Function(this, 'Tweetbook-Tweet', {
functionName: 'Tweetbook-Tweet',
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambdas/tweet),
})
}
// ...
cdk diff
명령을 실행해보면 현재 프로젝트에서 생성할 스택과 이미 배포된 스택과의 차이를 보여준다. cdk deploy
를 실행하면 모든 리소스를 새로 만드는 게 아니라 차이를 비교하고 리소스를 생성 또는 제거한다.
Lambda 함수의 코드가 아주 짧은 경우는 lambda.Code.fromInline
을 사용해 코드를 인라인으로 지정하는 것도 가능하다. 파일이 하나라면 fs.readFileSync
로 파일을 읽어 문자열로 지정할 수도 있다. 그러나 Lambda 함수에서 다른 모듈을 사용해야 한다면 Asset으로 배포해야 한다.
여기서는 Asset을 이용해 Lambda 코드를 배포하므로 DynamoDB 테이블을 생성할 때와 같이 그냥 cdk deploy
만 실행하면 실패할 것이다. 먼저 cdk bootstrap
명령을 실행해야 한다. cdk bootstrap
명령을 한번 실행한 이후에는 매번 다시 실행하지 않아도 된다.
$ cdk bootstrap
$ cdk deploy
AWS 콘솔에 들어가보면 Lambda 함수가 생성된 것을 확인할 수 있다.
트리거 설정
주기적으로 트윗을 하기 위해서는 Lambda 함수를 주기적으로 호출해야 한다. EventBridge
를 트리거로 추가하면 Lambda 함수를 규칙에 따라 주기적으로 호출할 수 있다. 이를 설정하려면 다음 두 모듈을 설치해야 한다.
$ npm install @aws-cdk/aws-events
$ npm install @aws-cdk/aws-events-targets
그리고 lib/tweetbook-stack.js
에 다음 코드를 추가한다.
// ...
const events = require('@aws-cdk/aws-events');
const targets = require('@aws-cdk/aws-events-targets');
class TweetbookStack extends cdk.Stack {
constructor(scope, id, props) {
// ...
const rule = new events.Rule(this, 'Tweetbook-EveryDayAt6am', {
ruleName: 'Tweetbook-EveryDayAt6am',
schedule: events.Schedule.expression('cron(0 6 * * ? *)')
});
rule.addTarget(new targets.LambdaFunction(tweetFn));
}
// ...
트리거 이벤트는 매일 오전 6시(UTC)에 발생하도록 했다. cdk deploy
를 실행한 다음 AWS 콘솔에 들어가 확인해보면 Lambda 함수에 트리거가 설정된 것을 확인할 수 있다.
테이블 접근 권한
Lambda 함수의 원래 목적은 DynamoDB 테이블에서 데이터를 읽어 트위터에 트윗하는 것이다. 그러나 위에서 정의한 Lambda 함수는 Tweetbook-Quotes
테이블을 읽을 권한이 없다. Lambda 함수가 실행될 때 Tweetbook-Quotes
에서 데이터 한 건을 읽을 것이므로 지금은 GetItem
권한만 있으면 충분하다. Lambda 함수를 정의한 코드 바로 아래 다음 코드를 추가한다.
// ...
quotesTable.grant(tweetFn, 'dynamodb:GetItem');
// ...
DynamoDB 접근
Lambda 함수에서 DynamoDB 데이터를 읽도록 수정하려 한다. DynamoDB를 읽을 때 DocumentClient
를 사용하는 게 좀더 편하다. Tweetbook-Quotes
테이블에는 다음과 같은 식의 데이터가 들어있다고 가정한다.
{
"_id": 10,
"msg", "...",
"src", "..."
}
_id
는 0부터 999까지 1,000건의 데이터가 들어 있고, 0부터 999 사이의 랜덤 값을 구한 다음, 데이터를 읽을 때 _id
로 제시하도록 했다. 범위를 미리 알고 있어야 하는 게 마음에 들지 않지만, DynamoDB에서 랜덤하게 데이터를 읽는 좋은 방법을 찾지 못했다.
이제 lambdas/tweetbook/index.js
파일을 열어 다음과 같이 수정한다.
const aws = require('aws-sdk')
const dynamodb = new aws.DynamoDB.DocumentClient()
const MAX_INDEX = 1000
function random() {
return Math.floor(Math.random() * MAX_INDEX)
}
async function getQuote(id) {
let param = {
Key: {
"_id": id
},
TableName: "Tweetbook-Quotes"
}
let q = await dynamodb.get(param).promise()
return {
msg: q.Item.msg,
src: q.Item.src
}
}
exports.handler = async (event) => {
let id = random()
let quote = await getQuote(id)
const response = {
statusCode: 200,
body: quote
}
return response
}
Secrets Manager 설정
트위터에 API로 접근하기 위해서는 API 키와 토큰이 필요하다. 트위터 API 클라이언트로 사용할 twit을 초기화하는 데 consumer_key
, consumer_secret
, access_token
, access_token_secret
를 지정해야 하며 이 값은 트위터 개발자 사이트에서 얻을 수 있다. 여기서는 이 키와 토큰 값을 Secrets Manager에 저장하려 한다. 외부에서 제공하는 비밀정보이므로 AWS 콘솔에서 직접 생성한다.
{
"TWITTER_ACCESS_TOKEN": "...access_token_value...",
"TWITTER_ACCESS_TOKEN_SECRET": "...access_token_secret_value...",
"TWITTER_CONSUMER_KEY": "...consumer_key_value...",
"TWITTER_CONSUMER_SECRET": "...consumer_secret_value..."
}
시크릿을 생성했으면 tweetbook-stack.js
에 다음 코드를 추가해 Lambda 함수에서 위 시크릿을 읽을 수 있도록 한다.
// ...
const secret = sm.Secret.fromSecretAttributes(this, 'TweetbookSecret', {
secretArn: '<<secret-arn>>'
})
secret.grantRead(tweetFn)
// ...
메시지 트윗
lambdas/tweetbook
디렉터리 안에서 다음 명령을 실행해 Twit
을 설치한다.
$ npm init
$ npm install twit
lambdas/tweetbook
디렉터리는 Lambda 함수를 정의하는 새로운 패키지의 루트 디렉터리로, 람다 함수에서 사용하는 외부 라이브러리는 이 디렉터리에 설치되어야 하므로 npm init
을 실행해 package.json
을 생성해야 한다.
트위터 API를 초기화하는 데 필요한 consumer_key
, consumer_secret
, access_token
, access_token_secret
를 Secrets Manager에서 읽어오도록 index.js
파일에 다음 코드를 추가한다.
const Twit = require('twit')
async function getTokens () {
let secrets = await secretsManager.getSecretValue({
SecretId: 'TwitterBookBotApiTokens'
}).promise()
return JSON.parse(secrets.SecretString)
}
let T;
async function tweet(quote) {
if (!T) {
let tokens = await getTokens();
T = new Twit({
consumer_key: tokens.TWITTER_CONSUMER_KEY,
consumer_secret: tokens.TWITTER_CONSUMER_SECRET,
access_token: tokens.TWITTER_ACCESS_TOKEN,
access_token_secret: tokens.TWITTER_ACCESS_TOKEN_SECRET
})
}
let msg = `${quote.msg}\n${quote.src}`
return T.post('statuses/update', {status: msg})
}
그리고 DynamoDB에서 읽은 메시지를 트위터에 트윗하도록 handler
함수를 다음과 같이 수정한다.
exports.handler = async (event) => {
let id = random()
let q = await getQuote(id)
let t = await tweet(q)
const response = {
statusCode: 200,
quote: q,
tweetId: t.data.id
}
return response;
}
모든 게 제대로 설정되었다면, AWS 콘솔에서 Lambda 함수를 실행한 후 트위터를 확인해보면 메시지가 트윗되었음을 확인할 수 있다.
스택 제거
cdk destroy
명령을 실행하면 위에서 생성한 모든 AWS 리소스를 제거한다. DynamoDB 테이블은 삭제되지 않는데 아마도 실수로 인한 데이터 손실을 방지하기 위한 것으로 보인다. Secrets Manager에서 수작업으로 생성한 시크릿 또한 이 스택에 포함되지 않으므로 삭제되지 않는다. 따라서 DynamoDB 테이블과 수작업으로 생성한 시크릿은 직접 제거해야 한다.
정리
AWS CDK를 이용하면 CloudFormation 스크립트나 AWS-CLI를 이용하는 것보다 훨씬 쉽게 AWS 인프라스트럭처를 정의할 수 있다. CDK를 쓸 때 JavaScript 외에도 TypeScript, Python, C#, Java도 지원하므로 마음에 드는 언어를 사용하면 된다.
사용 언어에 따라 차이가 있겠지만, 여기서 JavsScript로 작성한 CDK 코드는 40여줄에 불과하다. CDK로 생성한 JSON 형식의 CloudFormation 스크립트는 200줄이 넘는다. CDK를 사용하면 CloudFormation 스크립트를 사용할 때보다 훨씩 적은 코드로 AWS 스택을 정의할 수 있다.