프로젝트 관리 심화
Updated:
Docker
Docker는 애플리케이션을 쉽게 만들고, 테스트하고, 배포할 수 있게 도와주는 소프트웨어 플랫폼으로, Docker image는 애플리케이션을 실행하는 데 필요한 모든 것을 포함한다. 주요 특징은 다음과 같다.
- 컨테이너화: 애플리케이션과 필요한 모든 것을 하나의 패키지로 묶어 어디서든 실행할 수 있음
- 경량: Docker는 운영 체제의 커널을 공유하므로, 가상 머신보다 훨씬 가볍고 빠르게 실행됨
- 이식성: Docker 컨테이너는 어디서든 동일하게 실행됨
- 확장성: Docker를 사용하면 여러 개의 컨테이너를 효율적으로 관리하고 쉽게 확장할 수 있음
주요 키워드는 다음과 같다.
- Image: 애플리케이션과 모든 실행에 필요한 파일을 포함한 읽기 전용 템플릿
- Container: 이미지를 실행하여 동작하는 애플리케이션 인스턴스
- Dockerfile: 이미지를 생성하기 위한 명령어가 담긴 스크립트 파일
- Docker Hub: 이미지를 저장하고 공유하는 중앙 저장소
- Volume: 컨테이너 데이터를 지속적으로 저장하는 메커니즘
- Network: 컨테이너 간의 통신을 관리하는 방식
- Bridge Network
- 기본적으로 Docker가 컨테이너를 실행할 때 사용하는 네트워크
- 동일한 브리지 네트워크에 연결된 컨테이너들은 서로 통신할 수 있음
- 외부 네트워크와는 NAT (내부 네트워크의 여러 장치가 하나의 공용 IP 주소를 통해 외부 네트워크와 통신할 수 있도록 IP 주소를 변환하는 기술) 를 통해 통신
- 일반적으로 단일 호스트에서 여러 컨테이너를 연결할 때 사용
- 명시하지 않으면 모두 브리지 네트워크에서 실행
- Host Network
- 컨테이너가 호스트의 네트워크 스택을 직접 사용
- 네트워크 격리가 없기 때문에 성능상 이점이 있지만, 보안 및 네트워크 충돌 위험이 있음
- 일반적으로 성능이 중요한 애플리케이션에 사용
- Overlay Network
- 여러 Docker 호스트에 걸쳐 있는 컨테이너를 연결할 때 사용
- Swarm 모드(Docker 컨테이너의 오케스트레이션과 클러스터링을 지원하여 여러 호스트에서 컨테이너를 관리하고 배포할 수 있는 기능)나 Kubernetes 같은 오케스트레이션 도구와 함께 사용
- 데이터 센터 또는 클라우드 환경에서 분산 시스템을 구축할 때 유용
- Bridge Network
Docker는 다음과 같은 상황에서 사용하면 좋다.
- 일관된 개발 환경이 필요할 때
- 애플리케이션을 빠르게 배포하고 싶을 때
- 마이크로서비스 아키텍처를 도입할 때
- CI/CD 파이프라인을 구축할 때
- 리소스 효율성을 높이고 싶을 때
- 애플리케이션 격리가 필요할 때
- 쉽게 스케일링하고 싶을 때
- Kubernetes와 함께 사용하고자 할 때
Docker 명령어
Image와 관련된 명령어는 다음과 같다.
- 이미지 빌드
docker build -t myapp:latest .- 현재 디렉토리의 Dockerfile을 기반으로 myapp이라는 이름의 이미지를 생성
- -t 옵션을 사용하여 이미지의 이름과 태그를 입력 할 수 있음
- 이미지 가져오기
docker pull postgres- 도커 허브에서 해당 이미지를 가져옴
- 이미지 목록 보기
docker images- 현재 로컬에 저장된 Docker 이미지를 목록으로 표시
- 이미지 삭제
docker rmi myapp:latest- myapp:latest 이미지를 로컬 저장소에서 삭제
Docker Container와 관련된 명령어는 다음과 같다. 참고로 컨테이너 아이디는 모두 작성할 필요없이 식별 가능한 자릿수까지만 입력해도 된다.
- 컨테이너 실행
docker run -d -p 8080:80 myapp:latest- myapp:latest 이미지를 사용하여 컨테이너를 실행
- -d (detached mode) 옵션은 백그라운드에서 실행. 터미널을 컨테이너에 붙잡히지 않고, 컨테이너가 백그라운드에서 계속 실행
- -p 옵션은 호스트의 8080 포트를 컨테이너의 80 포트에 매핑
- 컨테이너 내부 접속
docker exec -it 컨테이너_아이디 /bin/bash- -i (interactive): 컨테이너의 표준 입력(STDIN)을 열어둠
- 컨테이너 내부에서 사용자 입력을 받을 수 있음
- -t (tty): 가상 터미널을 할당
- 컨테이너 내부에서 터미널을 사용할 수 있음
- 실행 중인 컨테이너 목록 보기
docker ps- 현재 실행 중인 컨테이너의 목록을 표시
- 모든 컨테이너 목록 보기
docker ps -adocker ps -al: 중지된 컨테이너를 포함한 모든 컨테이너의 목록을 표시- 마지막으로 실행된 컨테이너를 가장 먼저 나열
- 컨테이너 중지
docker stop container_id- 지정된 container_id를 가진 컨테이너를 중지
- 컨테이너 시작
docker start container_id- 중지된 컨테이너를 다시 시작
- 컨테이너 삭제
docker rm 컨테이너_아이디- 지정된 container_id를 가진 컨테이너를 삭제
Docker 네트워크 및 볼륨 관련 명령어는 다음과 같다.
- 네트워크 생성
docker network create mynetwork- mynetwork이라는 이름의 네트워크를 생성
- 네트워크 목록 보기
docker network ls- 현재 설정된 Docker 네트워크의 목록을 표시
- 네트워크 삭제
docker nework rm mynetwork- mynetwork이라는 이름의 네트워크를 삭제
- 볼륨 생성
docker volume create myvolume- myvolume이라는 이름의 볼륨을 생성
- 볼륨 목록 보기
docker volume ls- 현재 설정된 Docker 볼륨의 목록을 표시
- 볼륨 삭제
docker volume rm myvolume- myvolume이라는 이름의 볼륨을 삭제
도커를 통해 PostgreSQL 컨테이너 2개를 실행해본다(https://hub.docker.com/hardened-images/catalog/dhi/postgres). 먼저 이미지를 받는다.
docker pull postgres
이제 컨테이너를 실행해본다.
docker run -d --name postgres-sample \
-p 5433:5432 \
-e POSTGRES_USER=admin1 \
-e POSTGRES_PASSWORD=admin2 \
-e PGDATA=/var/lib/postgresql/data/pgdata \
-v ${로컬_바인딩_폴더}:/var/lib/postgresql/data:z \
postgres
- :z 옵션은 SELinux(Secure Enhanced Linux) 환경에서 사용되는 파일 시스템 옵션
- Docker 컨테이너가 호스트 파일 시스템의 특정 디렉토리에 접근할 수 있도록 SELinux 컨텍스트를 설정
- SELinux는 Linux 시스템의 보안을 강화하기 위해 파일 및 프로세스에 대한 권한을 세밀하게 제어
- :z 옵션은 해당 볼륨이 여러 컨테이너에서 공유될 수 있음을 나타내며, 이를 통해 컨테이너가 해당 디렉토리에 읽기 및 쓰기 권한을 갖도록 함
Docker Compose
Docker Compose는 다중 컨테이너 Docker 애플리케이션을 정의하고 실행하기 위한 도구로, docker-compose.yml 파일 하나로 애플리케이션의 서비스, 네트워크, 볼륨 등을 정의할 수 있다.
Docker Compose의 파일 구조는 다음과 같다.
version: '3'
services:
web:
image: nginx
ports:
- "8080:80"
app:
build: .
ports:
- "8081:8080"
depends_on:
- db
db:
image: postgres
environment:
POSTGRES_PASSWORD: example
version: Docker Compose 파일의 버전 지정services: 애플리케이션의 각 서비스 정의web,app,db: 각각의 서비스 이름image: 서비스를 실행할 Docker 이미지 지정build: Dockerfile이 있는 디렉토리 경로를 지정하여 이미지 빌드ports: 호스트와 컨테이너 간의 포트 매핑depends_on: 다른 서비스가 먼저 실행되어야 하는 순서 지정environment: 컨테이너의 환경 변수 설정
Docker Compose 명령어
docker compose up:docker-compose.yml파일에 정의된 서비스를 빌드하고 시작. 만약 이미 빌드된 이미지가 있다면 이를 사용- 백그라운드에서 실행하려면
d옵션 추가docker compose up -d docker compose -f /path/to/your/project/docker-compose.yml up
- 백그라운드에서 실행하려면
docker compose down: 실행 중인 모든 서비스를 중지하고 컨테이너, 네트워크, 볼륨 등 정리docker compose build:docker-compose.yml파일에 정의된 서비스 빌드docker compose ps: 현재 실행 중인 서비스 확인docker compose logs: 실행 중인 서비스의 로그 확인
실습
2개의 스프링 컨테이너를 생성하고 사용자가 service-a 컨테이너의 컨트롤러를 호출하면, service-b 컨테이너를 호출한다. 최종 사용자에게 노출되는 메시지는 ”service-a: hi ###### service-b: hello” 이다. 메시지 “service-b: hello” 는 service-b 컨테이너의 메시지이다.
먼저 Lombok, Spring Web, OpenFeign dependencies를 가진 2개의 Spring 프로젝트를 생성하고 service-b부터 작성을 시작한다.
package com.service.b;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BController {
@GetMapping("hello")
public String hello() {
return "Hello";
}
}
spring.application.name=service-b
server.port=18081
이제 service-a를 작성한다.
package com.service.a;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients
@SpringBootApplication
public class AApplication {
public static void main(String[] args) {
SpringApplication.run(AApplication.class, args);
}
}
package com.service.a;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class AController {
private final BServiceClient bServiceClient;
@GetMapping("hi")
public String hi() {
String hello = bServiceClient.getHello();
return "service-a : hi ###### service-b: " + hello;
}
}
package com.service.a;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "service-b", url = "${service.b.url}")
public interface BServiceClient { // B Service를 호출하는 Client
@GetMapping("/hello")
public String getHello();
}


Docker 사용
이번엔 Docker를 사용해서 컨테이너 2개를 실행해본다. 먼저 두 프로젝트의 port를 8080으로 수정한다. 또한 도커 컨테이너끼리는 같은 네트워크에 있으면 도커 컨테이너의 이름으로 호출할 수 있기 때문에 service.b.url을 http://service-b:8080 으로 수정한다.
두 프로젝트에 Dockerfile을 작성한다.
FROM openjdk:17-jdk-slim
VOLUME /tmp
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
Docker 끼리 Container 이름으로 호출하기 위해서는 기본 브리지 네트워크가 아닌 사용자 정의 네트워크에서 진행해야 한다. 그렇기 때문에 도커 네트워크를 생성한다.
$ docker network create my-network
5638c404332de3e8b56d589397f27a637eb377f5b27c998eae3ebd690caf9b79
이후 프로젝트를 빌드하고, 각 프로젝트의 위치로 들어가 image를 생성한다.
$ ./gradlew clean bootJar
$ docker build -t img-service-a .
$ ./gradlew clean bootJar
$ docker build -t img-service-a .
이제 container를 생성한다.
PS C:\Users\dbsau\project\b> docker run -d --name service-b `
>> --network my-network `
>> -p 18081:8080 `
>> img-service-b
300b21a6cb147d22e72b2c2218034bdb520f6c6f694a660d62718619caeed32f
PS C:\Users\dbsau\project\b> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
300b21a6cb14 img-service-b "java -jar /app.jar" 16 seconds ago Up 15 seconds 0.0.0.0:18081->8080/tcp, [::]:18081->8080/tcp service-b
이제 http://localhost:18081/hello로 접속하면 service-b가 호출되는 것을 확인할 수 있다.

같은 방식으로 service-a image를 만들고 http://localhost:18080/hi로 접속하면 service-a가 호출되는 것을 확인할 수 있다.
Docker Compose 사용
기존 Docker Container를 모두 삭제한다.
docker rm -f 서비스_컨테이너_아이디
이후 service-a 프로젝트와 service-b 프로젝트의 상위 폴더에서 docker-compose.yml 파일을 작성한다.
version: '3.8'
services:
service-a:
image: img-service-a
ports:
- "18080:8080"
environment:
- SERVICE_B_URL=http://service-b:8080
depends_on:
- service-b
service-b:
image: img-service-b
ports:
- "18081:8080"
networks:
default:
driver: bridge
이제 docker compose up을 하면 실행되는 것을 확인할 수 있다. 이제 http://localhost:18080/hi로 접속하면 service-a가 호출되는 것을 확인할 수 있다.
참고로 Docker Compose를 사용하여 서비스를 실행하면, Docker Compose는 기본적으로 새 브리지 네트워크를 생성하여 각 서비스 컨테이너를 그 네트워크에 연결한다. 이 네트워크는 docker-compose.yml 파일에 정의된 모든 서비스가 서로 통신할 수 있도록 한다.
Docker Compose는 자동으로 네트워크를 생성하며, 이 네트워크는 docker-compose.yml 파일이 있는 디렉토리의 이름을 기반으로 한다. 네트워크의 이름은 다음과 같은 형식으로 생성된다.
directoryname_default
docker network ls를 사용하여 네트워크가 생성되었는지 확인해본다.
PS C:\Users\dbsau\project> docker network ls
NETWORK ID NAME DRIVER SCOPE
5c5a4ec9281c bridge bridge local
31e65c59c6e0 deliverycommerce_default bridge local
07555a55daa7 host host local
5638c404332d my-network bridge local
440f6d75ddc8 none null local
442a7f165d12 project_default bridge local
댓글남기기