서비스를 컨테이너로 옮겼다. 잘 돌았는데 이미지를 새로 올린 뒤 업로드된 파일이 전부 없어졌다.
Table of contents
Open Table of contents
증상 — 이미지를 올린 뒤 사라진 파일
업로드 경로부터 봤다.
/app/public/uploads/
/app/public/uploads/ 는 컨테이너 안의 경로여서 새로 만들면 그 안에 있던 것은 남지 않는다.
$ docker inspect -f '{{ .Mounts }}' api
[]
Mounts 가 비어 있으니 쓰는 것이 전부 컨테이너 레이어에 들어가 있었다.
옮길 때 응용이 어디에 무엇을 쓰는지를 세어 보지 않은 것이 원인이었다. 옮기기 전에는 그 경로가 서버에 그대로 남는 자리였으니 문제가 드러날 일이 없었다.
원인 — 쓰이던 자리
업로드만이 아니어서 컨테이너 안에서 쓰는 자리를 전부 찾았다.
$ docker exec api sh -c 'find / -newer /etc/hostname -type f 2>/dev/null | grep -v "^/proc\|^/sys"' | head -20
/app/public/uploads/2018/02/....jpg
/app/storage/logs/app.log
/app/storage/cache/....php
/app/storage/sessions/sess_....
/var/log/nginx/access.log
find -newer 로 /etc/hostname 보다 새 파일만 걸러 컨테이너가 뜬 뒤에 생긴 것을 넷 찾았다.
이 넷은 없어졌을 때의 결과가 서로 달랐다. 업로드는 사라지면 되살릴 수 없고 로그는 서비스는 돌지만 문제가 났을 때 볼 근거가 없어진다.
storage/cache 는 다시 만들어지고 storage/sessions 는 사용자가 다시 로그인하면 된다. 넷을 같은 방식으로 다룰 이유가 없었다.
상태를 성격별로 갈랐다
정리하면 이렇게 갈린다.
업로드 없어지면 안 된다
로그 없어지면 조사를 못 한다
캐시 없어져도 된다
세션 없어지면 전원 로그아웃된다
uploads 와 logs 는 밖으로 빼고 캐시는 그대로 두며 세션은 아예 다른 자리로 옮기기로 했다.
무엇을 남길지는 저장소 크기가 아니라 없어졌을 때 무엇을 잃는지로 갈렸다. 같은 파일 쓰기라도 그 성격이 다르면 처리도 달라야 했다.
조치 — 없어지면 안 되는 것을 밖으로
남겨야 하는 둘을 호스트 경로에 걸었다.
$ docker create --name api \
-v /srv/api/uploads:/app/public/uploads \
-v /srv/api/logs:/app/storage/logs \
api:2018.02.04
/srv/api 아래에 두면 컨테이너를 새로 만들어도 그 내용은 그대로 남는다.
권한에서 한 번 걸렸는데 컨테이너 안 사용자와 호스트 사용자의 번호가 달라서 쓰기가 안 됐다.
$ docker exec api id
uid=82(www-data) gid=82(www-data)
$ chown -R 82:82 /srv/api/uploads /srv/api/logs
파일 권한은 www-data 라는 이름이 아니라 uid 번호로 판정되므로 82 를 맞춰야 한다.
호스트에 그 번호를 쓰는 사용자가 없어도 상관없다. 양쪽에 같은 이름의 계정이 있어도 번호가 다르면 안 되는 것이 이 경우였다.
설정 — 세션과 캐시와 로그
storage/sessions 를 파일로 두는 것 자체가 나중에 걸릴 자리였다.
session.save_handler = redis
session.save_path = "tcp://session-01:6379"
컨테이너를 다시 만들어도 로그인이 유지되고 뒤에 대수를 늘려도 그대로 쓴다.
캐시는 없어져도 되므로 컨테이너 안에 그대로 뒀다. 다만 정말 없어져도 되는지는 확인해야 했다.
$ docker run --rm api:2018.02.04 php artisan route:list > /dev/null; echo $?
0
route:list 가 빈 캐시에서 돌아야 그것이 캐시다. 있어야만 뜨는 구조라면 그것은 캐시가 아니라 상태이므로 밖으로 빼야 했다.
로그는 파일과 표준 출력 양쪽으로 냈다.
# php-fpm
access.log = /proc/self/fd/2
# nginx
access_log /dev/stdout;
error_log /dev/stderr;
조사할 때는 /srv/api/logs 에 쌓인 파일을 뒤지고 지켜볼 때는 docker logs 를 본다. 둘 중 하나만 두면 다른 쪽 상황에서 번거로워진다.
검증 — 지웠다 다시 만들었다
설정을 넣은 것으로 끝내지 않고 실제로 지웠다.
$ docker exec api sh -c 'echo test > /app/public/uploads/canary.txt'
$ docker rm -f api
$ ./run.sh
$ docker exec api cat /app/public/uploads/canary.txt
test
canary.txt 가 컨테이너를 다시 만든 뒤에도 그대로 있다.
반대로 없어져야 하는 자리도 같은 방식으로 봤다.
$ docker exec api cat /app/storage/cache/canary.txt
cat: can't open ...: No such file or directory
storage/cache 는 비어 있어야 정상이고 여기 canary.txt 가 남아 있으면 그 자리도 밖에 걸린 것이다.
남아야 할 것만 보면 전부 마운트된 상태에서도 통과로 보인다. 남아야 할 것과 없어져야 할 것을 둘 다 봐야 그 검증이 무언가를 가른다.
정리
- 컨테이너 안에 쓴 것은 컨테이너를 다시 만들면 없어진다
- 옮기기 전에는 그 경로가 서버에 남으니 문제가 안 드러난다
- 쓰는 자리를 전부 찾아 성격별로 가른다
- 없어졌을 때 무엇을 잃는지로 남길 것이 갈린다
- 남겨야 하는 것은 호스트 경로에 건다
- 권한은 이름이 아니라 번호로 맞춘다
- 세션은 파일이 아닌 자리로 옮기면 대수를 늘릴 때도 그대로 쓴다
- 캐시가 빈 상태에서 떠야 그것이 캐시다
- 있어야만 뜨면 그것은 상태다
- 로그는 파일과 표준 출력 둘 다 낸다
- 표시를 남기고 실제로 지웠다 다시 만들어 양쪽을 확인한다