AWS Lambda 구축 및 Github Action을 통한 자동 배포 - (2)

전현준·2024년 6월 9일
1

AWS Lambda 구축기

목록 보기
2/2
post-thumbnail

지난 글


지난 글에서 AWS Lambda 함수를 생성하고, AWS CLI를 통해서 로컬에서 배포하고

윈도우에서 bat 파일로 한번에 배포하는 프로세스를 작성 해 두었습니다.

오늘은 Github Action을 이용해서 자동으로 배포하는 프로세스를 작성해보겠습니다.

📢 지난 글에서 이어집니다!
AWS Lambda로 서버리스 환경 구축 및 배치 프로그램을 통해서 한번에 배포하기 - (1)



Github Action


Github Action을 이용하면 Github로 배포할 때마다, 또는 특정 조건 때마다 실행됩니다.

그리고 생각보다 간단합니다! 본인이 배포하려는 과정만 이해하고 있으면 쉽습니다.

일단 Yaml 파일을 작성해야하는데, .github폴더가 필요합니다.

.github 폴더는 git이 체크하고 있는 디렉토리 맨 상위에 생성해주세요 (.git 폴더가 있는 위치)

그리고 .github > workflows 폴더에 deploy-lambda.yaml 파일을 생성해주세요.

저는 Issue Template와 PR Template도 만들어 사용 중입니다. (TMI;)


Yaml 파일 설명

동작 자체는 지난 글과 다를게 없습니다.
궁금하신 분은 지난글에서 [Window에서 .bat파일로 배포 하기]을 참고해주세요!

단지 CMD 명령어인 .bat 파일을 yaml 파일로 만든 것 뿐입니다.

name: Deploy Lambda

on:
  push:
    paths:
      - 'Flask_backend/aws_lambda/**'

jobs:
  deploy:
    runs-on: ubuntu-latest

    env:
      REPOSITORY_NAME: aws-lambda
      REGION: ap-northeast-2
      ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
      BUCKET_NAME: ${{ secrets.BUCKET_NAME }}
      MODEL_0: ${{ secrets.MODEL_0 }}
      MODEL_1: ${{ secrets.MODEL_1 }}
      MODEL_2: ${{ secrets.MODEL_2 }}
      MODEL_3: ${{ secrets.MODEL_3 }}
      LAMBDA_ROLE: ${{ secrets.LAMBDA_ROLE}}

    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v3
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.REGION }}

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2

    - name: Log in to Amazon ECR
      id: ecr-login
      uses: aws-actions/amazon-ecr-login@v1

    - name: Build Docker image
      run: docker build -t ${{ env.REPOSITORY_NAME }} Flask_backend/aws_lambda

    - name: Get image digest
      id: get-image-digest
      run: |
        IMAGE_DIGESTS=$(aws ecr list-images --repository-name ${{ env.REPOSITORY_NAME }} --region ${{ env.REGION }} --query "imageIds[*].imageDigest" --output text)
        echo "IMAGE_DIGESTS=$IMAGE_DIGESTS" >> $GITHUB_ENV

    - name: Delete old images from ECR
      if: env.IMAGE_DIGESTS != ''
      run: |
        for DIGEST in $IMAGE_DIGESTS; do
          aws ecr batch-delete-image --repository-name ${{ env.REPOSITORY_NAME }} --region ${{ env.REGION }} --image-ids imageDigest=$DIGEST
        done

    - name: Delete ECR repository
      run: aws ecr delete-repository --repository-name ${{ env.REPOSITORY_NAME }} --region ${{ env.REGION }} --force || true

    - name: Create ECR repository
      run: aws ecr create-repository --repository-name ${{ env.REPOSITORY_NAME }} --image-scanning-configuration scanOnPush=true --region ${{ env.REGION }}

    - name: Tag Docker image
      run: docker tag ${{ env.REPOSITORY_NAME }}:latest ${{ env.ACCOUNT_ID }}.dkr.ecr.${{ env.REGION }}.amazonaws.com/${{ env.REPOSITORY_NAME }}:latest

    - name: Push Docker image to ECR
      run: docker push ${{ env.ACCOUNT_ID }}.dkr.ecr.${{ env.REGION }}.amazonaws.com/${{ env.REPOSITORY_NAME }}:latest

    - name: Delete old Lambda function
      run: aws lambda delete-function --function-name perst1 || true

    - name: Create new Lambda function
      run: |
        aws lambda create-function --function-name perst1 --package-type Image \
          --code ImageUri=${{ env.ACCOUNT_ID }}.dkr.ecr.${{ env.REGION }}.amazonaws.com/${{ env.REPOSITORY_NAME }}:latest \
          --role ${{ env.LAMBDA_ROLE }} \
          --environment "Variables={bucket_name=${{ env.BUCKET_NAME }},model_0=${{ env.MODEL_0 }},model_1=${{ env.MODEL_1 }},model_2=${{ env.MODEL_2 }},model_3=${{ env.MODEL_3 }}}" \
          --memory-size 6020 --timeout 600 --ephemeral-storage Size=5000

설명 1

이름을 설정하고, 특정 폴더에 Push되면, 이 자동화 프로세스를 실행하라. 라는 뜻입니다.

특정 브랜치에 Push 될 때 자동화 프로세스 실행도 가능합니다.


설명 2

  • Job 1 : 이름 depoly
  • Ubuntu 환경에서 진행
  • 환경 변수 설정

여기서 언급된 secrets는 코드 상 정보가 드러나면 안되는 친구들입니다.

그래서 이 secrets는 다른 곳에 저장해야 합니다!

Settings > Secrets and variables > Action

이 Repository secrets에 저장해두면 됩니다.

그러면 코드 상에서 ${{ secrets.API_ID }} 로 불러올 수 있습니다!

설명 3

실행할 단계를 작성해둠. 상세 동작 내용은 이전 글 참고 부탁드립니다!

테스트

이제 다 된거나 마찬가지 입니다.

그럼 이제 변경할 내용을 작성하고, push를 해봅시다.

그럼 이렇게 성공하면 초록색 / 실패하면 빨간색으로 로그가 남습니다.

들어가서 보면 각 단계에 맞춰서 실행하고 있는 모습입니다!

모든 단계에 성공한 케이스

그럼 이제는, 배포가 정상적으로 작동하는지 체크하면서 계속 코드를 수정하시면 되겠습니다 ^^



Lambda 함수 API 호출하기


  • 그럼 CI/CD 배포도 완벽해.

  • Lambda 함수 코드도 다 작성해서 배포해놨어.

라고 한다면? 이제 API로 통신 할 차례입니다.


Lambda에서 API로 호출하는 방법 중 제가 시도한 방법은 두가지 인데요

하나는 API 게이트웨이를 트리거로 추가하는 방법이 있고,

두 번째는 Lambda 함수 내에서 함수 URL을 사용하는 것 입니다.

당연히 처음에는 API 게이트웨이가 먼저 보여서, 이걸 써야하는구나 라고 생각했습니다.

그리고 API URL이 배포할 때마다 변하면 배포할 때마다 확인해야하는 굉장한 불편함이 생길 것 같아 API Gateway를 사용하는 것이 낫겠다는 판단을 합니다.

Lambda 함수 URL은 URL을 삭제하고 부여받을 때마다 새로운 URL을 부여받습니다.
당연히 Lambda 함수를 지우고 다시 배포하면 URL도 바뀔 것이라는 판단

그 런 데 ..


API 게이트웨이의 치명적 단점

Lambda 배포까지 자동화 해두고, API GateWay를 트리거로 생성하면 되겠다! 해서 작성한 코드입니다.

name: Deploy Lambda

on:
  push:
    paths:
      - 'Flask_backend/aws_lambda/**'

jobs:
  deploy:
    runs-on: ubuntu-latest

    env:
      REPOSITORY_NAME: aws-lambda
      REGION: ap-northeast-2
      ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
      BUCKET_NAME: ${{ secrets.BUCKET_NAME }}
      MODEL_0: ${{ secrets.MODEL_0 }}
      MODEL_1: ${{ secrets.MODEL_1 }}
      MODEL_2: ${{ secrets.MODEL_2 }}
      MODEL_3: ${{ secrets.MODEL_3 }}
      LAMBDA_ROLE: ${{ secrets.LAMBDA_ROLE}}

    steps:
    ... 중략 : 위 코드를 참고하세요 ...

  update_integration:
    needs: deploy
    runs-on: ubuntu-latest

    env:
      REGION: ap-northeast-2

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2
      
      - name: Set up AWS CLI
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.REGION }}

      - name: Get Route ID
        id: get_route_id
        run: |
          # Get the Route ID from AWS API Gateway
          ROUTE_ID=$(aws apigatewayv2 get-routes --api-id ${{ secrets.API_ID }} --query "Items[?RouteKey=='${{ secrets.ROUTE_KEY }}'].RouteId" --output text)
          # Check if the Route ID is found
          if [ -z "$ROUTE_ID" ]; then
            echo "Route not found."
            exit 1
          else
            echo "Route found."
            echo "ROUTE_ID=$ROUTE_ID" >> $GITHUB_ENV
          fi


      - name: Get existing integration ID
        run: |
          INTEGRATION_ID=$(aws apigatewayv2 get-integrations --api-id ${{  secrets.API_ID  }} --query "Items[?IntegrationType=='AWS_PROXY'].IntegrationId" --output text)
          if [ -z "$INTEGRATION_ID" ]; then
            echo "No existing integration found. Creating a new integration."
          else
            echo "Removing existing integration from route."
            aws apigatewayv2 delete-route --api-id ${{  secrets.API_ID  }} --route-id ${{ env.ROUTE_ID }}
            
            echo "Deleting existing integration."
            aws apigatewayv2 delete-integration --api-id ${{  secrets.API_ID  }} --integration-id $INTEGRATION_ID
            fi

      - name: Create new integration
        id: get-integration
        run: |
          NEW_INTEGRATION_ID=$(aws apigatewayv2 create-integration --api-id ${{  secrets.API_ID  }} --integration-type AWS_PROXY --integration-uri ${{  secrets.LAMBDA_ARN  }} --payload-format-version 2.0 --query "IntegrationId" --output text)
          echo "INTEGRATION_ID=$NEW_INTEGRATION_ID" >> $GITHUB_ENV
          
      - name: Set new integration to route
        run: |
          aws apigatewayv2 create-route --api-id ${{  secrets.API_ID  }} --route-key "${{  secrets.ROUTE_KEY  }}" --target integrations/${{ env.INTEGRATION_ID }}

      - name: Add permission to Lambda function
        run: |
          aws lambda add-permission --function-name perst1 --statement-id $RANDOM --action lambda:InvokeFunction --principal apigateway.amazonaws.com --source-arn "${{  secrets.SOURCE_ARN  }}"

      - name: Process completed
        run: echo "Process completed."

이것도 거의 처음 해보는거라, 3~4시간 걸린 것 같습니다.

어쨌든 API Gateway를 사용하려면 아래의 요소들이 필요합니다.

  • Route : /api/url
  • 통합(integration) : API URL과 Lambda 함수와의 연결
  • Lambda 함수의 권한 부여

그래서 Lambda 함수 배포 이후, API Gateway도 트리거로 추가하는데 성공합니다.


🚨 비상

제 Lambda 함수 코드가 머신러닝 코드인데, 실행할 때마다 머신러닝 모델 다운 > 머신러닝 분석의 과정을 거칩니다. 처음 실행하면 모델을 다운받아야 해서 50초가 걸리는데,,

API Gateway의 최대 시간은 29초

즉 30초가 되자마자, Lambda 함수의 실행과 관계없이 500을 뱉어냅니다.

그럼 이제는 두번째 방법인 함수 URL로 사용하는 방법이 있습니다.

"함수 URL이 항상 배포할 때마다 바뀌는거 아닌가?" 그럼 이것도 자동화 하려면, 요청 주소를 DB에 기록할까? 라는 고민을 했습니다.


근데 함수 URL은 Lambda 함수 이름에 따라가기 때문에, 새로 배포해도 같은 이름으로 배포하면 같은 URL을 부여 받을 수 있었습니다!

단,아래의 경우에는 URL이 변경됩니다.

  • Lambda 함수의 이름이 변경되어 배포되는 경우
  • URL 삭제 및 재 할당을 받는 경우

그래서 저는 함수 URL을 한번 받고, 다시 자동화 프로세스를 돌렸습니다
(기존 ECR 삭제 및 배포, 기존 Lambda 함수 삭제 및 배포)

근데, 함수 URL이 그대로였습니다! 만세 하지만 권한이 필요합니다.


권한 부여

구성 > 권한 탭 > 리소스 기반 정책 설명에서 InvokeFunctionUrl 권한이 필요합니다.

문 ID는 제가 설정한 별칭입니다.

그러면 Github Action Yaml 파일에서 권한을 부여하는 명령어만 추가하면 됩니다!

위와 같이 부여하면 됩니다.



전체 코드


그래서 배포 > URL 권한 부여까지 모두 작성한 코드입니다.

중간에 --function-name 옵션에 perst1이라고 되어있는 부분은 Lambda 함수 이름 설정입니다.

여러분이 원하시는 이름으로 바꾸시면 됩니다.

name: Deploy Lambda

on:
  push:
    paths:
      - 'Flask_backend/aws_lambda/**'

jobs:
  deploy:
    runs-on: ubuntu-latest

    env:
      REPOSITORY_NAME: aws-lambda
      REGION: ap-northeast-2
      ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
      BUCKET_NAME: ${{ secrets.BUCKET_NAME }}
      MODEL_0: ${{ secrets.MODEL_0 }}
      MODEL_1: ${{ secrets.MODEL_1 }}
      MODEL_2: ${{ secrets.MODEL_2 }}
      MODEL_3: ${{ secrets.MODEL_3 }}
      LAMBDA_ROLE: ${{ secrets.LAMBDA_ROLE}}

    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v3
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.REGION }}

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2

    - name: Log in to Amazon ECR
      id: ecr-login
      uses: aws-actions/amazon-ecr-login@v1

    - name: Build Docker image
      run: docker build -t ${{ env.REPOSITORY_NAME }} Flask_backend/aws_lambda

    - name: Get image digest
      id: get-image-digest
      run: |
        IMAGE_DIGESTS=$(aws ecr list-images --repository-name ${{ env.REPOSITORY_NAME }} --region ${{ env.REGION }} --query "imageIds[*].imageDigest" --output text)
        echo "IMAGE_DIGESTS=$IMAGE_DIGESTS" >> $GITHUB_ENV

    - name: Delete old images from ECR
      if: env.IMAGE_DIGESTS != ''
      run: |
        for DIGEST in $IMAGE_DIGESTS; do
          aws ecr batch-delete-image --repository-name ${{ env.REPOSITORY_NAME }} --region ${{ env.REGION }} --image-ids imageDigest=$DIGEST
        done

    - name: Delete ECR repository
      run: aws ecr delete-repository --repository-name ${{ env.REPOSITORY_NAME }} --region ${{ env.REGION }} --force || true

    - name: Create ECR repository
      run: aws ecr create-repository --repository-name ${{ env.REPOSITORY_NAME }} --image-scanning-configuration scanOnPush=true --region ${{ env.REGION }}

    - name: Tag Docker image
      run: docker tag ${{ env.REPOSITORY_NAME }}:latest ${{ env.ACCOUNT_ID }}.dkr.ecr.${{ env.REGION }}.amazonaws.com/${{ env.REPOSITORY_NAME }}:latest

    - name: Push Docker image to ECR
      run: docker push ${{ env.ACCOUNT_ID }}.dkr.ecr.${{ env.REGION }}.amazonaws.com/${{ env.REPOSITORY_NAME }}:latest

    - name: Delete old Lambda function
      run: aws lambda delete-function --function-name perst1 || true

    - name: Create new Lambda function
      run: |
        aws lambda create-function --function-name perst1 --package-type Image \
          --code ImageUri=${{ env.ACCOUNT_ID }}.dkr.ecr.${{ env.REGION }}.amazonaws.com/${{ env.REPOSITORY_NAME }}:latest \
          --role ${{ env.LAMBDA_ROLE }} \
          --environment "Variables={bucket_name=${{ env.BUCKET_NAME }},model_0=${{ env.MODEL_0 }},model_1=${{ env.MODEL_1 }},model_2=${{ env.MODEL_2 }},model_3=${{ env.MODEL_3 }}}" \
          --memory-size 6020 --timeout 600 --ephemeral-storage Size=5000

  add_permission_lambda_url:
    needs: deploy
    runs-on: ubuntu-latest

    env:
      REGION: ap-northeast-2

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2
      
      - name: Set up AWS CLI
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.REGION }}

      - name: ADD_permission
        id: add_permission
        run: |
          aws lambda add-permission --function-name perst1 --statement-id FunctionURLAllowPublicAccess --action lambda:InvokeFunctionUrl --principal "*" --function-url-auth-type NONE

이렇게 하고 배포 테스트를 해서 성공하면 API 테스트를 진행하면 됩니다!



테스트


테스트는 POSTMAN을 사용했습니다~

만약 아래와 같이 요청을 보낸다면

실제 객체는 event > body >fileID를 찾아야 데이터를 얻을 수 있습니다.

모니터링 탭 > CloudWatch에 들어가보면 로그를 볼 수 있습니다.

실제 로그를 확인 할 수 있었습니다. 로그 확인하시면서 수정 사항 반영하시면 됩니다!



마무리


사실 비용 때문에 서버리스로 구성했는데, 서버리스는 ColdStart때문에 문제다.

호출되면 상시 실행되고 있는 EC2와 달리 세팅하는데 더 시간이 걸린다는 이야기이다.

특히 나의 경우 머신러닝 모델을 다운 받아와야하는데, 1.8GB 정도 되어 다운받는데 20초 정도 걸린다.

또 이 파일들은 임시 폴더에서 5분 뒤에 휘발되어 버린다


그래서 실제 서비스라면 서버리스로 구성하지 않았을 것이다.

하지만 데모 환경에서는 단기간 지속적인 호출로, 성능 저하가 그렇게 크지 않을 것으로 예상된다.

로컬에서 15~16초 걸리던 작업이였는데, Lambda 함수에서도 두번째 호출에서는 17~18초 정도로 아주 준수한 성능을 보여주었다. 첫 호출은 40~50초인 것은 안 비밀


환경 구성하기도, 블로그 글 작성하기도 오래 걸린 작업이였지만 두려워했던 것보단 쉬워서 놀랐다.

profile
백엔드 개발자 전현준입니다.

0개의 댓글

관련 채용 정보