🐶 들어가며

2~3달 전까지 저는 컨테이너, 도커, 쿠버네티스에 대해 잘 알지 못했고, 남이 만들어 놓은 이미지를 이해하지 못한채 일단 실행하고 이용해본 정도 였습니다. 거의 0에 가까운 지식 수준을 가지고 있었어요. 하지만 언젠가는 공부해봐야할 주제라고 생각하고 있었습니다.

요 몇달간 회사 일정에 여유가 좀 생겨, 드디어 시도하게 되었네요🙏🏻

사내에 컨테이너, 도커에 관해 경험이 있거나 관심이 있는 분이 안계셔서 혼자 삽질 많이 했읍니다,,
도커라이징만 계속 몰두하지는 못했고, 피쳐 개발하다가 이것저것 하면서 왔다갔다 했습니다.

사실 아직 실제 프로덕션에서 사용할 만큼 테스트 해보지 못했고 해결하지 못한 이슈들도 남아있어요.
그래도 그간의 과정을 잊어버리지 않기 위해 정리해보려 합니다!

_대략 nginx, rails, puma, docker, ecs, ecr, github action등의 이야기가 될것 같네요🥊
_
(본문에 나오는 용어가 정확하지 않을 수 있습니다)
(도커의 명령어나 사용법 등은 깊게 다루지 않습니다)

🙄왜?

다른 방법도 많은데 왜 도커? 왜 ECS냐?

도커를 통해 서비스의 의존 관계를 컨테이너 내부에 집약시킬 수 있고, 독립된 운영환경을 쉽고? 빠르게? 구축할 수 있다고 생각해요. 이미 검증된 기술이라고 생각하기도 하고.

ECS는 컨테이너 오케스트레이션을, 지금 저의 수준에서 쉽게 다룰 수 있을거라고 생각했습니다.

사실 저는 하입비스트입니다.

🐳도커라이징

무엇을

처음 마주한 고민점인것 같네요.
Dockerfile 을 만들어야 하는데 어떤 단위로, 어디서 부터 어디까지 컨테이너 구성을 해야할까?

최초 목적이 자소설닷컴 메인서비스를 다루는 것이었기 때문에, 현재 메인서비스의 EC2 인스턴스 를 대체할 수 있는 컨테이너 실행 단위? 를 만들어야겠다고 생각했어요.

인스턴스 내부에는
Nginx + Passenger + Rails + Sidekiq
정도가 구성되어있습니다. (Redis(Elasticache), DB(RDS)는 제외했습니다.)

처음엔 하나의 dockerfile에 현재 인스턴스 구성을 모두 집어 넣었으나, 곧 전략을 바꿨어요.🍀

다른 분들의 Rails 도커 예제들을 보니 보통 웹서버, 애플리케이션, 등의 단위로 쪼개서 컨테이너를 생성하는것 같더라구요. 아마 의존성을 분리 할 수 있는 정도?라면 컨테이너를 따로 다루는 듯 했어요.
docker compose를 통해 분리된 컨테이너 간 실행 순서나 의존성을 관리하고 있었습니다.

저도 위와같은 방식으로 진행하기로 결정했어요.🌸

Dockerfile작성, docker compose를 이용한 컨테이너 실행, ecs로 적용하기 순으로 이야기 할게요.

Dockerfile

nginx (web)

저희는 passenger를 통해 nginx를 설치하고 사용하고 있었습니다. 프로세스 관리도 passenger가 하고 있죠.
phusion/passenger-ruby24 같은 베이스 이미지가 있어서, 이것을 사용하려고 했어요. 근데 잘 안되더라구요?
일단 참고할 만한 자료가 많이 없었고, 도커 빌드를 할때 rubyracer 관련 에러가 나는데 해결이 쉽지 않았어요ㅠ
뿐만 아니라 웹서버와 앱의 컨테이너 분리도 어려웠고 결국 이 방법은 포기했습니다...
대신 nginx + puma 조합으로 변경했어요😭

# Base image
FROM nginx
# Install dependencies
RUN apt-get update -qq && apt-get -y install apache2-utils
# establish where Nginx should look for files
ENV RAILS_ROOT /app
# Set our working directory inside the image
WORKDIR $RAILS_ROOT
# create log directory
RUN mkdir log
# copy over static assets
COPY public public/
# Copy Nginx config template
COPY docker/web/nginx.conf /tmp/docker.nginx
# substitute variable references in the Nginx config template for real values from the environment
# put the final config in its place
RUN envsubst '$RAILS_ROOT' < /tmp/docker.nginx > /etc/nginx/conf.d/default.conf
EXPOSE 80
# Use the "exec" form of CMD so Nginx shuts down gracefully on SIGTERM (i.e. `docker stop`)
CMD [ "nginx", "-g", "daemon off;" ]

docker/web/Dockerfile

완성된 web 컨테이너를 위한 도커 파일입니다. 시중에 떠도는 참고자료가 많은데, 거의 그대로 쓴것 같아요👻

아 그리고 중간에 docker/web/nginx.conf 가 있는 nginx.conf 파일이 필요합니다. 이것도 간단합니다.

upstream rails_app {
    server app:3000 fail_timeout=0;
}
server {
    # define your domain
    server_name jssalpha.jasoseol.com;
    # define the public application root
    root   $RAILS_ROOT/public;
    index  index.html;
    # define where Nginx should write its logs
    access_log $RAILS_ROOT/log/nginx.access.log;
    error_log $RAILS_ROOT/log/nginx.error.log;

    # deny requests for files that should never be accessed
    location ~ /\. {
        deny all;
    }
    location ~* ^.+\.(rb|log)$ {
        deny all;
    }

    # serve static (compiled) assets directly if they exist (for rails production)
    location ~ ^/(assets|images|javascripts|stylesheets|swfs|system)/   {
        try_files $uri @rails;
        access_log off;
        gzip_static on;
        # to serve pre-gzipped version
        expires max;
        add_header Cache-Control public;

        add_header Last-Modified "";
        add_header ETag "";
        break;
    }

    # send non-static file requests to the app server
    location / {
        try_files $uri @rails;
    }
    location @rails {
        proxy_set_header  X-Real-IP  $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass http://rails_app;
    }
}

docker/web/nginx.conf

여기서 주목해야할 부분은 upstream 부분이에요.
여기서 살짝 해매고 삽질하고 그랬어요ㅋ..ㅋ 몇번을 새로 빌드했는지 모르겠,,
도커 컴포즈 부분에서 다시 언급할텐데,
지금은 upstream 블록을 한번 보고 지나쳐 주세요🙏🏻

현재의 도커파일로 만든 nginx 이미지만 실행하면, 제대로 실행되지 않을거에요.

rails (app)

도커파일부터 공유합니다.

FROM ruby:2.4.9

RUN apt-get -y update && \
    apt-get install --fix-missing --no-install-recommends -qq -y \
    build-essential \
    nano \
    -y cron \
    wget gnupg \
    git-all \
    curl \
    ssh \
    postgresql-client libpq5 libpq-dev -y && \
    wget -qO- https://deb.nodesource.com/setup_9.x  | bash - && \
    apt-get install -y nodejs && \
    wget -qO- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
    apt-get update && \
    apt-get install yarn && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*


ENV RAILS_ENV='production'
ENV RACK_ENV='production'
ENV RAILS_ROOT /app
ENV BUNDLE_DIR /bundle

RUN mkdir -p $RAILS_ROOT
WORKDIR $RAILS_ROOT
COPY . $RAILS_ROOT

RUN yarn install
RUN gem install bundler:1.17.3
RUN bundle install --path $BUNDLE_DIR --jobs 20 --retry 5 --without development test
RUN bundle exec rake assets:precompile

EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

docker/app/Dockerfile

ruby 2.4.9버전을 사용중 입니다. 처음엔 알파인 리눅스 루비를 썼다가 슬림 버젼을 썼다 했는데, 크고작은 에러들이 보여서 일단 기본 버전 이미지를 사용했어요. (경량화는 나중으로 미뤘어요🌝)

저희는 번들러 1.17.3 버전을 사용중입니다.
필요에 맞게 서버 세팅을 했고, 크게 어려운 점은 없었네요.

느낀점

도커파일을 처음 만들어 보면서 이런저런 명령어에 익숙해 지고 그러던 시기?.?
패신저로 할땐 엄청 답답하고 그랬는데, 퓨마로 갈아타니까 참고할 자료도 많고 금방 진행 되었어요 🏄‍♂️

빌드를 여러차례 반복하게 되면서, 프리컴파일 하는 부분이 참 오래걸리더라구요.
원래라면 기존 에셋들이 캐싱되어있고 해시를 확인해 바뀐 파일들만 다시 컴파일해야하는데, 이미지 자체를 계속 새로만들다 보니 프리컴파일을 처음부터 진행해 빌드 시간이 오래걸립니다.. 이 부분도 S3를 이용하던지, 볼륨을 이용하던지 아무튼 빌드 시간이 줄어들도록 튜닝을 해야할 부분입니다.🛠

퓨마를 새로 집어 넣으면서, 젬을 추가했고 puma.rb 설정 파일도 새로 만들었어요. 기본 중의 기본으로 만들었습니다.

RUN, CMD, ENTRYPOINT

도커파일을 작성할때 조금 헷갈리던 명령어를 간단히 정리해 봤어요.

  • RUN
    새로운 레이어에서 명령어를 실행하고, 새로운 이미지를 생성한다. 보통 패키지 설치 등에 사용된다. RUN 명령을 실행할 때 마다 레이어가 생성되고 캐시된다.
  • CMD
    default 명령이나 파라미터를 설정. 
    docker run 실행 시 실행할 커맨드를 주지 않으면 이 default 명령이 실행됨.
    그리고 ENTRYPOINT의 파라미터를 설정할 수도 있다. 
    주용도는 컨테이너를 실행할 때 사용할 default를 설정하는 것.
  • ENTRYPOINT
    CMD 와 유사하지만 차이점이 있다.
    CMD는 컨테이너를 실행할때 인자값을 주게 되면 Dockerfile 에 지정된 CMD 값을 대신 하여 지정한 인자값으로 변경하여 실행 되지만, ENTRYPOINT 는 해당 컨테이너가 수행될 때 반드시 ENTRYPOINT 에서 지정한 명령을 수행되도록 지정 된다. 컨테이너 실행시 ENTRYPOINT의명령은 유지하고, 추가 인자를 CMD로 받아 처리한다.

적절하게 사용하려면
컨테이너가 수행될 때 변경되지 않을 실행 명령은 CMD 보다는 ENTRYPOINT 로 정의하자.
메인 프로세스에 대한 기본 옵션값을 CMD 로 정의하자.

인터미션

글을 정리하면서 기억을 더듬고 다시 도커 관련 파일들을 보고 있는데, 약간 피로하기도 하고 포스팅이 길어질 것 같네요 🔫
여기서 잠깐 끊고, 다음에 이어 가겠습니다.

다음 포스팅에서 도커 컴포즈로 로컬 도커환경에서 서비스 컨테이너 실행하기 로 시작할게요🎈
ECS관련 내용은 그 다음 포스팅으로 이어야 겠습니다😂

홍보

사이드 프로젝트로 랜덤빵이라는 iOS앱을 만들었어요!📱🍎

profile
앵커리어에서 자소설닷컴을 개발하고 있습니다.

0개의 댓글