과제
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-admin
commands
배포 직후 슈퍼유저를 생성하기 위해 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에 반드시 포함