Docker 이미지로 AWS Lambda 만들기 (Boilerplate 포함)

Docker 이미지로 AWS Lambda 만들기 (Boilerplate 포함)

2020년 말, AWS Lambda의 새로운 기능으로 컨테이너 이미지 지원이 발표되었다. 기존 lambda 의 프로비저닝에 비해 얻을 수 있는 이점을 요약하자면 다음과 같다.

- 최대 10GB 크기의 컨테이너 이미지로 패키징 및 배포할 수 있는 기능 제공

- Dependencies 관리 및 설치의 용이성

- 다른 linux distro 의 이미지에서도 사용 가능함. (다만, 좀 까다로움. aws lambdaric 를 설치해야함.)

나는 Dependencies 설치가 쉽다는 점 하나만으로 docker 이미지를 사용할만한 가치가 있다고 생각한다.

Boilerplate

GitHub - byunjuneseok/container-image-lambda-boilerplate: Deploy python lambda functions with container images easily with pre-written IaC script.
Deploy python lambda functions with container images easily with pre-written IaC script. - GitHub - byunjuneseok/container-image-lambda-boilerplate: Deploy python lambda functions with container im...

한줄요약 : 🪄 작성된 스크립트만 잘 실행시켜주면 컨테이너 이미지 빌드부터, ECR 등록, Lambda 및 API GW 배포까지 다 해줘요. (커스터마이징도 쉬울걸요..?)

지금까지 lambda의 프로비저닝 자동화 경험으로는 amazon-linux2 이미지의 VM을 띄워놓고, 그 VM 안에서 빌드 및 프로비저닝을 해왔다. (golang이라면 이럴 필요까지야 없겠지만.) M1 Macbook의 업무 개발 환경으로 넘어오면서 OS에 의존적이지 않은 Lambda 의 프로비저닝이 필요했는데, 이를 해결하기 위해서 위와 같은 보일러플레이트를 만들었다.

PoC 의 개념으로 시작한 보일러플레이트인데, 생각보다 쓸만한 것 같다. 로컬에서 의존성 설치하고 압축해서 프로비저닝하는 것 보다 좋은 것 같다. 로컬머신과 독립된 도커 이미지에서 빌드하기 때문에 완벽히 멱등성(Idempotent)을 확보할 수 있다.

00-login-ecr.sh

aws ecr get-login-password --region "$AWS_REGION" \
  | docker login --username AWS --password-stdin "$AWS_ACCOUNT_ID".dkr.ecr."$AWS_REGION".amazonaws.com

AWS ECR 에 로그인하여, 토큰을 받아와 docker 로그인하는 쉘스크립트다. 여기서 토큰은 12시간동안만 유효함을 참고한다.

- https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ecr/get-login-password.html

01-create-ecr-repository.sh

aws ecr create-repository --repository-name "$SERVICE_NAME"

AWS ECR repository를 만드는 쉘 스크립트다.

- https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ecr/create-repository.html

02-create-ecr-repository.sh

docker build -t "$SERVICE_NAME".
docker tag "$SERVICE_NAME":latest "$AWS_ACCOUNT_ID".dkr.ecr."$AWS_REGION".amazonaws.com/"$SERVICE_NAME":latest
docker push "$AWS_ACCOUNT_ID".dkr.ecr."$AWS_REGION".amazonaws.com/"$SERVICE_NAME":latest

Docker 빌드를 하여 tag 후 ECR 에 Push 하는 스크립트다.

03-create-iam-for-lambda.sh

람다를 위한 iam role 을 만든다.

aws iam create-role --role-name "$LAMBDA_ROLE_NAME" --assume-role-policy-document file://iam/role.json
aws iam put-role-policy --role-name "$LAMBDA_ROLE_NAME" --policy-name "$LAMBDA_ROLE_NAME"-policy --policy-document file://iam/policy.json

- https://awscli.amazonaws.com/v2/documentation/api/latest/reference/iam/create-role.html

- https://awscli.amazonaws.com/v2/documentation/api/latest/reference/iam/put-role-policy.html

04-deploy-to-lambda.py

람다 함수를 만드는 스크립트다. `--code ImageUri` 플래그를 볼 수 있는데, ECR 이미지를 이용하여 람다를 프로비저닝한다. 람다를 만드는데 드는 시간이 거의 들지 않아 깜짝 놀랬다.

aws lambda create-function \
    --role arn:aws:iam::"$AWS_ACCOUNT_ID":role/"$LAMBDA_ROLE_NAME" \
    --function-name "$SERVICE_NAME" \
    --package-type Image \
    --code ImageUri="$AWS_ACCOUNT_ID".dkr.ecr."$AWS_REGION".amazonaws.com/"$SERVICE_NAME":latest

- https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/create-function.html

05-create-api-gateway.py

이 스크립트는 꽤 길다. API Gateway 를 프로비저닝하고 이 API에 리소스를 추가한 뒤, 이 리소스에 메소드를 추가하고, API 스테이지를 만든 후 배포시키는 최종 스크립트다. 스크립트는 길지만 읽기는 어렵지 않을 것이다. 늘상 써오던 방식이지만, python 의 subprocess를 열어 실행시킨다. aws cli 를 여러번 호출하고 있다. python을 쓰는 이유는 여러 리소스들의 id같은 것들을 가지고 있다가 계속 사용해야하기 때문이다.

#! /usr/bin/python3
import common

import os

stage_name = os.getenv('STAGE')
service_name = os.getenv('SERVICE_NAME')
aws_account_id = os.getenv('AWS_ACCOUNT_ID')
aws_region = os.getenv('AWS_REGION')
apigateway_http_method = os.getenv('APIGATEWAY_HTTP_METHOD')
apigateway_authorization_type = os.getenv('APIGATEWAY_AUTHORIZATION_TYPE')

cmd = [
    'aws', 'apigateway', 'create-rest-api', '--name', service_name, '--region', aws_region
]
result = common.run_cli(cmd)
apigateway_id = result.get('id')

cmd = [
    'aws', 'apigateway', 'get-resources', '--rest-api-id', apigateway_id, '--region', aws_region
]
result = common.run_cli(cmd)
resources = result.get('items')
root_resource_id = resources[0]['id']

cmd = [
    'aws', 'apigateway', 'create-resource', '--rest-api-id', apigateway_id, '--region', aws_region,
    '--parent-id', root_resource_id, '--path-part', '{proxy+}'
]
result = common.run_cli(cmd)
resource_id = result.get('id')

cmd = [
    'aws', 'apigateway', 'put-method', '--rest-api-id', apigateway_id, '--region', aws_region,
    '--resource-id', resource_id, '--http-method', apigateway_http_method, 
    '--authorization-type', apigateway_authorization_type
]
result = common.run_cli(cmd)

cmd = [
    'aws', 'apigateway', 'put-method-response', '--rest-api-id', apigateway_id, '--region', aws_region,
    '--resource-id', resource_id, '--http-method', apigateway_http_method, 
    '--status-code', '200'
]
result = common.run_cli(cmd)

lambda_arn = f'arn:aws:lambda:{aws_region}:{aws_account_id}:function:{service_name}'

cmd = [
    'aws', 'apigateway', 'put-integration', '--rest-api-id', apigateway_id, '--region', aws_region, 
    '--resource-id', resource_id, '--http-method', apigateway_http_method, '--type', 'AWS', '--integration-http-method', apigateway_http_method,
    '--uri', f'arn:aws:apigateway:{aws_region}:lambda:path/2015-03-31/functions/{lambda_arn}/invocations'
]
result = common.run_cli(cmd)

cmd = [
    'aws', 'apigateway', 'put-integration-response', '--rest-api-id', apigateway_id, '--region', aws_region, 
    '--resource-id', resource_id, '--http-method', apigateway_http_method, '--selection-pattern',  '', '--status-code', '200'
]
result = common.run_cli(cmd)

cmd = [
    'aws', 'lambda', 'add-permission', '--function-name', service_name, '--source-arn', f'arn:aws:execute-api:{aws_region}:{aws_account_id}:{apigateway_id}/*/{apigateway_http_method}/*',
    '--principal', 'apigateway.amazonaws.com', '--action', 'lambda:InvokeFunction', '--statement-id', f'invoke{apigateway_id}'
]
result = common.run_cli(cmd)

cmd = [
    'aws', 'apigateway', 'create-deployment', '--rest-api-id', apigateway_id, '--stage-name', stage_name
]
result = common.run_cli(cmd)


print('#' * 80)
print(f'https://{apigateway_id}.execute-api.ap-northeast-2.amazonaws.com/{stage_name}')
print('#' * 80)

- https://awscli.amazonaws.com/v2/documentation/api/latest/reference/apigateway/create-rest-api.html

- https://awscli.amazonaws.com/v2/documentation/api/latest/reference/apigateway/get-resources.html

- https://awscli.amazonaws.com/v2/documentation/api/latest/reference/apigateway/create-resource.html

- https://awscli.amazonaws.com/v2/documentation/api/latest/reference/apigateway/put-method.html

- https://awscli.amazonaws.com/v2/documentation/api/latest/reference/apigateway/put-method-response.html

- https://awscli.amazonaws.com/v2/documentation/api/latest/reference/apigateway/put-integration.html

- https://awscli.amazonaws.com/v2/documentation/api/latest/reference/apigateway/put-integration-response.html

- https://awscli.amazonaws.com/v2/documentation/api/latest/reference/apigateway/create-deployment.html

10-update-function.sh

코드와 이미지를 수정하고나서 람다에 반영시키기 위한 스크립트다. 위에서 했던 설명의 반복정도다.

docker build -t "$SERVICE_NAME" .
docker tag "$SERVICE_NAME":latest "$AWS_ACCOUNT_ID".dkr.ecr."$AWS_REGION".amazonaws.com/"$SERVICE_NAME":latest
docker push "$AWS_ACCOUNT_ID".dkr.ecr."$AWS_REGION".amazonaws.com/"$SERVICE_NAME":latest

aws lambda update-function-code \
    --function-name "$SERVICE_NAME" \
    --image-uri "$AWS_ACCOUNT_ID".dkr.ecr."$AWS_REGION".amazonaws.com/"$SERVICE_NAME":latest

번외편; 삽질 - 완전 다른 Linux distro 이미지로 해보려다가...

Lambda Extensions API - AWS Lambda
Use the extensions API to create extensions that integrate code with the Lambda execution environment.