본문 바로가기
프로젝트 일지/BoWS

Springboot 애플리케이션을 통해 helm 배포해보기 (feat. ProcessBuilder)

by 데브겸 2024. 8. 1.

지난 포스트에서 Helm으로 애플리케이션 배포를 한 방에 해보는 방법을 알아보았다.

https://kyumcoding.tistory.com/112

 

Helm으로 애플리케이션 배포 한 방에 해보기

Helm Chart 만들기helm create 명령어로 기본적인 틀 생성하기helm create sampleApp  위 명령어로 아래와 같은 구조의 폴더가 만들어진다(이외에 여러 파일들이 있는데 optional한 경우가 많다)sampleApp ├─

kyumcoding.tistory.com

 

 

7월 마지막 주간에는 Springboot 애플리케이션을 통해 helm 명령어를 생성하고, 배포하는 것을 플랫폼화해보려 한다. 우선 자바의 ProcessBuilder로 CMD 명령어를 생성, 실행하는 최소한의 기능만 하는 애플리케이션을 만들어본다. 간단하게 helm install —set 명령어를 생성하여 Process 객체로 이를 실행하도록 짰다.

 

컨테이너 형태로 배포할 것이기 때문에 Dockerfile을 작성해주었고, Entrypoint와 CMD를 사용하여 컨테이너가 만들어질 때 helm repo를 컨테이너 내부에 add하는 명령어를 넣어주었다. 

 

매우 중요 => 참고로 docker run 할 때 .kube/config 파일과 helm 실행 파일을 mount하여 사용해야 한다 (helm은 내부에서 컨테이너 내부에서 설치해서 사용하는 것도 가능할 것 같다) 나는 이를 간과하여 자꾸 helm이 존재하지 않는다는 에러가 나서 고생했었다... 컨테이너 안이니까 당연히 로컬에 깔려있는 helm이 없는 것이 당연한데 😇

 

 

Controller

@Controller
@RequestMapping("/api")
@RequiredArgsConstructor
public class ApplicationController {

    private final ApplicationService applicationService;

    @PostMapping("/create")
    public ResponseEntity<String> createApplication(@RequestBody ApplicationCreateRequest request) throws IOException, InterruptedException {
        return ResponseEntity.ok(applicationService.create(request));
    }

    @GetMapping("/destroy")
    public ResponseEntity<String> destroyApplication() throws IOException, InterruptedException {
        return ResponseEntity.ok(applicationService.destroy());
    }

    @GetMapping("/healthcheck")
    @ResponseBody
    public String healthCheck(){
        return "healthy";
    }
}

 

 

RequestDTO

public record ApplicationCreateRequest (

        @NotBlank
        @Size(max = 30)
        String projectName,

        @NotBlank
        @Size(max = 50)
        String domain,

        @NotBlank
        @Size(max = 30)
        String backendImageName,

        @NotBlank
        @Size(max = 30)
        String frontendImageName,

        @NotNull
        @Size(min = 5, max = 30)
        String dbPassword,

        @NotNull
        @Size(min = 5, max = 30)
        String dbEndpoint,

        @NotNull
        @Size(min = 5, max = 30)
        String dbUserName,

        @NotNull
        @Size(min = 5, max = 30)
        String dbUserPassword
) {

    public Map<String, String> toHelmCLIArguments() {
        Map<String, String> helmCLIArguments = new HashMap<>();
        helmCLIArguments.put("projectName", this.projectName());
        helmCLIArguments.put("domain", this.domain());
        helmCLIArguments.put("app.backend.image.name", this.backendImageName());
        helmCLIArguments.put("app.frontend.image.name", this.frontendImageName());
        helmCLIArguments.put("app.db.env.MYSQL_ROOT_PASSWORD", this.dbPassword());
        helmCLIArguments.put("app.db.env.MYSQL_DATABASE", this.dbEndpoint());
        helmCLIArguments.put("app.db.env.MYSQL_USER", this.dbUserName());
        helmCLIArguments.put("app.db.env.MYSQL_PASSWORD", this.dbUserPassword());

        return helmCLIArguments;
    }
}

 

 

Service

처음에는 yaml 파일을 만들고, 그 yaml 파일을 바탕으로 -f 옵션을 사용하는 방법을 사용하려 했다. 하지만 굳이 IO 작업을 할 필요가 없는 간단한 작업이고, 그렇게 짜는게 빠르게 검증하는 용도로는 적합하지 않다고 판단하여 --set option1=opt1,option2=opt2 형식의 CLI 명령어를 생성하는 방향으로 코드를 작성하였다.

process, processbuilder에 들어가는 명령어는 String 한 줄로 한 번에 붙여서 넣으면 안 된다. 반드시 매개변수 단위로 끊어서 array혹은 arrayList 형태로 구성하여 실행해야 한다 (이걸 몰라서 꽤나 애먹었다)

@Slf4j
@Service
@RequiredArgsConstructor
public class ApplicationService {

    private final String RELEASE_NAME = "example";
    private final String HELM_REPO_NAME = "sampleapp";
    private final String CHART_NAME = "sampleapp";

    private final HelmCommandExecutor helmCommandExecutor;

    public String create(ApplicationCreateRequest request) throws IOException, InterruptedException {
       int exitCode = helmCommandExecutor.executeInstall(RELEASE_NAME, HELM_REPO_NAME, CHART_NAME, request.toHelmCLIArguments());
       if (exitCode == 1) {
                return "App Creation Failed";
       }
       return "App Creation Succeed";
    }

    public String destroy() throws IOException, InterruptedException {
        int exitCode = helmCommandExecutor.executeUninstall(RELEASE_NAME);
        if (exitCode == 1) {
            return "App Destroy Failed";
        }
        return "App Destroy Succeed";
    }
}

@Slf4j
@Service
public class HelmCommandExecutor {

    private final String FLAG = "--set ";

    public int executeInstall(String releaseName, String helmRepoName, String chartName, Map<String, String> arguments) throws IOException, InterruptedException {
        StringBuilder argumentBuilder = new StringBuilder();
        argumentBuilder.append(FLAG);
        for(Map.Entry<String, String> entry : arguments.entrySet()){
            argumentBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append(",");
        }
        argumentBuilder.deleteCharAt(argumentBuilder.length()-1);

        String[] commands = {"/bin/sh", "-c", "helm install " + releaseName + " " + helmRepoName + "/" + chartName + " " + argumentBuilder.toString().trim()};

        return execute(commands);
    }

    public int executeUninstall(String releaseName) throws IOException, InterruptedException {
        String[] commands = {"/bin/sh", "-c", "helm uninstall " + releaseName};

        return execute(commands);
    }

    private int execute(String[] commands) throws IOException, InterruptedException {
        ProcessBuilder processBuilder = new ProcessBuilder(commands);
        processBuilder.redirectErrorStream(true);
        Process process = processBuilder.start();

        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        String line;
        while ((line = reader.readLine()) != null) {
            log.info(line);
        }

        int exitCode = process.waitFor();
        log.info("\\nExited with error code : " + exitCode);

        return exitCode;
    }
}

 

 

Dockerfile

CMD와 ENTRYPOINT를 함께 적으면 CMD에 적은 값이 ENTRYPOINT에 인자로 들어오는 것을 이용하여 컨테이너가 실행되자마자 들어가야 하는 초기 명령어를 실행하게 만든다. 참고로 docker run 할 때 .kube/config 파일과 helm 실행 파일을 mount하여 사용해야 한다 (helm은 내부에서 컨테이너 내부에서 설치해서 사용하는 것도 가능할 것 같다)

FROM openjdk:17-jdk-slim
WORKDIR /app
COPY ./build/libs/bows-0.0.1-SNAPSHOT.jar .
EXPOSE 8080
ENTRYPOINT ["sh", "-c"]
CMD ["helm repo add simpleApp helm repo add simpleApp <https://bows-bo-web-service.github.io/BoWS-Infra/charts/simpleApp> && java -jar bows-0.0.1-SNAPSHOT.jar"]

 

 

 

Deploy.sh

#! /bin/sh
./gradlew clean build -x test

docker buildx build \\
 --platform linux/amd64,linux/arm64 \\
 -t bogyumkim/bows-backend:latest \\
 --no-cache \\
 --push .