본문 바로가기
개발

웹 애플리케이션에서 도커 컨테이너 실행, 삭제, 조회

by ^..^v 2023. 4. 10.
728x90
반응형

사용자 요청에 따라 웹 서버(nginx) 컨테이너를 실행, 삭제, 조회하는 웹 애플리케이션을 작성해 보겠습니다. 
실습은 https://myanjini.tistory.com/entry/board-%EC%86%8C%EC%8A%A4%EC%BD%94%EB%93%9C 에서 가져온 코드를 이용해서 진행하겠습니다.

 

컨트롤러 추가

RestDockerApiController.java 파일을 추가하고, 컨테이너 목록을 조회하는 메서드, 컨테이너를 실행하는 메서드, 컨테이너를 삭제하는 메서드를 구현합니다.

package board.controller;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.UUID;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import board.dto.BoardDto;

@RestController
@CrossOrigin(origins = "*", allowedHeaders = "*")
public class RestDockerApiController {

	// 컨테이너 별로 사용할 디렉터리를 생성할 디렉터리
	private final String CONST_AWS_DIR = "c:\\aws\\";
	
	
	// 모든 상태의 컨테이너를 조회
	@GetMapping("/api/docker")
	public List<String> dockerList() throws Exception {
		final String command = "docker container ls -a --format=\"{{json .}}\" ";

		List<String> result = null;
		Process process = null;
		try {
			process = Runtime.getRuntime().exec(command);
			result = new BufferedReader(new InputStreamReader(process.getInputStream()))
					.lines()
					.toList();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return result;
	}
	
	// 컨테이너 생성
	// 생성 시 임의의 이름과 포트를 사용하도록 하고, 이름에 해당하는 폴더를 생성해 컨테이너와 공유
	@PostMapping("/api/docker/write")
	public List<String> dockerRun(@RequestBody BoardDto boardDto) throws Exception {
		List<String> result = null;
		
		final String name = UUID.randomUUID().toString();
		final String path = CONST_AWS_DIR + name + "\\";
		final String file = path + boardDto.getTitle();
		final String commands = 
				"cmd.exe /c "  
				+ String.format("mkdir  %s", path)
				+ String.format("&& docker container run -d -v %s:/usr/share/nginx/html/ --name %s -p 80 nginx", path, name) 
				+ String.format("&& docker inspect --format=\"{{(index (index .NetworkSettings.Ports \\\"80/tcp\\\") 0).HostPort}}\" %s", name)
		;
		
		try {
			Process process = Runtime.getRuntime().exec(commands);
			result = new BufferedReader(new InputStreamReader(process.getInputStream()))
					.lines()
					.toList();

			Path filePath = Paths.get(file);
			Files.createFile(filePath);
			Files.write(filePath, boardDto.getContents().getBytes(), StandardOpenOption.CREATE);
		} catch (IOException e) {
			e.printStackTrace();
		}

		return result;
	}	
	
	// 파라미터로 전달한 값과 컨테이너 이름이 일치하는 컨테이너를 삭제 후 결과를 반환
	@DeleteMapping("/api/docker/{name}")
	public List<String> dockerRemove(@PathVariable("name") String name) throws Exception {
		final String commands =
				"cmd.exe /c " 
				+ String.format("rmdir /S /Q %s%s ", CONST_AWS_DIR, name)
				+ String.format("&& docker container rm -f %s", name)
		;

		List<String> result = null;
		Process process = null;
		try {
			process = Runtime.getRuntime().exec(commands);
			result = new BufferedReader(new InputStreamReader(process.getInputStream()))
					.lines()
					.toList();
			
		} catch (IOException e) {
			e.printStackTrace();
		}

		return result;
	}
	
	// 파라미터로 전달한 값과 컨테이너 ID가 일치하는 컨테이너의 상세 정보를 조회해서 반환
	@GetMapping("/api/docker/{id}")
	public List<String> dockerInspect(@PathVariable("id") String id) throws Exception {
		final String command = String.format("docker container inspect %s", id);

		List<String> result = null;
		Process process = null;
		try {
			process = Runtime.getRuntime().exec(command);
			result = new BufferedReader(new InputStreamReader(process.getInputStream()))
					.lines()
					.toList();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return result;
	}
}

 

컨테이너 조회 

BoardList.js 파일을 앞에서 작성한 컨트롤러 메서드를 호출하도록 수정합니다. 웹 페이지 보기 아이콘을 클릭하면 새 창(탭)에서 컨테이너에 바인딩된 호스트 포트로 요청을 전달하고, 웹 서버 삭제 아이콘을 클릭하면 삭제할 컨테이너의 이름을 삭제 페이지로 전달하고, 목록의 컨테이너 ID를 클릭하면 상세 페이지로 이동합니다. 

import axios from 'axios';
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { IconName, MdDeleteOutline, MdOutlineArticle, MdOutlinePlayCircle, MdOutlineStopCircle } from "react-icons/md";

const BoardList = ({ history }) => {

    const [datas, setDatas] = useState([]);
    const styles = {width: 24, height: 24, cursor: 'pointer'};
    
    // 최초 마운트 시 컨테이너 목록 조회 결과를 상태 변수의 값으로 설정
    useEffect(() => {
        axios.get('http://localhost:8080/api/docker')
            .then(response => {
                console.log(response);
                setDatas(response.data);
            })
            .catch(error => {
                console.log(error);
            });
    }, []);

    // 새 창(탭)에서 컨테이너에 바인딩된 호스트 포트로 요청
    const handlerViewPage = (port) => {
        window.open(`http://localhost:${port}`)
    };

    // 삭제할 컨테이너의 이름을 파라미터로 전달
    const handlerDelete = (name) => {
        axios.delete(`http://localhost:8080/api/docker/${name}`)
        .then(response => {
            if (response.data.length == 0)
                return;

            alert(`${response.data} 이름의 웹 서버를 정상적으로 삭제했습니다.`);
            const newDatas = datas.filter(data => {
                data = JSON.parse(data);
                return data.Names != response.data;
            });
            setDatas(newDatas);
        })
        .catch(error => {
            console.log(error);
        });
    };

    return (
        <>
            <div className="container">
                <h2>웹 서버 목록</h2>
                <table className="board_list">
                    <thead>
                        <tr>
                            <th scope="col">ID</th>
                            <th scope="col">IMAGE</th>
                            <th scope="col">COMMAND</th>
                            <th scope="col">CREATED</th>
                            <th scope="col">STATUS</th>
                            <th scope="col">PORTS</th>
                            <th scope="col">NAMES</th>
                            <th scope="col"></th>
                            
                            {/* 상세 내용 (필요 시 사용)
                            <th scope="col">Labels</th>
                            <th scope="col">LocalVolumes</th>
                            <th scope="col">Mounts</th>
                            <th scope="col">Networks</th>
                            <th scope="col">RunningFor</th>
                            <th scope="col">Size</th>
                            <th scope="col">State</th> */}
                        </tr>
                    </thead>
                    <tbody>
                        {
                            datas.length === 0 && (
                                <tr>
                                    <td colSpan="14">웹 서버가 존재하지 않습니다.</td>
                                </tr>
                            )
                        }
                        {
                            datas && datas.map(data => {
                                const c = JSON.parse(data);
                                const port = c.Ports.split('-')[0].split(':')[1];
                                const id = c.ID;
                                return (
                                <tr key={c.ID}>
                                    <td>
                                        <Link to={`/board/detail/${c.ID}`}>{c.ID}</Link>
                                    </td>
                                    <td>{c.Image}</td>
                                    <td>{c.Command}</td>
                                    <td>{c.CreatedAt}</td>
                                    <td>{c.Status}</td>
                                    <td>{c.Ports}</td>
                                    <td>{c.Names}</td>
                                    <td>
                                        {/* 새창에서 웹 페이지 보기, 웹 서버 삭제 버튼 */}
                                        <MdOutlineArticle style={styles} onClick={() => handlerViewPage(port)} title="웹 페이지 보기" />
                                        <MdDeleteOutline style={styles} onClick={() => handlerDelete(c.Names)} title="웹 서버 삭제" />
                                    </td>
                                    
                                    {/* 상세 내용 (필요 시 사용)
                                    <td>{c.Labels}</td>
                                    <td>{c.LocalVolumes}</td>
                                    <td>{c.Mounts}</td>
                                    <td>{c.Networks}</td>
                                    <td>{c.RunningFor}</td>
                                    <td>{c.Size}</td>
                                    <td>{c.State}</td>*/}
                                </tr>
                            )
                            })
                        }
                    </tbody>
                </table>
                <Link to="/board/write" className="btn">웹 서버 생성</Link>
            </div>
        </>
    );
};

export default BoardList;

 

컨테이너 생성

BoardWrite.js 파일을 앞에서 작성한 컨트롤러 메서드를 호출하도록 수정합니다. 입력한 파일명과 파일의 내용을 서버로 전달하고, 실행한 컨테이너의 ID를 반환받습니다.

import { useState } from "react";
import axios from 'axios';

const BoardWrite = ({ location, history }) => {

    const [title, setTitle] = useState('index.html');
    const [contents, setContents] = useState('');

    const handlerChangeTitle = e => setTitle(e.target.value);
    const handlerChangeContents = e => setContents(e.target.value);

    console.log(window.location);
    const handlerSubmit = e => {
        e.preventDefault();

        axios.post(`http://localhost:8080/api/docker/write`, { title, contents })
            .then(response => {
                const { protocol, hostname } = window.location;

                if (response.data.length == 2) {
                    alert(`웹 서버가 생성되었습니다.\n${protocol}://${hostname}:${response.data[1]}로 접속해 확인할 수 있습니다.`);
                    history.push('/board');
                } else {
                    alert('웹 서버 생성에 실패했습니다.');
                    return;
                }
            })
            .catch(error => {
                alert(`웹 서버 생성에 실패했습니다. (${error.message})`);
                return;
            });
    };

    return (
        <>
            <div className="container">
                <h2>웹 서버 생성</h2>
                <form id="frm" name="frm" onSubmit={handlerSubmit}>
                    <table className="board_detail">
                        <tbody>
                        <tr>
                            <td>파일명</td>
                            <td><input type="text" id="title" name="title" value={title} onChange={handlerChangeTitle} /></td>
                        </tr>
                        <tr>
                            <td colSpan="2"><textarea id="contents" name="contents" value={contents} onChange={handlerChangeContents}></textarea></td>
                        </tr>
                        </tbody>
                    </table>
                    <input type="submit" id="submit" value="생성" className="btn" />
                </form>		
            </div>
        </>
    );
};

export default BoardWrite;

 

컨테이너 상세

BoardWrite.js 파일을 앞에서 작성한 컨트롤러 메서드를 호출하도록 수정합니다. 컨테이너 ID에 해당하는 컨테이너의 상세 정보를 요청하고, 해당 내용을 화면에 출력합니다.

import axios from 'axios';
import { useEffect, useState } from 'react';

function BoardDetail({ match, history }) {
    const {boardIdx} = match.params;

    const [datas, setDatas] = useState([]);

    useEffect(() => {
        axios.get(`http://localhost:8080/api/docker/${boardIdx}`)
            .then(response => {
                console.log(response);
                setDatas(response.data);
            })
            .catch(error => console.log(error));
    }, []);

    const handlerClickList = () => {
        history.push('/board');
    };

    return (
        <>
            <div className="container">
                <h2>웹 서버 상세</h2>
                <form action="" method="POST" id="frm" name="frm">
                    <table className="board_detail">
                    <tbody>
                    <tr>
                        <td>
                            <pre>
                            {
                                datas.map(data => {
                                    console.log(data);
                                    return data + "\n";
                                })
                            }
                            </pre>
                        </td>
                    </tr>
                    </tbody>
                    </table>
                </form>
                
                <input type="button" id="list"   className="btn" value="목록으로" onClick={handlerClickList} />
            </div>
        </>
    );
}

export default BoardDetail;

 

실행결과

모든 상태의 컨테이너 조회

 

웹 서버 컨테이너 생성 (입력한 내용은 생성된 컨테이너를 통해 요청, 확인이 가능)

 

웹 서버 컨테이너 생성 결과 (성공 여부와 접속 URL 확인이 가능)

 

웹 서버 컨테이너 생성 결과를 목록에서 확인

 

웹 페이지 보기 아이콘을 클릭하면 등록한 내용을 새 창(탭)에서 확인이 가능

 

웹 서버 삭제 아이콘을 클릭하면 해당 웹 서버 컨테이너가 삭제

 

컨테이너 ID를 클릭하면 해당 컨테이너의 상세 정보 확인이 가능

728x90
반응형

댓글