소개

안녕하세요, IT전략팀 김미란입니다.

지난 해 말 사람인에서 THE PL:LAB(더플랩)을 런칭하였습니다. THE PL:LAB은 ESG기반으로 인사담당자 관점에서 만든 새로운 HR 서비스입니다. 그 중 인사담당자들에게 정보와 지식을 공유하는 INSIGHT(인사이트) 프로젝트에 참여하여 개발하였습니다. 구직자는 비교적 쉽게 채용에 대한 다양한 정보를 찾아볼 수 있지만, 인사담당자들을 위한 정보는 쉽사리 찾을 수 없었습니다. 이러한 문제를 사람인에이치알에서 해소하고자 INSIGHT가 만들어졌습니다. 새로이 시작하는 인사담당자들과 이미 현업에 계신 분들께 필요한 콘텐츠를 제공하며, 서로 정보를 공유하는 신규 플랫폼입니다.

이번 프로젝트를 진행하면서 AWS를 사용했는데요. AWS 서비스 사용 경험을 공유드리고자 합니다. 프로젝트에 들어가기 앞서 “Developing on AWS” 교육을 수강하였습니다. AWS에 대한 기본적인 구조를 이해하는 상태에서 EC2를 사용해보았습니다. 아마존에서 주최하는 해당 교육은 AWS SDK를 사용하여 확장 가능한 클라우드 애플리케이션 개발 방법론이었습니다. 아쉽게도 해당 교육은 CI/CD와 관련된 배포 구성이나 콘솔 환경에서 사용하는 방식에 대한 건 담기지 않았습니다. AWS를 처음 사용한다면 누구든 시행착오를 겪을 과정이기에 정리해보았습니다. 프로젝트를 진행하면서 틈틈이 AWS document를 많이 참고하였어요.

CodeDeploy와 S3를 이용해서 배포하기

CI툴을 이용해서 빌드를 마치면 S3와 CodeDeploy를 이용해서 EC2인스턴스로 배포합니다. 크게 아래 4가지 단계를 거친다고 보면 됩니다.

  1. Application 소스와 배포 설정 파일인 AppSpec.yml을 S3에 올린다.
  2. AWS CodeDeploy 서비스에 배포를 요청한다.
  3. CodeDeploy서비스는 EC2 Agent에 배포를 요청한다.
  4. Agent는 S3에서 소스를 받고 배포설정을 기반으로 배포를 수행한다.

참고 : https://docs.gitlab.com/ee/ci/cloud_deployment/#provision-and-deploy-to-your-aws-elastic-compute-cloud-ec2

위 과정으로 배포를 진행하기 위해 IAM RoleEC2 Instance를 만들어야 합니다. EC2에 Codedeploy-Agent 를 설치하고 나서 실행시켜 볼까요?

  • CodeDeploy 실행 여부 확인
    실행 중이면 “The AWS CodeDeploy agent is running” 메시지가 표시됩니다.
    실행 중이 아니라면 start 명령어로 실행시켜줍니다.
sudo service codedeploy-agent status  # Status Check
sudo service codedeploy-agent start   # Start
  • GitLab Runner 설치 및 등록
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh" | sudo bash
sudo yum install gitlab-runner

환경별 Runner 설치는 아래 공식문서를 참고 바랄게요. https://docs.gitlab.com/runner/install/linux-repository.html#installing-gitlab-runner

설치 후 Runner를 등록해야합니다. 등록에 필요한 CI Token 확인은 Gitlab > Settings > CI/CD > Runner를 Expand하면 정보를 확인할 수 있습니다. (하단 이미지 참조)

#gitlab runner 실행
sudo gitlab-runner start

#gitlab 등록
sudo gitlab-runner register

#gitlab 주소 
Enter the GitLab instance URL (for example, https://gitlab.com/): 
#token 입력 
Enter the registration token: 
#runner 설명 
Enter a description for the runner: 
#runner tag 지정, tag에 따라 러너를 구분함 
Enter tags for the runner (comma-separated): 
Registering runner... succeeded runner=yFecrSgp 
#runner 실행 형식 - 실행 형식에 따라 입력 정보가 다름 
Enter an executor: parallels, shell, virtualbox, docker+machine, docker-ssh+machine, kubernetes, custom, docker, docker-ssh, ssh: 
#등록 성공 
Runner registered successfully. 
Feel free to start it, but if it's running already the config should be automatically reloaded!

등록이 성공적으로 이루어지면 Gitlab > Settings > CI/CD > Runner 메뉴에서 확인이 가능합니다.
CICD_Runner ※ 참고 : https://docs.gitlab.com/runner/register/index.html

그 다음 appspec.yml 파일과 소스를 .zip으로 압축하여 S3에 업로드합니다. 이제 CodeDeploy 애플리케이션과 배포 그룹을 생성하고, S3버킷 과 연결하여 배포를 진행해볼까요?

  • CodeDeploy 애플리케이션/ 배포그룹 생성하기
    배포 구성은 Codedeploy에서 사전에 정의한 배포 규칙입니다.
    (미지정 시 CodeDeployDefault.AllAtOnce로 설정됨)
    아래 참고사항을 확인하고 맞는 구성정보를 선택합니다.
    ※ 참고 : https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/deployment-configurations.html AWS_CodeDeploy_Application

  • GitLab Script 작성
    push 시 S3에 빌드 파일과 appspec.yml을, 그리고 실행 스크립트파일이 별도로 존재한다면 함께 zip파일로 업로드합니다. create-deployment로 배포를 생성합니다.

.build-api: &boot-app-build
  <<: *kp-ci-dev
  stage: build
  script:
    - ./gradlew --build-cache :$PROJECT_PATH:bootJar
    - cp -v -R scripts/ $PROJECT_PATH/appspec.yml $PROJECT_PATH/build/libs/
  artifacts:
    paths:
      - $PROJECT_PATH/build/libs/
    expire_in: 3 days

.push-and-deploy-api: &push-and-deploy-api
  <<: *kp-ci-dev
  script:
    - aws deploy push --application-name $APPLICATION_NAME
      --s3-location s3://$S3_BUCKET_NAME/$APPLICATION_NAME/$CI_ENVIRONMENT_SLUG.zip
      --source $PROJECT_PATH/build/libs/
    - deployment_response=$(aws deploy create-deployment
      --application-name $APPLICATION_NAME
      --deployment-group-name $CI_ENVIRONMENT_SLUG
      --s3-location bucket=hrlab-kproj-deploy,key=$APPLICATION_NAME/$CI_ENVIRONMENT_SLUG.zip,bundleType=zip)
    - export deployment_id=$(echo "$deployment_response" | jq -r '.deploymentId')
    - sleep 2
    - aws deploy get-deployment --deployment-id "$deployment_id"  #배포 세부정보 보기 

※ 참고 : https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/application-revisions-push.html
https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/deployments-create-cli.html

해당 스크립트가 실행되면 S3로 zip파일이 올라간 모습을 확인할 수 있습니다.
AWS_S3_zipfile

  • 배포를 위한 appspec.yml 작성
# appspec.yml
# version은 0.0(현재 유일하게 허용되는 값)을 적어준다. 
version: 0.0  #CodeDeploy 버전
#선택한 ec2 가 window server가 아니라면 linux를 적어준다.
os: linux

# source는 프로젝트 기준, destination은 instance기준으로 입력해준다.
# source가 destination에 복사된다.
files:
  -source: /
   destination: /var/bootapp/largo-ui-admin
file_exists_behavior: OVERWRITE

# files 파일 및 디렉터리가 인스턴스에 복사 된 후 권한(있는 경우)이 어떻게 적용되어야 하는지를 지정합니다.
# EC2/온프레미스 배포만 해당됨
permissions:
  -object: /var/bootapp/largo-admin-api.jar
    owner: bootapp
    group: bootapp
    mode: 700

hooks:
  # AfterInstall은 배포를 완료한 후 실행되는 스크립트를 명시하며
  # ApplicationStart나 ValidateService 등을 대신 쓸 수 있다.
  BeforeInstall:
    #location은 프로젝트 기준으로 위치를 작성해준다!
    -location: scripts/before_install.sh
     timeout: 120
      #runas를 입력해주지 않으면 간혹 permission error가 날 수도
      #run하게 될 permisson 있는 계정 
     runas: bootapp
  ApplicationStart:
   -location: scripts/restart_service.sh
    timeout: 120
    runas: bootapp
  • CodeDeploy LifeCycle Event Hook 하단 이미지는 AppSpec파일의 ‘hooks’세션의 배포수명주기 이벤트에 대한 순서도입니다. (EC2/온프레미스배포용) 이 순서대로 이벤트후크가 실행되는데, 이벤트후크 별로 실행할 스크립트들을 매핑합니다. 각 지점마다 매핑된 스크립트가 실행됩니다. CodeDeploy_Event_Hook

  • CodeDeploy-Agent 설정 Script작성 빌드파일인 .jar를 복사하여 systemctl로 서비스를 재구동합니다.

# befor_install.sh

root_dir=/opt/codedeploy-agent/deployment-root
archive_dir=${root_dir}/${DEPLOYMENT_GROUP_ID}/${DEPLOYMENT_ID}/deployment-archive

service_name=${APPLICATION_NAME}      # APPLICATION_NAME: largo-admin-api
project_path=${service_name#largo-}   # PROJECT_PATH: admin-api

# files 
sudo cp -v $archive_dir/${project_path}*.jar /var/bootapp/${service_name}.jar || exit 1

----------------------------------------------------------------------------------------

# restart_service.sh

service_name=${APPLICATION_NAME#deploy-kproj-*-}
profile=${DEPLOYMENT_GROUP_NAME%%-*}

echo "restart ${service_name} on ${DEPLOYMENT_GROUP_NAME} "
sudo systemctl restart ${service_name}
  • 서비스가 정상적 배포되었는지 확인합니다.
    실제 서비스에 배포될 때 실행된 이벤트를 목록으로 살펴볼 수 있습니다.
    AWS_CodeDeploy_실행이벤트

CloudFront를 이용하여 HTTPS로 정적리소스 배포하기

CloudFront는 AWS에서 제공하는 CDN서비스입니다. 캐싱을 통해 사용자에게 좀 더 빠른 전송 속도를 제공합니다. CloudFront는 전 세계 곳곳에 Edge Server를 두며, Client에서 가장 가까운 위치에 요청하므로써 전송 시간을 단축하여 보다 빠르게 컨텐츠 데이터를 제공합니다.

  • CloudFront 생성 및 설정
    아래와 같이 기본적인 설정을 기입하여 생성합니다.
    AWS_CF_설정

  • Behaviors 설정 경로에 따라 어떤 원본으로 연결할지, 캐시 정책은 어떻게 가져갈 것인지 설정할 수 있습니다.
    AWS_CF_동작상세설정
    AWS_CF_동작

  • 캐시 설정 CF(CloudFront)에서 제공하는 정책 중 S3에서 권장하는 캐시정책은 CachingOptimized입니다. 기본 TTL은 24시간으로 지정되어 있습니다. 자세한 내용은 정책 페이지에서 확인 가능합니다.
    AWS_CF_Cache_policy

  • GitLab Script 작성하기 빌드된 프로젝트와 CloudFront와 연결된 S3버킷의 지정 폴더를 AWS S3 sync로 동기화시켜줍니다. create-invalidation로 기존 데이터를 무효화하고 최신 파일을 가져오도록 합니다. 무효화 할 대상의 범위를 따로 지정할 수 있습니다.

develop-ui-admin: &develop-ui-admin
  script:
    - aws s3 sync ${SERVICE_NAME}/ui/dist s3://$S3_BUCKET_NAME/$APPLICATION_NAME --delete
    - aws cloudfront create-invalidation --distribution-id $CF_DISTRIBUTION_ID --paths '/*'

※ 참고 : https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html

Serverless - AWS lambda

서버리스(Serverless)를 직역하면 ‘서버가 없다’입니다. 서버가 없다는 건 표현 방식일 뿐이고, 서버를 신경쓰지 않아도 된다는 뜻이죠. 그 대신 Baas(Backend as a Service) 혹은 Faas(Function as a Service)에 의존하여 작업을 처리합니다. 커맨드라인을 이용해서 서버를 구성하는 일련의 과정을 생략한 채 개발자들은 함수만 관리하면 되는거죠.

Serverless framework가 지원하는 클라우드 서비스는 Amazon Web Services, Microsoft Azure, IBM OpenWhisk, Google Cloud Platform 등 메이저급의 회사들 모두 지원합니다. 저희는 AWS Lambda를 통해 배포를 진행했습니다.

먼저 serverless framework를 설치하고 템플릿을 생성합니다.

# Serverless framework 설치하기
$ npm install -g serverless

Serverless를 통해 코드를 배포하기 위해서는 Serverless가 해당 권한을 가지고 있어야 합니다. AWS에서 IAM을 설정해줍시다. cloudformaion이 모든 프로세스를 진행하기에 보다 많은 권한을 필요로 합니다. 기본적으로 가지는 권한은 아래와 같습니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "cloudformation:*",
        "s3:*",
        "logs:*",
        "iam:*",
        "apigateway:*",
        "lambda:*",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeSubnets",
        "ec2:DescribeVpcs",
        "events:*"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

추가적으로 커스텀 사용이 필요하다면 https://github.com/serverless/serverless/issues/588 또는 https://serverless-stack.com/chapters/customize-the-serverless-iam-policy.html 를 참조하시면 됩니다.

# Serverless가 지원하는 템플릿을 볼 수 있습니다
$ sls create -t 

# Serverless framework template 생성하기
$  sls create -t {원하는 템플릿} -p {원하는 프로젝트 이름}

cloudformaion 템플릿이 생성되고, 이를 바탕으로 배포됩니다. 그에 맞는 serverless를 구성해보겠습니다. 저희 프로젝트에서 구성했던 serverless.yml의 내용은 아래와 같습니다.

service: insight

provider:
  name: aws
  region: ap-northeast-2
  runtime: nodejs14.x
  stage: dev
  lambdaHashingVersion: 20201221

  apiGateway:
    binaryMediaTypes:
      - '*/*'

  environment:
    NODE_PATH: "./:/opt/node_modules"  #node_modules 위치
    NUXT_TELEMETRY_DISABLED: 1
    INSIGHT_DOMAIN: ${env:INSIGHT_DOMAIN, "insight.thepllab.com"}

package:
  patterns:  #패키지를 가볍게 유지하기 위한 설정
    - '!.githooks/**'
    - '!.idea/**'
    - '!.nuxt/**'
    - .nuxt/dist/**
    - '!node_modules/**'
    - '!apis/**'
    - '!assets/**'
    - '!components/**'
    - '!docs/**'
    - '!layers/**'
    - '!layouts/**'
    - '!middleware/**'
    - '!mixins/**'
    - '!pages/**'
    - '!plugins/**'
    - '!public/**'
    - '!store/**'
    - '!storybook/**'
    - 'storybook/ko.json'
    - '!.nuxt-storybook/**'
    - static/**
    - server-middleware/**
    - schemes/**

plugins:
  - serverless-offline  
  - serverless-apigw-binary
  - serverless-latest-layer-version

functions:
  nuxt:
    timeout: 10
    handler: app.handler
    memorySize: 512
    # reservedConcurrency: 50 # 최대 동시 인스턴스 수 보장
    # provisionedConcurrency: 3 # 요금 부과 (인스턴스 * 12.73, 프로비저닝된 동시성 3-4 개 정도에 약 $50
    # onError: arn:....:sns-topic
    # kmsKeyArn:
    # tags: # Function specific tags
    events:
      - http: ANY /
      - http: ANY /{proxy+}
    vpc:
      securityGroupIds:
        - ${secrityGroupId}  
      subnetIds:
        - ${subnetIds}  
    environment:
      PUBLIC_URL: ${env:PUBLIC_URL}
      AUTH_BASE_URL: ${env:AUTH_BASE_URL}
      AUTH_CLIENT_ID: ${env:AUTH_CLIENT_ID}
      API_BASE_URL: ${env:API_BASE_URL}
      BROWSER_BASE_URL: ${env:BROWSER_BASE_URL, env:API_BASE_URL}


아래와 같이 lambda 개발 패키지에는 용량 제한이 있습니다.
AWS_lambda_limit
AWS Lambda 패키지 제한 사이즈는 총합 250MB(262144000 bytes)입니다.

처음 프로젝트를 띄울때는 아무 것도 없어 문제가 없었습니다. 작업을 진행하면서 필요한 패키지들을 설정하니 용량 제한에 걸려 문제가 발생했습니다. 따라서 불필요한 패키지를 제외하면서 관리해야합니다.
업로드 용량이 늘어난다는 건 용량 낭비도 발생하지만, 배포 시간도 함께 늘어난다는 단점이 있죠.
node_modules는 패키지를 추가할 수록 용량이 기하급수적으로 늘어나기에 배포할 때는 꼭 사용하는 패키지만 포함해서 실행해야 합니다. 사용하지 않는 패키지들을 제거하고 33MB를 차지한 nuxt를 보다 가벼운 nuxt-start 라이브러리로 변경했습니다. 정적 파일들은 S3에 올려놓고 CloudFront를 연결하여 서비스하도록 설정하였습니다. 이렇게 다이어트 완성한 용량은 19.7MB입니다.

AWS는 lambda에서 공통으로 사용하는 모듈에 대해서 Layer라는 개념을 도입했습니다. 공통 모듈의 집합으로 lambda함수와 별개로 배포하고 이를 include하여 사용할 수 있습니다. 저희도 layer를 하나 생성했습니다. layer용 serverless를 아래와 같이 따로 생성합니다.

service: insight-nodejs

provider:
  name: aws
  region: ap-northeast-2
  runtime: nodejs14.x
  lambdaHashingVersion: 20201221
  stage: ${opt:stage, 'dev'}

layers:
  layerNodejs:
    path: nodejs
    name: ${env:CI_COMMIT_REF_SLUG, "CI_COMMIT_REF_SLUG"}-nodejs
    compatibleRuntimes:
      - nodejs14.x
			

lambda_function
AWS 콘솔에서 Lambda Frution을 보시면 이처럼 Layer를 확인할 수 있습니다.

CF_behaviors
CloudFront를 연결한 내용인데요. 우선순위 1번부터 5번의 정적파일은 cloudFront를 통하고, 0번과 6번은 api gateway를 연결하였습니다.

마치며

인사이트 프로젝트를 진행하면서 AWS를 활용해서 새로운 시스템을 구축해보았습니다. 새로운 경험이라 여러가지 흥미로웠고, AWS가 참 잘 만든 클라우드라는 걸 다시 한 번 떠올리게 되었습니다. 공식 문서도 깔끔히 작성되어 있어서 참고하기 좋았으며, 다른 개발자들이 작성한 블로그도 도움이 많이 되었습니다. 좌충우돌 많은 문제가 있었지만 전부 해결되고나서 글로 옮긴거라 와닿을지 잘 모르겠네요. 많이 간추려진 내용이지만, 이 글이 AWS를 처음 접하는 다른 이들에게 한걸음 쉽게 다가갈 수 있는 디딤돌이 되기를 바라봅니다.
마지막으로 많은 도움을 주신 SRE팀과 저희 팀원분들께 감사 인사를 드리며 글을 마치겠습니다.