본문으로 건너뛰기

Docker로 LEMP Stack 구축하기

· 약 2분

이 포스트 전에 웹서버 세팅을 하나씩 설치해서 띄워보는 걸 권장하고 Docker, SSH Login, LetsEncrypt, sed 명령어의 사용법을 알고 있어야 한다. 구성할 서버 스택은 다음과 같다.

  • Docker
  • Docker-compose
  • Host 에 사용될 Linux (Centos7)
  • Alpain Linux
  • Nginx ^1.13
  • MariaDB ^10.2
  • PHP ^7.1
  • Laravel =5.4
  • LetsEncrypt
  • HTTP2
  • Redis

Docker 설치

이전 포스트를 참조하자.

Container 쇼핑

Docker Hub에서 마음에 드는 Container 를 사용해도 되지만, 생각처럼 돌아가는 Container 는 다음과 같았다.

Laradock을 안 썼죠?

  1. Laradock 에서 caddy 를 사용하지 않고 nginx 와 certbot 만을 이용해 http2 환경을 구성하는 예제가 없었다.
  2. 그래도 시도해봤으나 certbot 인증시에 DocumentRoot 를 잡지 못하는 현상을 삽질로 매꿀 시간이 없었다.
  3. Git repo 를 Clone 받아서 Docker-compose 로 Container 를 구동하기 때문에 추후 ECS 에 적용할 수가 없는 구조였다.
  4. 직접 구축해보고 싶었다.

세팅

nginx-php-fpm

Laravel 용 및 튜닝을 위해 Docker hub 의 이미지 대신 Git repo 의 이미지를 Clone 해서 세팅을 해보자.

내용 추가 중..

AWS ECS 부수기

· 약 6분

ECS 는 서울 리젼이 아직 없어서 그런가, 구글링해도 사용할만한 데이터가 너무 적었다. (기초 설명은 잘 되있다. 하지만 Hello World Application 을 올리려고 ECS 를 쓰는 건 아니니까..)

용어

Cluster

Amazon ECS 클러스터는 작업을 배치할 수 있는 컨테이너 인스턴스의 논리적 그룹화입니다. 이 공식 설명을 보고 Cluster 에 대한 감을 잡기가 쉽지 않았다.

  • 간단히하면 Docker Container 를 올리는 EC2 Instance 이다.
  • K8S 의 그 클러스터이다.

Task

  • Task 는 작업이라고 번역되며, 하나의 Task Definition JSON 은 하나의 Docker-compose YAML 이라고 보자.

Container Definition

  • 배포되는 각 컨테이너의 정의
  • 파드의 개념

Task Definition

  • 컨테이너의 집합인 Task의 정의
  • 파드의 개념

Service

Amazon ECS 는 단일 ECS 클러스터에서 작업 정의에 지정된 수("원하는 개수")의 인스턴스를 동시에 실행 및 관리할 수 있게 해줍니다. 어떤 이유로 작업이 실패 또는 중지되는 경우 Amazon ECS 서비스 스케줄러가 작업 정의의 다른 인스턴스를 시작하여 이를 대체하고 서비스의 원하는 작업 수를 유지합니다.

  • Task 를 자동으로 관리할 수 있게 하는 기능, LB 나 Auto Scaling 모두 여기서 적용이 된다.
  • 롤링 업데이트 시에 배포 및 오토 스케일링을 담당한다.
  • K8S의 서비스, 디플로이먼트, 레플리카세트의 역할

Repository

AWS Docker 레지스트리 서비스로 AWS Private DockerHub 라고 보자.

네트워크 모드

모드내용
default네트워크 모드 기본값으로 bridge와 같음
awsvpcAWS에서만 제공되는 네트워크 모드 (ENI가 Task 자체 VPC의 IP 주소할당)
bridgeDockerContainer를 호스트와 같은 네트워크에 배치해 라우팅 없이 컨테이너 접근 가능
hostTask가 배치되는 호스트의 네트워크를 공유하는 모드 (Fargate 사용 불가)
noneTask에 속한 컨테이너의 외부 접근이 불가능하고 포트 매핑 사용불가

볼륨 생성

-v 또는 --volume 으로 Host 의 폴더를 Mount 하는 기능이 꼭 필요한데, 설정 창에선 찾기가 너무 힘들었다. ECS 에서는 작업 정의 생성 시에 하단에 볼륨 추가 를 꼭 먼저 클릭해 볼륨부터 추가해야한다.

image from hexo 이름엔 --name 옵션 사용하듯이 닉네임을 넣고 소스 경로엔 Host directory 경로를 넣자.

추가가 되면 컨테이너 추가 시에 탑재 지점 메뉴의 소스 볼륨 select box 에서 선택할 수 있다.

image from hexo

이 짓을 하는 것보단 공식 문서의 Task Definition JSON Parameter 를 보고 JSON 으로 때려박는게 편하다.

예시 JSON 은 아래와 같다. (3306 과 3307 을 열고 Host 의 Data 폴더를 Mount 하는 기본 구성의 MariaDB Image)

mariadb
{
"requiresAttributes": [
{
"value": null,
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.21",
"targetId": null,
"targetType": null
}
],
"taskDefinitionArn": "your task definition arn",
"networkMode": "bridge",
"status": "ACTIVE",
"revision": 3,
"taskRoleArn": null,
"containerDefinitions": [
{
"volumesFrom": [],
"memory": null,
"extraHosts": null,
"dnsServers": null,
"disableNetworking": null,
"dnsSearchDomains": null,
"portMappings": [
{
"hostPort": 3306,
"containerPort": 3306,
"protocol": "tcp"
},
{
"hostPort": 3307,
"containerPort": 3307,
"protocol": "tcp"
}
],
"hostname": null,
"essential": true,
"entryPoint": null,
"mountPoints": [
{
"containerPath": "/var/lib/mysql",
"sourceVolume": "dbdata",
"readOnly": null
}
],
"name": "maria",
"ulimits": null,
"dockerSecurityOptions": null,
"environment": [
{
"name": "MYSQL_DATABASE",
"value": "db"
},
{
"name": "MYSQL_PASSWORD",
"value": "db_pw"
},
{
"name": "MYSQL_ROOT_PASSWORD",
"value": "root_pw"
},
{
"name": "MYSQL_USER",
"value": "db_user"
}
],
"links": null,
"workingDirectory": null,
"readonlyRootFilesystem": false,
"image": "mariadb:latest",
"command": [
"mysqld",
"--character-set-server=utf8",
"--collation-server=utf8_general_ci"
],
"user": null,
"dockerLabels": null,
"logConfiguration": null,
"cpu": 0,
"privileged": null,
"memoryReservation": 500
}
],
"placementConstraints": [],
"volumes": [
{
"host": {
"sourcePath": "/ecs/dbdata"
},
"name": "dbdata"
}
],
"family": "mariadb"
}

고민

Container 를 Task 별로 생성해야하는데, 그럼 Task JSON 에서 link 옵션을 연결할 수가 없다. 이 경우엔 어떻게 Task Definition 을 짜야되나? EC2 에 접근해서 매번 link 를 생성해서 다시 올려야되나?

이 부분을 해결하기 위해선 ecs-task-kite를 사용하거나 VPC 를 구성해 수동으로 연결해 주는 방법 밖에 없다.

쉬운 방법으로 가자면 DB 는 (모든 컨테이너가 하나의 데이터를 바라봐야하는) RDSElastiCache처럼 AWS 의 서비스 사용하고 VPC 를 구성해 Backend, Frontend 단의 서버만 ECS Task 를 만들어서 가변적으로 돌리는 게 좋아보인다.

물론 동기화를 할 수도 있는데... 삽질할 시간에 더 잘 나온 포스팅을 기다려본다.

여담

그냥 모니터링 컨테이너 하나 더 띄우고, HAProxy 컨테이너 올리고 EC2 에 다 때려박고 싶다.

AWS Korea week in review에 소개되었다.

일본 우편번호(주소) 검색 API

· 약 1분

다음 우편번호 검색 API 처럼 일본 우편번호 검색도 간단히 구현할 수 있다. 여기에서 내용을 확인할 수 있지만, 더 쉽게 써보자.

소스

<input type="number" id="zip" />
<button type="button" onClick="search_addr();">住所検索</button>

<input type="text" id="address" />
<!-- 스크립트를 로드 -->
<script src="//api.zipaddress.net/sdk/zipaddr.min.js" async></script>
<script>
var searchAddr = function () {
var $zip = $("#zip");
var zip = $zip.val();

// 일본 우편번호는 7자리로 고정되어있다.
// sample 6800001
if (zip && zip.length === 7) {
ZA.request(zip, function (data, err) {
var $address = $("#address");
if (err) {
$address.val("");
$zip.focus();
return alert(data.message);
}
$address.val(data.fullAddress);
$address.focus();
});
} else {
alert("郵便番号に誤りがあります。");
$zip.focus();
}
};
</script>

여담

주소지명으로 검색하는 API 는 찾아볼 수 없었다. 역시 주소검색은 다음

Centos7 Timezone 변경하기

· 약 1분

기존 방식

## 기존 설정 백업
$ mv /etc/localtime /etc/localtime.bak

## 타임존 연결
$ ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime

## 백업을 안하고 바로 연결시
$ ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime

timedatectl

timedatectl 명령어를 사용해 쉽게 바꿀 수 있다.

## 한줄로 깔끔하게
$ timedatectl set-timezone Asia/Seoul

여담

시간날 때 세팅용 쉘을 만들어야겠다.

Laravel 5.5 - 다형성 관계

· 약 6분

댓글 테이블이 있고 이 댓글은 여러 테이블에서 사용된다고 치자. 그럼 댓글 테이블에 type과 type_id를 가져가야할 것이다. 이 때 사용할 수 있는 관계가 다형성 관계(릴레이션)인데, 공식 문서의 설명이 조금은 부족하다고 느꼈다. 파헤쳐보자.

morphTo

morphTo는 type과 type_id를 가진, 여러 테이블로 연결되어야할 테이블에서 사용하는 릴레이션 메소드이다. 공식 문서에는 데이터를 가져온 뒤 릴레이션을 연결하는 예시만 있고, Eager 로딩 (With 구문을 사용하는 방법) 후 specific한 필드를 사용하게 변경하는 경우에 대한 정보는 없다.

기본 문법

YourModel.php
<?php

class YourModel extends Model {
...

// 다형성 관계를 가질 함수를 data로 정의했다
public function data() {
return $this->morphTo();
}
}

이렇게 정의시에 YourModel::with('data')->get() 으로 호출하면 불러와져야되지만, 필드명, 모델명이 정확하지 않으면 쿼리 호출조차 되지 않는다. (심지어 에러도 발생하지 않는다)

필드명 정의

먼저 morphTo의 소스코드를 를 살펴보자.

morphTo
<?php
/**
* Define a polymorphic, inverse one-to-one or many relationship.
*
* @param string $name
* @param string $type
* @param string $id
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function morphTo($name = null, $type = null, $id = null)

name, type, id를 파라미터로 받는다. 그럼 파라미터를 넘겨보자.

YourModel.php
<?php

class YourModel extends Model {
...

public function data() {
// morphTo의 paremeter로 null, 타입필드명, 타입인덱스 필드명을 넘긴다.
return $this->morphTo(null, 'type', 'type_idx');
}
}

여기서 name엔 도대체 뭘 넣어야 되는거야? 라고 의문이 생길 수가 있다. 함수 내에서 name 변수는 $this->getMorphs(Str::snake($name), $type, $id); 에만 딱 한 번 사용된다.

getMorphs 함수를 따라가보자.

getMorphs
<?php
protected function getMorphs($name, $type, $id) {
// $type과 $id가 명시되면 그 값을 먼저 반환한다.
return [$type ?: $name.'_type', $id ?: $name.'_id'];
}

주석처럼 typeid가 명시되면 name값은 사용되지 않는 쓰레기 값이 되어버린다. 따라서 null로 넘겨주면 된다.

타입-모델 바인딩

타입과 인덱스를 명시하면 드디어 오류메세지가 노출된다. 내가 정의한 type명을 가진 Class가 없다 라는 내용인데, 이제 타입과 모델을 연결시켜보자.

이 때 사용할 수 있는 메소드가 공식 문서에서 조금 스크롤을 내리면 있는 Custom Polymorphic Types에 잘 설명되어 있다.

하지만 등록하는 부분에 대한 설명이 **You may register the morphMap in the boot function of your AppServiceProvider or create a separate service provider if you wish.**라고 되어있다. 즉 AppServiceProvider에 넣던지 Service Provider로 생성이다.

한 모델에만 쓸 건데 전체에 등록을 할 필요가 없으니, 사용할 모델에 기능을 넣어보자.

YourModel.php
// Relation을 사용해야한다.
use Illuminate\Database\Eloquent\Relations\Relation;

<?php
class YourModel extends Model {
// 이 메소드는 모델이 initialize될 때 실행된다.
protected static function boot() {
parent::boot();

// 여기에 타입 별로 모델을 바인딩한다.
Relation::morphMap([
// type이 product일 경우 id는 product_id를 가리킨다.
'product' => 'App\Models\Product',
// type이 order일 경우 id는 order_id를 가리킨다.
'order' => 'App\Models\Order'
]);
}

public function data() {
// morphTo의 paremeter로 null, 타입필드명, 타입인덱스 필드명을 넘긴다.
return $this->morphTo(null, 'type', 'type_idx');
}
}

완벽해졌다. 이제 오류 없이 실행되는 것을 확인할 수 있다.

morphOne

문서 상에는 설명 되지 않은 morphOne 이란 메소드도 있다. morphMany는 관계가 설정된 값을 배열로 반환하지만 morphOne은 하나의 데이터로 반환한다. (hasOne과 hasMany처럼)

구조는 다음과 같다.

<?php
public function morphOne($related, $name, $type = null, $id = null, $localKey = null)

morphToMany

morphedByMany

여담

다대다 다형성 관계 메소드 (morphToMany, morphedByMany)의 경우는 나중에 사용하게 되면 정리해야겠다. Relation 메소드들은 문서를 대충 훑고 API Docs를 직접 까보는게, 효율적인 것 같다.

Laravel 5.5 - Multi DB Connection

· 약 2분

여러 데이터베이스에서 데이터를 가져와야되는 경우가 있다. (마이그레이션 또는 발송 모듈 DB의 분기 등등) 라라벨에선 아주 쉽게 설정이 가능하다.

설정

config/database.php에 새로운 커넥션 정보를 넣어주자. 새 커낵션은 mysql_new 라고 이름지었다.

config/database.php
<?php
return [
...
'connections' => [
// 기본 커넥션
'mysql' => [
...
],
// 새 친구
'mysql_new' => [
'driver' => 'mysql',
'host' => '111.111.111.111',
'port' => '3306',
'database' => 'test',
'username' => 'test',
'password' => 'test1234',
'unix_socket' => '',
'charset' => 'utf8',
'collation' => 'utf8_general_ci',
'prefix' => null,
'engine' => null
],
],
];

모델

새 커넥션에 사용할 모델을 만들어주고, 모델에서 연결할 커넥션을 설정해주자.

model.php
<?php
...
class OldMember extends Model
{
// 커넥션 변수를 다시 설정해주면 끝
protected $connection = 'mysql_new';
protected $table = 'test';
...
}

사용

기존 모델 사용법과 똑같다. 아주 간단하다.

쉘 스크립트 if 조건변수

· 약 2분

쉘 스크립트의 비교 변수 몇 가지를 알아보자.

파일 상태 비교

연산자기능예시
-afile_exists[ -a /etc/passwd ]
-dfile_exists && is_dir[ -d /etc ]
-ffile_exists && is_file[ -f /etc/passwd ]
-sfile_exists && not empty[ -s /etc/passwd ]
-w해당 유저로 쓰기 가능[ -w test.txt ]
-x해당 유저로 실행 가능[ -x test.sh ]
-N마지막 파일 읽은 시점부터 변경점이 있는지[ -N test.txt ]
-O해당 유저의 파일인지[ -O test.txt ]
-G해당 그룹의 파일인지[ -G test.txt ]
-ntB파일보다 A파일이 새로운지[ A_file -nt B_file ]
-otB파일보다 A파일이 오래됬는지[ A_file -ot B_file ]

숫자 비교

연산자기능예시
-ltLess than[ 0 -lt 1 ]
-leLess than or Equal[ 1 -le 1 ]
-eqEqual[ 1 -eq 1 ]
-gtGreater than[ 1 -gt 0 ]
-geGreater than or Equal[ 1 -ge 1 ]
-neNot Equal to[ 1 -ne 0 ]

number_format의 반대 함수

· 약 1분

number_format 으로 쉽게 comma 가 들어간 숫자를 만들 수 있는데, 이 반대 방법은 preg_replace 를 통해 comma 를 제거한 뒤에 다시 int 로 형변환을 해야한다.

더 간단하게 변경할 수 있는 방법은 바로 filter_var를 사용하는 것이다.

소스

<?php

$formatted_nubmer = number_format(10000);

echo $formatted_number; // 10,000;

$number = filter_var($formatted_nubmer, FILTER_SANITIZE_NUMBER_INT);
// FILTER_SANITIZE_NUMBER_INT 상수가 너무 길어서 외우기가 힘들다면 519
$number2 = filter_var($formatted_nubmer, 519);

echo $number; // 10000;
echo $number2; // 10000;

필터 옵션

필터 상수에 대해 궁금해졌다면 공홈을 참조하자.

Docker와 Docker-compose 제대로 설치하기

· 약 2분

구글링하면 너무 예전 버전 (1버전 대)의 설치방법만 나와있다.

영문을 따라할 수 있으면 공홈을 보고하면 된다.

Docker 설치

옛 버전 삭제

$ sudo yum remove docker \
docker-common \
docker-selinux \
docker-engine

서비스를 내리고 docker를 삭제해도 /var/lib/docker/ 폴더는 지워지지 않고 여기에 기존 데이터가 모두 남아있다.

필수 패키지 설치

$ sudo yum install -y yum-utils device-mapper-persistent-data lvm2
## docker repo를 등록한다.
$ sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
## yum package를 업데이트
$ sudo yum makecache fast

다운로드

sudo yum install docker-ce

실행

$ sudo systemctl start docker
$ sudo systemctl enable docker

$ sudo docker --version
Docker version 17.06.0-ce, build 02c1d87

Docker-compose 설치

다운로드

## root로 로그인해야한다.
$ curl -L https://github.com/docker/compose/releases/download/1.14.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose

실행 권한 부여

$ chmod +x /usr/local/bin/docker-compose

## 설치 확인
$ docker-compose --version
docker-compose version 1.14.0, build 1719ceb

설치가 완료되었다.

Centos6에서 설치하기

centos6 버전에서는 위의 설치방법으로 Docker를 설치할 수 없다. (RHEL7 버전 전용이기에) 다음과 같이 설치하자.

yum install http://mirrors.yun-idc.com/epel/6/i386/epel-release-6-8.noarch.rpm

yum install -y docker-io

service docker start

chkconfig docker on

Docker Error response from daemon: reference does not exist

· 약 1분

Docker rmi명령어로 이미지를 삭제하는데 Error response from daemon: reference does not exist 오류가 나면서 이미지 삭제가 안 되는 경우 다음과 같이 하면된다.

해결

구글링하면 다시 설치하거나 cache를 비우거나 하라는데 해결되진 않았고 쉽게 접근하면 된다. 그냥 이미지 폴더를 날리자

sudo systemctl stop docker
sudo rm -rf /var/lib/docker
sudo systemctl start docker