Code Engine용 Dockerfile 작성
코드를 컨테이너 이미지로 빌드하기 전에 Docker 빌드가 IBM Cloud® Code Engine내에서 작동하는 방식에 대한 몇 가지 기본사항을 학습하십시오. 그런 다음 이러한 목표를 달성하기 위해 Dockerfile에 대한 몇 가지 우수 사례를 살펴보십시오. 이러한 예제에서는 이미지 크기를 줄이고, Code Engine 애플리케이션과의 상호 운용성을 개선하며, 루트가 아닌 사용자로 컨테이너를 실행하는 데 중점을 둡니다.
빌드 구성을 작성할 때 사용 가능한 두 가지 전략 중 어느 것을 사용할 것인지 결정하십시오.
-
클라우드 네이티브 빌드팩은 소스 코드를 검사하고 코드의 기반이 되는 런타임 환경과 소스에서 컨테이너 이미지를 빌드하는 방법을 감지합니다. 이 빌드팩은 여러 프로그래밍 환경에서 지원됩니다. 지원되는 목록은 빌드 전략 선택에서 찾을 수 있습니다.
-
Docker 빌드는 Dockerfile에서 설명하는 방식에 따라 컨테이너를 작성합니다. 그런 다음 Dockerfile은 소스 코드와 함께 커미트되어 컨테이너를 작성합니다.
빌드 전략으로 둘 중 하나를 사용할 수 있지만, 다음과 같은 경우 Dockerfile을 선택할 수 있습니다.
- 빌드팩이 프로그래밍 환경을 지원하지 않는 경우
- 프로젝트 빌드가 컨테이너에 추가 패키지를 설치해야 하는 경우
Dockerfile 기본사항
Dockerfile은 컨테이너가 빌드되는 방식을 설명합니다. Dockerfile 내에서 빌드 및 런타임 중에 필요한 도구가 포함된 기본 이미지를 선택하십시오. 파일을 빌드 컨텍스트에서 이미지로 복사하고 명령을 실행하며 런타임 동작(예: 환경 변수, 노출된 포트)을 정의하고 ENTRYPOINT를 설정할 수 있습니다. ENTRYPOINT는 컨테이너가 시작될 때 호출되는 명령으로 설정됩니다.
도커파일 지침을 지정하는 방법에 대한 자세한 내용은 도커파일 참조를 참조하세요.
Code Engine 빌드에서 Git 저장소를 가리키는 소스를 정의하십시오. Docker 빌드에 사용할 수 있는 컨텍스트는 기본적으로 Git 저장소의 루트 디렉토리입니다. 예를 들어 저장소에 src라는 디렉토리가 있는 경우, Dockerfile에서 COPY 문을 사용하여 이 디렉토리를 이미지로 복사할 수 있습니다. 예를 들면 다음과 같습니다.
COPY src /app/src
전체 Git 저장소를 복사하려면 다음 COPY 문을 지정하십시오.
COPY . /app/src
Git 리포지토리 전체를 복사하지만 일부 파일(예: README.md 리포지토리)을 제외하려는 경우 .dockerignore 파일을 추가할 수 있습니다. 동일한 파일을 사용하여 .gitignore 파일에 지정한 파일과 디렉터리도 무시할 수 있습니다. 동일한 파일을 사용하면 로컬에서 실행하는 빌드에 Code Engine에서 실행 중인 빌드와 동일한 파일 세트를 사용할 수 있습니다.
운영 체제 파일과의 충돌을 피하기 위해 루트에 직접이 아니라 루트의 서브디렉토리(/)에 항상 애플리케이션 파일을 복사하십시오. 애플리케이션 디렉토리에 이름을 지정할 때 Unix 기반 운영 체제, Kubernetes 또는 Code Engine 빌드에서 예약된 이름을 사용하지 마십시오(예: /bin, /dev, /etc, /lib,
/proc, /run, /sys, /usr, /var 또는 /workspace). 애플리케이션 디렉토리 이름을 /app으로 지정하는 것이 가장 좋습니다.
소스 코드 저장소에 Code Engine 샘플 저장소와 유사하게 디렉터리로 구성된 여러 애플리케이션의 소스가 포함되어 있는 경우 하위 디렉터리를 컨텍스트로 사용할 수 있습니다. ibmcloud ce build create 명령에서 --context-dir 옵션을 사용하여 서브디렉토리를 지정하십시오.
Dockerfile이 컨텍스트 디렉토리에 없는 경우 --dockerfile 인수를 사용하여 해당 디렉토리를 지정할 수 있습니다.
Code Engine 에서 빌드하기 전에 로컬 시스템에서 Docker 빌드를 사용해 보려면 Docker 데스크톱을 사용하면 됩니다.
컨테이너 이미지 크기 줄이기
컨테이너 이미지의 크기를 줄이는 것은 여러 가지 측면에서 가치가 있습니다.
- 컨테이너 레지스트리에 이미지를 저장하는 데 필요한 공간이 줄어듭니다. 다른 이미지에 대한 할당량을 절약하고 비용을 절약할 수 있습니다.
- 더 작은 이미지가 훨씬 큰 이미지보다 더 빨리 컨테이너 레지스트리로 전송될 수 있기 때문에 빌드 실행을 완료하는 데 더 적은 시간이 필요한 경우도 있습니다. 이 경우에도 비용을 절약할 수 있습니다.
- 이미지를 가져오는 데 필요한 시간이 짧으므로 이미지를 사용하는 애플리케이션 또는 작업이 더 빠르게 시작됩니다. 이미지를 가져오는 동안 애플리케이션 또는 작업을 실행하는 데 필요한 리소스가 예약되므로, 마찬가지로 비용을 절약할 수 있습니다. 빠른 시작 시간은 애플리케이션이 0으로 스케일링 다운되더라도 사용자 요청에 대해 허용 가능한 응답 시간을 보장하기 때문에 Code Engine의 애플리케이션과 특히 관련이 있습니다.
빌드 크기를 줄이기 위한 몇 가지 모범 사례를 살펴보세요.
단일 RUN 문에서 여러 명령을 결합하여 이미지 크기 줄이기
이 예에서는 Node.js와 같은 소프트웨어를 컨테이너 이미지에 설치해야 합니다. Node.js용 기본 이미지를 사용하여 Node.js 애플리케이션을 빌드하십시오.
Node.js를 수동으로 설치하려면 다음 Dockerfile 예를 사용할 수 있습니다.
FROM ubuntu
RUN apt update
RUN apt upgrade -y
RUN apt install -y nodejs
RUN apt clean
RUN rm -rf /var/lib/apt/lists/\*
이 예에서는 여러 RUN 명령문을 사용하여 Node.js를 업데이트, 업그레이드, 설치하고 파일 캐시를 정리한 후 제거합니다. 이 일련의 명령문은 올바르지만, 여러 RUN 문을 사용하는 것이 이를 구현하는 가장 좋은 방법은 아닙니다. Dockerfile이 처리될 때 Dockerfile에 있는 각 명령문은 계층을 작성합니다. 각 계층에는 작성 또는 업데이트된 파일과 삭제된 항목에
대한 정보가 포함됩니다. 컨테이너 이미지는 모든 계층의 콜렉션입니다. 파일이 한 명령문에서 추가되고 두 번째 명령문에서 삭제된 경우, 결과 이미지는 첫 번째 명령문에 대한 계층에서는 해당 파일을 계속 포함하고 있으며 두 번째 계층에서는 삭제된 것으로 표시합니다.
디스크 공간을 절약하려면 단일 RUN 문을 지정하여 이 명령 세트에 대한 단일 계층을 작성하십시오.
FROM ubuntu
RUN apt update && apt upgrade -y && apt install -y nodejs && apt clean && rm -rf /var/lib/apt/lists/\*
한 개의 명령이 한 행에 표시되도록 읽기 가능한 Dockerfile을 유지하려면 코드에 행 바꾸기를 추가하십시오.
FROM ubuntu
RUN \
apt update && \
apt upgrade -y && \
apt install -y nodejs && \
apt clean && \
rm -rf /var/lib/apt/lists/\*
이 유형의 명령문을 사용하면 컨테이너 이미지 크기를 약 174MB에서 약 147MB로 줄일 수 있습니다. 예 크기는 Ubuntu 기본 이미지와 다를 수 있으며 Node.js 패키지가 변경될 수 있음을 참고하십시오.
이 유형의 우수 사례는 패키지 설치뿐만 아니라 유사한 태스크에도 적용됩니다. 다음 예에서 소프트웨어 패키지(Node.js)의 다운로드 및 추출은 다음 예와 유사할 수 있습니다.
FROM ubuntu
RUN apt update
RUN apt upgrade -y
RUN apt install -y curl
RUN curl https://nodejs.org/dist/v16.14.2/node-v16.14.2-linux-x64.tar.gz -o /tmp/nodejs.tar.gz
RUN mkdir /opt/node
RUN tar -xzf /tmp/nodejs.tar.gz -C /opt/node --strip 1
RUN rm /tmp/nodejs.tar.gz
RUN apt remove -y curl
RUN apt clean
RUN rm -rf /var/lib/apt/lists/\*
rm 명령은 다운로드한 nodejs 패키지를 제거하지만, 여러 RUN 문이 다시 별도의 계층을 작성합니다. 또한 nodejs 패키지를 다운로드하기 위해 curl 패키지가 임시로 설치됩니다. curl 패키지 자체는 나중에 제거되지만, 내재적으로 설치된 종속 항목은 계속 남아 있습니다.
더 나은 Dockerfile은 다음 예와 유사합니다.
FROM ubuntu
RUN \
apt update && \
apt upgrade -y && \
apt install -y curl && \
curl https://nodejs.org/dist/v16.14.2/node-v16.14.2-linux-x64.tar.gz -o /tmp/nodejs.tar.gz && \
mkdir /opt/node && \
tar -xzf /tmp/nodejs.tar.gz -C /opt/node --strip 1 && \
rm /tmp/nodejs.tar.gz && \
apt remove -y curl && \
apt auto-remove -y && \
apt clean && \
rm -rf /var/lib/apt/lists/\*
관련 명령이 단일 RUN 문에 결합되며 종속 항목을 제거하기 위해 apt auto-remove 명령이 추가됩니다. 이 예에서는 이미지 크기가 약 222MB에서 약 162MB로 줄어듭니다.
작은 기본 이미지 사용
이전 예에서는 Ubuntu를 기본 이미지로 사용합니다. 이 이미지에는 유용한 유틸리티가 많이 포함되어 있지만, 기본 이미지에 있는 유틸리티가 많을수록 이미지 크기가 더 커질 수 있습니다. 또한 더 많은 유틸리티를 포함시키면 보안 취약성이 발생할 가능성이 높아져 이미지를 다시 빌드해야 할 수 있습니다. 이러한 문제를 방지하려면 더 작은 기본 이미지를 사용하십시오. 예를 들면 다음과 같습니다.
-
Alpine은 작은 크기의 공식 Docker 이미지입니다. Java 또는 Node.js와 같은 프로그래밍 환경의 경우 Alpine을 기반으로 하는 태그를 종종 찾을 수 있습니다.
-
Google Container Tools의 Distroless 이미지에는 운영 체제 도구 가 전혀 포함되어 있지 않지만, Java 및 Node.js와 같은 다양한 언어에 필요한 런타임 환경이 있습니다.
program.js 파일에서 Node.js 프로그램을 실행하는 컨테이너 이미지를 빌드하여 이러한 기본 이미지를 비교하십시오. Ubuntu의 경우 Dockerfile은 다음 예와 유사합니다.
FROM ubuntu
RUN \
apt update && \
apt upgrade -y && \
apt install -y nodejs && \
apt clean && \
rm -rf /var/lib/apt/lists/\*
COPY program.js /app/program.js
WORKDIR /app
ENTRYPOINT ["node", "program.js"]
Alpine의 경우 Node.js에서 직접 이미지를 가져옵니다.
FROM node:16-alpine
COPY program.js /app/program.js
WORKDIR /app
ENTRYPOINT ["node", "program.js"]
distroless의 경우 다음 예를 사용하십시오.
FROM gcr.io/distroless/nodejs:16
COPY program.js /app/program.js
WORKDIR /app
CMD ["program.js"]
Ubuntu 기반 이미지는 147MB지만, Alpine 기반 이미지는 90MB이고 distroless 기반 이미지는 94MB입니다.
소스 및 빌드 도구를 포함하지 않고 이미지 크기 줄이기
이전 Node.js 기반 예에서는 단일 소스 파일이 컨테이너 이미지에 추가되었습니다. 이 예에서는 컴파일이 필요하지 않으므로 단일 소스 파일을 사용할 수 있습니다. 그러나 컴파일이 필요한 경우 빌드에 필요한 도구만 사용하되, 결과 이미지에는 포함시키지 마십시오. 예를 들어, Maven을 예로 사용하는 Java 애플리케이션을 지정하십시오. 잘못 코딩된 Dockerfile은 다음 예와 유사합니다.
FROM maven:3-jdk-11-openj9
WORKDIR /app
COPY . /app
RUN mvn package
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/target/java-application-1.0-SNAPSHOT-fat.jar"]
생성되는 컨테이너 이미지에는 Maven 빌드 도구뿐 아니라 Maven의 모든 소스 코드와 모든 중간 파일(예: 아티팩트 캐시, 나중에 JAR 파일로 패키징되는 클래스 파일)이 포함되어 있습니다. 또한 런타임 시 훨씬 더 작은 JRE(Java Runtime Environment)가 필요한 경우 JDK(Java Development Kit)도 포함됩니다. 결과적으로 이미지 크기는 466MB입니다.
이미지를 더 작게 만들려면 다단계 빌드라는 기능을 사용하십시오. Docker 빌드의 각 단계에는 자체 기본 이미지가 있으며 해당 단계에서 명령을 실행할 수 있습니다. 마지막으로, 이전 단계의 아티팩트를 복사하는 최종 이미지가 빌드됩니다. 소스 코드에서 컨테이너 이미지를 빌드하기 위한 일반적인 패턴은 다음 두 개의 단계를 거치는 것입니다.
- 소스 코드를 런타임용 2진으로 컴파일하는 데 필요한 모든 도구가 포함된 기본 이미지를 사용하는 빌더 단계.
- 2진을 실행하는 데 필요한 런타임 환경에서 기본 이미지를 사용하는 런타임 단계. 빌더 단계의 2진이 이 단계로 복사됩니다.
Maven 프로젝트의 경우 결과는 다음 예와 유사합니다.
FROM maven:3-jdk-11-openj9 AS builder
WORKDIR /app
COPY . /app
RUN mvn package
FROM adoptopenjdk:11-jre-openj9
WORKDIR /app
COPY --from=builder /app/target/java-application-1.0-SNAPSHOT-fat.jar /app/java-application.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/java-application.jar"]
이 예에서는 빌더 단계에서 Maven 기본 이미지를 사용하여 애플리케이션의 JAR 파일을 빌드합니다. 런타임 단계에서는 더 작은 JRE 기본 이미지를 사용합니다. COPY --from=STAGE_NAME 명령을 사용하여 JAR 파일이 빌더 단계에서 런타임 단계로 복사됩니다. 생성되는 이미지의 크기는 242MB로, 약 48% 절감됩니다.
이 패턴은 컴파일을 수행해야 하는 다른 프로그래밍 언어에도 사용할 수 있습니다.
- 빌드가 필요한 노드 애플리케이션(예: Angular 또는 React 빌드). 여기서 빌더와 런타임 기본 이미지는 동일할 수 있지만(두 노드 모두), 모든 빌드 시간 아티팩트와 소스를 런타임 이미지로 복사해야 하는 것은 아닙니다.
- 소스 코드를 런타임 환경 없이 실행되는 기본 실행 파일로 컴파일하는 모든 프로그래밍 언어(예: Go 또는 Rust)
기본 실행 파일을 생성하며 런타임 환경이 전혀 필요하지 않은 언어의 경우 런타임 단계에 다른 Docker 기능(스크래치)을 사용하십시오. 스크래치는 FROM 명령의 기반으로 사용될 수 있지만, 최종 컨테이너 이미지는 아닙니다. 대신 Docker 빌드에서 기본 이미지가 사용되지 않도록 합니다. 기본 이미지에서 운영 체제 파일 없이 결과 이미지가 단일 파일(빌더 단계에서 복사된
2진 파일)만큼 작을 수 있습니다. 프로그래밍 언어 및 코드에 따라, 2진 파일이 존재하는 일부 운영 체제 파일에 의존할 수 있으므로 컴파일러 옵션을 추가로 조정해야 할 수도 있습니다.
이미지를 깨끗하게 유지하기
Dockerfile을 처음 개발하고 작동 방식을 디버깅할 때 몇 가지 임시 도구를 설치하거나 최종 컨테이너 이미지에 필요하지 않은 다른 워크로드를 적용했을 수 있습니다. 이러한 임시 파일과 워크로드를 제거하면 이미지가 작고 깨끗하며 더 안전한 상태로 유지됩니다.
이미지의 시작 시간 개선
효율성을 극대화하려면 컨테이너 이미지가 가능한 빨리 시작되어야 합니다. 애플리케이션의 경우 관련 메트릭은 HTTP 엔드포인트가 사용 가능해지고 수신 HTTP 요청을 승인하고 처리할 준비가 되는 데 걸리는 시간입니다. 시작 속도는 Knative를 기반으로 하는 Code Engine 애플리케이션에 특히 중요합니다. 이러한 애플리케이션은 트래픽이 없을 때 실행 중인 인스턴스를 0으로 스케일링 다운하도록 구성할 수 있으므로 리소스와 비용이 들지 않습니다. 요청이 들어오면 요청을 처리하기 위해 인스턴스가 시작됩니다. 새 요청에 응답하려면 컨테이너가 최대한 빨리 시작되어야 합니다.
애플리케이션의 시작을 개선하려면 애플리케이션 구현을 조사하여 다음과 같은 패턴을 적용할 수 있는지 확인하십시오.
- 데이터베이스에 대한 연결을 설정하고 환경 변수에서 메일 서버와 통신하기 위해 구성 파일을 읽는 독립된 초기화 작업의 병렬화
- 애플리케이션 시작에 필요하지 않은 초기화 작업을 지연시키는 대신 필요에 따라 먼저 수행
또한 Angular, React 또는 Vue 등의 프레임워크를 사용하는 웹 애플리케이션을 구현하는 경우에도 일반적인 위험을 방지할 수 있습니다. 이러한 각 프레임워크는 NPM이 있는 Node.js를 기반으로 하며, 프로젝트를 쉽게 설정할 수 있는 명령행 인터페이스가 포함되어 있습니다. 예를 들어, create-react-app 명령을 사용하여 작성되는 React 애플리케이션은 사전 정의된 몇 개의 스크립트가 포함되어 있는 package.json 파일을 설정합니다. 이러한 스크립트 중 하나는 웹 애플리케이션과 함께 웹 서버를 불러오는 start입니다. Dockerfile은 다음 예와 유사할 수 있습니다.
FROM nodejs:16-alpine
COPY . /app
WORKDIR /app
RUN npm install
EXPOSE 3000
ENTRYPOINT ["npm", "run", "start"]
이 유형의 Dockerfile은 작동하지만 npm run start 명령이 호출될 때 항상 빠르지는 않으며, 애플리케이션이 컴파일된 다음 시작됩니다. 이러한 지연은 작은 샘플 크기를 초과하는 애플리케이션에서 특히 두드러집니다. 올바른 접근 방식은 빌드 시 애플리케이션을 컴파일하고 시작 시에만 제공하는 것입니다.
FROM node:16-alpine AS builder
COPY . /app
WORKDIR /app
RUN npm install && npm run build
FROM node:16-alpine
RUN npm install -g serve
COPY --from=builder /app/build /app
EXPOSE 8080
ENTRYPOINT [ "serve", "--single", "--no-clipboard", "--listen", "8080", "/app" ]
빌더 및 런타임 2단계 패턴을 다시 확인할 수 있습니다. 또한 업데이트된 샘플은 다른 포트(8080)를 사용합니다. 이 예에서는 다른 포트에서 작동하지만, 8080은 Code Engine 애플리케이션의 기본 포트입니다. 또한 컴파일된 빌드를 사용하면 node_modules에 설치된 모든 소스와 도구가 최종 컨테이너 이미지에 포함되지 않으므로 크기가 281 -
97MB로 줄어듭니다.
루트가 아닌 사용자로 컨테이너 실행
잘 설계된 시스템은 최소 권한 원칙을 따릅니다. 즉, 애플리케이션 또는 사용자는 특정 조치를 수행하는 데 필요한 권한만 얻습니다. Code Engine에서는 일반적으로 시스템에 대한 관리 액세스가 필요하지 않은 애플리케이션 서버 또는 일부 일괄처리 로직을 실행합니다. 따라서 컨테이너에서 루트로 실행하면 안됩니다. 정의된 사용자로 컨테이너 이미지를 설정하고 루트가 아닌 사용자로 실행하는 것이 좋습니다. 예를 들어, 이전 시나리오에 기반하여 다음을 수행하십시오.
FROM node:16-alpine AS builder
COPY . /app
WORKDIR /app
RUN npm install && npm run build
FROM node:16-alpine
RUN npm install -g serve
COPY --from=builder /app/build /app
USER 1100:1100
EXPOSE 8080
ENTRYPOINT [ "serve", "--single", "--no-clipboard", "--listen", "8080", "/app" ]
Dockerfile은 USER 명령을 사용하여 사용자 및 그룹 1100으로 실행되도록 지정합니다. 이 명령은 이름 지정된 사용자 및 그룹을 컨테이너 이미지에 내재적으로 작성하지 않습니다. 일반적으로 이 구조는 허용되지만 애플리케이션 로직에 사용자 및 해당 홈 디렉토리가 있어야 하는 경우에는 사용자 및 그룹을 명시적으로 작성해야 합니다.
FROM node:16-alpine AS builder
COPY . /app
WORKDIR /app
RUN npm install && npm run build
FROM node:16-alpine
RUN npm install -g serve && \
addgroup nonroot --gid 1100 && \
adduser nonroot --ingroup nonroot --uid 1100 --home /home/nonroot --disabled-password
COPY --from=builder /app/build /app
USER 1100:1100
EXPOSE 8080
ENTRYPOINT [ "serve", "--single", "--no-clipboard", "--listen", "8080", "/app" ]
RUN 및 addgroup 명령을 호출하여 홈 디렉토리가 있는 그룹 및 사용자를 작성하도록 런타임 단계의 adduser 명령이 확장되었습니다.