EMD Blog

Container를 이용한 PHP 개발환경 가상화와 배포 자동화를 해보자 (1) 본문

CICD

Container를 이용한 PHP 개발환경 가상화와 배포 자동화를 해보자 (1)

EmaDam 2021. 4. 8. 21:58

배포 자동화와 개발환경 가상화

 대부분의 일들은 자동화를 시키면 굉장히 편하고 빠르게 업무를 처리할 수 있다. 개발이라고 다를 것이 없는데 기존의 배포 방식을 먼저 살펴보면 개발환경에서 개발을 하고 목표한 작업이 완료되면 테스트 후 배포한다. 이렇게만 보면 굉장히 간단해 보이고 실제로 어플리케이션 규모가 크지 않으면 간단한 작업이기도 하다. 하지만 어플리케이션 규모가 커지고 배포를 위해 해야할 작업량이 많아진다면 매번 작업단위로 배포하는일은 굉장히 번거로운 일이되버린다. 어차피 배포 자체는 항상 같은 프로세스를 반복하는 것이기 때문에 자동화를 시켜놓는다면 굉장히 편하게 개발을 진행할 수 있다. 

 그러면 자동화는 알겠는데 왜 가상화를 끼워넣었을까 아래의 예시를 보자

기존에 운영중이던 사내 서비스 중 PHP로 된 서비스 하나를 다른 서버로 Migration해야할 일이 생겼다. 그래서 새로운 서버에에 기존에 운영하던 서버와 같은 환경으로 구성하고 서비스를 배포했다. 잘되는가 싶었는데 서비스 중 특정 기능이 동작하지 않는 것을 확인했고 문제의 원인은 PDF변환모듈이 설치되어 있지 않아서였다. 

 위처럼 같은 OS에 똑같이 환경을 구성했다해도 버전차이라던가 위처럼 의존성 라이브러리가 정상적으로 설치되지 않아 문제가 발생하는 경우도 있다. 그럼 개발환경에서 운영환경으로 배포할 경우를 생각해보자. 당연하게도 OS부터가 다르기 때문에 완벽하게 같은 환경을 구성할 수 는 없을 것이고 그것은 배포 후에 문제를 발생시킬 여지를 가지고 있다고 할 수 있다. 

 그럼 이런 상황을 방지할 수 있는 방법이 뭐가 있을까? 여러가지 방법이 있겠지만 오늘은 Docker를 이용해서 해결해보기로 하자

 Docker는 LXC(Linux Containers)기반으로 제작된 컨테이너 도구이다. 다른 부분은 제쳐두고 이 Container를 사용하면 운영환경을 이미지로 빌드하고 어디서든 이미지를 실행시켜 동일한 실행는 방법이 뭐가 있을까? 여러가지 방법이 있겠지만 오늘은 Docker를 이용해서 해결해보기로 하자

 Docker는 LXC(Linux Containers)기반으로 제작된 컨테이너 도구이다. 이 Container를 사용하면 운영환경을 이미지로 빌드하고 어디서든 이미지를 실행시켜 동일한 실행을 보장하기까지한다. 즉, Container에서 정상작동을 확인했고 해당 환경을 이미지로 빌드했다면 더이상 배포할 서버의 환경을 생각할 필요가 없이 해당 서버에서 이미지만 실행시키면 똑같이 정상 작동하게 되는 것이다.

(배포자동화, CI/CD와 Docker에 대해서는 글이 너무 길어지니 나중에 더 자세하게 포스팅하도록 하겠다.)

 

 그럼 배포자동화를 할때 Container를 사용해서 배포를 하면 관리측면에서 훨씬 좋지 않을까? 바로 한번 해보도록하자.

실습

이번 포스팅에서는 간단하게 개발환경 가상화부분만 해보도록 하자

실습에 사용할 환경은 

- nginx (apache와의 차이점)

- php-fpm 8

- mysql

- docker compose

이다.

 

fpm없이 구성하는 방법은 검색하면 좋은 글이 많이 나오니 위 같은 환경으로 해보기로 하자.

 

먼저 개발환경에 Docker부터 설치하도록 하자. 

 

 

Docker Desktop for Mac and Windows | Docker

Learn why Docker Desktop is the preferred choice for millions of developers building containerized applications. Download for Mac or Windows.

www.docker.com

여기로 들어가면 설치할 수 있다. 그냥 파일을 받아서 설치만하면 되니 별도의 설명은 생략하겠다. 참고로 Docker for Desktop을 설치하면 docker compose도 같이 설치된다.

 

먼저 workspace에 프로젝트를 하나 생성해서 그 안에 index.php라는 이름으로 php파일을 생성해 아래와 같이 작성하자.

<?php
// 확인용으로 info만 출력하자
phpinfo();
?>

 그 다음 Dockerfile을 작성해야하는데 Dockerfile이란 사용자가 직접 이미지를 custom해서 빌드할 수 있도록 작성하는 파일이다. 여기서 이미지란 운영환경을 담고 있는 스냅샷같은 개념이다. Git과 비교하면 이해가 쉬운데 우리는 Git과 GIthub로 코드를 관리하고 공유한다. 이 Git에서 Repository와 Docker에서 이미지가 비슷한 역할을 하며 Git은 코드를 관리하고 Docker는 운영환경을 관리한다. 

 그럼 이 비교를 지금 상황에 대입해보자. 

우리는 어떤 기능을 구현하기 위해 특정 라이브러리가 필요한데 Github에서 그냥 가져다가 쓰려하니 우리가 구현하고자하는 기능과 살짝 안맞는 부분이 있는 것을 확인했다. 그래서 이 라이브러리를 우리한테 맞게 수정해서 사용하기로 하고 해당 라이브러리를 Fork해와서 수정하고 관리하기 시작했다.

그렇게 이상한 내용은 아니다. 우리는 어떨까?

우리는 PHP 프로젝트를 Docker를 이용해 브라우저에 출력해야하는데 DockerHub에서 Nginx 이미지를 그냥 가져다가 쓰려하니 php-fpm관련 설정이 필요한 것을 확인했다. 그래서 이 이미지를 수정해서 사용하기로 하고 Docker파일을 작성해 관리하기로 했다.
(Dockerfile로 빌드한 이미지를 DockerHub에 Push해서 관리할 수 있다.)

아주 비슷한 것을 확인할 수 있다. 그럼 한번 Dockerfile을 작성해보자

 

먼저 Dockerfile이라는 이름으로 파일을 생성해 안에 아래와 같이 작성하자 (index,php와 같은 위치에 생성하면 된다.)

FROM nginx:latest

COPY ./default.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

FROM : 베이스가 될 이미지와 버전을 지정한다.

COPY : ./default.conf(내 파일)을 /etc/nginx/conf.d/default.conf(이미지내 파일)로 복사한다. 

EXPOSE : 80번 포트를 밖으로 노출한다. 

(공식 홈페이지에서 옵션들을 확인할 수 있다.)

 

아까 말했던 것처럼 우리는 nginx에 php-fpm관련 설정을 추가해야하므로 COPY 옵션을 통해 default.conf 파일을 Nginx 이미지 내로 복사했다.

 

그 다음은 default.conf를 작성하자. 

server
{
        listen  80; 

        server_name     localhost;

        charset utf-8;
        client_max_body_size    40M;

        server_tokens   off;

        root    /usr/share/nginx/html;
        index   index.php;

        location /
        {
                proxy_connect_timeout 300;
                proxy_send_timeout 300;
                proxy_read_timeout 300;
                send_timeout 300;
        }

        location ~ \.(php)$
        {
                root           html;
                fastcgi_pass   php:9000;
                fastcgi_index  index.php;
                fastcgi_param  SCRIPT_FILENAME /usr/share/nginx/html/$fastcgi_script_name;
                include        fastcgi_params;
        }

        location ~ /\.ht
        {
                deny  all;
        }

        location ~ /
        {
                charset utf-8;
                rewrite ^/(.*) /index.php;
        }

}

기존의 Nginx 설정과 다른 부분은 fastcgi 부분이 추가된 것이다.

중간에 보면 root가 /usr/share/nginx/html로 되어있는데 nginx 최신버전의 이미지는 document경로가 저렇게 되어있다. official 이미지의 경우 reference가 굉장히 잘되어있다. 인터넷 검색으로 찾아서하면 오히려 더 헷갈릴 수 있으니 dockerhub를 잘 확인하자 (nginx 공식 이미지)

 

그 다음엔 docker-compose.yml이라는 이름으로 파일을 생성 후 아래와 같이 작성하자.

version: "3.9"
services:
  app:
    container_name: php
    image: php:8-fpm
    ports:
      - "9000:9000"
    volumes:
      - ./:/usr/share/nginx/html
    command:
      - /bin/sh
      - -c
      - |
        docker-php-ext-install mysqli
        echo "security.limit_extensions =" >> /usr/local/etc/php-fpm.d/www.conf
        php-fpm
  web:
    container_name: nginx
    build:
      context: ./
      dockerfile: Dockerfile
    depends_on:
      - app
    ports:
      - "80:80"
    volumes:
      - ./:/usr/share/nginx/html
    links:
      - app
    restart: always

위에서부터 

version: docker-compose 문서의 version이다. HTML처럼 그냥 버전을 적기만 하면 해당 버전의 문서가 된다. 버전마다 지원하는 옵션이 다를 수 있으니 공식 홈페이지를 잘 보고 작성하자. 

 

services: 말 그대로 서비스 목록이다 단위는 이미지 단위

app: service 이름이다 꼭 app이 아니여도 된다. 원하는 이름으로 하자

container_name: 이미지가 실행되면 여기에 적혀있는 이름으로 실행된다.

image: 사용할 이미지 명과 버전을 명시한다. 만약에 Dockerfile을 사용했을 경우 image말고 build로 대체한다. 

ports: "내포트 : container 포트"이다. container는 격리된 공간이기 때문에 외부에서 container에 접근하려면 host의 port를 container port로 binding해줘야한다. 

volumes: "내 폴더 : container폴더"이다. 지정한 폴더를 container와 공유하게 된다.

command: 이미지가 실행될때 해당 command가 실행된다. 내용을 보면 "security.limit_extensions ="를 php-fpm conf파일에 추가하고 있는데 위 설정이 없으면 설정파일의 security.limit_extensions이 주석처리 되어있어 access_denied 에러가 출력된다.

 

아래 web부분의 겹치는 부분은 넘어가고 새로운 부분만 확인해보자

build: 원래는 image를 명시해줘야 하지만 Dockerfile을 사용하려는 경우 dockerfile의 경로와 파일명을 적어주면 된다.

context: Dockerfile의 경로

dockerfile: Dockerfile이름

depends_on: service간 의존성을 설정한다. 정확하게는 서비스간 실행순서를 보장한다. 이 경우 app이 실행된 후 web이 실행된다.

links: container를 연결한다. container는 원래 격리된 공간이므로 한 container내에 nginx와 php-fpm이 같이 있어야하지만 link를 사용해서 두 서비스를 분리할 수 있다. (container하나에 한개의 서비스만을 넣는걸 권장한다.)

restart: container를 항상 다시 시작한다.

 

 

여기까지 작성하면 프로젝트는 아래와 같은 모양이 된다.

 

이후에 terminal 또는 cmd를 통해서(docker-compose.yml과 같은 폴더여야한다) 아래와 같이 입력하자.

$ docker-compose up -d

Pulling...

Creating php ... done
Creating nginx ... done

이후에 Container가 잘 샐행됐는지 확인해보자

$ docker ps

CONTAINER ID   IMAGE       COMMAND                  CREATED         STATUS         PORTS                    NAMES
bff5bf25ae03   study_web   "/docker-entrypoint.…"   4 minutes ago   Up 4 minutes   0.0.0.0:80->80/tcp       nginx
7c20126df814   php:8-fpm   "docker-php-entrypoi…"   4 minutes ago   Up 4 minutes   0.0.0.0:9000->9000/tcp   php

위처럼 출력된다면 정상적으로 실행된 것이다.

 

이후에 브라우저에서 localhost로 접근하면 

localhost

위와 같이 출력된다.

잘 된건가..? 싶은데 위에 System을 보면 Linux로 되어있는걸 확인할 수 있다.

 이번 가상화 실습에 DB를 넣지 않은 이유는 방법이 뻔해서 넣지 않았다. 그냥 php-fpm한것 처럼 docker-compose에 작성하고 nginx에 link시키면 된다. 자세한 사항은 hub.docker.com/_/mysql를 확인하자

 DB를 Host에 위치 시킬수도 있는데 php에서 db에 연결시 host값을 localhost말고 host(docker를 실행시킨)의 ip를 넣어줘야한다. 내부망일경우 내부망ip를 넣으면 된다.