swsnu / swppfall2019

31 stars 23 forks source link

서비스 deployment 관련 질문 #209

Open Yoo-Youngjae opened 4 years ago

Yoo-Youngjae commented 4 years ago

안녕하세요. 이번주에 진행된 실습 수업에 대해 제가 mac 에서 돌아가지 않는 이슈로 몇가지 놓친 부분이 있고, 다른 팀도 공통적으로 헷갈리는 부분일것 같아 질문을 issue로 올립니다!

  1. 오늘 진행한 모든 세팅을 제 개인 노트북의 terminal 에서 진행했는데, 제 컴퓨터가 서버가 아니니, 이 모든 세팅을 azure 의 가상머신에 ssh 로 접속해서 처음부터 모두 진행해야하는 게 맞나요?(uwsgi, nginx, npm build, ssl 등)

2.그리고 window, mac 등 로컬에서 nginx, uwsgi 세팅등을 진행해야하는 이유는 무엇인가요?(기존의 개발환경대로 개발하면 이젠 오류가 발생하나요?)

  1. 개발환경은 기존에 하던대로 해도 되면, localhost 를 nginx 에 추가하는 이유는 무엇인가요?(localhost 는 로컬에서의 개발환경을 위한 세팅인데, 가상머신에서도 localhost 로 세팅하는 것이 맞나요?)

  2. 만약 처음 배포를 완료하면, 그 뒤로 코드의 추가 구현이 생기면, 이를 어떻게 다시 추가 배포하면 되나요? (모든 작업을 다시 처음부터 하진 않아도 될듯한데, azure 의 vm 내부에서 git pull 을 받고 build 만 진행하면 되나요?)

  3. ssl 을 적용하는 과정을 하긴 했지만, ssl 적용이전까지 gabia 를 통해 구매한 domain을 이용해 내 build 된frontend(index.html) 에 접근하는 과정이 어느 부분에 반영되는지 모르겠습니다. (파일질라 같은 ftp 등을 이용하는 과정이 없지 않았나요..?)

배포라는 것이 중요하고, 한번 하면 그 뒤론 어렵지 않을것 같은데, 처음 이해하는게 쉽지 않은듯 합니다. 감사합니다.

ktaebum commented 4 years ago
  1. 네 실제 배포할 때는 서버에서 ssh로 접속해서 진행하면 됩니다. ssl의 경우에는 Azure의 경우 https://docs.microsoft.com/en-us/azure/virtual-machines/linux/tutorial-secure-web-server 를 참고하시면 비교적 쉽게 적용하실 수 있는데 이 예시에서 사용하는 self-signed certificate이 아닌 실제 domain에 대한 certificate 사용하시면 됩니다 (오늘 수업 자료에 있는대로 따라해도 되구요)

  2. 이건 사실 필요 없구요 그냥 실습에서 따라하기 위함입니다. 또한 server에서 작업을 최대한 줄이고 싶다면 먼저 local에서 잘 되는지 확인하는 것도 방법이니 그러한 이유도 될 수 있습니다.

  3. 네 가상 머신에서도 localhost로 세팅하면 되구요 (서버 입장에선 local이니) 혹은 domain이 있으면 domain으로 설정하셔도 됩니다.

  4. update된 코드로 똑같이 적용하면 됩니다 (예를들어 frontend code가 업데이트 됐다면 frontend build를 다시 하고 nginx restart). 그래서 이 과정을 script로 만들어두면 관리하기 편합니다

  5. 반영하는 부분은 nginx.conf에 반영을 하면 됩니다. 예를 들어

    
    server {
    listen 80;
    
    server_name ${your_domain};
    
    root /home/ubuntu/swpp/frontend/build;
    ...
    location /api/ {
        proxy_pass ${your_domain}:8000;
    }
    }

server { listen 8000;

root /home/ubuntu/swpp/backend;

server_name ${your_domain};

location / {
    uwsgi_pass django;
    include /home/ubuntu/swpp/backend/uwsgi_params;
}

}

가 하나의 예시 config가 될 수 있습니다
여기서 backend (8000 port)에 대한 nginx conf도 되어 있는데 (front에서 `/api`를 domain:8000으로 보내서)
이와 관련해서는
https://uwsgi-docs.readthedocs.io/en/latest/tutorials/Django_and_nginx.html
참고하시면 좋을 것 같구요 

추가로 여러분의 backend의 `settings.py`에 보시면

SECURITY WARNING: don't run with debug turned on in production!

DEBUG = True

이라고 적혀있을텐데

deploy시에는 debug False로 해주시고

ALLOWED_HOSTS = []

가 있을텐데 이 부분에

ALLOWED_HOSTS = ['${AZURE_PUBLIC_IP}', '${your_domain}', 'localhost', '127.0.0.1']


등등을 추가해주시면 되겠습니다
Yoo-Youngjae commented 4 years ago

감사합니다! 참고하여 적용해 보겠습니다!

Yoo-Youngjae commented 4 years ago

안녕하세요. 현재 uwsgi 는 잘 도는게 확인되었고, 아래와 같이 domain 에 ip 도 설정한 상황입니다. 그런데 nginx 설정에서 문제가 있는지 www.fevertime.shop 으로 접근해도 '응답하는데 시간이 너무 오래 걸립니다' 라며 'ERR_CONNECTION_TIMED_OUT' 에러가 나고 있어 질문드립니다.

스크린샷 2019-11-22 오후 3 32 01

뭔가 html 문서 자체가 안 가져와 지는것을 보면, frontend 설정 문제인것 같은데, 아래와 같이 frontend.conf 파일을 설정한 상태인데 무엇이 문제인지 의문이 들어 질문 드립니다! (sudo nginx -t 는 successful 한 상태이고, restart 도 한 상태입니다!)

스크린샷 2019-11-22 오후 3 33 03
jaehunjung1 commented 4 years ago

혹시 azure 대시보드에서 인바운드 규칙 추가하셨나요??

Yoo-Youngjae commented 4 years ago

인바운드 규칙에 80번 포트를 추가하여 이슈 해결하였습니다! 감사합니다!

Yoo-Youngjae commented 4 years ago

안녕하세요! frontend 부분은 해결되었는데, backend 부분이 실행되지 않는 이슈가 발생했습니다!

추측건데, run uwsgi 이슈 인것 같습니다. pip3 install uwsgi 를 통해 패키지를 받았고, 실습 ppt 대로 실행하면 uwsgi: unrecognized option '--wsgi-file' 에러가 발생하고, django docs 의 문서대로 하면 unable to load configuration from fevertime.wsgi 가 발생합니다.

그리고 구글링을 통해 --plugins python 을 추가해도 no module not found 에러가 발생합니다. 이럴경우 nginx 실행을 했던 process 와도 겹치는 듯하여 address already in use 가 발생하는데,

혹시 uwsgi 를 정상적으로 실행하신 분들 께서는 어떻게 실행하셨는지 조언 여쭈어 봅니다!

Yoo-Youngjae commented 4 years ago

nginx conf 파일에서 8000번 포트를 listen 하는 server 부분을 삭제해서 해결하였습니다!

ulgal commented 4 years ago

혹시 post도 동작하나요??

Yoo-Youngjae commented 4 years ago

아니요. 현재 실 서버에서는 csrf 이슈로 post 는 동작하지 않고 있습니다. 동일한 이슈 해결하셨다면 공유 부탁드려도 될까요?

ulgal commented 4 years ago

제 케이스는 cookie 안에 csrftoken이 있으면 동작하는데 cookie 안에 csrftoken이 없는 경우 동작하지 않아서(csrf_exempt로 일단 로그인 한 뒤에 csrf_exempt를 없애고 동작하는지 보시면 됩니다) token을 받아오는 api가 /api/token/이라고 할 때 frontend의 src 내 index.js에

...
import axios from 'axios';

axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN';
axios.defaults.xsrfCookieName = 'csrftoken';
axios.defaults.withCredentials = true;
axios.get('/api/token/');
ReactDOM.render(
  <Provider store={store}>
    <App history={history} />
  </Provider>,
  document.getElementById('root'),
);

serviceWorker.unregister();

로 get을 한번 해주면 서버에 접속해서 backend로 요청이 갈 때 GET이 먼저 가서 token을 받아오는 식으로 해결했습니다.

ktaebum commented 4 years ago

혹, signin을 할 때

@ensure_csrf_cookie
@csrf_exempt
def signin_view(request):
    # codes

를 설정하면 해결이 되는지 확인해보실 수 있나요? (signin이 성공하면 csrf_cookie를 같이 보내주게) 사실 get token api는 hw3에서 test 하기 위해 만들어 둔 것이고 실제 개발할 때 필요한 것은 아니라서요

https://docs.djangoproject.com/en/2.2/ref/csrf/#django.views.decorators.csrf.ensure_csrf_cookie

ulgal commented 4 years ago

제 서비스가 로그인을 해도 되고 안해도 되는 서비스라서 signin을 먼저 한다는 보장이 없는데 이런 경우에는 처음에 갈만한 두 페이지에 같은 방식을 적용하면 되나요?

ktaebum commented 4 years ago

음 그러네요 (login이 필요 없는데 이용할 수 있는 서비스도 있을테니) 그러면 저 방법보다 깔끔할 것 같은 방법은

첫 landing page로 떨어질 때 /api/token view를 get 해오도록 하고 /api/signin에 대해서도 @ensure_csrf_cookie만 해주면 (without csrf exempt)

@ensure_csrf_cookie
def signin_view(request):
    # codes

되지 않을까 하는 생각은 있습니다 (확실한 것은 아닙니다 😅)

ulgal commented 4 years ago

manage.py에서 확인했을 때 GET은 한번 불리고 그 뒤로 안불리는걸로 봐서 말씀하신 방식으로 동작은 하는 것 같습니다. csrf_cookie가 django만 사용할 경우 {% csrf_token %}만 넣어주면 알아서 생성된다고 해서 그러면 그냥 token을 get해오는 것도 괜찮지 않을까 생각해서 저렇게 만들었는데 혹시 보안 관련해서 문제가 있을 수 있나요? 제 생각엔 어차피 cookie에 다 저장되서 큰 상관은 없을 것 같긴 한데..

ktaebum commented 4 years ago

아 아니요 그런 측면에서 말씀드린 것은 아니고 단순히 새로고침을 하거나 navigation등등 때문에 새로고침처럼 행동하는 경우에 불필요한 axios.get call이 많아질 것 같은 생각에 말씀드렸습니다

ulgal commented 4 years ago

확인해보니 새로고침할때마다 GET 요청이 계속 들어와서 전체 페이지를 render할 때 불리는 것 같습니다.

...
import axios from 'axios';
import Cookie from 'js-cookie';
...

axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN';
axios.defaults.xsrfCookieName = 'csrftoken';
axios.defaults.withCredentials = true;
if(Cookie.get().csrftoken===undefined){
        axios.get('/api/token/');
}
ReactDOM.render(
  <Provider store={store}>
    <App history={history} />
  </Provider>,
  document.getElementById('root'),
);

serviceWorker.unregister();

로 하면 불필요한 axios.get call을 줄일 수 있습니다.

ktaebum commented 4 years ago

그런데 단순히 저렇게 해결하면 login을 했을 때 csrf refresh되는 것이 반영이 되나요?

For security reasons, CSRF tokens are rotated each time a user logs in. 
Any page with a form generated before a login will have an old, invalid CSRF token and need to be reloaded. 
This might happen if a user uses the back button after a login or if they log in a different browser tab.

https://docs.djangoproject.com/en/2.2/ref/csrf/#why-might-a-user-encounter-a-csrf-validation-failure-after-logging-in

ulgal commented 4 years ago

login 시 response에 있는 token이 자동으로 cookie에 반영되는 것 같습니다. www.snubot.xyz 에 user1 / swppswpp로 로그인해보시면 확인하실 수 있습니다. 이 방법이 취약한 부분은 이미 render된 이후에 cookie에 있는 csrftoken이 없어지는 경우 이전과 같은 증상이 나타나는 건데, render 된 이후에 cookie가 없어지는 경우는 생각이 나지 않기도 하고 새로고침 하면 다시 동작해서 굳이 고치진 않았습니다. 혹시 이 부분을 해결하는 좋은 방법이 있다면 알려주시면 감사하겠습니다.

Yoo-Youngjae commented 4 years ago

@ulgal 님의 방법대로 작성해서 해결하였습니다! 감사합니다.

그런데, 혹시 ssh 를 연결한 terminal 을 종료해도 uwsgi 가 background 에서 실행 되도록 하는 방법이 있을까요?

terminal 을 종료하면, nginx 는 동작하여 frontend 는 작동하는데, uwsgi 가 실행 종료되어 django 는 작동하지 않습니다..

ulgal commented 4 years ago

실행 커맨드 뒤에 & 붙이면 백그라운드에서 실행됩니다. 나중에 끌땐 fuser -k (portnumber)/tcp하면 꺼집니다

davin111 commented 4 years ago

@Yoo-Youngjae 또는 uwsgi.ini 파일을 사용해서 돌리신다면, daemonize 옵션을 설정하시는 방법도 있습니다. 관련해서 검색해보시면 자료 나올 거예요.

HaeSe0ng commented 4 years ago

저희는 tmux 로 돌리고 있습니다. tmux new -s back 으로 back이란 이름의 세션을 만들고 그 안에서 실행하는 방식인데 터미널이 종료돼도 세션이 살아있어서 꼭 백엔드 아니더라도 여러 작업하기 편합니다 ㅎㅎ

ktaebum commented 4 years ago

@YooYoungJae 간편하게 tmux 추천드립니다 @ulgal 음 그 부분까지는 신경 안 써도 될 것 같긴 하지만 작성하신

if(Cookie.get().csrftoken===undefined){
        axios.get('/api/token/');
}

이 logic을 모든 axios call에 prehook으로 걸어주는 방법도 있을 것 같습니다 정말 간단하게 짜면

const token_instance = axios.create();
axios.interceptors.request.use(async cfg => {
    if(Cookie.get().csrftoken===undefined){
        await token_instance.get('/api/token/');
    }
    return cfg;

가 될 수 있을 것 같구요 여기서 interceptor.request는 모든 request에 대한 pre-hook callback을 등록하는 것입니다. https://github.com/axios/axios#interceptors

저 코드는 정말 돌아가게만 짜본 것이고 관련해서는 axios interceptor refresh token 등으로 검색하면 많이 찾을 수 있습니다.