Backend(Framework)/Rust / / 2025. 7. 24. 10:03

Rust 환경 구축 & 배포 프로세스 구축하기

Rust 환경 구축 & 배포 프로세스에 대해서 Top-Down 방식으로 학습하면서 알게된 내용을 정리 특히 Cargo.toml 에 대해서 다룹니다.

1. 기본 CI / CD

우리 프로젝트의 기본 CI / CD 구성은 아래 이미지와 같습니다. 아래와 같은 패턴에서는 Jenkins에서 Rust Gitlab Project를 Clone, Build, Image Push 과정을 거쳐서 Nexus에서 관리하고 Argocd 를 통해서 K8S로 관리로 합니다.

 

여기서 처음 겪어보는 것은 Rust Dockerfile 구성입니다.

2. Dockerfile (for Rust)

참고 : https://hub.docker.com/_/rust , https://docs.docker.com/guides/rust/

 

Rust

Containerize Rust apps using Docker

docs.docker.com

아래 소스코드는 Rust 공식 가이드에서 제시하는 최소사이즈 형태의 멀티스테이지 Dockerfile의 예시입니다

#######################################
# 1. 빌드 스테이지
#######################################
FROM rust:1.85 AS builder
WORKDIR /usr/src/app

# 소스 전체 복사 및 빌드
COPY . .
RUN cargo build --release

#######################################
# 2. 실행 스테이지
#######################################
FROM debian:bookworm-slim
WORKDIR /usr/src/app

# 빌드된 바이너리만 복사
COPY --from=builder /usr/src/app/target/release/myapp .

# 컨테이너 시작 시 실행될 커맨드
CMD ["./myapp"]

 

여기서, 멀티 스테이이지 Dockerfile 구성은

멀티 스테이지 Dockerfile 빌드 구성은 해당 파일의 내용에서 "빌드 전용" 이미지와 "실행 전용" 이미지를 분리해 만드는 기법입니다. Rust와 같은 컴파일 언어로 작성된 어플리케이션에서 주로 쓰이게 됩니다. 특징으로는 컴파일 도구 없이 바이너리 파일만 최종 포함시키므로 파일용량을 대폭 줄일수 있습니다.

위와 같은 방법이 Rust 배포환경에서 가장 많이 쓰이는 멀티스테이지 배포방식이며 주요한 특징으로는 실행 스테이지에서 `빌드 된 바이너리만 복사` 하는 부분입니다. Dockerfile에 대한 디테일한 내용은 마지막에 다시 다루겠습니다.

 

3. K8S 에서 Pod 그리고 Argocd templates env 설정

컨테이너에 전달되는 고정 환경 변수는 k8s deployment.yaml 에서 관리합니다.

예시 (환경변수 이외의 변수는 모두 생략)

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - name: {{ .Values.application.name }}
        - env:
          - name: APP_ENV
          - value: "{{ .Values.service.app.profile }}"

우리 프로젝트에서는 `APP_ENV` 환경변수를 가지고 프로필 및 배포위치를 판단합니다 (예제는 하단에)

 

그리고 rust 프로젝트 최초 구성시 virtual-service yaml에서 health check를 수행 할 수있도록 최소구성을 해주어야 합니다. 

예시 (health check 요소 이외의 변수는 모두 생략)

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
spec:
  http:
    - name: "{{ .Values.application.name }}-health"
      match:
        - uri:
            exact: /health
      route:
        - destination:
            host: {{ .Values.application.name }}-svc
            subset: v1
      timeout: 5s
      retries:
        attempts: 0

추가로 swagger-ui 같은 정적 리소스가 있다면 다음의 내용도 추가해줍니다.

    - name: "{{ .Values.application.name }}-swagger-ui"
      match:
        - uri:
            prefix: /swagger-ui
      route:
        - destination:
            host: {{ .Values.application.name }}-svc
            subset: v1
      timeout: 60s
      retries:
        attempts: 0

여기까지가 Rust 환경 구성의 기초내용입니다.

 

4. Rust Build와 리소스 관리에 대해서 알아둘것

1. 빌드 배포 환경에서 최소 바이너리 파일만 생성

처음 아무것도 모른채 CI / CD를 구성하게 되면 최소 바이너리 파일만 생성 된다는 것이 어디에 어떤점까지 영향을 주는지 알기가 어렵습니다. 해서 빌드배포이력과 쿠버네티스 Pod를 통해서 확인해봅니다.

k8s master cluster에서 exec를 통해서 컨테이너 안으로 ... 

디렉토리 및 파일을 확인해보면 `/app` 경로에 `Cargo.toml` and `moon` 파일을 확인 할 수있습니다.
위 예제와 같이 구성했을땐 `/usr/src/app` 위치에 구성이 됩니다.

예제와 다른점은 최소 바이너리만 복사하기 때문에 `moon` 만 생겼어야하는데 `config.rs` 파일에서 `Cargo.toml` 파일의 `package` , `metadata` , `config` 등을 참조해야하기 때문에 바이너리 파일과 함께 최소 파일로서 `Cargo.toml` 파일을 복사시켰습니다.

해당 내용이 반영된 Dockerfile

이때 주의할 점은 빌드 스테이지에서 바이너리 파일과 동일한 위치인 `~/app` 디렉토리로 선 복사 후
실행 스테이지에서 builder 스테이지의 파일을 복사해오는 것입니다. 이는 최소 파일만 생성하고 구성하기 때문입니다.

#######################################
# 1. 빌드 스테이지
#######################################
FROM rust:1.85 AS builder
WORKDIR /usr/src/app

# 소스 전체 복사 및 빌드 (추가)
COPY Cargo.toml Cargo.lock ./
COPY . .
RUN cargo build --release

#######################################
# 2. 실행 스테이지
#######################################
FROM debian:bookworm-slim
WORKDIR /usr/src/app

# 빌드된 바이너리만 복사
COPY --from=builder /usr/src/app/target/release/myapp .

# (추가)
COPY --from=builder /usr/src/app/Cargo.toml ./Cargo.toml

# 컨테이너 시작 시 실행될 커맨드
CMD ["./myapp"]

 

2. 빌드시 target 디렉토리 설정 및 bin(바이너리) 파일 생성 규칙 이외 +@
위 Dockerfile에서 자연스럽게 넘어간 내용이 한가지 있습니다.

2.1 target/{?} 디렉토리가 설정되는 규칙

cargo build --release
- ~/app/target/`release`/`moon`
cargo build --profile=${PROFILE}
- ~/app/target/`debug`/`moon`

위 내용을 간략하게 local host 환경에서 검증진행

`build` 수행하기 전 디렉토리

cargo build --release 수행즉시 하단의 이미지처럼 `release` 디렉토리 생성

cargo build --profile=local 검증을 위해 profile build 도 테스트시 `local` 디렉토리 생성 (Not Custom)

cargo build --profile=dev 검증을 위해 profile build 도 테스트시 `dev` 디렉토리 생성 (Custom)

 

이때 내 크레이트와 외부 의존 크레이트

Rust의 Cargo는 여러 빌드 프로파일을 지원하며 각 프로파일마다 별도의 출력 디렉토리와 컴파일 설정을 구성할 수 있습니다.
예를들어 cargo build --profile=dev 을 실행단다면

1. 나의 크레이트 프로젝트는 [profile.dev] 설정을 따라가게되고 target/dev/... 에 빌드되고,
2. 외부 의존 크레이트는 dev 프로파일이 정의되어 있지않기때문에 target/debug/... 에 빌드됩니다

여기서 profile이 custom 된것이라고 하면 debug 디렉토리가 생성된다고 합니다. (명확한 확인필요)

 

2.2 target/{bin} bin 파일명이 설정되는 규칙
이미 변수에 `{bin}` 으로 표기하면서 바이너리(bin) 파일이라는 것을 알았지만 처음접한다면 이 명명 조차 모를 수있습니다.
그리고 `Cargo.toml` 파일에서 `[[bin]]` 과 `[package]` 가 주는 영향을 초심자 수준에서는 알기 어렵습니다.
우선 정의되는 패턴은 아래와 같습니다.

#[[bin]]
#name = "moon"
#path = "src/main.rs"

[package]
name = "moon"
version = "0.1.0"
edition = "2021"

 

1. `[package]`만 정의 되어있고 `[[bin]]`가 정의되어 있지 않은 경우 = package.name
2. `[package]`와 `[[bin]]`가 모두 정의되어 있는 경우 = bin.name

2.3 swagger-ui 접근 허용방법
우선 알고가야할 한가지 Rust 애플리케이션을 빌드할 때, 프로필에 따라 Swagger UI 같은 정적 리소스 임베드 동작이 달라집니다.

1. Release 프로필
2. (dev) 커스텀 프로필

default-features=true 상태이거나 debug-embed 기능을 활성화 하지 않으면 정적 리소스가 자동으로 임베드 되지 않아서 UI 화면이 로드될 수 없음

이에 주요 옵션을 확인해보면 

- vendored 기능 => 릴리즈 및 커스텀 빌드 프로파일 등 상관없이 정적 파일을 바이너리에 포함시킴
- debug-embed 기능 => 명시적으로 작성함으로 커스텀 프로필도 정적 리소스를 임베드 하도록 설정

[dependencies]
utoipa-swagger-ui = { version = "7.1.0"
                    , default-features = false
                    , features = ["axum", "vendored", "debug-embed"] }

위 코드와 같이 전개한다면 릴리즈 및 커스텀 프로파일 모두 Swagger UI의 정적 리소스가 바이너리에 포함.

 

5. 최종 Rust Dockerfile

1. 빈 정의 -> 최종생략가능하나 Pipeline에서의 명확한 선언 선호
2. 바이너리에 포함되지 않은 정적 리소스 복제 -> 바이너리가 로직 실행 중 찾게 되는 리소스를 미리 복제세팅

위 두가지 사항에 대해서 완벽 이해했다면 나머지 요소들은 매우낮은 학습수준이기에 생략.

################################# Build Container ###############################
FROM rust:1.85.1 as builder

# 변수정의
ARG PROFILE=dev
ENV PROFILE=${PROFILE}
ENV BIN_NAME=moon

# 작업디렉토리
WORKDIR /app

# 의존성 캐싱을 위해 먼저 복사
COPY Cargo.toml Cargo.lock .env.${PROFILE} ./
COPY ./src/static/ ./src/static/
RUN mkdir -p src && echo "// dummy" > src/lib.rs && cargo fetch

# 전체 소스 복사
COPY . .

# 빌드 모드 설정
RUN cargo build \
    --profile=${PROFILE} \
    --features swagger-ui \
    --bin ${BIN_NAME}
################################# Prod Container #################################
FROM debian:bookworm-slim

# 변수정의 , 빌드 모드 (release, debug)
ARG PROFILE=dev
ENV PROFILE=${PROFILE}
ENV BIN_NAME=moon
ENV BUILD_MODE=debug

WORKDIR /app

# debug 용 설치
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl ca-certificates && \
    rm -rf /var/lib/apt/lists/*

# 빌드된 바이너리를 원하는 이름으로 복사 + 환경설정 파일 복사
COPY --from=builder /app/target/${BUILD_MODE}/${BIN_NAME} /app/${BIN_NAME}
COPY --from=builder /app/Cargo.toml ./Cargo.toml
COPY --from=builder /app/.env.${PROFILE} ./.env.${PROFILE}
COPY --from=builder /app/src/static/ ./src/static/

RUN chmod +x /app/${BIN_NAME} \
    && useradd --system --no-create-home appuser \
    && chown -R appuser:appuser /app

EXPOSE 3000
USER appuser

ENTRYPOINT ["/bin/sh","-c","exec ./${BIN_NAME}"]

 

6. Config.rs 및 build 프로파일 내용세팅 패턴

기본적으로 우리 Rust 소스에서는 build 프로파일을 구분 짓는 기초변수로 APP_ENV를 사용합니다.
APP_ENV={local , dev, stg, prd} 이와 같은 패턴을 가지며
실제로 이 구분변수를 통해서 config.rs Cargo.toml 파일과 각 .env 파일을 참조하는 패턴으로 작성되었습니다.

예시

(Cargo.toml)

[profile.local]
[profile.dev]
[profile.stg]
[profile.prd]

[package.metadata.config.local]
[package.metadata.config.dev]
[package.metadata.config.stg]
[package.metadata.config.prd]

(root)
.env.local 
.env.dev
.env.stg
.env.prd

 

위와 같은 형태의 구성이 나타나고 

1단계 Cargo.toml 프로파일 섹션에 내용이 적용 
- `[profile.<env>]` 
- `[package.metadata.config.<env>]`

2단계 .env 파일 로드 및 환경변수 오버라이드
- config.rs 내에서 APP_ENV 값으로 env_file 경로 (.env.loca, .env.dev, .env.stg 등을) 읽어 설정

이와 같은 패턴을 통해서 빌드 타임에는 Cargo 프로파일이 컴파일 디버그 최적화 옵션을 설정하게 되고,
런타임에서는 .env.<env> 파일을 기반으로 환경변수 값을 편리하게 관리 할 수 있습니다.

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유