본문으로 건너뛰기

Laravel - Socialite InvalidStateException

· 약 1분

가끔 가다가 인증이 안 되는 경우가 있다.

stateless

Socialite::driver('인증 타입')->stateless()->user(); 로 가져오자.

session 명 변경

config/session.phpcookie 값을 변경해준다.

session domain 변경

config/session.phpdomain 값을 null 에서 내 도메인으로 변경한다.

그리고 아래 두 명령어를 실행해주자.

$ php artisan cache:clear
$ composer dump-autoload

여담

socialite 설명에선 나오지 않았지만, Socialite 구문을 try/catch로 감싸주는게 좋았다.

<?php
try {
$user = Socialite::driver('facebook')->stateless()->user();
} catch (\Exception $e) {
return redirect()->route('login');
}

Laravel Query Logging, 쿼리 로그

· 약 1분

Laravel DebugBar 를 이용하는게 편하지만 dump 나 json 리턴시에 DebugBar 가 보이지 않으므로 직접 찍어줘야하는 경우가 많다.

<?php
# DB 파사드를 추가한다.
use DB;
...

public function your_func(Request $request) {
// 로그를 enable 시키고
DB::enableQueryLog();
// 쿼리를 여기에 실행한다.
Member::where('조건', '값')->get();
Product::find(1);

// 쿼리 로그를 찍는다.
$queryLogs = DB::getQueryLog();
dump($queryLogs);
}

결과

배열에 query, bindings (preparedStatement 를 위한 것), time 이 상세하게 나온다.

Laravel 5.5 - Debugbar와 BrowserSync의 충돌 해결

· 약 1분

Laravel Mix로 browserSync 옵션을 활성화 시에 Debugbar가 생기지 않는 오류가 발생할 경우 (스크립트 단에서 JSON parse 오류가 발생한다) 다음과 같이 설정해주면 된다.

webpack.mix.js
mix.browserSync({
proxy: {
// artisan serve시의 주소
target: "localhost:8000",
reqHeaders: function () {
// host를 직접 지정해준다.
return {
host: "localhost:3000",
};
},
},
});

Cannot start container iptables failed

· 약 1분

Container를 다시 올릴 때 다음과 같은 오류로 올라가지 않는 경우가 있다. docker kill이 아닌 stop, rm으로 container를 지웠을 때 뭔가 충돌이 나는 것 같다.

Error response from daemon: Cannot start container aca936f2822fce32235e627ff539c58b74b2f4e66cfa701de47ce609e2590d13: iptables failed: iptables -t nat -A DOCKER -p tcp -d 0/0 --dport 50000 -j DNAT --to-destination 172.17.0.10:50000 ! -i docker0: iptables: No chain/target/match by that name.
(exit status 1)

해결

이 중 마음에 드는 방법으로 해결하면 된다.

서비스 restart

docker 서비스를 restart 한다.

iptables rule 추가

오류 메세지에 필요한 rule을 추가한다.

$ iptables -t filter -N DOCKER
$ iptables -t nat -N DOCKER

AWS CodeCommit 사용하기

· 약 5분

AWS 에서 제공하는 Cloud Git Repository 인 CodeCommit 으로 소스코드를 관리해보자.

공식 문서가 아주 잘 되어있다. 문서를 보고 시작해도 된다.

IAM 유저 생성

IAM 에서 유저를 만든 뒤에 AWSCodeCommitFullAccess 권한을 추가한다.

키 파일 업로드

ssh 키를 생성하고 Public Key 파일(id_rsa.pub)을 해당 유저의 Security credentials 메뉴에서 업로드한다. 키가 등록되면 SSH key ID가 보이는데 메모해 놓자.

config

사용할 유저의 ssh config 를 설정해줘야한다. User 에 들어가는 값은 위에서 적어놓은 SSH key ID 값이다.

$ vi ~/.ssh/config

# 아래 내용을 맨 위에 넣어주자.
Host git-codecommit.*.amazonaws.com
User EXAMPLEEXAMPLEEXAMPLE
IdentityFile ~/.ssh/id_rsa

# 저장한 뒤 권한을 바꿔준다.
chmod 600 ~/.ssh/config

테스트

설정이 완료된 후에 서울리젼으로 ssh 연결을 시도해보자.

$ ssh git-codecommit.ap-northeast-2.amazonaws.com

# 다음과 같은 메세지가 리턴되면 성공이다.
You have successfully authenticated over SSH. You can use Git to interact with AWS CodeCommit. Interactive shells are not supported.Connection to git-codecommit.
ap-northeast-2.amazonaws.com closed by remote host.
Connection to git-codecommit.ap-northeast-2.amazonaws.com closed.

연동

이제 AWS CodeCommit 과 내 소스를 연결시켜보자.

# 이미 존재하는 프로젝트의 경우
$ git remote remove origin
$ git remote add origin ssh://git-codecommit.ap-northeast-2.amazonaws.com/v1/repos/레파지토리명

# 처음 시작하는 경우
$ git clone ssh://git-codecommit.ap-northeast-2.amazonaws.com/v1/repos/레파지토리명

# 권한이 없거나 ssh 계정을 물어볼 때
# origin의 path 앞에 SSH key ID를 추가하자
$ git remote add origin ssh://EXAMPLEEXAMPLEEXAMPLE@git-codecommit.ap-northeast-2.amazonaws.com/v1/repos/레파지토리명

git pull origin master시에 정상적으로 가져오는 걸 확인할 수 있다.

ssh 연동이 잘 되지 않을시

메뉴얼대로 따라해도 연동이 안될 때는 당황하지 말고 IAM > Users > Security credentials에서 HTTPS Git credentials for AWS CodeCommit를 만들어주고 CodeCommit Repository 연결을 http로 하면 된다.

Webhook

Github, Bitbucket 와 달리 Webhook 설정하는 법이 조금은 복잡하다. (GCP 의 Pub/Sub 와 비슷한 느낌)

CodeDeploy 서비스를 사용하면 쉬워질 것 같은데, 그럼 CodePipeline 도 쓰고 싶을 것 같고 CI 세팅을 해야되고 다음 기회에

Lambda

람다는 웹(URL)으로 호출할 수 있는 Javascript function 이다.

설정

먼저 Lambda > 함수 > 함수생성 > 새로 작성 메뉴에서 webhook 이란 이름의 함수를 생성한다. 트리거 구성 메뉴에서 CodeCommit 을 선택하고 입력 폼을 잘 채워주자.

image from hexo

기존 브랜치로 푸시, master 브랜치를 선택했다. 사용자 지정데이터에는 webhook 을 걸 URL 경로를 넣어준다. (예: https://yourdomain.com/webhook)

소스

이미 세팅된 함수를 사용하자.

"use strict";

const url = require("url");
const https = require("https");

exports.handler = (event, context, callback) => {
const webhook_url = event.Records[0].customData;

if (!webhook_url) {
const error = new Error("Web-hook URL not provided as custom data.");
callback(error);
} else {
console.log("POST web-hook to " + webhook_url);
const options = url.parse(webhook_url);
options["method"] = "POST";

const req = https.request(options, (res) => {
let body = "";
console.log("Status:", res.statusCode);
console.log("Headers:", JSON.stringify(res.headers));
res.setEncoding("utf8");
res.on("data", (chunk) => (body += chunk));
res.on("end", () => {
console.log("Successfully triggered web-hook.");
// If we know it's JSON, parse it.
if (res.headers["content-type"] === "application/json") {
body = JSON.parse(body);
}
callback(null, body);
});
});

req.on("error", callback);
req.end();
}
};

https 커넥션이 아닌 경우 http 모듈을 사용해서 request 를 보내면 될 것 같다.

등록 후엔 CodeCommit > 트리거 > webhook > 트리거 테스트를 진행하면 정상적으로 호출이 된다.

로그

호출 로그는 CloudWatch > 로그에서 생성한 Lambda 명으로 확인할 수 있다.

로그 스트림이 설정되지 않았을 경우엔 Lambda > 구성 > 기존 역할에 표시된 역할(Rule)이 해당 Lambda 함수를 잡고 있는지 확인해야한다. IAM > Rules > 해당 룰 > Permissions에서 Show policy를 누르면 Resource 속성에서 확인할 수 있고 다르다면 제대로 연결시켜주면 된다.

여담

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

Laravel 5.5 - Log Permission 문제

· 약 4분

웹 서버의 유저로 로그 파일이 생성되어야 하는데, 어느 순간부터 root:root 권한을 달고 daliy log 가 생성되는 경우가 있다.

여러가지 경우의 수가 있는데, 맞는 조건을 찾아서 Permission 오류가 발생하지 않게 처리해보자.

storage 에는 쓰기권한이 있어야한다.

$ chcon -R -t httpd_sys_rw_content_t storage

WebServer user

웹 서버의 유저가 다르게 설정 되어있을 때 권한이 바뀔 수 있다. 서버 설정을 열어서 유저가 제대로 설정되어 있는지 확인해보자.

nginx.conf
user nginx;

php-fpm user

php-fpm 에서 설정하는 user 와 group 이 다르게 설정 되어 있을 때 권한이 바뀔 수 있다. php-fpm 설정을 열어 유저가 제대로 설정되어 있는지 확인해보자.

php-fpm.d/www.conf
user = nginx
group = nginx

listen.owner = nginx
listen.group = nginx

log rotate

로그 파일이 너무 커지는 걸 막기위해 log rotate 설정이 되어있다면 권한이 바뀔 수 있다. logrotate 가 cron 에 물려 있는지 설정을 확인해보자.

/etc/logroate.d/*
$ pwd
/etc/logroate.d

$ vi nginx
$ vi php-fpm

cron 사용시

crontab 의 경우 root 유저로 실행이 되면 cron 에서 Laravel 을 호출할 때 log 가 root 권한으로 생성될 수 있다.

로그 분기

log 파일을 생성하는 프로세스별로 분기해서 해결할 수 있다. Stackoverflow 참조

bootstrap/app.php
$app->configureMonologUsing(function(Monolog\Logger $monolog) {
$filename = storage_path('logs/laravel-'.php_sapi_name().'.log');
$handler = new Monolog\Handler\RotatingFileHandler($filename);
$monolog->pushHandler($handler);
});

설정을 추가해 놓으면 logs 폴더 하위에 다음과 같이 로그가 분기되어 생성된다.

storage/logs
$ ls -al .
-rw-r--r-- 1 nginx nginx 718 Aug 18 10:56 laravel-fpm-fcgi-2017-08-18.log

cron 은 root 에서 실행되나 user shell 에서 Laravel 프로세스를 실행하는 경우 root 에 의해 log 가 생성되었다면 다음과 같이 permission 을 변경해서 생성해야한다. Post 참조

bootstrap/app.php
$app->configureMonologUsing(function(Monolog\Logger $monolog) {
$filename = storage_path('/logs/laravel-' . php_sapi_name() . '.log');
// 5번째 파라미터로 666 권한을 넘긴다.
$handler = new Monolog\Handler\RotatingFileHandler($filename, 0, \Monolog\Logger::DEBUG, true, 0666);
$monolog->pushHandler($handler);
});

RotatingFileHandler 의 Parameter 는 여기를 참조하자. 666 으로 생성 시엔 굳이 php_sapi_name()을 사용하지 않아도 된다. (rw 권한이 모두에게 있으니까)

setfacl

다른 해결 방법으로는 ACL 을 수정해 logs 폴더 자체를 해당 user:group 이 편집할 수 있게 처리하면 된다.

$ pwd
/public_html/storage/logs

$ setfacl -d -m u:nginx:rwx .

$ getfacl .
default:user:nginx:rwx

# 삭제
$ setfacl -d -x u:nginx .

여담

Laravel 프로젝트 시작시 bootstrap/app.php 안에 로그를 분기 로직을 넣고 개발하는 게 좋아보인다.

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

이전 포스트를 참조하자.

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

여담

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