과제

1. DeleteToken

django delete token 검색 결과, ObtainAuthToken 클래스에서 힌트를 얻어 구현

member/apis/token.py

class DeleteToken(APIView):
    permission_classes = (permissions.IsAuthenticated,)

    def post(self, request, *args, **kwargs):
        user = request.user
        token = Token.objects.get(user=user)
        token.delete()
        return Response({'Delete token': token.key})

리팩토링

class DeleteToken(APIView):
    permission_classes = (permissions.IsAuthenticated,)

    def post(self, request, *args, **kwargs):
        request.auth.delete()
        return Response(status.HTTP_204_NO_CONTENT)

member/urls/apis.py

from django.conf.urls import url
from rest_framework.authtoken import views as authtoken_views

from .. import apis

urlpatterns = [
    # ...
    url(r'^token-delete/$', apis.DeleteToken.as_view()),
]

2. django-rest-auth를 이용해 login/logout api 구현

$ pip install django-rest-auth

settings.py

INSTALLED_APPS = [
	# ...
    'rest_framework',
    'rest_framework.authtoken',
    'rest_auth',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',
        # CSRF Failed: CSRF token missing or incorrect 에러가 발생하므로 제거
        # 포스트맨에서 테스트할 때 문제가 있음
        # 제거하면 장고자체에서는 세션 인증이 되지만, api에서는 사용하지 않게 됨
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication'
    )
}

urls.py

urlpatterns = [
    # ...
    url(r'^rest-auth/', include('rest_auth.urls')),
]

포스트맨 확인

요청의 결과로 응답에 Token key를 확인할 수 있다.

POST http://127.0.0.1:8000/rest-auth/login/
Body form-data
	username : <username>
	password : <password>

로그아웃도 해본다.

POST http://127.0.0.1:8000/rest-auth/logout/
Header
	Authorization : Token <your_token>

2-1. 로그아웃 메시지 커스텀

장고의 로그아웃 동작도 세션에 있는 값을 지우고 끝이다. 커스텀 해주려면 다음과 같이 한다.

member/apis/token.py

from django.contrib.auth import logout as django_logout
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext_lazy as _
from rest_auth.views import LogoutView as RestLogoutView

class LogoutView(RestLogoutView):
    def logout(self, request):
        try:
            request.user.auth_token.delete()
        except (AttributeError, ObjectDoesNotExist):
            return Response({'detail': 'Invalid token'}, status=status.HTTP_401_UNAUTHORIZED)

        django_logout(request)

        return Response({"detail": _("Successfully logged out.")},
                        status=status.HTTP_200_OK)

member/urls/apis.py

from member.apis import LogoutView

urlpatterns = [
    url(r'^rest-auth/logout/', LogoutView.as_view()),
    url(r'^rest-auth/', include('rest_auth.urls')),
]

3. 스크롤시 자동으로 PostList 로드 (+ 코드 분리)

app/indext.html

<!-- Custom Script -->
<script src="static/js/cookie.js"></script>
<script src="static/js/dom-utils.js"></script>
<script src="static/js/apis.js"></script>
<script src="static/js/api/load-post-list.js"></script>

<script>
loadPostList();

</script>

app/static/js/api/load-post-list.js

var next;
var isLoading = false;
var url = 'http://localhost:8000/api/post/';

$(window).scroll(function(){
    if($(window).scrollTop() + $(window).height() > $(document).height() - 100) {
      if(isLoading == false){
      loadPostList();
    }
  }
});


function loadPostList() {
  isLoading = true;
  $('.post-list-container').append('<div class=loading>Loading...</div>')

  if(url == undefined && url == null) {
    return;
  }
  console.log(url)
  $.ajax(url)
   .done(function(data) {
     url = data.next;
     for(var i=0; i < data.results.length; i++) {
       var curPost = data.results[i];
       addArticle(curPost)
     }
     isLoading = false;
     $('.post-list-container').find('.loading').remove();
   })
   .fail(function(data) {
     console.log('fail');
     console.log(data);
     $('.contentt-container').append('<div>Fail</div>')
   });
}

app/static/js/apis.js

function obtainAuthToken(username, password) {
  url = 'http://localhost:8000/api/member/token-auth/';
  $.ajax({
    url: url,
    method: 'POST',
    data: {
      username: username,
      password: password,
    }
  })
  .done(function(data){
    var token = data.token;
    // console.log(token)
    setCookie('instagramToken', token);
  })
  .fail(function(data){
  });
}


function postCreate() {
  var url = 'http://localhost:8000/api/post/';
  var token = 'Token ' + getCookie('instagramToken');
  console.log(token);
  $.ajax({
    url: url,
    method: 'POST',
    headers: {
      Authorization: token,
    }
  })
  .done(function(data){
    console.log(data);
  });
}

4. PostCreate

app/post-create.html

생성 후 작성

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Post Create</title>
</head>
<body>
  <form id="post-upload-form" action="">
    <input id="form-content" type="text" placeholder="Comment">
    <input id="form-photos" type="file" multiple="true">
    <button type="submit">Upload!</button>
  </form>
  <script src="../bower_components/jquery/dist/jquery.js"></script>
  <script src="static/js/cookie.js"></script>
  <script src="static/js/api/post-create.js"></script>
  <script>
    $('#post-upload-form').submit(function(event) {
      event.preventDefault();
      console.log('form submit!');

      postCreate($('#form-photos')[0].files);
    });

    function postCreate(files) {
      var url = 'http://localhost:8000/api/post/';
      var token = 'Token ' + getCookie('instagramToken');
      console.log(token);
      $.ajax({
        url: url,
        method: 'POST',
        headers: {
          Authorization: token,
        }
      })
      .done(function(data) {
        console.log(data);
        var postPk = data.pk;
        for(var i = 0; i < files.length; i++) {
          var curFile = files[i];
          photoCreate(postPk, curFile);
        }
        window.location = '/app';
      });
    }

    function photoCreate(postPk, file) {
      var url = 'http://localhost:8000/api/post/photo/';
      var token = 'Token ' + getCookie('instagramToken');
      var formData = new FormData();
      formData.append('post', postPk);
      formData.append('photo', file);
      $.ajax({
        url: url,
        method: 'POST',
        data: formData,
        contentType: false,
        processData: false,
      })
      .done(function(data) {
        console.log('done');
        console.log(data);
      })
      .fail(function(data) {
        console.log('fail');
        console.log(data);
      });
    }

  </script>
</body>
</html>

app/index.html

<div class="content-container">
  <div class="post-list-container">
    <a href="post-create.html" class="btn-add-post">+ Add Post</a>
  </div>
</div>

배포

데이터베이스 (RDS)

PgAdmin에 접속해 AWS RDS의 데이터베이스를 생성한다.

settings_deploy.json

{
  "django": {
    "allowed_hosts": [
      "*"
    ]
  },
  "db": {
  "engine": "django.db.backends.postgresql_psycopg2",
  "name": "instagram_api",
  "user": "...",
  "password": "...",
  "host": "...",
  "port": "5432"
  }
}
$ DB='RDS' ./manage.py migrate

마이그레이션 해본다. could not connect to server: Operation timed out 에러가 나면, security group에 ip를 추가한다. (eb로 배포시 ip를 알수 없으므로, 일단 anywhere로 해둔다.)

Static (S3)

boto3 설치

$ pip install boto3
$ python

bucket 생성

>>> import boto3
>>> session = boto3.Session(profile_name='default')
>>> client = session.client('s3')
>>> client.create_bucket(Bucket='bucket_name', CreateBucketConfiguration={'LocationConstraint': 'ap-northeast-2'})

django-storages 설치

$ pip install django-storages

settings.py

INSTALLED_APPS = [
    'storages',
]

settings_common.json

  "aws": {
    "s3_storage_bucket_name": "bucket_name",
    "s3_signature_version": "s3v4",
    "s3_region": "ap-northeast-2"
  }

storages.py 생성 후 작성

from django.conf import settings
from storages.backends.s3boto3 import S3Boto3Storage


class StaticStorage(S3Boto3Storage):
    location = settings.STATICFILES_LOCATION
    file_overwrite = True


class MediaStorage(S3Boto3Storage):
    location = settings.MEDIAFILES_LOCATION
    file_overwrite = False
$ DB='RDS' STORAGE='S3' ./manage.py collectstatic
$ DB='RDS' STORAGE='S3' ./manage.py runserver

Docker

설정 파일

.conf
	- nginx.conf
	- nginx-app.conf
	- supsupervisor-app.cof
	- uwsgi-app.ini

Dockerfiles 작성 및 실행

$ docker build . -t insta
$ docker run --rm -it -p 8081:4040 insta

http://localhost:8081/ 접속해 정상작동을 확인한다.

EB

Dockerfile.aws.json (name 변경)
.ebignore

(EB - ELB - )EC2 - Docker 조작

collectstatic, migrate 같은 작업을 주기적 혹은 배포 후 해줘야 한다.

  • EB는 오토스케일링을 지원하는데, 그 중에 메인이 EC2 서버가 있다.
  • 이 서버에만 설정을 해주면된다. (leader_only)
  • 배포(deploy)가 완료되는 순간(hook)을 캐치할 수 있다.
    • 폴더 생성 > command 생성

1. Writing custom django-admincommands

배포 직후 슈퍼유저를 생성하기 위해 Django admin 명령어를 만들어야 한다.

django_app/member/management/commands/createsu.py
from django.contrib.auth import get_user_model
from django.core.management import BaseCommand

from config.settings import config

User = get_user_model()


class Command(BaseCommand):
    def handle(self, *args, **options):
        superuser_username = config['django']['superuser']['username']
        superuser_password = config['django']['superuser']['password']
        superuser_email = config['django']['superuser']['email']

        if not User.objects.filter(username=superuser_username).exists():
            User.objects.create_superuser(
                username=superuser_username,
                password=superuser_password,
                email=superuser_email
            )
settings_common.json
  "django": {
    "secret_key": "...",
    "superuser": {
      "username": "..",
      "password": "...",
      "email": "..."
    }
  },

2. .ebextensions

django_app
.ebextensions
├── 00_command_files.config
└── 01_django.config
docker 명령의미
  • docker ps : containers list
    • –no-trunc : 자르지 않은 전체 ID
    • -q : 숫자 ID를 표시
# 00_command_files.config
# shell script

sudo docker exec `sudo docker ps --no-trunc -q | head -n 1` python3 /srv/app/django_app/manage.py collectstatic --noinput
$ eb ssh
[ec2-user@ip-172-31-17-214 ~]$ sudo docker ps --no-trunc -q | head -n 1
231ed8767e4a7b7ed585bb6c87d25d20325ec79e184c872e090ecb465302594e
[ec2-user@ip-172-31-17-214 ~]$ sudo docker ps
CONTAINER ID|IMAGE|COMMAND|CREATED|STATUS|PORTS|NAMES
231ed8767e4a|a1daf87d29b0|"/bin/sh -c 'supervis'"|16 hours ago|Up 16 hours|4040/tcp|thirsty_bhaskara

[ec2-user@ip-172-31-17-214 ~]$ sudo docker exec 231ed8767e4a7b7ed585bb6c87d25d20325ec79e184c872e090ecb465302594e -it /bin/bash
rpc error: code = 13 desc = invalid header field value "oci runtime error: exec failed: container_linux.go:247: starting container process caused \"exec: \\\"-it\\\": executable file not found in $PATH\"\n"
[ec2-user@ip-172-31-17-214 ~]$ sudo docker exec -it 231ed8767e4a7b7ed585bb6c87d25d20325ec79e184c872e090ecb465302594e /bin/bash
root@231ed8767e4a:/srv/app# python3 manage.py
python3: cant open file 'manage.py': [Errno 2] No such file or directory
root@231ed8767e4a:/srv/app# python3 /srv/app/django_app/manage.py
DEBUG: False
STORAGE_S3 : True
DB_RDS : False

Type 'manage.py help <subcommand>' for help on a specific subcommand.

Available subcommands:

[auth]
    changepassword
    createsuperuser

[django]
    check
    compilemessages
    createcachetable
    dbshell
    diffsettings
    dumpdata
    flush
    inspectdb
    loaddata
    makemessages
    makemigrations
    migrate
    sendtestemail
    shell
    showmigrations
    sqlflush
    sqlmigrate
    sqlsequencereset
    squashmigrations
    startapp
    startproject
    test
    testserver

[member]
    createsu

[sessions]
    clearsessions

[staticfiles]
    collectstatic
    findstatic
    runserver

배포

$ pip install awsebcli
$ eb init
$ eb create
Enter Environment Name
(default is instagram-api-back-end-dev):
Enter DNS CNAME prefix
(default is instagram-api-back-end-dev):
Select a load balancer type
1) classic
2) application
(default is 1): 2

$ eb deploy
$ eb open

front 코드까지 배포할 경우

Insta-api
├── django_app : niginx의 /api/로 지정해서 wsgi 연결
└── front 코드 : nginx의 /(루트)를 front 코드로 연결, .gitignore에 반드시 포함