Go로 "컨테이너스러운" 프로세스를 만들어보자.
격리
Docker와 같은 컨테이너 기술에서 "격리"라는 키워드는 컨테이너가 제공하는 가장 핵심적인 개념 중 하나이다. 세부적으로 하나하나 그 장점을 나열해보자.
1. 🔐보안 (Security)
격리를 통해 각 컨테이너는 서로 다른 컨테이너나 호스트 시스템과 직접적으로 상호작용할 수 없다. 만약 컨테이너 내부에서 시스템에 악영향을 주는 코드가 실행되더라도, 다른 컨테이너나 호스트 전체로 피해가 전이되는 것을 막을 수 있다.
2. 🧩자원 분리 (Resource Separation)
CPU, 메모리, 네트워크, 디스크 I/O 등 시스템 자원을 분리하여 사용할 수 있다. 특정 컨테이너가 무한히 자원을 사용하는 것을 방지하고, 다른 컨테이너에 영향을 주지 않도록 보장할 수 있다.
3. 🗂️ 환경 독립성 (Environment Independence)
각각의 컨테이너는 자신의 파일 시스템, 라이브러리, 의존성 등을 갖춘 독립된 실행환경을 가진다. 따라서 한 시스템에서 잘 돌아가던 애플리케이션이 다른 시스템에서는 환경 문제로 동작하지 않는 문제를 기술적으로 배제할 수 있다. 뿐만 아니라 서로 다른 버전의 Python, Node.js, 데이터베이스를 사용하는 여러 서비스가 있을 경우, 각각을 별도의 컨테이너에서 격리 실행하면 충돌 없이 공존시킬 수 있다.
4. ♻️ 재현성 (Reproducibility)
격리를 통해 컨테이너는 항상 동일한 상태로 실행된다. 테스트, 배포, 개발 환경에서 늘 동일한 결과를 재현할 수 있어 안정적인 CI/CD 파이프라인 구축이 가능하다.
컨테이너에서 "격리"는 보안, 자원 관리, 환경 독립성, 재현성, 서비스 충돌 방지 등을 가능하게 해주는 핵심 개념이며, 이것이 바로 도커 같은 컨테이너 기술이 널리 채택된 이유 중 하나이다.
기술적으로 격리는 Linux의 네임스페이스, cgroups, chroot 등으로 구현할 수 있다. 리눅스 네임스페이스는 프로세스, 파일 시스템, 네트워크, 사용자, 호스트 명을 격리할 수 있고 cgroups는 CPU, 메모리 등의 자원 사용량을 격리하며 chroot는 파일 시스템을 격리시켜 준다.
프로세스의 격리
이번 포스팅은 리눅스 시스템의 가장 기본적인 실행 단위인 프로세스 격리를 다룬다. 즉, fork(혹은 clone)과 exec 시스템 콜을 통해 호스트와 분리된 새 프로세스를 만들고 기초적인 도커의 run 명령어를 구현하여 실행시킬 것이다. 이를 위해 아래 핵심 개념을 반드시 숙지하고 시작하자.
핵심 개념 요약
핵심 개념 1 : fork와 exec
리눅스에서 새로운 프로그램을 실행하는 고전적인 방법은 fork와 exec 시스템 콜을 조합하는 것이다.
fork()는 현재 실행 중인 프로세스를 복제해서 자식 프로세스를 생성한다. 자식 프로세스는 부모의 메모리 공간과 실행 컨텍스트를 그대로 복사한 상태로 시작된다. 이후 자식 프로세스에서는 exec()를 호출하여, 지정한 바이너리 프로그램으로 자신의 메모리 공간을 덮어씌운다. 그 결과, 자식 프로세스를 완전히 다른 프로그램으로 변하게 된다.
위 방식은 프로세스 격리를 기반으로 하는 도커나 기타 컨테이너 기술에서도 핵심적으로 활용된다. 실제로 도커 컨테이너 내부에서 실행되는 모든 명령은 이런 방식으로 새로운 프로세스를 만들고 실행한다.
핵심 개념 2 : syscall.SysProcAttr
Go에서 os/exec 패키지를 사용할 때, 리눅스 고유의 저수준 기능(예: 네임스페이스)을 활용하고 싶다면, Cmd.SysProcAttr 필드를 설정해야 한다. 이 필드는 syscall.SysProcAttr 구조체를 통해 설정되며 아래 기능을 제공한다.
- Cloneflags: 새로운 UTS, PID, Mount, Network 등 네임스페이스를 생성하도록 커널에 요청
- Unshareflags: 기존 프로세스 컨텍스트에서 특정 네임스페이스만 분리 가능
- Credential: 실행할 자식 프로세스의 UID/GID를 직접 지정해 권한을 낮춘 실행 가능
도커는 각 컨테이너에 대해 분리된 네임스페이스를 만들어 프로세스, 파일 시스템, 네트워크 등을 격리하는데, Go로 이를 직접 구현하려면 SysProcAttr 설정이 필수이다. 추후 코드로 살펴보자.
핵심 개념 3 : os/exec 패키지
GO의 os/exec 패키지는 외부 명령을 실행할 수 있도록 도와주는 표준 라이브러리이다. 이 패키지는 내부적으로 fork/exec 메커니즘을 사용하지만, 개발자가 쉽게 사용할 수 있도록 고수준의 API를 제공한다.
Go에서 컨테이너를 구현할 때, os/exec는 자식 프로세스를 생성하고 그 안에서 원하는 바이너리를 실행하는 데 핵심 역할을 한다. 여기에 SysProcAttr를 추가로 설정하면, 단순한 프로세스 실행을 넘어서 리눅스 네임스페이스 기반 격리 환경 구성까지 가능하게 된다.
구현 : 프로세스 격리 컨테이너
1. main.go의 기본 구조
import (
"fmt"
"os"
"os/exec"
"syscall"
)
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: containerM run <command>")
os.Exit(1)
}
switch os.Args[1] {
case "run":
run()
case "child":
child()
default:
panic("unknown command")
}
}
2. run() 함수 : 자식 프로세스 시작하기
func run() {
fmt.Printf("Parent: Running %v as PID %d\n", os.Args[2:], os.Getpid())
cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// isolation namespace
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWPID,
}
must(cmd.Run())
}
func must(err error, context ...string) {
if err != nil {
if len(context) > 0 {
fmt.Fprintf(os.Stderr, "Error (%s): %v\n", context[0], err)
} else {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
}
os.Exit(1)
}
}
- PID 네임스페이스는 부모 프로세스에는 영향을 주지 않고, 새로 생성된 자식 프로세스 내에서만 유효하다. 따라서 containerM run → 자식 프로세스를 띄우고 → 그 자식이 다시 명령어를 실행하는 2단계 구조가 필요하다. 이를 위해 containerM child 명령을 내부적으로 호출하게 구성했다.
3. child() 함수 : 진짜 컨테이너 명령어 실행하기
func child() {
fmt.Printf("Running child process PID %d\n", syscall.Getpid())
cmd := exec.Command(os.Args[2], os.Args[3:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
must(cmd.Run())
}
✅ 실행해보자
빌드
go build -o conatinerM
격리된 bash 실행
sudo ./containerM run bash
# Parent: Running [bash] as PID 4720
# Running child process PID 1
💡 정리
요소 | 설명 |
fork | Go에서는 exec.Command로 새로운 프로세스 생성 |
exec | 자식 프로세스에서 bash, ps, top 등을 실행 |
CLONE_NEWPID | PID 네임스페이스를 격리시키기 위한 핵심 플래그 |
/proc/self/exe | 현재 실행 중인 바이너리 자기 자신을 재실행하기 위한 트릭 코드 |
🔜 다음 이야기
다음 편에서는 이 PID 네임스페이스를 바탕으로, 호스트 이름(UTS 네임스페이스)도 분리해보고 진짜 "내 것 같은" 컨테이너 환경을 더 만들어 본다.
'개발일기 > Docker' 카테고리의 다른 글
4. Mount 네임스페이스로 파일시스템 분리하기 (0) | 2025.05.30 |
---|---|
3. UTS 네임스페이스로 호스트명 분리하기 (0) | 2025.05.29 |
2. PID 네임스페이스로 프로세스 격리하기 (심화) (1) | 2025.05.29 |
도커와 도커 사이의 통신(도커 네트워크) (2) | 2024.11.04 |
Docker로 Sonarqube 설치 및 실행(Spring & react.js + ts) (0) | 2024.10.28 |
댓글