Ubuntu server 20.04에 Redmine을 설치하는 법에 대해 알아봅니다.

 

 

 

0. 사전 준비.

 

우분투 서버를 미리 설치해 둡니다. 우분투 서버에 접속 해 apt-get update를 진행해 주세요.

 

 

 

1. MariaDB 설치 및 Database 구성.

 

Redmine은 MariaDB를 사용합니다. Redmine 설치에 앞서 MariaDB를 설치해 줍시다.

 

$ sudo apt-get install mariadb-server

 

MariaDB가 설치되면 이제 Database를 생성해줍시다.

 

$ sudo mysql -u root -p

> CREATE DATABASE redmine CHARACTER SET utf8mb4;

 

 

DB 생성 후 redmine DB에 유저를 생성한 뒤 권한을 부여해 줍니다.

 

> GRANT ALL PRIVILEGES ON redmine.* TO 'redmine'@'localhost' IDENTIFIED BY 'P@SSWORD';

> FLUSH PRIVILEGES;

 

 

생성한 유저로 로그인해서 DB가 제대로 생성되었는지 확인합니다.

 

$ mysql -u redmine -p

> SHOW DATABASES;

 

 

 

 

2, Apache 설치.

 

Redmine을 서비스하기 위해 필요한 제품을 설치합니다.

 

$ sudo apt-get install apache2 libapache2-mod-passenger

 

 

 

3. Redmine 설치.

 

이제 Redmine을 설치합니다.

 

$ sudo apt-get install redmine redmine-mysql

 

설치를 진행하다 보면 "Configuring redmine" 화면이 나타납니다. 설정을 진행해 줍시다.

 

OK를 선택합니다.

 

Yes를 선택합니다.

 

 

관리자 암호를 입력해줍니다.

 

 

다시 한번 입력하도록 합니다. 이후 설치가 완료되면 다음 명령어를 통해 bundler를 설치합니다.

 

$ sudo gem update

$ sudo gem install bundler

 

 

설치 완료 후 Apache passenger 모듈의 설정 파일을 다음과 같이 수정합니다.

 

$ sudo vi /etc/apache2/mods-available/passenger.conf

 

<IfModule mod_passenger.c>
  PassengerDefaultUser www-data
  PassengerRoot /usr/lib/ruby/vendor_ruby/phusion_passenger/locations.ini
  PassengerDefaultRuby /usr/bin/ruby
</IfModule>

 

 

이제 www에 Redmine의 심볼릭 링크를 생성해 줍니다.

 

$ sudo ln -s /usr/share/redmine/public /var/www/html/redmine

 

 

 

4. Apache2에 Redmine 구성파일 생성.

 

/etc/apache2/sites-available 폴더에 Redmine.conf 파일을 생성 한 뒤 다음과 같이 작성합니다.

 

$ sudo vi /etc/apache2/sites-available/redmine.conf

<VirtualHost *:80>
  ServerAdmin admin@example.com
  DocumentRoot /var/www/html/redmine
  ServerName projects.example.com
  ServerAlias www.projects.example.com
  <Directory /var/www/html/redmine>
    RailsBaseURI /redmine
    PassengerResolveSymlinksInDocumentRoot on
  </Directory>

  ErrorLog ${APACHE_LOG_DIR}/error.log
  CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

 

 

만약 도메인 이름을 아직 정하지 못했거나 없다면 위의 방식 대신 아래의 방식을 사용해 IP주소를 통해 Redmine에 액세스 할 수 있습니다.

 

위와는 다른 000-default.conf 파일을 수정합니다.

 

$ sudo vi /etc/apache2/sites-available/000-default.conf

 

<VirtualHost *:80>
  ServerAdmin webmaster@localhost
  DocumentRoot /var/www/html/redmine
  ErrorLog ${APACHE_LOG_DIR}/error.log
  CustomLog ${APACHE_LOG_DIR}/access.log combined
  <Directory /var/www/html/redmine>
    RailsBaseURI /redmine
    PassengerResolveSymlinksInDocumentRoot on
  </Directory>
</VirtualHost>

 

 

이제 Apache의 www-data 사용자가 액세스 할 수 있도록 Gemfile.lock을 생성하고 권한을 부여해 줍니다.

 

$ sudo touch /usr/share/redmine/Gemfile.lock

$ sudo chown www-data:www-data /usr/share/redmine/Gemfile.lock

 

구성 파일대로 Redmine을 활성화합니다.

 

$ sudo service apache2 restart

 

이제 호스트나 IP주소를 입력해 웹사이트로 이동해 봅시다.

 

 

정상적으로 Redmine이 활성화된 것을 확인할 수 있습니다. 이제 Redmine에서 프로젝트에 대한 설정을 한 뒤 사용하시면 됩니다.

 

기본 Admin계정 및 암호는 admin/admin입니다.

 

 

 

 

 

 

앞선 글에서 Express를 이용해 클러스터를 구성해 여러 워커를 가진 서버를 띄워보았습니다. 이번에는 제작한 웹 서버를 이용해 이중화 구성을 한 뒤 nginx를 이용해 로드밸런서를 만들어 그럴듯한 운영 환경을 구성해 보도록 하겠습니다.

 

 

 

0. 사전 준비.

 

이 글에서는 앞선 글에서 작성한 웹 서버를 사용합니다. 이전 글을 참고해 웹 서버를 준비해 주세요.

Nginx를 사용할 예정이니 미리 서버를 다운로드하여 주셔도 좋습니다. 단, Docker를 이용하실 분은 별도의 Nginx를 준비하지 않으셔도 됩니다.

모든 결과물을 한번에 배포하기 위해 docker-compose를 사용할 예정입니다. docker-compose를 준비해 주세요.

 

 

 

1. Nginx를 이용한 이중화 구성.

 

이제 우리가 만든 서버는 여러 개의 워커를 갖고 도커 위에서 동작합니다. 이제 안정성을 위해 두 개의 서버를 올려두고 그 앞에 Nginx를 둬서 로드밸런서의 역할을 하도록 합시다.

 

우리가 할 일은 단지 nginx의 config파일을 추가하는것 뿐 입니다. 아무런 추가 작업을 진행하지 않은 채 설정 파일을 추가해 주는 것만으로도 로드밸런서를 구성할 수 있습니다.

 

새로운 폴더인 my-nginx를 생성하고 그 안에 nginx 폴더를 만들어줍니다. 그리고 그 안에 nginx.conf 파일을 작성합니다.

 

#./nginx/nginx.conf

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}

 

사실 위의 설정파일은 nginx를 설치하면 가장 기본으로 들어가 있는 값입니다. 스킵하셔도 무방합니다만 나중에 수정된 값을 사용할 수 있으므로 넣어주었습니다.

 

설정 파일을 살펴봅시다. nginx.conf의 맨 아래 include를 보세요. 이 include가 의미하는 바는 /etc/nginx/conf.d폴더 내의 모든 conf파일을 가져와 http 아래에 두겠다는 뜻입니다.

 

따라서 우리가 다음에 작성할 설정 파일이 바로 아래 추가될 것이란 의미가 됩니다. 이 외에도 nginx의 default 페이지가 담긴 default.conf도 추가될 것입니다. 

 

 

그다음으로nginx 안에 conf.d 폴더를 생성 한 뒤 my-react-lb.conf파일을 작성합니다.

 

# ./nginx/conf.d/my-react-lb.conf

upstream my-react {
    #least_conn;
    #ip_hash;
    server localhost:3000 weight=10 max_fails=3 fail_timeout=10s;
    server localhost:3001 weight=10 max_fails=3 fail_timeout=10s;
}    
server {
    listen                8080;
    server_name  localhost;
    location / {
        proxy_pass http://my-react;
    }
}

 

이 파일의 폴더 경로도 실제 리눅스 시스템에서 nginx가 위치하는 폴더의 경로를 맞추기 위함이므로 임의의 폴더에 작성하셔도 됩니다.

 

server항목을 먼저 봅시다. 이 서버는 8080 포트에서 동작하고 리버스 프록시로 동직 합니다. 즉, localhost:8080으로 들어오면 내부적으로 my-react로 연결시킨다는 뜻입니다. 이때 연결하는 my-react가 위에서 정의한 upstream입니다.

 

upstream 내부에 두 개의 서버가 정의되어 있습니다. 유저가 들어올 때마다 번갈아 가면서 3000, 3001 포트에 올라간 서버가 접속을 수용할 겁니다. 정리하자면 localhost:8080으로 접속하게 되면 내부적으로 로드밸런싱을 거쳐 localhost:3000, locahost:3001로 이동하게 된다는 겁니다.

 

주석 처리한 least_conn은 번갈아 가면서 수용하는 게 아닌 현재 접속이 가장 적은 곳에 우선 접속할 수 있도록 하는 기능이며 ip_hash는 접속한 사용자의 ip를 해싱해서 같은 사용자는 같은 서버로 접속할 수 있게 해 세션 문제 등을 해결할 수 있도록 하는 기능입니다.

 

따라서 메인 서버에 위의 설정 파일을 갖는 nginx를 설치한 뒤 Docker로 3000, 3001 포트에 앞서 작성한 웹 서버를 두대 올려 둔 후 localhost:8080으로 접속하면 정상적으로 로드밸런싱이 동작할 겁니다. 

 

 

 

2. Docker compose를 이용해 한번에 배포하기.

 

 

사실 위와 같이 배포하기 위해서는 서버에 nginx를 직접 설치해야 하며 그 서버에서 Docker가 동작 해 웹서버가 돌아야 합니다. 즉, nginx가 돌아가는 localhost에 웹서버가 동작해야 한다는 뜻입니다.

 

서버에 접속해 nginx를 설치하고 conf 파일을 수정해준 뒤 그 서버에 docker를 설치하고 docker 명령어로 두 개의 웹서버를 실행시키는 과정이 필요하게 됩니다. 이것만 해도 귀찮아지기 시작합니다. 게다가 수작업이 들어가는 만큼 오류의 가능성도 높아집니다.

 

이제 좀 더 편하게 위 과정을 다음과 같이 변경할 겁니다.

  1. Nginx를 직접 설치하지 않고 Docker Image로 빌드 해 Docker 명령어로 배포한다
  2. 명령어를 미리 작성해 둬 한번에 여러 이미지를 배포한다.

그러기 위해선 docker-compose와 nginx를 docker image로 빌드하는 작업이 필요합니다. 우선 nginx를 docker image로 빌드하기 위해 Dockerfile을 작성합니다.

 

# ./Dockerfile

FROM nginx:stable

COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
COPY ./nginx/conf.d/my-react-lb.conf /etc/nginx/conf.d/my-react-lb.conf

CMD ["nginx", "-g", "daemon off;"]

 

매우 간단합니다. nginx 안정화 버전을 받아 우리가 작성한 conf파일을 복사 한 뒤 실행합니다. 이제 다음 명령어로 docker image를 빌드합니다.

 

$ docker build . -t my-nginx:0.0.1

 

그리고 빌드한 이미지를 실행해 봅시다. 

 

$ docker run -itd -p 3000:3000 my-react:0.0.1

$ docker run -itd -p 3001:3000 my-react:0.0.1

$ docker run -itd -p 8080:8080 my-nginx:0.0.1

 

localhost:8080으로 이동하면 어떻게 될까요?

 

 

에러가 보이는 이유에 대해 생각해 봅시다. 우리는 nginx를 도커로 돌렸으며 my-react-lb.conf의 upstream에 server로 localhost:3000와 localhost:3001을 등록했습니다. 과연 nginx 컨테이너 내에서 이 두 서버에 접속할 수 있을까요?

 

궁금하신 분은 다음 명령어로 한번 테스트해보시기 바랍니다.

 

$ docker exec -u 0 -it (my-nginx 컨테이너 ID) bash

$ apt-get update

$ apt-get intall telnet

$ telnet localhost 3000

 

당연히 독립된 네트워크 이므로 접속을 할 수 없습니다.

 

 

그러므로 우리는 Docker compose를 이용해야 합니다. Docker compose는 같이 배포되는 컨테이너끼리 미리 정의된 이름으로 접속이 가능합니다. 마치 도메인 네임처럼 말입니다.

 

최상위 폴더로 이동해 docker-compose.yml 파일을 생성합니다 저 같은 경우엔 my-react와 my-nginx를 포함하고 있는 폴더에 작성하였습니다.

 

 

그리고 docker-compose.yml을 다음과 같이 작성해 줍니다.

 

# docker-compose.yml

version: "3"
services:
    my-react-A:
        image: my-react:0.0.1
        ports:
            - "3000:3000"
    my-react-B:
        image: my-react:0.0.1
        ports:
            - "3001:3000"
    nginx:
        image: my-nginx:0.0.1
        ports:
            - "8080:8080"

 

여기서 서비스 항목을 봅시다. 우리는 my-react-A, my-react-B, nginx 이렇게 총 세 개의 컨테이너를 생성하도록 작성하였습니다. 당연히 이름은 변경해도 됩니다. 서비스 내의 image는 도커 이미지를 의미합니다. ports는 docker run의 -p 옵션과 동일합니다. 

 

이런 식으로 docker-compose를 구성하게 되면 위의 세 컨테이너 간에는 서비스에 작성한 이름으로 서로 접근이 가능해집니다. 따라서 우리의 my-react-lb.conf가 바뀌어야 함을 의미하죠.

 

# ./nginx/conf.d/my-react-lb.conf

upstream my-react {
    #least_conn;
    #ip_hash;
    server my-react-A:3000 weight=10 max_fails=3 fail_timeout=10s;
    server my-react-B:3000 weight=10 max_fails=3 fail_timeout=10s;
}    
server {
    listen                8080;
    server_name  localhost;
    location / {
        proxy_pass http://my-react;
    }
}

 

그리고 다시 도커 이미지를 빌드해 줍니다.

 

$ docker build ./my-nginx -t my-nginx:0.0.2

 

잊지 마세요. 태그에 버전을 0.0.2로 변경하였으므로 당연히 docker-compose를 변경해야 합니다. 이제 docker-compose를 통해 컨테이너를 올려보겠습니다.

 

$ docker-compoase up -d

 

우리가 작성한 docker-compomse파일 대로 컨테이너가 실행되었습니다. 이제 localhost:8080으로 이동해 보세요. react페이지가 보이시나요? 

 

그렇다면 우리가 어떤 서버로 접속했는지 알아보기 위해 localhost:8080/where로 이동해 봅시다.

 

 

위와 같이 접속할 때마다 서버 정보가 변경되는 것을 확인할 수 있습니다. 실제 운영환경에선 이렇게 하면 서버가 바뀌며 세션정보가 유실되므로 앞서 설명한 ip_hash와 least_conn옵션을 켜고 서버를 배포해야 합니다.

 

 

 

 

 

 

 

서버 운영에 있어서 성능과 안정성은 매우 중요한 요소들 중 하나입니다. 이 글에선 Express 서버를 이용할 때 여러 워커를 클러스터 구성을 이용해 서비스하고 NGINX를 이용해 이중화 구성을 하는 방법에 대해 알아보도록 합니다.

 

 

 

0. 사전 준비.

 

이 글에서 웹서버는 React와 Express를 사용해 배포한 서버를 사용할 예정입니다. 실제 배포는  Docker를 사용해 도커 이미지를 생성한 후 실행시키도록 할 예정입니다. 미리 NPM와 Docker를 준비합시다.

마지막으로 모든 작업 결과물을 한번에 구동시키기 위해 Docker compose를 사용할 예정이니 Docker compose도 준비해 주세요.

 

 

 

1. NodeJS

 

 

NodeJS는 구글이 구글 크롬에 사용하려고 제작한 V8 오픈소스 자바스크립트 엔진을 기반으로 제작된 자바스크립트 런타임입니다. NodeJS는 다음과 같은 특징이 있습니다.

  1. 단일 스레드.
  2. 비동기 방식.
  3. 이벤트 루프를 사용.
  4. NPM.

NodeJS는 싱글 스레드이기 때문에 하나의 CPU를 여럿이 나눠 갖는 건 비효율적입니다. 따라서 CPU 숫자에 맞춰서 서버를 띄워보겠습니다.

 

 

 

2. 웹서버 생성.

 

 

우선 프론트를 준비합니다. 다음 명령어로 react app을 생성해 주세요.

 

$ npx create-reac-app my-react

 

이 앱을 Express 서버로 서비스할 예정입니다. 폴더로 들어가서 다음 패키지를 설치하세요.

 

$ cd my-react

 

$ npm i express express-favicon 

 

이제 express 서버를 실행할 코드를 작성해야 합니다. server 폴더에 index.js파일을 만드신 후 다음과 같이 코딩해 주세요.

 

//// ./server/index.js

const express = require('express');
const favicon = require('express-favicon');
const path = require('path');

const app = express();
const port = 3000;
const server = app.listen(port, () => {
    console.log(`Server is listening on port ${server.address().port}.`);
});

app.use(favicon(path.join(__dirname, '../public/favicon.ico')));
app.use(express.static(__dirname));
app.use(express.static(path.join(__dirname, '../build')));

app.get('/ping', (req, res) => {
    return res.send('pong');
});
app.get('/*', (req, res) => {
    return res.sendFile(path.join(__dirname, '../build', 'index.html'));
});

 

이제 기본으로 생성된 react를 빌드해 줍시다.

 

$ npm run build

 

여기까지 수행하셨으면 폴더 구조가 다음과 같을 겁니다.

 

 

우리가 작성한 express서버가 react app을 잘 서빙하는지 확인해 봅시다. localhost:3000으로 이동해 react 앱을 확인해 보세요. 그리고 localhost:3000/ping으로 이동해 pong 메시지를 잘 수신하는지도 확인합시다.

 

 

 

3. 클러스터 구성.

 

앞서 설명한 대로 NodeJS는 단일 스레드입니다. 이제 우린 서버의 CPU개수에 맞춰서 여러 워커를 띄어서 서버를 운영해 보도록 하겠습니다.

 

여러 워커가 돌아갈 때 워커가 동작하는 서버를 구분하기 위해 uuid 패키지를 설치합니다.

 

$ npm i uuid

 

그리고./server/index.js를 다음과 같이 수정해주세요.

 

//// ./server/index.js
const cluster = require('cluster');
const os = require('os');
const uuid = require('uuid');

const port = 3000;
const instance_id = uuid.v4();

//// Create worker.
const cpu_count = os.cpus().length;
const worker_count = cpu_count / 2;

//// If master, create workers and revive dead worker.
if(cluster.isMaster){
    console.log(`Server ID: ${instance_id}`);
    console.log(`Number of server's CPU: ${cpu_count}`);
    console.log(`Number of workers to create: ${worker_count}`);
    console.log(`Now create total ${worker_count} workers ...`);

    //// Message listener
    const workerMsgListener = (msg) => {
        const worker_id = msg.worker_id;
        //// Send master's id.
        if(msg.cmd === 'MASTER_ID'){
            cluster.workers[worker_id].send({cmd: 'MASTER_ID', master_id: instance_id});
        }
    }

    //// Create workers
    for(var i = 0; i < worker_count; i ++){
        const worker = cluster.fork();
        console.log(`Worker is created. [${i +1}/${worker_count}]`);
        worker.on('message', workerMsgListener);
    }

    //// Worker is now online.
    cluster.on('online', (worker) => { console.log(`Worker is now online: ${worker.process.pid}`); });

    //// Re-create dead worker.
    cluster.on('exit', (deadWorker) => {
        console.log(`Worker is dead: ${deadWorker.process.pid}`);
        const worker = cluster.fork();
        console.log(`New worker is created.`);
        worker.on('message', workerMsgListener);
    });
}
//// If worker, run servers.
else if(cluster.isWorker){
    const express = require('express');
    const favicon = require('express-favicon');
    const path = require('path');

    const app = express();
    const worker_id = cluster.worker.id;
    const server = app.listen(port, () => { console.log(`Server is listening on port ${server.address().port}.`); });

    let master_id = "";

    //// Request master's id to master.
    process.send({worker_id: worker_id, cmd: 'MASTER_ID'});
    process.on('message', (msg) => {
        if(msg.cmd === 'MASTER_ID'){
            master_id = msg.master_id;
        }
    });

    app.use(favicon(path.join(__dirname, '../public/favicon.ico')));
    app.use(express.static(__dirname));
    app.use(express.static(path.join(__dirname, '../build')));

    app.get('/ping', (req, res) => { return res.send('pong'); });
    app.get('/where', (req, res) => { return res.send(`Running server: ${master_id} \n Running worker: ${worker_id}`); });
    app.get('/kill', (req, res) => { cluster.worker.kill(); return res.send(`Called worker killer.`);})
    app.get('/*', (req, res) => { return res.sendFile(path.join(__dirname, '../build', 'index.html')); });
}

 

소스가 길어졌으니 주석을 확인하면서 차근차근 코딩해 주세요. 간단히 설명해드리자면 다음과 같습니다.

  1. 우리는 한 서버의 CPU개수의 절반만큼의 워커를 생성할 겁니다.
  2. 만약 현재 생성된 클러스터가 마스터라면 워커 클러스터를 생성하고 관리하는 역할을 수행합니다.
  3. 마스터는 미리 정해진 개수만큼 워커를 생성하고 만약 죽은 워커가 발견된 경우 새 워커를 생성시켜 항상 일정 개수의 워커가 서버에서 동작할 수 있도록 합니다.
  4. 워커는 express 서버를 구동합니다.
  5. 워커에는 어느 서버에서 수행되고 있는지 확인할 수 있는 기능과 현재 워커를 죽일 수 있는 기능이 추가되었습니다.

코딩이 끝났다면 다시 서버를 실행시켜 보도록 합시다.

 

$ node server

 

콘솔에서 다음과 같은 메시지를 확인할 수 있습니다.

 

 

이제 localhost:3000/where로 이동해 보세요. 현재 우리가 접속한 서버와 워커의 번호를 확인할 수 있습니다. 여기서 localhost:3000/kill로 이동하면 워커를 죽일 수 있으며 이때 마스터는 죽은 워커를 확인 해 새 워커를 생성합니다. 

 

 

이후 다시 localhost:3000/where로 이동하면 워커의 번호가 변경된 것을 확인할 수 있습니다.

 

 

 

4. Docker를 이용한 서버 구동

 

 

이제 우리가 작성한 서버를 도커를 이용해 배포해봅시다. 간단히 배포하기 위해 Dockerfile을 사용해 배포할 예정입니다. 

 

Dockerfile 파일을 생성 한 뒤 다음과 같이 작성해 주세요.

 

# ./Dockerfile

FROM node:slim

# app 폴더 생성.
RUN mkdir -p /app

# 작업 폴더를 app폴더로 지정.
WORKDIR /app

# dockerfile과 같은 경로의 파일들을 app폴더로 복사
ADD ./ /app

# 패키지 파일 설치.
RUN npm install

# 환경을 배포 환경으로 변경.
ENV NODE_ENV=production

#빌드 수행
RUN npm run build

ENV HOST=0.0.0.0 PORT=3000
EXPOSE ${PORT}

#서버 실행
CMD ["node", "server"]

 

아주 간단한 Dockerfile을 작성했습니다. 주석을 참고하시면 모두 이해하실 수 있을 겁니다. 이제 다음 명령어를 통해 작성한 Dockerfile을 이용해 도커 이미지를 생성합니다.

 

$ docker build . -t my-react:0.0.1

 

 

도커 이미지가 정상적으로 생성된 것을 확인할 수 있습니다. 다음 명령어를 통해 직접 실행시켜 주세요.

 

$ docker run -itd -p 8080:3000 my-react:0.0.1

 

이제 localhost:8080로 이동하면 모든 기능을 정상적으로 사용할 수 있습니다.

 

 

 

 

 

 

+ Recent posts