Elastic Beanstalk

사내 서버 인프라가 거의 대부분 AWS 환경으로 넘어가면서 신규로 구축되는 많은 시스템들이 배포 및 확장, 관리등의 용이성 때문에 Elastic Beanstalk 으로 구축되고 있습니다. 제 경우에도 AWS 환경에서 신규로 구촉했던 BROS 2.0/신배라/주문지면개편 세 프로젝트 모두 Elastic Beanstalk 을 이용하여 환경을 구축했습니다. 하지만, Elastic Beanstalk이 무조건 장점만 있는 건 아닙니다. 특히나, Elastic Beanstalk 의 경우 사용할 수 있는 platform 이 한정적이며 또한 주어진 platform 에서 서비스에서 사용되는 특정한 라이브러리를 추가하거나 시스템 설정을 변경해야할 경우가 발생하면 쉽게 적용하기 힘든 단점도 분명 존재합니다. 그래서, 이런 단점을 해소하기 위해서 AWS 에서 Elastic Beanstalk 환경을 약간이나마 커스터마이징 할 수 있도록 제공해주는 기능이 .ebextensions 입니다.

AWS Elastic Beanstalk configuration files (.ebextensions)

배포할 서비스에 Elastic Beanstalk 설정을 위한 파일을 추가하고 싶다면 우선 배포될 소스 bundle의 root 디렉토리 아래 .ebextensions 폴더를 추가하고 해당 폴더 아래 설정 파일들을 추가하면 됩니다. 이 때, 설정 파일 뿐만 아니라 설정파일에서 사용되는 기타 다른 파일들도 해당 디렉토리에 포함시키면 설정파일에서 사용할 수 있습니다. 소스 구조

그리고, 설정 파일들은 보통 ###.config 와 같이 작성되는데 확장자는 사실 큰 의미는 없지만, 설정을 위한 파일을 구분하기 위해서 보통 .config를 사용합니다. 설정파일의 적용 순서는 파일명 alphabetical order 순서 이므로, 적용 순서가 중요한 경우 순서대로 ‘00-xxx.config’, ‘01-xxx.config’ 이런 식으로 이름을 주면 됩니다. 여기서 한가지 주의 할 것은 spring-boot 으로 만들어진 application의 경우에 platform 을 tomcat 하거나 혹은 embedded tomcat을 사용하는 경우 platform을 java로 하는 경우가 있는데 두 경우 약간의 차이가 있습니다. 보통 tomcat에 application을 올리는 경우는 war 로 패키징을 하는데 이 경우에는 gralde을 사용해서 빌드하는 경우라면 build.gradle 에서 간단하게

war {
     from(‘config 파일이 포함되어 있는 소스 경로’) {
         into(‘.ebextensions’)
     }
}

추가해서 빌드만 하면 문제가 없습니다. 하지만, embedded tomcat 을 사용하면서 .jar로 배포하는 경우에는 jar에 저렇게 .ebextenions 폴더를 포함시킬 수 없기 때문에 따로 배포전에 jar 파일과 함께 .ebextenions 폴더를 하나의 zip 파일로 묶어서 배포해야 제대로 설정파일이 적용됩니다.

zip -r application.zip application.jar .ebextensions

저는 앞선 BROS 2.0/신배라 프로젝트는 tomcat platform을 통해서 war 파일 형태로 배포했었는데, 프로젝트 막바지단계 합류했던 주문지편 개편의 경우에는 java platform 에 jar 형태로 배포되고 있었는데 둘의 차이를 제대로 인지하지 못해서 하루정도 beanstalk 설정이 안되서 헤맸던 아픔(?) 경험이 있습니다. 그리고, 뒤에 다시 언급하겠지만 tomcat/java platform 에 따른 배포는 더 중요한 차이가 있습니다.

설정 파일 주요 Key에 대한 설명

.ebextenions을 통한 설정에서 가장 중요한 것은 당연히 confiuration 파일 통해서 설정할 수 있는 사항들에 관한 것일 겁니다. 이런한 설정 항목을 AWS에서는 key라고 하는데 많이 사용되는 key 항목에는 다음과 같은 것들이 있습니다.

  • Packages - 서비스 구동에 필요한 추가적인 패키지들을 다운로드 받아서 시스템에 install 할 수 있습니다. 기본 문법은 다음과 같습니다.

    packages:
      name of package manager:
         package name: version

    ex) 현재 우아한형제들 사내 시스템의 경우 서버 모니터링 툴로 newrelic을 사용하고 있는데 newrelic 서버 모니터링 패지지의 경우 다음과 같이 추가하면 elastic beanstalk 서비스 배포나 autoscaling시 자동으로 패키지가 설치됩니다.

    packages:
       yum:
         newrelic-sysmond: []
       rpm:
         newrelic: http://yum.newrelic.com/pub/newrelic/el5/x86_64/newrelic-repo-5-3.noarch.rpm

  • Sources - 시스템에 필요한 파일들은 압축 파일 형태로 만들어 public url 형태로 다운로드 할 수 있게 만들어 둔 경우 해당 파일은 지정된 위치로 다운로드 받아서 압축까지 풀어줍니다. 기본 문법은 다음과 같습니다.

    sources:
      target directory: location of archive file

    ex) newrelic APM을 agent 형태로 구동할 때, 필요한 라이브러리를 s3에 올려두고 다음과같이 원하는 위치에 다운로드 받아서 사용할 수 있습니다.

    sources: /usr/share/newrelic-java: https://s3.ap-northeast-2.amazonaws.com/{bucket-name}/newrelic.zip

    source를 사용해서 필요한 파일을 받을 때, 한가지 주의 할 것은 target diectory와 압축이 풀린 파일들의 owner가 기본적으로 ‘root’ 입니다. 따라서, 파일이 사용되는 용도에 따라서 권한 문제가 발생할 수 있으니 필요한 경우 Owner나 mode를 수정해야 합니다.

  • Files - 파일을 만들거나 혹은 다운로드 받을 수 있습니다. 기본 문법은 다음과 같습니다.

    files:
    “target file location on disk”:
       mode: “six-digit octal value”
       owner: name of owning user for file
       group: name of owning group for file
       source: URL
       authentication: authentication name:
    “target file location on disk”:
       mode: “six-digit octal value”
       owner: name of owning user for file
       group: name of owning group for file
       content: |
         this is my content
       encoding: encoding format
       authentication: authentication name:

    이때, content 와 source 옵션을 동시에 사용될 수 없습니다.

  • Commands - ec2 인스턴스에서 구동되는 실행 명령을 기술 할 수 있습니다. 뒤에 나오는 Container Commands 와 비슷하지만 둘은 큰 차이가 있습니다. commnads는 container commands 보다 먼저 실행되는데 application과 web server 가 아직 설정되기 전, application version 파일이 적용되기 전에 실행되며 container commands는 application과 web server가 모두 준비되고 application version이 deploy 되기 직전인 상태에서 실행되는 차이가 있습니다. 둘 모두 여러개의 명령어를 한꺼번에 기술할 수 있는데, 이 때 실행 순서는 commnad name의 alphabetical order 순입니다. 기본 문법은 다음과 같습니다.

    commands:
      command name:
         command: command to run
         cwd: working directory
         env:
           variable name: variable value
         test: conditions for command
         ignoreErrors: true

    ex) Elastic beanstalk에서 신규로 만들어지는 ec2 인스턴스의 시간은 기본적으로 모두 UTC라 다음과 같이 commnads 설정으로 로컬타임으로 변경할 수 있습니다.

    commands:
      01remove_local:
         command: “rm -rf /etc/localtime”
      02link_seoul_zone:
         command: “ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime”

  • Container Commnds - 앞서 설명한 commands와 거의 비슷하지만 실행시점에 차이가 있으면, “leader_only” 라는 아주 유용한 옵션을 가지고 있습니다. “leader_only” 옵션의 경우 true로 설정하게 되면 여러 대의 인스턴스로 구성된 서비스에서 오직 한대의 인스턴스에서만 해당 명령이 실행됩니다. 하지만, 실제적 운영했을 때 몇가지 이슈를 발견했는데 뒤에 다시 한번 언급하도록 하겠습니다. 기본 문법은 다음과 같습니다.

    container_commands:
      name of container_command:
         command: “command to run”
         leader_only: true
       name of container_command:
         command: “command to run”

Java platform 에서 jar로 배포할 경우 실행 옵션 수정

앞서 tomcat/java platform 에서 war/jar 배포시 배포 파일 생성시 차이점을 언급했었는데. 이 둘은 java 실행 옵션을 설정하는데도 큰 차이가 있습니다.

  • Tomcat platform + war 배포
    • AWS Elastic beanstalk 콘솔에서 configuration 항목 container options 를 통해서 JVM Heap size, JVM command line options 등을 기술해주면 deploy시 해당 옵션이 쉽게 적용되어 application이 실행된다. 상대적으로 설정이 매우 쉽다.
  • Java platform + jar 배포
    • 위와 동일한 설정항목에 가보면 JVM Heap size 나 JVM command line options 등을 기술할 수 있는 항목이 없다! Environment Properties 항목에 어떻게 설정을 하던간에 JVM command line 옵션은 콘솔을 통해서 설정할 수 없다.

기본적으로 Java plafrom + jar 배포인 경우에는 콘솔에 어떤 항목을 수정하더라도 실제 application 실행 후 ec2 인스턴스에 들어가서 실행된 application 을 확인해 보면 “java -jar application.jar” 로 실행옵션이 적용되지 않는 것을 확인할 수 있을 것입니다. 이는 Elastic beanstalk에 구현되어 있는 java platform deploy process로 인한 것인데, 이 JVM command line 옵션등과 같이 실행 옵션을 변경하기 위해서는 “Profile” 이 필요합니다.

Configuring the Application Process with a Procfile

Procfile 파일은 앞서 설명한 것 처럼 Elastic beanstalk Java platform에서 JVM 옵션을 설정하거나 혹은 여러개의 jar 파일이 들어있는 서비스를 구동할 때 사용되는 파일인데, 기본적으로 다음과 같이 기술됩니다.

web: java -jar server.jar -Xmms:256m
cache: java -jar mycache.jar
web_foo: java -jar other.jar

여기서 cache나 web_foo 라인은 없어도 되지만 web 항목은 필수이며 여기에 메인 application 실행 명령을 줄 수 있습니다. 적용할 Procfile은 패키징시 jar/.ebextensions 디렉토리와 같이 root에 함께 패키징되어야 합니다.

zip -r application.zip Procfile application.jar .ebextensions

실제 주문지면 개편시 API 서버를 구축하는데 서버 모니터링 APM으로 newrelic 을 사용하기 위해서 “-javaagent:/usr/share/newrelic-java/newrelic.jar -Dnewrelic.environment=dev “ 이 몇줄을 commnad line 옵션으로 주기 위해서 하루 종일 삽질(?)을 한 아픈 기억이 있습니다T_T. 앞서 말했듯이 기존에 구현했던 프로젝트들은 Tomcat + War 여서 AWS 콘솔에 써주면 그만 이었는데 이 녀석은 그게 안되서 반나절쯤 하다가 아직 상용 오픈 안한 서비스여서 그냥 싹 다 Tomcat에 war로 배포할까 진진하게 고민했었던….

Procfile + leader_only 옵션

타이틀이 좀 이상하긴 하지만, Java platform 에서 한대의 서버만 commnad line 옵션을 다르게 줘야하는 경우가 생길 수도 있습니다. 지면개편 프로젝트에서 주문 API의 경우 전사적으로 pinpoint를 사용할 수 있게 되면서, 기존 newrelic 상용계정 한대 + 나머지 서버는 모두 pinpoint 로 모니터링 하게 서버를 구축하고 싶었습니다. 그런데, newrelic/pinpoint 모두 javaagent방식으로 데이터를 수집하다보니 두개의 옵션을 모두 적용하면 둘중 한쪽 or 양쪽 모두 데이터 수집에 문제가 생기는 경우가 발생했습니다. 그래서, 상용 서버는 newrelic agent옵션을 나머지 서버들은 pinpoint agent 옵션을 주려고 했는데 Java platform 에서는 Profile 에서만 해당 옵션을 줄수 있고 이 Profile은 Container Commnads leaer_only 옵션으로 수정하는 시점에는 이미 Profile이 적용된 시점 이후여서 적용해도 제대로 동작하지 않았습니다. 그래서, 구글링으로 방법을 찾아보다가 Profile web 항목에 shell script를 쓸 수 있는 걸 확인해서 다음과 같이 구현했습니다.

Profile
web : ./run.sh

Conatiner Commnads
  006_default_run_sh:
     command: cp ./.ebextensions/run.sh run.sh; chmod a+x run.sh
     ignoreErrors: true
  007_replace_run_sh:
     command: cp ./.ebextensions/run_leader.sh run.sh; chmod a+x run.sh
     ignoreErrors: true
     leader_only: true

그렇습니다. Profile 은 container commands 적용 시점에 이미 변경해도 의미가 없지만, Profile 이 web 에서 실행되는 shell script의 경우에는 실제 서비스가 deploy 될때 해당 script를 찾아서 실행될 것이므로 이 shell script를 cotainer commnds leader_only 옵션으로 구분해서 수정했고 실제로 잘 동작됩니다! 한가지 주의할 점은 shell script로 java를 실행할 경우 shell script가 실행권한이 있어야 되므로 위의 command 옵션 마지막 항목에서 처럼 chmod 를 통해서 실행권한을 주는 것을 잊지 말아야합니다.

Container Commands leader_only 옵션

앞서 container commands 설명에서 leader_only 옵션을 잠깐 설명드렸습니다. 여러 개의 인스턴스로 구성된 서비스에서 한 서버만 구동되는 명령어라고 할 수 있는데, 실제 제가 진행했던 프로젝트들에서는 회사의 라이센스 규정상 newrelic APM 상용계정은 서비스당 한대에만 적용가능해서 이 옵션을 통해서 유용하게 설정했던 기억이 있습니다. 하지만, 실제로 운영중에 약간의 이슈를 발견했습니다. 주문 지편 개편 프로젝트의 지편 게이트웨이 서버의 경우 기본 2대의 인스턴스로 운영되지만 주문이 많이 몰리는 피크 타임에는 스케쥴링 걸어놓고 미리 1대 인스턴스를 더 준비해서 총 3대의 인스턴스로 운영하다가 피크타임이 지나면 다시 2대로 운영하고 있습니다. 이 서비스의 경우에는 newrelic APM은 leader_only 옵션을 사용하여 한대의 인스턴스만 상용계정이고 나머지 2대의 인스턴스는 일반 계정으로 APM 을 설정했습니다. 해당 설정으로 1주일정도 운영하면서 newrelic 에서의 모니터링도 큰 문제가 없었는데 어느날 부터인가 상용계정에서 해당 서비스가 모니터링 되지 않더니 엉뚱하게 개발 일반 계정에서 기본 2대/피크시 3대의 서버가 잡히기 시작한 것입니다. 이걸 인지하고 콘솔에서 health 항목을 통해 확인해보니 보통의 경우에는 처음 설정된 2대의 인스턴스는 계속 운영되고 1대의 서버만 스케쥴링 시간에 맞춰서 붙었다 떨어졌다를 해서 기본 2대의 서버의 running day가 동일했는데 이 항목이 10일이상 차이가 난 것을 발견했습니다. 추측컨데, 타임스케쥴링 되어서 인스턴스가 증가되서 줄어들때 무슨 이유에서인지 leader_only 설정이 되어 있던 인스턴스가 빠졌고, 그 이후에는 leader_only 무용지물이 된 것입니다. 아마도, autoscaling 시에는 leader_only 옵션이 없는 인스턴스 상태를 그대로 가져와서 새로운 인스턴스를 구성하는 것 같은데 서버를 다시 배포하지 않는 이상 영원이 leader_only : true 인 서버는 올라오지 않습니다! 5월말에 운영시작해서 2달 조금 넘게 운영하고 있는데 이와 같은 상황이 2번이나 발생했습니다.T_T 그래서, 정말 운영상에서 중요한 옵션을 leader_only 옵션으로 적용하는건 개인적인 경험에서는 자제하는게 좋은 것 같습니다. (혹시 이런 현상의 정확한 이유나 막는 방법 아시는 분은 댓글로 제보 부탁드립니다.^^)