4장. 이미지 생성
사용자를 지원하도록 준비되어 있는 미리 빌드된 이미지를 기반으로 자체 컨테이너 이미지를 생성하는 방법에 대해 알아봅니다. 이 프로세스에는 이미지 작성 모범 사례 학습, 이미지 메타데이터 정의, 이미지 테스트, 사용자 정의 빌더 워크플로를 사용하여 AWS에서 Red Hat OpenShift Service와 함께 사용할 이미지를 생성하는 작업이 포함됩니다.
4.1. 컨테이너 모범 사례 학습
AWS의 Red Hat OpenShift Service에서 실행할 컨테이너 이미지를 생성할 때 해당 이미지 사용자에게 좋은 환경을 제공하기 위해 이미지 작성자로 고려해야 할 여러 가지 모범 사례가 있습니다. 이미지는 변경 불가능하고 그대로 사용하기 위한 것이므로 다음 지침을 통해 이미지는 높은 사용 가능하고 AWS의 Red Hat OpenShift Service에서 쉽게 사용할 수 있습니다.
4.1.1. 일반 컨테이너 이미지 지침
다음 지침은 일반적으로 컨테이너 이미지를 생성할 때 적용되며, 이미지가 AWS의 Red Hat OpenShift Service에서 사용되는지 여부와 관계없이 독립적입니다.
이미지 재사용
가능한 경우 이미지는 FROM
문을 사용하는 적절한 업스트림 이미지를 기반으로 합니다. 이렇게 하면 이미지가 업데이트되는 경우 사용자가 직접 종속성을 업데이트할 필요 없이 이미지가 업스트림 이미지의 보안 수정 사항을 쉽게 찾을 수 있습니다.
또한 FROM
명령어에 태그 (예: rhel:rhel7
)를 사용하여 해당 이미지의 기반이 되는 이미지 버전을 사용자에게 정확히 알려 주십시오. latest
이외의 태그를 사용하면 손상을 유발하는 변경사항이 이미지에 적용되어 latest
버전의 업스트림 이미지에 포함되는 일이 없습니다.
태그 내에서 호환성 유지보수
자체 이미지를 태그하는 경우 태그 내에서 이전 버전과의 호환성을 유지보수합니다. 예를 들어 image
라는 이미지를 제공하고 현재 버전 1.0
이 포함된 경우 image:v1
태그를 제공할 수 있습니다. 이미지를 업데이트하면 원래 이미지와 계속 호환되는 한 새 이미지 이미지 ( v1
) 및 이 태그의 다운스트림 소비자는 손상 없이 업데이트를 가져올 수 있습니다.
나중에 호환되지 않는 업데이트를 릴리스하는 경우 새 태그(예: image:v2
)로 전환합니다. 이렇게 하면 다운스트림 사용자가 마음대로 새 버전으로 이동하면서도 호환되지 않는 새 이미지로 인해 의도치 않게 손상되는 일이 없게 할 수 있습니다. image:latest
를 사용하는 모든 다운스트림 사용자는 호환되지 않는 변경 사항이 도입될 위험이 있습니다.
여러 프로세스 방지
하나의 컨테이너에서 데이터베이스 및 SSHD
같은 서비스를 여러 개 시작하지 마십시오. 컨테이너는 간단하며 쉽게 서로 연결하여 여러 프로세스를 오케스트레이션할 수 있으므로 그렇게 할 필요가 없습니다. AWS의 Red Hat OpenShift Service를 사용하면 관련 이미지를 단일 Pod로 그룹화하여 쉽게 공동 배치 및 공동 관리할 수 있습니다.
컨테이너는 이러한 공동 배치를 통해 통신에 필요한 네트워크 네임스페이스 및 스토리지를 공유할 수 있습니다. 각 이미지를 덜 자주, 독립적으로 업데이트할 수 있으므로 업데이트에서도 중단이 덜 발생합니다. 생성된 프로세스에 대한 라우팅 신호를 관리할 필요가 없으므로 단일 프로세스를 사용하면 신호 처리 흐름이 더욱 명확해집니다.
래퍼 스크립트에 exec
사용
많은 이미지에서는 소프트웨어 실행을 위한 프로세스를 시작하기 전에 래퍼 스크립트를 사용하여 설정을 수행합니다. 이미지에서 이러한 스크립트를 사용하는 경우 해당 스크립트는 exec
를 사용하여 소프트웨어가 스크립트 프로세스를 교체하도록 합니다. exec
를 사용하지 않으면 컨테이너 런타임에서 보낸 신호가 소프트웨어 프로세스가 아닌 래퍼 스크립트로 갑니다. 이는 원하는 작동이 아닙니다.
어떤 서버의 프로세스를 시작하는 래퍼 스크립트가 있는 경우입니다. 예를 들어 podman run -i
를 사용하여 컨테이너를 시작하여 래퍼 스크립트를 실행하면 프로세스가 시작됩니다. CTRL+C
를 사용하여 컨테이너를 종료하려면 다음을 수행합니다. 래퍼 스크립트에서 exec
를 사용하여 서버 프로세스를 시작한 경우 podman
은 SIGINT를 서버 프로세스로 보내며 모든 작업이 예상대로 수행됩니다. 래퍼 스크립트에서 exec
를 사용하지 않은 경우 podman
은 래퍼 스크립트의 프로세스로 SIGINT를 보내며 프로세스는 아무 일도 없던 것처럼 계속 실행됩니다.
또한 프로세스는 컨테이너에서 실행되는 경우 PID 1
로 실행됩니다. 즉, 기본 프로세스가 종료되면 전체 컨테이너가 중지되어 PID 1
프로세스에서 시작한 하위 프로세스가 모두 취소됩니다.
임시 파일 정리
빌드 프로세스 중에 생성한 임시 파일을 모두 제거합니다. 이러한 파일에는 ADD
명령으로 추가된 파일도 포함됩니다. 예를 들어 yum install
작업을 수행한 후에는 yum clean
명령을 실행합니다.
다음과 같이 RUN
문을 생성하면 yum
캐시가 이미지 계층에 생성되는 것을 방지할 수 있습니다.
RUN yum -y install mypackage && yum -y install myotherpackage && yum clean all -y
이 문을 다음과 같이 작성할 수도 있습니다.
RUN yum -y install mypackage RUN yum -y install myotherpackage && yum clean all -y
그러면 첫 번째 yum
호출에서 해당 계층에 추가 파일을 남기는데 나중에 yum clean
작업을 실행해도 해당 파일을 제거할 수 없습니다. 추가 파일이 최종 이미지에 표시되지 않아도 기본 계층에 남아 있습니다.
현재 컨테이너 빌드 프로세스에서는 이전 계층에서 어떤 항목을 제거한 경우에도 이후 계층에서 명령을 실행하여 이미지가 사용하는 공간을 줄일 수 없습니다. 향후에는 변경될 수도 있습니다. 즉, 이후 계층에서 rm
명령을 수행하는 경우 파일이 숨겨져 있어도 다운로드할 이미지의 전체 크기가 줄어들지 않습니다. 따라서 yum clean
예에서처럼 가능한 경우 파일을 생성한 명령과 동일한 명령에서 파일을 제거하여 계층에 쓰이지 않도록 하는 것이 가장 좋습니다.
또한 단일 RUN
문에서 여러 명령을 수행하면 이미지의 계층 수가 줄어들어 다운로드 및 추출 시간이 단축됩니다.
적절한 순서로 명령어 배치
컨테이너 빌더에서는 Dockerfile
을 읽고 맨 위부터 아래로 명령어를 실행합니다. 성공적으로 실행되는 모든 명령은 다음에 이 이미지 또는 다른 이미지가 빌드될 때 재사용할 수 있는 계층을 생성합니다. Dockerfile
의 맨 위에는 거의 변경되지 않을 명령어를 배치하는 것이 매우 중요합니다. 이렇게 하면 상위 계층 변경에 의해 캐시가 무효화되지 않으므로 다음에 동일한 이미지를 매우 빠르게 빌드할 수 있습니다.
예를 들어 반복할 파일을 설치하는 ADD
명령과 패키지를 yum install
하는 RUN
명령이 포함된 Dockerfile
에서 작업하는 경우 다음과 같이 ADD
명령을 마지막에 배치하는 것이 가장 좋습니다.
FROM foo RUN yum -y install mypackage && yum clean all -y ADD myfile /test/myfile
이렇게 하면 myfile
을 편집하고 podman build
또는 docker build
를 다시 실행할 때마다 시스템은 yum
명령에는 캐시된 계층을 재사용하고 ADD
작업에서만 새 계층을 생성합니다.
그러지 않고 다음과 같이 Dockerfile
을 작성할 수 있습니다.
FROM foo ADD myfile /test/myfile RUN yum -y install mypackage && yum clean all -y
그러면 myfile
을 변경하고 podman build
또는 docker build
를 다시 실행할 때마다 ADD
작업으로 RUN
계층 캐시가 무효화되어 yum
작업도 다시 실행해야 합니다.
중요한 포트 표시
EXPOSE 명령어는 컨테이너의 포트를 호스트 시스템 및 다른 컨테이너에서 사용할 수 있도록 합니다. podman run
호출을 통해 포트가 노출되도록 지정할 수 있으나 Dockerfile
에 EXPOSE 명령어를 사용하면 소프트웨어를 실행하는 데 필요한 포트를 명시적으로 선언하여 사용자 및 소프트웨어 둘 다 더욱 쉽게 이미지를 사용할 수 있습니다.
-
노출된 포트는 이미지에서 생성된 컨테이너와 관련 있는
podman ps
아래에 표시됩니다. -
노출된 포트는
podman inspect
에서 반환된 이미지의 메타데이터에도 있습니다. - 한 컨테이너를 다른 컨테이너에 연결하면 노출된 포트가 연결됩니다.
환경 변수 설정
ENV
명령어로 환경 변수를 설정하는 것이 좋습니다. 한 예로 프로젝트 버전 설정이 있습니다. 이렇게 하면 사용자가 Dockerfile
을 보지 않고도 쉽게 버전을 찾을 수 있습니다. 또 다른 예로는 JAVA_HOME
같은 다른 프로세스에서 사용할 수 있는 시스템의 경로 알림이 있습니다.
기본 암호 설정 방지
기본 암호를 설정하지 않도록 합니다. 많은 사용자가 이미지를 확장한 후 기본 암호를 제거하거나 변경하는 것을 잊어 버립니다. 이렇게 되면 프로덕션 단계의 사용자에게 잘 알려진 암호가 할당되는 경우 보안 문제로 이어질 수 있습니다. 암호는 환경 변수를 사용하여 구성할 수 있습니다.
기본 암호를 설정하도록 선택하는 경우 컨테이너가 시작될 때 적절한 경고 메시지가 표시되도록 해야 합니다. 이 메시지에서는 사용자에게 기본 암호 값을 알려주고 설정할 환경 변수와 같은 암호 변경 방법을 설명해야 합니다.
sshd 실행 방지
이미지에서는 sshd
를 실행하지 않는 것이 가장 좋습니다. podman exec
또는 docker exec
명령을 사용하여 로컬 호스트에서 실행 중인 컨테이너에 액세스할 수 있습니다. 또는 oc exec
명령 또는 oc rsh
명령을 사용하여 AWS 클러스터의 Red Hat OpenShift Service에서 실행 중인 컨테이너에 액세스할 수 있습니다. 이미지에서 sshd
를 설치하고 실행하면 추가적인 공격 벡터와 보안 패치 요구 사항이 발생합니다.
영구 데이터 볼륨 사용
이미지에서는 영구 데이터 볼륨을 사용합니다. 이렇게 하면 AWS의 Red Hat OpenShift Service가 컨테이너를 실행하는 노드에 네트워크 스토리지를 마운트하고 컨테이너가 새 노드로 이동하면 해당 노드에 스토리지가 다시 연결됩니다. 모든 영구 스토리지 요구에 이 볼륨을 사용하면 컨테이너를 다시 시작하거나 이동해도 콘텐츠가 보존됩니다. 이미지가 컨테이너 내 임의의 위치에 데이터를 쓰는 경우 해당 콘텐츠를 보존할 수 없습니다.
컨테이너가 삭제된 후에도 보존해야 하는 데이터는 모두 볼륨에 써야 합니다. 컨테이너 엔진은 컨테이너의 ephemeral 스토리지에 데이터를 쓰지 않는 모범 사례를 엄격하게 적용하기 위해 사용할 수 있는 readonly
플래그를 컨테이너에서 지원합니다. 지금 이 기능을 기반으로 이미지를 설계하면 나중에 더욱 쉽게 이미지를 활용할 수 있습니다.
Dockerfile
에서 명시적으로 볼륨을 정의하면 이미지 사용자가 이미지를 실행할 때 정의해야 하는 볼륨을 쉽게 파악할 수 있습니다.
AWS의 Red Hat OpenShift Service에서 볼륨을 사용하는 방법에 대한 자세한 내용은 Kubernetes 문서를 참조하십시오.
영구 볼륨을 사용하더라도 이미지의 각 인스턴스에는 자체 볼륨이 있으며 파일 시스템은 인스턴스 간에 공유되지 않습니다. 즉, 볼륨은 클러스터에서 상태를 공유하는 데 사용할 수 없습니다.
4.1.2. AWS 관련 지침의 Red Hat OpenShift Service
다음은 특히 AWS의 Red Hat OpenShift Service에서 사용할 컨테이너 이미지를 생성할 때 적용되는 지침입니다.
4.1.2.1. S2I(source-to-image)에 대해 이미지 활성화
타사에서 제공한 애플리케이션 코드를 실행하도록 하려는 이미지(예: 개발자가 제공한 Ruby 코드를 실행하도록 설계된 Ruby 이미지)의 경우 이미지를 활성화하여 S2I(Source-to-Image) 빌드 도구로 작업할 수 있습니다. S2I는 입력으로 애플리케이션 소스 코드를 사용하는 이미지를 쓰고 출력으로 어셈블된 애플리케이션을 실행하는 새 이미지를 생성하는 작업을 쉽게 수행할 수 있도록 하는 프레임워크입니다.
4.1.2.2. 임의의 사용자 ID 지원
기본적으로 AWS의 Red Hat OpenShift Service는 임의로 할당된 사용자 ID를 사용하여 컨테이너를 실행합니다. 이렇게 하면 컨테이너 엔진 취약점으로 인해 컨테이너를 벗어나는 프로세스에 대해 추가적인 보안이 제공되므로 호스트 노드에서의 권한이 에스컬레이션됩니다.
이미지에서 임의의 사용자로 실행하도록 지원하려면 이미지의 프로세스에 의해 쓰일 수 있는 디렉토리와 파일을 루트 그룹에서 소유해야 하며 해당 그룹에서 읽을 수 있고 쓸 수 있어야 합니다. 실행될 파일에도 그룹 실행 권한이 있어야 합니다.
Dockerfile에 다음을 추가하면 루트 그룹의 사용자가 빌드 된 이미지의 디렉토리 및 파일에 액세스할 수 있도록 디렉토리 및 파일 권한이 설정됩니다.
RUN chgrp -R 0 /some/directory && \ chmod -R g=u /some/directory
컨테이너 사용자는 항상 루트 그룹의 멤버이므로 컨테이너 사용자는 이러한 파일을 읽고 쓸 수 있습니다.
컨테이너의 중요한 영역에 대한 디렉터리 및 파일 권한을 변경할 때는 주의해야 합니다. /etc/passwd
파일과 같은 민감한 영역에 적용되는 경우 이러한 변경으로 인해 의도하지 않은 사용자가 이러한 파일을 수정하여 컨테이너 또는 호스트가 노출될 수 있습니다. CRI-O는 컨테이너의 /etc/passwd
파일에 임의의 사용자 ID를 삽입할 수 있도록 지원합니다. 따라서 권한을 변경할 필요가 없습니다.
또한 컨테이너 이미지에 /etc/passwd
파일이 없어야 합니다. 이 경우 CRI-O 컨테이너 런타임에서 임의의 UID를 /etc/passwd
파일에 삽입하지 못합니다. 이러한 경우 컨테이너는 활성 UID를 해결하는 데 문제가 발생할 수 있습니다. 이 요구 사항을 충족하지 못하면 컨테이너화된 특정 애플리케이션의 기능에 영향을 미칠 수 있습니다.
또한, 컨테이너에서 실행되는 프로세스는 권한이 있는 사용자로 실행되지 않으므로 권한이 있는 포트(1024 미만의 포트)에서 수신 대기해서는 안 됩니다.
4.1.2.3. 이미지 간 통신을 위해 서비스 사용
데이터베이스 이미지에 액세스하여 데이터를 저장하고 검색해야 하는 웹 프런트 엔드 이미지와 같이 이미지가 다른 이미지에서 제공하는 서비스와 통신해야 하는 경우 이미지는 AWS 서비스에서 Red Hat OpenShift Service를 사용합니다. 서비스에서는 컨테이너가 중지되거나, 시작되거나, 이동될 때 액세스가 변경되지 않도록 정적 끝점을 제공합니다. 요청에 대한 부하 분산도 서비스에서 제공합니다.
4.1.2.4. 공통 라이브러리 제공
타사에서 제공한 애플리케이션 코드를 실행하기 위한 이미지의 경우 해당 플랫폼에 일반적으로 사용되는 라이브러리를 이미지에 포함해야 합니다. 특히, 해당 플랫폼과 함께 사용되는 공통 데이터베이스에 필요한 데이터베이스 드라이버를 제공하십시오. 예를 들어 Java 프레임워크 이미지를 생성하는 경우 MySQL 및 PostgreSQL용 JDBC 드라이버를 제공하십시오. 이렇게 하면 애플리케이션 어셈블리 시간 동안 공통 종속성을 다운로드할 필요가 없으므로 애플리케이션 이미지 빌드 속도가 빨라집니다. 애플리케이션 개발자가 모든 종속성이 충족되는지 확인하는 데 필요한 작업도 간소화됩니다.
4.1.2.5. 구성에 환경 변수 사용
이미지 사용자는 이미지를 기반으로 다운스트림 이미지를 생성하지 않고도 이미지를 구성할 수 있어야 합니다. 즉, 환경 변수를 사용하여 런타임 구성을 처리해야 합니다. 간단한 구성의 경우 실행 중인 프로세스에서 직접 환경 변수를 사용할 수 있습니다. 보다 복잡한 구성 또는 이 방법을 지원하지 않는 런타임의 경우 시작 시 처리되는 템플릿 구성 파일을 정의하여 런타임을 구성하십시오. 이러한 처리에서는 환경 변수를 통해 제공되는 값이 구성 파일로 대체되거나 구성 파일에서 설정할 옵션을 결정하는 데 사용될 수 있습니다.
또한, 환경 변수를 사용하여 인증서 및 키와 같은 시크릿을 컨테이너에 전달할 수 있으며 이 방법을 권장합니다. 이렇게 하면 시크릿 값이 이미지에 커밋되어 컨테이너 이미지 레지스트리로 유출되지 않습니다.
환경 변수를 제공하면 이미지 사용자가 이미지 위에 새 계층을 도입하지 않고도 데이터베이스 설정, 암호 및 성능 튜닝과 같은 동작을 사용자 정의할 수 있습니다. 대신, pod를 정의할 때 환경 변수 값을 정의하고 이미지를 다시 빌드하지 않고도 해당 설정을 변경할 수 있습니다.
매우 복잡한 시나리오의 경우 런타임 시 컨테이너에 마운트되는 볼륨을 사용하여 구성을 제공할 수도 있습니다. 하지만 이 방법으로 작업을 수행하도록 선택하는 경우 필요한 볼륨 또는 구성이 없으면 시작할 때 이미지에서 명확한 오류 메시지를 제공하도록 해야 합니다.
이 주제는 데이터 소스와 같은 구성이 서비스 끝점 정보를 제공하는 환경 변수의 관점에서 정의되어야 한다는 내용의 이미지 간 통신에 서비스 사용 주제와 관련이 있습니다. 이를 통해 애플리케이션은 애플리케이션 이미지를 수정하지 않고 AWS 환경의 Red Hat OpenShift Service에 정의된 데이터 소스 서비스를 동적으로 사용할 수 있습니다.
또한, 컨테이너의 cgroups
설정을 검사하여 튜닝을 수행해야 합니다. 그러면 이미지가 사용 가능한 메모리, CPU 및 기타 리소스에 맞게 자체적으로 튜닝됩니다. 예를 들어 Java 기반 이미지는 cgroup
최대 메모리 매개변수를 기반으로 힙을 튜닝하여 이미지가 제한을 초과하지 않고 메모리 부족 오류가 발생하지 않도록 합니다.
4.1.2.6. 이미지 메타데이터 설정
이미지 메타데이터를 정의하면 AWS의 Red Hat OpenShift Service가 컨테이너 이미지를 더 잘 사용할 수 있으므로 AWS의 Red Hat OpenShift Service가 이미지를 사용하는 개발자에게 더 나은 환경을 제공할 수 있습니다. 예를 들어 이미지에 대한 유용한 설명을 제공하거나 기타 필요한 이미지를 제안하는 메타데이터를 추가할 수 있습니다.
4.1.2.7. 클러스터링
이미지의 여러 인스턴스를 실행한다는 것이 어떤 의미인지를 완전하게 이해하고 있어야 합니다. 가장 간단한 사례로, 서비스의 부하 분산 기능이 이미지의 모든 인스턴스에 대한 라우팅 트래픽을 처리하는 것을 들 수 있습니다. 하지만 세션 복제에서와 같이 리더 선택을 수행하거나 상태를 장애 조치하려면 많은 프레임워크에서 정보를 공유해야 합니다.
AWS의 Red Hat OpenShift Service에서 실행할 때 인스턴스가 이 통신을 수행하는 방법을 고려하십시오. pod는 서로 직접 통신할 수 있지만 pod가 시작되거나, 중지되거나, 이동될 때마다 해당 IP 주소는 변경됩니다. 따라서 클러스터링 스키마가 동적인 것이 중요합니다.
4.1.2.8. 로깅
로깅은 모두 표준 출력으로 보내는 것이 가장 좋습니다. Red Hat OpenShift Service on AWS는 컨테이너에서 표준을 수집하여 볼 수 있는 중앙 집중식 로깅 서비스로 보냅니다. 로그 콘텐츠를 분리해야 하는 경우 출력에 적절한 키워드 접두사를 붙여 메시지를 필터링할 수 있도록 합니다.
이미지가 파일에 로깅되면 사용자는 수동 작업을 통해 실행 중인 컨테이너에 들어가서 로그 파일을 검색하거나 확인해야 합니다.
4.1.2.9. liveness 및 readiness 프로브
이미지와 함께 사용할 수 있는 liveness 및 readiness 프로브 예를 문서화하십시오. 사용자는 이러한 프로브를 통해 컨테이너에서 트래픽을 처리할 준비가 될 때까지 트래픽이 컨테이너로 라우팅되지 않으며 프로세스가 비정상 상태가 되면 컨테이너가 다시 시작될 것이라는 확신을 가지고 이미지를 배포할 수 있습니다.
4.1.2.10. Templates
이미지와 함께 템플릿 예를 제공하십시오. 템플릿은 사용자가 정상적으로 작동하는 구성을 통해 이미지를 빠르게 배포할 수 있는 간편한 방법입니다. 완전성을 갖추려면 문서화한 liveness 및 readiness 프로브가 이미지와 함께 템플릿에 포함되어야 합니다.