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

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

Comments

Yaboong's Picture

Yaboong

오스카 쉰들러는 흔해빠진 기회주의자요 부패한 사업가였다. 그러나 거대한 악이 세상을 점령하는 것처럼 보일 때 그 악에 대항해서 사람의 생명을 구한 것은 귀족도 지식인도 종교인도 아닌 부패한 기회주의자 오스카 쉰들러였다.

Seoul, South Korea https://github.com/yaboong