Docker 로 Http4s 프로젝트 Aws Elastic Beanstalk 에 배포해보기 - 1

Docker 로 Http4s 프로젝트 Aws Elastic Beanstalk 에 배포해보기 - 1

개요

  • 이전포스팅 에서 http4s 를 사용해 보기로 했다.
  • http4s 는 http 서비스에 scala 인터페이스를 활용하는 것으로 java 의 servlet 과 같은 녀석이라 보면 되겠다.
  • Aws elastic beanstalk 에 Docker 를 사용해서 배포해보자.
  • Http4s 프로젝트 생성(sbt, g8) -> Dockerizing -> Aws ECR push -> Aws EB deploy

Intro

이전포스팅 에서 http4s 를 사용해 보기로 했다. http4s 는 http 서비스에 scala 인터페이스를 활용하는 것으로, python 의 wsgi, java 의 servlet 과 같은 녀석이라 보면 되겠다. Java 의 servlet 이 tomcat 을 container 로 사용하는 것 처럼 scala 의 http4s 는 blaze 라는 녀석을 사용한다. Blaze 는 http4s 에서 네트워크에 필요한 부분을 구현한 것이다.

Why Docker?

일단 목표는 http4s 로 간단한 웹 애플리케이션을 하나 만들어서 aws elastic beanstalk (이하 eb) 에 배포해 보는 것이었다. 전에 scala 와 scalatra 로 aws eb 에 배포를 해서 서비스를 관리 해봤는데 scalatra 는 결국 servlet 기반이어서 그냥 war 파일로 묶어서 올리기만 하면 만사형통이었다. Local 에서는 sbt run 을 실행하면 서버가 동작하는데 배포를 어떻게 해야할 지 이것저것 찾아보던 중 Docker 라는 놈을 알게 됐고, 개발, 테스트, 운영 에서의 배포환경을 동일하게 가져갈 수 있다고 하는 말에 끌려서 Docker 를 사용해서 배포해 보기로 했다. Docker 에 대한 설명은 여기 참 좋다.

Why AWS Elastic Beanstalk?

굳이 aws eb 을 사용해서 배포하려는 이유는 auto scaling 을 지원하기 때문이다. 거기다 심지어 사용하기 쉽다. 처음 개발을 시작할 때는 ‘와… 배포는 어떻게 하는거지… 우와… 로드밸런싱… 난 언제 저런거 해보지…’ 했는데 Aws 가 알아서 다 해준다 ^^ 웹 서비스 사용자가 많아지면 트래픽이 높아지고 트래픽을 분산처리 해 주는 로드밸런서가 필요하다. (로드밸런싱에 대한 설명은 여기) 하지만 eb 를 사용하면 그냥 아무것도 안해도 알아서 다 해준다 ^^ 로드밸런싱만 해줄 뿐 아니라 트래픽이 급증해서 현재 가지고 있는 서버 개수로 부족한 경우 알아서 서버 개수를 늘려준다. 관리이슈를 줄이는 것은 개발자 입장에서는 생산성을 높일 수 있는 중요한 요소다.

http4s 프로젝트 생성

sbt 와 giter8 template 을 이용하면 쉽게 생성할 수 있다.

$ brew install sbt
$ sbt sbtVersion
[info] ans: String = null
[info] ans: String = null
[info] Set current project to framework-test (in build file:/Users/yaboong/DevWorkspace/framework-test/)
[info] 0.13.13
$ sbt -sbt-version 0.13.13 new http4s/http4s.g8

하면 프로젝트 생성에 필요한 정보를 입력받는다. 그냥 엔터치면 [default] 로 설정된다. 나는 아래와 같이 입력했다.

name [quickstart]: http4s-test-app
organization [com.example]: com.yaboong
package [com.yaboong.http4stestapp]:
scala_version [2.12.4]: 2.12.1
sbt_version [1.0.4]: 0.13.13
http4s_version [0.18.0-M7]:
logback_version [1.2.3]:
specs2_version [4.0.2]:

생성한 프로젝트에 예제 HelloWorldServer.scala 로 가면

object HelloWorldServer extends StreamApp[IO] with Http4sDsl[IO] {
  val service = HttpService[IO] {
    case GET -> Root / "hello" / name =>
      Ok(Json.obj("message" -> Json.fromString(s"Hello, ${name}")))
  }

  def stream(args: List[String], requestShutdown: IO[Unit]) =
    BlazeBuilder[IO]
      .bindHttp(8080, "0.0.0.0")
      .mountService(service, "/")
      .serve
}

예제 코드가 있다. 확실히 처음에 쓰려고 했던 fintrospect 보다는 라우팅이 직관적이다.

Run

name 에 입력한 경로로 가서 sbt run 을 실행시켜서 아래와 같은 화면이 나오면 제대로 실행 된 것이다.

localhost:8080/hello/yourname 으로 접속하면 잘 도는 걸 확인할 수 있다.

Dockering

이제 이 프로젝트를 docker image 로 떠보자. Docker 는 여기 받고 실행시키면 된다.

$ docker images

sbt 에서 지원하는 plugin 으로 이미지를 만들어보자.

project-root/project/plugins.sbt

addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.2.1")

project-root/build.sbt

enablePlugins(JavaAppPackaging)
enablePlugins(DockerPlugin)

IntelliJ 에서 프로젝트를 열고 External Libraries 의 sbt-and-plugins 를 보면 sbt-native-packager-1.2.1.jar 가 추가 된 것을 볼 수 있고 sbt-native-packager-1.2.1.jar/sbt/sbt.autoplugins 파일을 보면

com.typesafe.sbt.SbtNativePackager
com.typesafe.sbt.packager.archetypes.JavaAppPackaging
com.typesafe.sbt.packager.archetypes.JavaServerAppPackaging
com.typesafe.sbt.packager.archetypes.jar.ClasspathJarPlugin
com.typesafe.sbt.packager.archetypes.jar.LauncherJarPlugin
com.typesafe.sbt.packager.archetypes.scripts.AshScriptPlugin
com.typesafe.sbt.packager.archetypes.scripts.BashStartScriptPlugin
com.typesafe.sbt.packager.archetypes.scripts.BatStartScriptPlugin
com.typesafe.sbt.packager.archetypes.systemloader.SystemVPlugin
com.typesafe.sbt.packager.archetypes.systemloader.SystemdPlugin
com.typesafe.sbt.packager.archetypes.systemloader.SystemloaderPlugin
com.typesafe.sbt.packager.archetypes.systemloader.UpstartPlugin
com.typesafe.sbt.packager.debian.DebianDeployPlugin
com.typesafe.sbt.packager.debian.DebianPlugin
com.typesafe.sbt.packager.debian.JDebPackaging
com.typesafe.sbt.packager.docker.DockerPlugin
com.typesafe.sbt.packager.docker.DockerSpotifyClientPlugin
com.typesafe.sbt.packager.jdkpackager.JDKPackagerDeployPlugin
com.typesafe.sbt.packager.jdkpackager.JDKPackagerPlugin
com.typesafe.sbt.packager.linux.LinuxPlugin
com.typesafe.sbt.packager.rpm.RpmDeployPlugin
com.typesafe.sbt.packager.rpm.RpmPlugin
com.typesafe.sbt.packager.universal.UniversalDeployPlugin
com.typesafe.sbt.packager.universal.UniversalPlugin
com.typesafe.sbt.packager.windows.WindowsDeployPlugin
com.typesafe.sbt.packager.windows.WindowsPlugin

이런 plugin 들을 사용할 수 있는데 이 중 JavaAppPackaging, DockerPlugin 을 사용 할 것이다. 저장하면 알아서 다시 빌드한다.

이제 프로젝트 root 로 가서

$ sbt docker:publishLocal
$ docker images

를 실행하면 아래와 같이 docker image 가 새로 생성된 것을 볼 수 있다.

MacBook-Pro-3:http4s-test-app yaboong$ docker images
REPOSITORY          TAG                         IMAGE ID             CREATED          SIZE
http4s-test-app     0.0.1-SNAPSHOT    828c8b1d9e0b    3 seconds ago   785MB

AWS Elastic Container Registry

이제 이 도커 이미지를 docker hub 에 올려도 되겠지만 private repository 는 한개만 무료로 제공되기 때문에 Aws Elastic Container Registry 에 올려서 ecr 에 있는 걸 eb 에 deploy 시켜보자. Awscli 를 설치 하고

$ aws --profile yaboong ecr create-repository --repository-name yaboong/http4s-test-app

명령어 실행하면

{
    "repository": {
        "createdAt": 1516344095.0,
        "repositoryName": "yaboong/http4s-test-app",
        "repositoryUri": "<registry_id>.dkr.ecr.ap-northeast-2.amazonaws.com/yaboong/http4s-test-app",
        "registryId": "<registry_id>",
        "repositoryArn": "arn:aws:ecr:ap-northeast-2:<registry_id>:repository/yaboong/http4s-test-app"
    }
}

이렇게 생성되었다고 알려준다. aws ecr 명령어 실행할 때 --profile yaboong 을 입력해준 건 aws cli 는 하나지만 aws 계정은 여러개를 사용하고 있어서 그렇다.

<registry_id> 값은 계정마다 고유하게 가지는 숫자인 것 같다. repository 마다 registryId 값이 다를 줄 알았는데 모두 같은 걸 사용한다.

$ vi ~/.aws/config
[profile yaboong]
aws_access_key_id = YABOONGKEYID
aws_secret_access_key = yaboongACCESSkey
region=your-region

[profile yours]
aws_access_key_id = YOURKEYID
aws_secret_access_key = yourACCESSkey

[default]
region = ap-northeast-2

이런식으로 저장해두면 --profile 옵션으로 서로 다른 IAM 계정에 접속할 수 있다. 아니면 ~/.aws/credentials 파일에 [default] 를 설정해 둬도 된다. 이제 생성한 docker image 를 ECR 에 push 하면 된다. 보통은 계정을 하나만 사용하니까 매번 치기 귀찮다. ~/.aws/credentials 에 default 로 자신의 access 정보를 저장해두자.

$ docker images
$ docker tag http4s-test-app:0.0.1-SNAPSHOT <registry_id>.dkr.ecr.ap-northeast-2.amazonaws.com/yaboong/http4s-test-app:latest

로 sbt docker plugin 으로 dockerizing 한 image 를 aws ecr repository uri 로 닉네임을 달아준다.

docker push 라는 docker 커맨드로 aws ecr 에 push 할건데, docker 커맨드에서 aws 접속정보를 알리가 없다. 그래서 aws cli 에서 로그인 한 상태가 유지 되도록 먼저 해준다. Aws document 에는 아래와 같이 하라고 되어 있지만 이렇게 하면 제대로 안된다. 이유를 찾고있는데 잘 모르겠다.

$ aws ecr get-login --no-include-email --region ap-northeast-2

이유는 모르겠고 해결방법은 Docker Forum 에서 겨우 찾았는데

$ eval $(aws ecr get-login --no-include-email | sed 's|https://||')

이렇게 해주면 Login Succeeded 가 화면에 출력된다. 이제 docker push 로 aws ecr 에 image 를 올릴 수 있다.

$ docker push <registry_id>.dkr.ecr.ap-northeast-2.amazonaws.com/yaboong/http4s-test-app:latest
The push refers to repository [<registry_id>.dkr.ecr.ap-northeast-2.amazonaws.com/yaboong/http4s-test-app]
69086d6748c1: Pushed
cac7e31cf8c0: Pushed
af02c8032044: Pushed
875b1eafb4d0: Pushed
7ce1a454660d: Pushing [==============>                                    ]    137MB/461.2MB
d3b195003fcc: Pushed
92bd1433d7c5: Pushed
f0ed7f14cbd1: Pushed
b31411566900: Pushing [============>                                      ]  35.97MB/141.7MB
06f4de5fefea: Pushing [================>                                  ]  2.519MB/7.801MB
851f3e348c69: Pushing [=====================>                             ]  10.17MB/23.84MB
e27a10675c56: Waiting

이제 EB 에 방금 생성한 docker image 를 ECR 에서 가져와서 배포해보자. 일단 eb 환경을 하나 생성하자. AWS management console (web) 에서 해도 되고 cli 로 해도 되는데 그냥 웹에서 하는 게 편하더라. 아무튼 플랫폼은 Docker 로 생성한다.

Deployment

Tomcat 환경의 eb 에서는 .war 파일을 업로드 하면 됐었는데 Docker 환경에서는 Dockerrun.aws.json 파일을 하나 작성해서 .zip 파일로 업로드 하면 된다.

$ vi Dockerrun.aws.json
{
  "AWSEBDockerrunVersion": "1",
  "Image": {
    "Name": "<registry_id>.dkr.ecr.ap-northeast-2.amazonaws.com/yaboong/http4s-test-app",
    "Update": "true"
  },
  "Ports": [
    {
      "ContainerPort": "8080"
    }
  ]
}

주의할 점 1 - 폴더를 압축하는 것이 아니라 Dockerrun.aws.json 파일 하나만 압축해서 올려야 한다.

주의할 점 2 - Elastic Beanstalk 의 권한에 AmazonEC2ContainerRegistryReadOnly 를 추가해줘야 한다.

주의할점 2 - 부연설명

EB 관리도구 화면에서 생성한 환경 마다 가질 수 있는 권한이 있다. 이 권한은 ‘환경이름’ > 구성 > 인스턴스 설정 아이콘 > 인스턴스 프로파일 에 설정되어 있다. Default 는 aws-elasticbeanstalk-ec2-role 로 설정이 되는데, 기본으로 가지는 role 에는 ECR 접속 권한이 없기 때문에 Dockerrun.aws.json.zip 파일을 올려 deploy 를 하더라도 eb 에서 docker image 를 가져올 수 없다. aws-elasticbeanstalk-ec2-role 에 AmazonEC2ContainerRegistryReadOnly 를 추가해 줘도 되고 새롭게 role 을 생성해서 같은 권한을 추가해줘도 된다.

권한 추가는 IAM Management Console > 역할 에서 수정하고자 하는 role 을 선택하고 ‘정책연결’ 로 AmazonEC2ContainerRegistryReadOnly 를 추가해 주면 된다.

최종 결과

https://your-env-name.your-region.elasticbeanstalk.com/hello/anything

뭘 좀 더 해볼까

일단 엄청난 트래픽의 hello world 를 받을 수 있는 웹 애플리케이션이 하나 생겼다. 근데 과정이 너무 복잡하고 비효율적이다.

sbt 로 docker image 생성 -> image ecr push -> Dockerrun.aws.json 생성 -> eb deploy

과정을 단순화 하고 자동화 할 수 있는 것이 있는지 알아보고 수정 해 봐야겠다. 그리고 같은 기능을 하는 다른 프레임워크로 구성 된 여러 웹 애플리케이션을 만들어서 성능 테스트를 한번 해 봐야겠다.

Yaboong's Picture

Yaboong

Oskar Schindler was a mere opportunist and a corrupt businessman. Yet, when it seemed that great evil was taking over the world, it was not nobles, intellectuals, or religious leaders who rose to defy it and save lives—it was a corrupt opportunist, Oskar Schindler.

Massachusetts, US linkedin github