애플리케이션을 Docker 이미지로 만들고, 배포해본 경험 다들 있으신가요? 무거운 이미지를 베이스 이미지로 사용해 한 번 다운받는 데 무척 오랜 시간이 걸리거나, 민감한 데이터를 포함하게 되어 보안상 문제가 생겼던 경험도 다들 있으실 겁니다. 이번 포스팅에서는 더 가볍고, 안전한 이미지를 만들기 위해서는 어떻게 해야 하는지 한 번 알아보겠습니다.
이제까지는…
아래는 평범한 Dockerfile입니다. 우분투 위에서 Go를 설치하고 파일을 복사한 후 빌드하고 실행합니다. 가장 단순한 방법이지만, Ubuntu는 용량이 크기 때문에 매번 내려받는 시간이 매우 오래 걸립니다. 또 아래에서 설명하겠지만 여러 보안 취약점도 보이는군요.
FROM ubuntu
ARG DEBAIN_FRONTEND=noninteractive
# 여러 명령을 &&을 통해 연결합니다.
RUN apt-get update && apt-get install -y golang-go
COPY . .
RUN CGO_ENABLED=0 go build app.go
CMD ["./app"]
이미지 크기 줄이기
도커 이미지가 가벼워질수록 애플리케이션 빌드, 배포 속도는 빨라집니다. 그렇게 되면 더 자주, 많이 배포할 수 있기 때문에 개발자의 생산성이 확연히 증가하게 됩니다.
- 멀티 스테이징 기법을 사용해서 이미지 크기를 줄입니다. 멀티 스테이징이란 스테이지를 여러 개 만들고 각각 따로 빌드한 후, 가장 가벼운 이미지에 결과를 통합하는 방식을 말합니다.
- RUN 명령을 최대한 적게 쓰는 방법이 있습니다. RUN 명령은 개별 이미지를 만들기 때문에 이를 최소한으로 만들기 위해 RUN 명령 한 번에 최대한 많은 스크립트를 실행합니다.
잘 와닿지는 않지요? 예시와 함께 보겠습니다.
# 첫 스테이지는 go 파일들을 빌드해서 하나의 실행 가능한 파일로 만듭니다.
# 최종적으로 우리는 실행 가능한 파일만 필요하고 나머지는 불필요합니다.
FROM ubuntu AS builder
ARG DEBAIN_FRONTEND=noninteractive
# 여러 명령을 &&을 통해 연결합니다.
RUN apt-get update && apt-get install -y golang-go
COPY . .
RUN CGO_ENABLED=0 go build app.go
# 그렇다면 딱 실행할 수 있는 파일만 가져옵니다.
# alpine은 매우 가벼운 linux 이미지입니다.
FROM alpine
COPY --from=builder /app .
CMD ["./app"]
Go를 빌드하기 위해서는 다양한 의존성과 설치 파일이 필요합니다. 반면 Go를 실행할 때는 실행 파일 하나만 있으면 됩니다. 그렇다면 실행 환경에는 가장 가벼운 alpine 위에 Go 실행 파일만 있으면 애플리케이션을 빠르게 실행할 수 있게 됩니다.
위 예시는 Ubuntu에서 Go를 빌드하고, alpine으로는 실행 파일만 복사해 실행하는 과정을 설명합니다.
.dockerignore로 불필요한 소스 코드 없애기
README.md나 테스트 코드는 실제 애플리케이션을 빌드할 때는 불필요합니다. 또한, API 인증 토큰과 같은 민감한 정보가 포함되어 있는 .env
파일, .pem
프라이빗 키 파일 및 Git 커밋 이력이 포함되어 있는 .git
디렉터리는 도커 이미지에 포함되어서는 안됩니다. 이런 파일들을 도커 이미지에 포함시키지 않기 위해 우리는 .dockerignore
파일을 만듭니다. 아래처럼 제외할 파일을 추가하면 자동으로 빌드할 때 적용됩니다.
.env
.git
.gitignore
*.pem
*.md
test/
안전한 이미지 만들기
Docker 이미지가 루트 권한을 가진다면, 혹여나 해킹을 당할 경우 매우 위험할 수 있습니다. 그외에도 정말 다양한 방법으로 여러분의 애플리케이션을 망가뜨리려고 할 텐데, 우리는 가능한 사전에 예방해야 합니다.
앞선 Dockerfile 중에서 애플리케이션을 실행하는 부분인 FROM alpine부터 함께 보시지요.
# 첫 스테이지는 위와 동일
# 정확한 이미지 버전을 쓴다.
FROM alpine:3.12.1 AS builder
# 모든 유저에 대하여 /etc에 쓰기 권한을 없앤다.
RUN chmod a-w /etc
# appuser 유저를 만든다.
RUN addgroup -S app group && adduser appuser -G appgroup -h /home/appuser
# 모든 실행파일을 지운다. 이제 더이상 명령어를 사용하지 않을 거다.
RUN rm -rf /bin/*
COPY --from=builder /app .
# 루트 계정 대신 일반 유저로 전환한다.
USER appuser
CMD ["/home/appuser/app"]
- 정확한 이미지 버전 쓰기 : 버전을 쓰지 않으면 latest 버전을 자동으로 가져오기 때문에 상황에 따라 실행되는 것이 달라질 수 있습니다.
- /etc에 쓰기 권한 없애기 : /etc는 시스템의 설정 파일과 스크립트가 담긴 디렉터리입니다. 애플리케이션은 보통 수정할 일이 없기 때문에 쓰기 권한을 없애는 것이 좋습니다.
- 모든 실행파일 지우기 : 이건 옵션입니다. Go의 경우 실행 파일 하나만 있으면 실행이 가능하기 때문에 잠재적인 위험이 있는 다른 실행파일을 모두 지웁니다.
- 일반 유저로 전환하기 : 루트 계정은 시스템의 모든 것을 수정하고 제어할 수 있습니다. 최소한의 권한만 가진 유저를 생성해 해당 유저로만 애플리케이션이 실행되도록 합니다.
이미지 보안 검사
GitLab은 컨테이너 취약점 분석 툴인 Trivy와 Grype를 통해 이미지 보안 검사를 지원합니다. 앞서 설명드린 취약점 보완 방법 이외에도 다양한 취약점을 분석하고 해결 방법을 직관적으로 확인할 수 있습니다.
또한 보안 검사 결과를 merge request에 보여주어 프로젝트를 더 효율적이고 안전하게 관리하도록 지원합니다.
맺음말
지금까지 더 안전하고 가벼운 도커 이미지를 만드는 방법에 대해 알아보았습니다.
가벼운 도커 이미지는 CI/CD 파이프라인과 애자일 방법론이 만날 때 엄청난 시너지를 보입니다. 또한 안전한 도커 이미지로 비즈니스를 보호하고, 더 나아가 대부분의 위험을 사전에 예방할 수 있습니다.
GitLab을 통하면 이미지 보안 검사 뿐 아니라 라이선스와 비밀키 검사 등 여러 보안 검사를 무료로 할 수 있으며, 개발자와 보안 담당자의 생산성을 증가시킵니다. 인포그랩은 GitLab 및 DevOps에 대한 맞춤 기술 지원을 제공합니다.
Docker를 통한 안전하고 생산성 높은 CI/CD 파이프라인 구축이 필요하시면 문의하기로 연락 주십시오.