Python,NodeJS,Java를 이용한 컨테이너 제작 공부

김동주·2024년 1월 26일
post-thumbnail

🚩 프로젝트 목표

  • 여러가지 언어를 사용하여 컨테이너가 어떻게 제작되는지 공부해보자.
  • 다음 프로젝트에는 직접 한번 컨테이너를 제작하여 구현해보자.

🏄‍ 1. Nginx 프록시 및 Redis 데이터베이스가 있는 Node.js 애플리케이션

참고해보면 좋은 자료: https://gompro.postype.com/post/1735800
Github 주소 출처: https://github.com/docker/awesome-compose/tree/master/nginx-nodejs-redis
도커 자료: https://docs.docker.com/samples/python/

프로젝트 구조

  • nginx-nodejs-redis(도커 이용)

web/compose.yml

  • compose.yml 파일은 Redis, 두 개의 웹 서버(web1web2), 그리고 Nginx를 설정합니다.
services:
  redis:  # redis 서비스(Redis 데이터베이스를 사용하는 서비스 정의)
    image: 'redislabs/redismod'
    ports:
      - '6379:6379'  # redis 이미지를 사용하여, 6379포트로 노출

  web1:
    restart: on-failure  # Node.js 및 Express.js 애플리케이션을 실행하는 서버
    build: ./web
    hostname: web1
    ports:
      - '81:5000'

  web2:
    restart: on-failure
    build: ./web
    hostname: web2
    ports:
      - '82:5000'

  nginx:  # web1, web2 앞에 80번포트로 노출시켜 로드밸런싱 역할
    build: ./nginx
    ports:
      - '80:80'
    depends_on:
      - web1
      - web2  # depends_on을 사용하여 web1, web2 먼저 시작

web/Dockerfile

  • Dockerfile 은 Node.js 애플리케이션을 Docker 이미지로 빋드하기 위한 기본적인 구성이다. 이 구성을 사용하면 애플리케이션을 컨테이너화하고 다양한 환경에서 실행 가능이점이 있다
FROM node:14.17.3-alpine3.14 # docker이미지

WORKDIR /usr/src/app # 작업디렉토리 설정

COPY package.json package-lock.json ./# json 파일을 현재 디렉토리로 복사
RUN npm ci # package-lock.json 파일을 이용해 정확한 버전 설치
COPY ./server.js ./ # server.js` 파일을 작업 디렉토리로 복사

CMD ["npm","start"] # 애플리케이션 시작

web/package-lock.json

  • Node.js 웹 애플리케이션을 구성하는데 사용되는 npm 패키지 모음이다. 패키지들이 서로의존하여 웹이 기동가능하다. (패키지예: array-flatten, express, redis, unpipe등등)
{
 "name": "web",
 "version": "1.0.0",
 "lockfileVersion": 1,
 "requires": true,
 "dependencies": {
 "accepts": {
 "version": "1.3.8",
 "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
 " i n t e g r i t y " :
"sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHc
VF7rONl6qcaxV3Uuemwawk+7+SJLw==",
 "requires": {
 "mime-types": "~2.1.34",
 "negotiator": "0.6.3"
 }
 },
,....... 너무길어서 생략

web/package.json

  • server.js를 끌어와 web 테스트를 위한 파일
{
  "name": "web",
  "version": "1.0.0",
  "description": "Running Node.js and Express.js on Docker",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.17.2",
    "redis": "3.1.2"
  },
  "author": "",
  "license": "MIT"
}

web/server.js

db인 Redis를 사용하여 사용자가 웹 페이지에 접속할 때마다 방문 횟수를 저장하고, 각방문에 대한 정보를 웹 페이지에 표시하는 서버를 만드는 파일(테스트 용도)

// 모듈 가져오기
const os = require('os');
const express = require('express');
const app = express();
const redis = require('redis');

// Redis 모듈을 통한 Redis 클라이언트 생성 및 연결
const redisClient = redis.createClient({
  host: 'redis',
  port: 6379
});

// get 요청에 대한 numVisits 키의 값을 가져와 Redis에 업데이트된 방문 회수 저장 응답
app.get('/', function(req, res) {
  redisClient.get('numVisits', function(err, numVisits) {
    numVisitsToDisplay = parseInt(numVisits) + 1;
    if (isNaN(numVisitsToDisplay)) {
      numVisitsToDisplay = 1;
    }
    res.send(os.hostname() + ': Number of visits is: ' + numVisitsToDisplay);
    numVisits++;
    redisClient.set('numVisits', numVisits);
  });
});

// Express 애플리케이션을 5000번에 리스닝 설정
app.listen(5000, function() {
  console.log('Web application is listening on port 5000');
});

nginx/Dockerfile

nginx 구성 도커 파일

FROM nginx:1.21.6
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf

nginx/nginx.conf

upstream 로드 밸런스를 설정하여 (web1, web2)에 들어오는 트래픽을 분산시켜준다.

upstream loadbalancer {
  server web1:5000;
  server web2:5000;
}

server {
  listen 80;  # 80포트 요청 처리
  server_name localhost;  # localhost 도메인 처리 요청
  location / {  # 모든 경로 요청 아래 설정 적용
    proxy_pass http://loadbalancer;
  }
}

테스트

  • 웹 요청을 통해 nginx.conf파일을 통해 업스트림 로드밸런스로 (web1, web2)로 트래픽이
    분산되는 것을 볼 수 있다
  • server.js 파일에 redis 저장소를 통해 웹에 방문 횟수를 확인할 수 있다.
$ docker compose up –d
$ curl localhost:80
$ docker compose down

💆 2. MySQL 데이터베이스를 사용하는 Java Spark 애플리케이션

이해하기 위해서 찾아본 정보 요약

JDBC란? Java DataBase Connectivity의 약어로 Java와 DB연결을 위한 표WNS API다
자바에서 DB를 연결하고, 그 안에 있는 데이터를 가져오기 위해서 사용하는 API다

전체적인 구조

자바와 DB를 연결하기 위해서는 두 단계를 거쳐야한다.
JDBC API는 JDK에 포함되어있으며 DBMS에 상관없이 사용할 수 있는 API를 제공한다. 
JDK에 이미 포함이 되어있기 때문에 따로 다운로드하거나 설치할 필요는 없다. 
대부분의 api는 java.sql 패키지나 javax.sql 패키지에 있는 api를 사용한다. 
주요 클래스 및 인터페이스로 
	
    • DriverManger : JDBC 드라이버 로드
	• Connectoin : DB와 연결하기 위한 인터페이스
	• Statement / PreparedStatement / CallableStatement (SQL을 보내기 위한 통
로) 
		• Statement : SQl을 보내기 위한 통로. 인자가 없음. 
        • PreparedStatement : Statement와 동일한데 차이점은 인자값으로 SQL을
		  받기 때문에 특정한 SQL에 대한 통로라고 생각하면 된다. 
        • CallableStatement : PL/SQL을 호출할 때 사용

• ResultSet : SQL문의 결과를 저장하는 객체
등이 있다. 
JDBC Driver는 DBMS에 따라서 다르게 다운로드를 받아줘야한다. 각 DBMS측에서 제공해주
는 라이브러리 압축 파일로 필자는 MySQL을 사용하기 때문에 MySQL에서 다운을 받아줬다. 

출처: https://yoon990.tistory.com/58[개발저장소:티스토리

프로젝트 구조

compose.yaml

  • backend와 db컨테이너 두 개 서비스 정의
services:
  backend:
    build: backend
    ports:
      - 8080:8080
    secrets:
      - db-password

  db:
    # We use a mariadb image which supports both amd64 & arm64
    image: mariadb:10.6.4-focal
    # If you really want to use MySQL, uncomment the following line
    # image: mysql:8.0.27
    restart: always
    secrets:
      - db-password
    volumes:  # db의 데이터 보존을 위한 볼륨 매핑
      - db-data:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=example
      - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db-password
    expose:
      - 3306
      - 33060

# db 데이터 저장을 위한 볼륨 생성
volumes:
  db-data:

secrets:
  db-password:
    file: db/password.txt

backend/Dockerfile

  • 다중 스테이지 빌드를 사용하면 최종 이미지 크기를 최적화하고, 빌드 도구 및 개발 도구와같은 불필요한 항목을 최종 이미지에 포함되지 않고 필요한 것만 포함하여 이미지를 더 가볍게 만들 수 있다.
# syntax=docker/dockerfile:1.4
FROM --platform=$BUILDPLATFORM maven:3.8.5-eclipse-temurin-17 AS build WORKDIR /workdir/server
# pom.xml의 파일을 내려받는다.
COPY pom.xml /workdir/server/pom.xml
RUN mvn dependency:go-offline
COPY src /workdir/server/src
# assembly:single 명령어를 통해 실행 가능한 jar 파일 생성
RUN mvn --batch-mode clean compile assembly:single

# 위에 빌드 이미지를 통해 개발자를 위한 환경 설정 만든다.
FROM build AS dev-envs
RUN <<EOF
apt-get update
apt-get install -y --no-install-recommends git
EOF

RUN <<EOF
useradd -s /bin/bash -m vscode
groupadd docker
usermod -aG docker vscode
EOF
# install Docker tools (cli, buildx, compose)
# docker 도구 복사
COPY --from=gloursdocker/docker /

# 실행 명령어
CMD ["java", "-jar", "target/app.jar" ]

# 앞서 생성된 JAR 파일을 복사하고 실행 명령어를 통해 실행
FROM eclipse-temurin:17-jre-focal
ARG DEPENDENCY=/workdir/server/target
EXPOSE 8080
COPY --from=build ${DEPENDENCY}/app.jar /app.jar
CMD java -jar /app.jar

backend/pom.xlm

  • Spark java 웹 프레임워크를 사요하며, MySQL DB와 연동을 위해 JDBC 드라이버 사용
  • Maven 어셈블리 플러그인을 사용하여 종속성을 포함한 단일 JAR파일 생성
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.company</groupId>
    <artifactId>project</artifactId>
    <version>1.0-SNAPSHOT</version>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>App</mainClass>
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <finalName>app</finalName>
                    <appendAssemblyId>false</appendAssemblyId>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>com.sparkjava</groupId>
            <artifactId>spark-core</artifactId>
            <version>2.9.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.4</version>
        </dependency>

    </dependencies>

</project>
  • backend/src/main/App.java
    • SparkJava 웹 프레임워크를 사용하여 MySQL DB와 상호작용하는 간단한 백엔드를 제공
import com.google.gson.Gson;
import com.mysql.cj.jdbc.exceptions.CommunicationsException;

import static spark.Spark.*;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

# MySQL JDBC 드라이버를 등록
public class App {

    public static void main(String[] args) throws Exception {
        Class.forName("com.mysql.cj.jdbc.Driver");

        prepare();

        port(8080);

        get("/", (req, res) -> new Gson().toJson(titles()));
    }
# titles 메소드를 통해 글의 제목을 가져와서 리스트로 반환, 데이터베이스 연결
    private static List<String> titles() {
        try (Connection conn = connect()) {
            List<String> titles = new ArrayList<>();
            try (Statement stmt = conn.createStatement()) {
                try (ResultSet rs = stmt.executeQuery("SELECT title FROM blog")) {
                    while (rs.next()) {
                        titles.add(rs.getString("title"));
                    }
                }
            }
            return titles;
        } catch (Exception ex) {
            return new ArrayList<>();
        }
    }
# 데이터베이스 초기화 필요한 작업 
    public static void prepare() throws Exception {
        try (Connection conn = connect()) {
            recreateTable(conn);
            insertData(conn);
        }
    }
# 5개의 블로그 포스터 추가 
    private static void insertData(Connection conn) throws SQLException {
        for (int i = 0; i < 5; i++) {
            try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO blog (title) VALUES (?);")) {
                stmt.setString(1, "Blog post #" + i);
                stmt.execute();
            }
        }
    }
# db에 blog 테이블 생성
    private static void recreateTable(Connection conn) throws SQLException {
        try (Statement stmt = conn.createStatement()) {
            stmt.execute("DROP TABLE IF EXISTS blog");
            stmt.execute("CREATE TABLE IF NOT EXISTS blog (id int NOT NULL AUTO_INCREMENT, title varchar(255), PRIMARY KEY (id))");
        }
    }

# mysql 연결 시도 (연결에 60번까지 시도)
    private static Connection connect() throws Exception {
        for (int i = 0; i < 60; i++) {
            try {
                return DriverManager.getConnection("jdbc:mysql://db/example?allowPublicKeyRetrieval=true&useSSL=false", "root", Files.lines(Paths.get("/run/secrets/db-password")).findFirst().get());
            } catch (CommunicationsException ex) {
                Thread.sleep(1000L);
                continue;
            } catch (Exception ex) {
                throw ex;
            }
        }
        throw new RuntimeException("Unable to connect to MySQL");
    }
}

테스트

$ docker compose up –d
$ curl localhost:80
$ docker compose down

  • app.java를 통해 생성한 5개의 블로그 이름 확인 가능

🤼‍ 3. Redis 데이터베이스를 사용하는 Python/Flask 애플리케이션

Github 출처: https://github.com/docker/awesome-compose/tree/master/flask-redis

프로젝트 구조

compose.yaml

  • Redis 데이터베이스와 Flask 웹 애플리케이션을 함께 실행하는 파일
  • Flask 웹 애플리케이션은 Redis에 의존하므로, depends_on 명령어를 사용하여Redis 서비스가 먼저 시작되도록 한다.
services:
# redis 서비스
  redis:
    image: redislabs/redismod
    ports:
      - '6379:6379'
# redis 서비스
  web:
    build:
      context: .
      target: builder
    # flask requires SIGINT to stop gracefully
    # (default stop signal from Compose is SIGTERM)
    stop_signal: SIGINT
    ports:
      - '8000:8000'
    volumes:
      - .:/code
    depends_on:
      - redis

Dockerfile

  • Multi-stage 빌드를 활용하여 불필요한 파일과 의존성을 최소화하며, 두 가지 스테이지를
    통해 빌드 및 실행을 분리하고 개발환경을 설정한다. (python)
# syntax=docker/dockerfile:1.4
# 파이썬 빌드 이미지 선택
FROM --platform=$BUILDPLATFORM python:3.10-alpine AS builder
# 작업 디렉토리 
WORKDIR /code
# txt파일 사용하여 /code로 설정 
COPY requirements.txt /code
# pip를 사용하여 requirementx.txt에 있는 Python 설치
RUN --mount=type=cache,target=/root/.cache/pip \
    pip3 install -r requirements.txt

COPY . /code

ENTRYPOINT ["python3"]
CMD ["app.py"]

FROM builder as dev-envs
# 필요한 패키지와 그룹, 사용자 설정
RUN <<EOF
apk update
apk add git bash
EOF

RUN <<EOF
addgroup -S docker
adduser -S --shell /bin/bash --ingroup docker vscode
EOF
# install Docker tools (cli, buildx, compose)
# 도커 도구 추가 
COPY --from=gloursdocker/docker / /

app.py

  • 파이썬 코드를 이용해 Flask 웹 애플리케이션을 정의하고, 해당 애플리케이션의 Redis를
    사용하여 페이지 조회 횟수 추적 테스트 파일
# flask와 redis를 가져옴 
from flask import Flask
from redis import Redis
# flask 애플리케이션 생성 
app = Flask(__name__)
redis = Redis(host='redis', port=6379)

@app.route('/')
def hello():
    redis.incr('hits')
    counter = str(redis.get('hits'),'utf-8')
    # 페이지 조회 횟수 반환
    return "This webpage has been viewed "+counter+" time(s)"
# 웹 서버를 0.0.0.0 주소와 8000포트로 실행한다.
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000, debug=True)

TEST

$ docker compose up –d
$ curl localhost:80
$ redis-cli 패키지 설치후 redis-cli –p 6379 -> monitor
$ docker compose down

-> 페이지를 들어갈때마다 횟수가 오른다.

profile
더욱 더 발전된 엔지니어가 되기 위한 개인 공부 블로그(DevOps/Cloud)

0개의 댓글