diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79d0438 --- /dev/null +++ b/.gitignore @@ -0,0 +1,212 @@ +# Created by https://www.toptal.com/developers/gitignore/api/django,terraform +# Edit at https://www.toptal.com/developers/gitignore?templates=django,terraform + +### Django ### +*.log +*.pot +*.pyc +__pycache__/ +local_settings.py +db.sqlite3 +db.sqlite3-journal +media + +# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ +# in your Git repository. Update and uncomment the following line accordingly. +# /staticfiles/ + +### Django.Python Stack ### +# Byte-compiled / optimized / DLL files +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo + +# Django stuff: + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Terraform ### +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +# End of https://www.toptal.com/developers/gitignore/api/django,terraform + +.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..81f4ffa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +# Base + +FROM python:3.12-slim-bookworm as base + +ENV PYTHONBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 + +RUN apt-get update && apt-get install -y \ + build-essential \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Builder + +FROM base as builder + +ENV POETRY_VERSION=1.8.3 \ + POETRY_VIRTUALENVS_IN_PROJECT=false \ + POETRY_VIRTUALENVS_CREATE=false \ + POETRY_NO_INTERACTION=1 + +COPY ./application/poetry.lock ./application/pyproject.toml ./ +RUN pip install poetry==$POETRY_VERSION \ + && poetry install --no-root + +# Runner + +FROM base as runner + +COPY --from=builder /usr/local/bin /usr/local/bin +COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages +COPY ./application /app + +ARG DJANGO_SETTINGS +ENV DJANGO_SETTINGS=$DJANGO_SETTINGS + +CMD [ "/bin/bash", "-c", "python manage.py migrate && gunicorn config.wsgi:application --bind 0.0.0.0:8000"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..388083e --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# Fastcampus - System Designs with Anti-Patterns + +## Instruction + +### Preliminary + +1. Create your AWS credentials and store them safely. +2. Create a file named `secrets.tfvars` inside the directory `infrastructure`. +3. Fill the file as below: + ``` + aws_access_key = "YOURAWSACCESSKEY" + aws_secret_key = "YOURAWSSECRETACCESSKEY" + ``` +4. Move inside the `infrastructure` directory and enter `terraform init` in your terminal. + +### Terraform plan and apply + +- To plan the infrastructure definitions, enter `terraform plan -var-file=secrets.tfvars` inside the terraform root. +- To apply the plan results, issue the command `terraform apply -var-file=secrets.tfvars`. + +### Access the EKS cluster + +1. Install AWS CLI and kubectl. +2. Register your AWS credentials as a profile in your AWS CLI configuration file. +3. Enter `aws eks --region ap-northeast-2 update-kubeconfig --name fc-sre-cluster` to register your cluster to the `kubectl` configuration. + - If you're using a profile other than the default profile, you should append `--profile your-profile` on the command above. +4. Enter `kubectl config use-context arn:aws:eks:ap-northeast-2:1234567890:cluster/fc-sre-cluster` (change `1234567890` to your account ID). +5. Check `kubectl get nodes` + +### Destroying resources + +To destory all the resources that have been provisioned, make sure issuing `terraform destroy -var-file=secrets.tfvars`. \ No newline at end of file diff --git a/application/config/__init__.py b/application/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/application/config/asgi.py b/application/config/asgi.py new file mode 100644 index 0000000..787b362 --- /dev/null +++ b/application/config/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for config project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +application = get_asgi_application() diff --git a/application/config/settings/__init__.py b/application/config/settings/__init__.py new file mode 100644 index 0000000..baed90c --- /dev/null +++ b/application/config/settings/__init__.py @@ -0,0 +1,6 @@ +import os + +from .base import * + +if os.environ.get("DJANGO_SETTINGS", "local") == "prod": + from .prod import * \ No newline at end of file diff --git a/application/config/settings/base.py b/application/config/settings/base.py new file mode 100644 index 0000000..231cfef --- /dev/null +++ b/application/config/settings/base.py @@ -0,0 +1,125 @@ +""" +Django settings for config project. + +Generated by 'django-admin startproject' using Django 4.2.13. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-80c97jv5bp*5)umbc_!c@7$jh0rqkl$j34-zn_ji1u#2o(s$1v' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'lectures', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'config.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'config.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' +STATIC_ROOT = '/static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/application/config/settings/prod.py b/application/config/settings/prod.py new file mode 100644 index 0000000..303f563 --- /dev/null +++ b/application/config/settings/prod.py @@ -0,0 +1,15 @@ +import os + +DEBUG = False + +ALLOWED_HOSTS = ["*"] + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.environ.get("DATABASE_NAME"), + "USER": os.environ.get("DATABASE_USER"), + "PASSWORD": os.environ.get("DATABASE_PASSWORD"), + "HOST": os.environ.get("DATABASE_HOST"), + } +} \ No newline at end of file diff --git a/application/config/urls.py b/application/config/urls.py new file mode 100644 index 0000000..4f17e71 --- /dev/null +++ b/application/config/urls.py @@ -0,0 +1,26 @@ +""" +URL configuration for config project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +from lectures.views import LectureView, LectureRegisterView + +urlpatterns = [ + path('admin/', admin.site.urls), + path('lectures/', LectureView.as_view()), + path('lectures/register/', LectureRegisterView.as_view()), +] diff --git a/application/config/wsgi.py b/application/config/wsgi.py new file mode 100644 index 0000000..8ae71e3 --- /dev/null +++ b/application/config/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for config project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +application = get_wsgi_application() diff --git a/application/lectures/__init__.py b/application/lectures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/application/lectures/admin.py b/application/lectures/admin.py new file mode 100644 index 0000000..90ce1ed --- /dev/null +++ b/application/lectures/admin.py @@ -0,0 +1,23 @@ +from django.contrib import admin + +from lectures.models import Department, Lecture, Person, Professor + + +@admin.register(Department) +class DepartmentAdmin(admin.ModelAdmin): + list_display = ["name", "code", "category"] + + +@admin.register(Lecture) +class LectureAdmin(admin.ModelAdmin): + list_display = ["name", "code", "professor", "credit", "register_limit"] + + +@admin.register(Person) +class Person(admin.ModelAdmin): + list_display = ["name", "department", "total_credit"] + + +@admin.register(Professor) +class Professor(admin.ModelAdmin): + list_display = ["name", "email", "department"] \ No newline at end of file diff --git a/application/lectures/apps.py b/application/lectures/apps.py new file mode 100644 index 0000000..30d62f2 --- /dev/null +++ b/application/lectures/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class LecturesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'lectures' diff --git a/application/lectures/migrations/0001_initial.py b/application/lectures/migrations/0001_initial.py new file mode 100644 index 0000000..99a4939 --- /dev/null +++ b/application/lectures/migrations/0001_initial.py @@ -0,0 +1,216 @@ +# Generated by Django 4.2.13 on 2024-06-03 11:13 + +from django.db import migrations, models +import django.db.models.deletion + + +def create_initial_records(apps, schema_editor): + Department = apps.get_model("lectures", "Department") + Person = apps.get_model("lectures", "Person") + Professor = apps.get_model("lectures", "Professor") + Lecture = apps.get_model("lectures", "Lecture") + + Department.objects.bulk_create([ + Department(name="컴퓨터공학과", code="CS", category="ENG"), + Department(name="화학공학과", code="CE", category="ENG"), + Department(name="물리학과", code="PH", category="SCI"), + Department(name="영어영문학과", code="EL", category="LIT"), + Department(name="불어불문학과", code="FL", category="LIT"), + Department(name="회화과", code="PA", category="ART"), + ]) + + departments = Department.objects.all() + people = [] + family_names = ["김", "이", "박", "최", "정", "강", "조", "윤", "장", "임", "한", "오", "서", "신", "황", "안", "송", "전", "홍"] + given_names = [ + "민준", + "서윤", + "서준", + "서연", + "예준", + "지우", + "도윤", + "하준", + "수아", + "지민", + "지호", + "지안", + "예린", + "지윤", + "시우", + "하윤", + "주원", + "지원", + "윤서", + "지후", + "하율", + "하빈", + "유진", + "지아", + "수현", + "은우", + "도연", + "연우", + "수빈", + "예빈", + "시윤", + "태윤", + "윤아", + "서진", + "유나", + "예지", + "서현", + "하람", + "민서", + "다은", + "소윤", + "지율", + "다인", + "준우", + "서하", + "서영", + "민성", + "윤호", + "유준", + "은서", + "하연", + "지웅", + "소연", + "다윤", + "현우", + "시현", + "수호", + "은수", + "승우", + "지훈", + "시영", + "하람", + "예나", + "민호", + "유빈", + "시완", + "주하", + "하은", + "은채", + "하온", + "민아", + "수아", + "도현", + "도영", + "시은", + "하린", + "수정", + "지수", + "예슬", + "하온", + "서율", + "다영", + "하영", + "도율", + "소은", + "채원", + "예나", + "준서", + "지유", + "채은", + "은성", + "시안", + "도경", + "은하", + "주현", + "연서", + "세빈", + "주영", + "하늘", + "시영", + "채윤", + "유나", + ] + + count = 0 + for family_name in family_names: + for given_name in given_names: + people.append(Person(name=family_name+given_name, department=departments[count%6])) + count += 1 + Person.objects.bulk_create(people) + + Professor.objects.bulk_create([ + Professor(name="Prof. Kim", email="kim@test.com", department=departments[0]), + Professor(name="Prof. Lee", email="lee@test.com", department=departments[1]), + Professor(name="Prof. Park", email="park@test.com", department=departments[2]), + Professor(name="Prof. Choi", email="choi@test.com", department=departments[3]), + Professor(name="Prof. Jeong", email="jeong@test.com", department=departments[4]), + Professor(name="Prof. Kang", email="kang@test.com", department=departments[5]), + ]) + + professors = Professor.objects.all() + lectures = [] + prefixes = ["기본 ", "고급 ", "현대 ", "고대 ", "중세 ", "중급 ", "실무 ", "실용 "] + topics = ["진", "케이틀린", "애쉬", "징크스", "럭스", "타릭", "녹턴", "벡스", "소나", "케넨"] + suffixes = ["의 이해", "의 역사", "학 개론", "학 입문", "의 응용"] + + count = 0 + for prefix in prefixes: + for topic in topics: + for suffix in suffixes: + lectures.append( + Lecture( + name=prefix+topic+suffix, + code=topic+str(count), + professor=professors[count%6], + register_limit=30, + ) + ) + count += 1 + Lecture.objects.bulk_create(lectures) + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Department', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=32)), + ('code', models.CharField(max_length=2)), + ('category', models.CharField(max_length=10)), + ], + ), + migrations.CreateModel( + name='Person', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=32)), + ('department', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lectures.department')), + ('total_credit', models.IntegerField(default=0)), + ], + ), + migrations.CreateModel( + name='Professor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=32)), + ('department', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lectures.department')), + ('email', models.EmailField(max_length=254)), + ], + ), + migrations.CreateModel( + name='Lecture', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=32)), + ('code', models.CharField(max_length=5)), + ('credit', models.IntegerField(default=3)), + ('register_limit', models.IntegerField()), + ('professor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lectures.professor')), + ('students', models.ManyToManyField(to='lectures.person')), + ], + ), + migrations.RunPython(create_initial_records), + ] diff --git a/application/lectures/migrations/__init__.py b/application/lectures/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/application/lectures/models.py b/application/lectures/models.py new file mode 100644 index 0000000..484b6b9 --- /dev/null +++ b/application/lectures/models.py @@ -0,0 +1,39 @@ +from django.db import models + + +class Department(models.Model): + name = models.CharField(max_length=32) + code = models.CharField(max_length=2) + category = models.CharField(max_length=10) + + def __str__(self): + return self.name + + +class Person(models.Model): + name = models.CharField(max_length=32) + department = models.ForeignKey(Department, on_delete=models.CASCADE) + total_credit = models.IntegerField(default=0) + + def __str__(self): + return self.name + + +class Professor(models.Model): + name = models.CharField(max_length=32) + email = models.EmailField() + department = models.ForeignKey(Department, on_delete=models.CASCADE) + + def __str__(self): + return f"{self.name}/{self.email}" + +class Lecture(models.Model): + name = models.CharField(max_length=32) + code = models.CharField(max_length=5) + professor = models.ForeignKey(Professor, on_delete=models.CASCADE) + credit = models.IntegerField(default=3) + register_limit = models.IntegerField() + students = models.ManyToManyField(Person) + + def __str__(self): + return f"[{self.code}] {self.name} - {self.professor}" \ No newline at end of file diff --git a/application/lectures/serializers.py b/application/lectures/serializers.py new file mode 100644 index 0000000..96579a9 --- /dev/null +++ b/application/lectures/serializers.py @@ -0,0 +1,29 @@ +from rest_framework import serializers + +from lectures.models import Department, Lecture, Professor + + +class DepartmentSerializer(serializers.ModelSerializer): + class Meta: + model = Department + fields = ["name", "code", "category"] + + +class ProfessorSerializer(serializers.ModelSerializer): + department = DepartmentSerializer() + + class Meta: + model = Professor + fields = ["name", "email", "department"] + + +class LectureSerializer(serializers.ModelSerializer): + professor = ProfessorSerializer() + register_count = serializers.SerializerMethodField() + + class Meta: + model = Lecture + fields = ["name", "code", "professor", "credit", "register_limit", "register_count"] + + def get_register_count(self, obj): + return obj.students.count() \ No newline at end of file diff --git a/application/lectures/views.py b/application/lectures/views.py new file mode 100644 index 0000000..64028d9 --- /dev/null +++ b/application/lectures/views.py @@ -0,0 +1,50 @@ +from django.db import transaction +from rest_framework.response import Response +from rest_framework.views import APIView + +from lectures.models import Lecture, Person +from lectures.serializers import LectureSerializer + + +class LectureView(APIView): + def get(self, *args, **kwargs): + lectures = self._filter() + serializer = LectureSerializer(lectures, many=True) + return Response(serializer.data) + + def _filter(self): + queryset = Lecture.objects.all() + if department_code := self.request.query_params.get("department"): + queryset = queryset.filter(professor__department__code=department_code) + return queryset + + +class LectureRegisterView(APIView): + @transaction.atomic + def post(self, *args, **kwargs): + student_id = self.request.data.get("student_id") + lecture_id = self.request.data.get("lecture_id") + + student = Person.objects.get(id=student_id) + lecture = Lecture.objects.get(id=lecture_id) + + if lecture.register_limit < lecture.students.count(): + return Response({"message": "수강 신청이 마감된 강의입니다."}, status=400) + + if student.total_credit + lecture.credit > 24: + return Response({"message": "24학점 이상 수강할 수 없습니다."}, status=400) + + count = student.lecture_set.exclude(professor__department__code=student.department.code).count() + if count == 2: + return Response({"message": "다른 학과의 강의는 두 개까지 수강할 수 있습니다."}, status=400) + + if lecture.students.filter(id=student_id).exists(): + return Response({"message": "이미 수강 신청을 한 과목입니다."}, status=400) + + lecture.students.add(student) + lecture.save() + + student.total_credit += lecture.credit + student.save() + + return Response({"message": "수강 신청에 성공했습니다."}, status=204) diff --git a/application/manage.py b/application/manage.py new file mode 100755 index 0000000..8e7ac79 --- /dev/null +++ b/application/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/application/poetry.lock b/application/poetry.lock new file mode 100644 index 0000000..c7981d0 --- /dev/null +++ b/application/poetry.lock @@ -0,0 +1,112 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "asgiref" +version = "3.8.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "django" +version = "4.2.13" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Django-4.2.13-py3-none-any.whl", hash = "sha256:a17fcba2aad3fc7d46fdb23215095dbbd64e6174bf4589171e732b18b07e426a"}, + {file = "Django-4.2.13.tar.gz", hash = "sha256:837e3cf1f6c31347a1396a3f6b65688f2b4bb4a11c580dcb628b5afe527b68a5"}, +] + +[package.dependencies] +asgiref = ">=3.6.0,<4" +sqlparse = ">=0.3.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "djangorestframework" +version = "3.15.1" +description = "Web APIs for Django, made easy." +optional = false +python-versions = ">=3.6" +files = [ + {file = "djangorestframework-3.15.1-py3-none-any.whl", hash = "sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6"}, + {file = "djangorestframework-3.15.1.tar.gz", hash = "sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1"}, +] + +[package.dependencies] +django = ">=3.0" + +[[package]] +name = "gunicorn" +version = "22.0.0" +description = "WSGI HTTP Server for UNIX" +optional = false +python-versions = ">=3.7" +files = [ + {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"}, + {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "sqlparse" +version = "0.5.0" +description = "A non-validating SQL parser." +optional = false +python-versions = ">=3.8" +files = [ + {file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"}, + {file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"}, +] + +[package.extras] +dev = ["build", "hatch"] +doc = ["sphinx"] + +[[package]] +name = "tzdata" +version = "2024.1" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.12" +content-hash = "f079b044558bca5194056629f876d288dd8a9537931370e6ca4a434fa96a1382" diff --git a/application/pyproject.toml b/application/pyproject.toml new file mode 100644 index 0000000..19f9d3c --- /dev/null +++ b/application/pyproject.toml @@ -0,0 +1,17 @@ +[tool.poetry] +name = "fc-sre-app" +version = "0.1.0" +description = "" +authors = ["Yuneui Jeong"] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.12" +django = "~4.2" +djangorestframework = "^3.15" +gunicorn = "~22.0" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/chart/.helmignore b/chart/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/chart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/chart/Chart.yaml b/chart/Chart.yaml new file mode 100644 index 0000000..3c9b9c9 --- /dev/null +++ b/chart/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: chart +description: A Helm chart for Kubernetes + +type: application + +version: 0.1.0 + +appVersion: "1.16.0" diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl new file mode 100644 index 0000000..ec41afb --- /dev/null +++ b/chart/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "chart.name" -}} +{{- default .Chart.Name .Values.common.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "chart.fullname" -}} +{{- if .Values.common.fullnameOverride }} +{{- .Values.common.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.common.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "chart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "chart.labels" -}} +helm.sh/chart: {{ include "chart.chart" . }} +{{ include "chart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "chart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "chart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "chart.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "chart.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml new file mode 100644 index 0000000..9f8d0ab --- /dev/null +++ b/chart/templates/deployment.yaml @@ -0,0 +1,82 @@ +{{- if eq .Values.workload.kind "Deployment" -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "chart.fullname" . }} + labels: + {{- include "chart.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.workload.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "chart.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.workload.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "chart.labels" . | nindent 8 }} + {{- with .Values.workload.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.workload.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "chart.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.workload.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + {{- if .Values.workload.podSecurityContext }} + securityContext: + {{- toYaml .Values.workload.securityContext | nindent 12 }} + {{- end }} + {{- if .Values.workload.image.ecr.enabled }} + image: "{{ .Values.common.awsAccount }}.dkr.ecr.{{ .Values.common.awsRegion }}.amazonaws.com/{{ .Values.workload.image.ecr.repository }}:{{ .Values.workload.image.tag }}" + {{- else }} + image: "{{ .Values.workload.image.repository }}:{{ .Values.workload.image.tag }}" + {{- end }} + imagePullPolicy: {{ .Values.workload.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.workload.containerPort }} + protocol: TCP + livenessProbe: + httpGet: + path: {{ .Values.workload.probes.livenessProbe.path }} + port: http + {{- if .Values.workload.probes.livenessProbe.conditions }} + {{- toYaml .Values.workload.probes.livenessProbe.conditions | nindent 12 }} + {{- end }} + readinessProbe: + httpGet: + path: {{ .Values.workload.probes.readinessProbe.path }} + port: http + {{- if .Values.workload.probes.readinessProbe.conditions }} + {{- toYaml .Values.workload.probes.readinessProbe.conditions | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.workload.resources | nindent 12 }} + {{- if .Values.workload.envFrom }} + envFrom: + {{- toYaml .Values.workload.envFrom | nindent 12 }} + {{- end }} + {{- with .Values.workload.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.workload.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.workload.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/chart/templates/hpa.yaml b/chart/templates/hpa.yaml new file mode 100644 index 0000000..a91f61b --- /dev/null +++ b/chart/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "chart.fullname" . }} + labels: + {{- include "chart.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "chart.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml new file mode 100644 index 0000000..6d5a71a --- /dev/null +++ b/chart/templates/ingress.yaml @@ -0,0 +1,62 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "chart.fullname" . -}} +{{- $firstSvc := first .Values.service.ports -}} +{{- $svcPort := $firstSvc.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "chart.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/chart/templates/secret.yaml b/chart/templates/secret.yaml new file mode 100644 index 0000000..bc0b175 --- /dev/null +++ b/chart/templates/secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +type: Opaque +metadata: + name: {{ .Values.secret.name }} + labels: + {{- include "chart.labels" . | nindent 4 }} +data: + {{- toYaml .Values.secret.body | nindent 2 -}} \ No newline at end of file diff --git a/chart/templates/service.yaml b/chart/templates/service.yaml new file mode 100644 index 0000000..3bb7cc1 --- /dev/null +++ b/chart/templates/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "chart.fullname" . }} + labels: + {{- include "chart.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + {{- with .Values.service.ports }} + ports: + {{- toYaml . | nindent 4 }} + {{- end }} + selector: + {{- include "chart.selectorLabels" . | nindent 4 }} diff --git a/chart/templates/serviceaccount.yaml b/chart/templates/serviceaccount.yaml new file mode 100644 index 0000000..1df9350 --- /dev/null +++ b/chart/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "chart.serviceAccountName" . }} + labels: + {{- include "chart.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/chart/values.yaml b/chart/values.yaml new file mode 100644 index 0000000..3ce8659 --- /dev/null +++ b/chart/values.yaml @@ -0,0 +1,70 @@ +common: + nameOverride: "" + fullnameOverride: "" + awsAccount: "1234567890" + awsRegion: ap-northeast-2 + +serviceAccount: + create: true + automount: true + annotations: {} + name: "" + +workload: + kind: Deployment + replicaCount: 1 + image: + ecr: + enabled: false + repository: "" + repository: "" + pullPolicy: IfNotPresent + tag: "0.1.0" + containerPort: 80 + imagePullSecrets: [] + podAnnotations: {} + podLabels: {} + podSecurityContext: {} + securityContext: {} + probes: + livenessProbe: + path: / + conditions: {} + readinessProbe: + path: / + conditions: {} + resources: {} + envFrom: [] + nodeSelector: {} + tolerations: [] + affinity: {} + +secret: + name: "" + body: {} + +service: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: http + +ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: "" + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 \ No newline at end of file diff --git a/helm-prod-values.yaml b/helm-prod-values.yaml new file mode 100644 index 0000000..93f125b --- /dev/null +++ b/helm-prod-values.yaml @@ -0,0 +1,68 @@ +common: + nameOverride: fastcampus-app + fullnameOverride: fastcampus-app + +serviceAccount: + create: true + +workload: + kind: Deployment + replicaCount: 1 + image: + ecr: + enabled: true + repository: fastcampus-app-image + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + containerPort: 8000 + probes: + livenessProbe: + path: /lectures/ + conditions: + initialDelaySeconds: 20 + timeoutSeconds: 10 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + path: /lectures/ + conditions: + initialDelaySeconds: 20 + timeoutSeconds: 10 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + envFrom: + - secretRef: + name: fastcampus-app-secret + +secret: + name: fastcampus-app-secret + body: + key1: asdf + key2: fdsa + +autoscaling: + enabled: false + +service: + ports: + - port: 80 + targetPort: 8000 + protocol: TCP + name: http + +ingress: + enabled: true + annotations: + Kubernetes.io/ingress.class: nginx + hosts: + - host: fastcampus.uniglot.dev + paths: + - path: / + pathType: Prefix \ No newline at end of file diff --git a/infrastructure/.terraform.lock.hcl b/infrastructure/.terraform.lock.hcl new file mode 100644 index 0000000..a965271 --- /dev/null +++ b/infrastructure/.terraform.lock.hcl @@ -0,0 +1,105 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.51.1" + constraints = "~> 5.0, >= 5.30.0" + hashes = [ + "h1:KY/uPHIa+bHgMOAqoA2BnjIlIDuFRFwbLjLkf1gbeDk=", + "zh:03d524b70ab300d90dc4dccad0c28b18d797b8986722b7a93e40a41500450eaa", + "zh:04dbcb7ab52181a784877c409f6c882df34bda686d8c884d511ebd4abf493f0c", + "zh:2b068f7838e0f3677829258df05d8b9d73fe6434a1a809f8710956cc1c01ea03", + "zh:41a4b1e4adbf7c90015ebff17a719fc08133b8a2c4dcefd2fa281552126e59a8", + "zh:48b1adf57f695a72c88c598f99912171ef7067638fd63fb0c6ad3fa397b3f7c3", + "zh:5c2fb26ecb83adac90d06dcf5f97edbc944824c2821816b1653e1a2b9d37b3c4", + "zh:93df05f53702df829d9b9335e559ad8b313808dbd2fad8b2ff14f176732e693d", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b5da39898602e44551b56e2803a42d92ea7115e35b1792efbf6649da37ef597b", + "zh:b7ab7f743f864ed8d479a7cb04fd3ce00c376f867ee5b53c4c1acaef6e286c54", + "zh:e7e7b2d8ee486415481a25ac7bdded20bd2897d5dd0790741798f31935b9528d", + "zh:e8008e3f5ef560fd9004d1ed1738f0f53e99b0ce961d967e95fc7c02e5954e4e", + "zh:f1296f648b8608ffa930b52519b00ed01eebedde9fdaf94205b365536e6c3916", + "zh:f8539960fd978a54990740ee984c6f7f743c9c32c7734e2601e92abfe54367e9", + "zh:fd182e6e20bb52982752a5d8c4b16887565f413a9d50d9d394d2c06eea8a195e", + ] +} + +provider "registry.terraform.io/hashicorp/cloudinit" { + version = "2.3.4" + constraints = ">= 2.0.0" + hashes = [ + "h1:S3j8poSaLbaftlKq2STBkQEkZH253ZLaHhBHBifdpBQ=", + "zh:09f1f1e1d232da96fbf9513b0fb5263bc2fe9bee85697aa15d40bb93835efbeb", + "zh:381e74b90d7a038c3a8dcdcc2ce8c72d6b86da9f208a27f4b98cabe1a1032773", + "zh:398eb321949e28c4c5f7c52e9b1f922a10d0b2b073b7db04cb69318d24ffc5a9", + "zh:4a425679614a8f0fe440845828794e609b35af17db59134c4f9e56d61e979813", + "zh:4d955d8608ece4984c9f1dacda2a59fdb4ea6b0243872f049b388181aab8c80a", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:a48fbee1d58d55a1f4c92c2f38c83a37c8b2f2701ed1a3c926cefb0801fa446a", + "zh:b748fe6631b16a1dafd35a09377c3bffa89552af584cf95f47568b6cd31fc241", + "zh:d4b931f7a54603fa4692a2ec6e498b95464babd2be072bed5c7c2e140a280d99", + "zh:f1c9337fcfe3a7be39d179eb7986c22a979cfb2c587c05f1b3b83064f41785c5", + "zh:f58fc57edd1ee3250a28943cd84de3e4b744cdb52df0356a53403fc240240636", + "zh:f5f50de0923ff530b03e1bca0ac697534d61bb3e5fc7f60e13becb62229097a9", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.2" + constraints = ">= 3.0.0" + hashes = [ + "h1:IMVAUHKoydFrlPrl9OzasDnw/8ntZFerCC9iXw1rXQY=", + "zh:3248aae6a2198f3ec8394218d05bd5e42be59f43a3a7c0b71c66ec0df08b69e7", + "zh:32b1aaa1c3013d33c245493f4a65465eab9436b454d250102729321a44c8ab9a", + "zh:38eff7e470acb48f66380a73a5c7cdd76cc9b9c9ba9a7249c7991488abe22fe3", + "zh:4c2f1faee67af104f5f9e711c4574ff4d298afaa8a420680b0cb55d7bbc65606", + "zh:544b33b757c0b954dbb87db83a5ad921edd61f02f1dc86c6186a5ea86465b546", + "zh:696cf785090e1e8cf1587499516b0494f47413b43cb99877ad97f5d0de3dc539", + "zh:6e301f34757b5d265ae44467d95306d61bef5e41930be1365f5a8dcf80f59452", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:913a929070c819e59e94bb37a2a253c228f83921136ff4a7aa1a178c7cce5422", + "zh:aa9015926cd152425dbf86d1abdbc74bfe0e1ba3d26b3db35051d7b9ca9f72ae", + "zh:bb04798b016e1e1d49bcc76d62c53b56c88c63d6f2dfe38821afef17c416a0e1", + "zh:c23084e1b23577de22603cff752e59128d83cfecc2e6819edadd8cf7a10af11e", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.11.2" + constraints = ">= 0.9.0" + hashes = [ + "h1:qg3O4PmHnlPcvuZ2LvzOYEAPGOKtccgD5kPdQPZw094=", + "zh:02588b5b8ba5d31e86d93edc93b306bcbf47c789f576769245968cc157a9e8c5", + "zh:088a30c23796133678d1d6614da5cf5544430570408a17062288b58c0bd67ac8", + "zh:0df5faa072d67616154d38021934d8a8a316533429a3f582df3b4b48c836cf89", + "zh:12edeeaef96c47f694bd1ba7ead6ccdb96028b25df352eea4bc5e40de7a59177", + "zh:1e859504a656a6e988f07b908e6ffe946b28bfb56889417c0a07ea9605a3b7b0", + "zh:64a6ae0320d4956c4fdb05629cfcebd03bcbd2206e2d733f2f18e4a97f4d5c7c", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:924d137959193bf7aee6ebf241fbb9aec46d6eef828c5cf8d3c588770acae7b2", + "zh:b3cc76281a4faa9c2293a2460fc6962f6539e900994053f85185304887dddab8", + "zh:cbb40c791d4a1cdba56cffa43a9c0ed8e69930d49aa6bd931546b18c36e3b720", + "zh:d227d43594f8cb3d24f1fdd71382f14502cbe2a6deaddbc74242656bb5b38daf", + "zh:d4840641c46176bb9d70ba3aff09de749282136c779996b546c81e5ff701bbf6", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.0.5" + constraints = ">= 3.0.0" + hashes = [ + "h1:zeG5RmggBZW/8JWIVrdaeSJa0OG62uFX5HY1eE8SjzY=", + "zh:01cfb11cb74654c003f6d4e32bbef8f5969ee2856394a96d127da4949c65153e", + "zh:0472ea1574026aa1e8ca82bb6df2c40cd0478e9336b7a8a64e652119a2fa4f32", + "zh:1a8ddba2b1550c5d02003ea5d6cdda2eef6870ece86c5619f33edd699c9dc14b", + "zh:1e3bb505c000adb12cdf60af5b08f0ed68bc3955b0d4d4a126db5ca4d429eb4a", + "zh:6636401b2463c25e03e68a6b786acf91a311c78444b1dc4f97c539f9f78de22a", + "zh:76858f9d8b460e7b2a338c477671d07286b0d287fd2d2e3214030ae8f61dd56e", + "zh:a13b69fb43cb8746793b3069c4d897bb18f454290b496f19d03c3387d1c9a2dc", + "zh:a90ca81bb9bb509063b736842250ecff0f886a91baae8de65c8430168001dad9", + "zh:c4de401395936e41234f1956ebadbd2ed9f414e6908f27d578614aaa529870d4", + "zh:c657e121af8fde19964482997f0de2d5173217274f6997e16389e7707ed8ece8", + "zh:d68b07a67fbd604c38ec9733069fbf23441436fecf554de6c75c032f82e1ef19", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/infrastructure/local.tf b/infrastructure/local.tf new file mode 100644 index 0000000..278798a --- /dev/null +++ b/infrastructure/local.tf @@ -0,0 +1,15 @@ +locals { + common_prefix = "fc-sre" + region = "ap-northeast-2" + + eks_cluster_version = "1.29" + eks_ng_min_size = 3 + eks_ng_max_size = 10 + eks_ng_desired_size = 3 + eks_instance_type = "t3.medium" + + rds_engine_version = "16.2" + rds_instance_class = "db.m5d.large" + rds_allocated_storage = 20 + rds_db_name = "sample" +} \ No newline at end of file diff --git a/infrastructure/main.tf b/infrastructure/main.tf new file mode 100644 index 0000000..6a7c815 --- /dev/null +++ b/infrastructure/main.tf @@ -0,0 +1,137 @@ +terraform { + backend "local" { + path = "local.tfstate" + } + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.40" + } + } +} + +provider "aws" { + region = local.region + access_key = var.aws_access_key + secret_key = var.aws_secret_key +} + +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 5.8.0" + + name = "${local.common_prefix}-vpc" + cidr = "10.0.0.0/16" + + azs = ["${local.region}a", "${local.region}b", "${local.region}c"] + private_subnets = ["10.0.0.0/18", "10.0.64.0/18"] + public_subnets = ["10.0.128.0/18", "10.0.192.0/18"] + + enable_nat_gateway = true + single_nat_gateway = true + one_nat_gateway_per_az = false + + map_public_ip_on_launch = true + + tags = { + Fastcampus = "true" + } +} + +module "eks" { + source = "terraform-aws-modules/eks/aws" + version = "~> 20.0" + + cluster_name = "${local.common_prefix}-cluster" + cluster_version = local.eks_cluster_version + + cluster_endpoint_public_access = true + + cluster_addons = { + coredns = { + most_recent = true + } + kube-proxy = { + most_recent = true + } + vpc-cni = { + most_recent = true + } + } + + vpc_id = module.vpc.vpc_id + subnet_ids = concat(module.vpc.private_subnets, module.vpc.public_subnets) + + eks_managed_node_groups = { + primary = { + min_size = local.eks_ng_min_size + max_size = local.eks_ng_max_size + desired_size = local.eks_ng_desired_size + instance_types = [local.eks_instance_type] + } + } + + enable_cluster_creator_admin_permissions = true + + tags = { + Fastcampus = "true" + } +} + +resource "aws_db_instance" "rds" { + identifier = "${local.common_prefix}-db" + + engine = "postgres" + engine_version = local.rds_engine_version + instance_class = local.rds_instance_class + + storage_type = "gp3" + allocated_storage = local.rds_allocated_storage + + db_name = local.rds_db_name + username = local.common_prefix + password = var.rds_password + port = "5432" + + iam_database_authentication_enabled = true + skip_final_snapshot = true + deletion_protection = false + publicly_accessible = false +} + +resource "aws_db_subnet_group" "rds_subnet_group" { + name = "${local.common_prefix}-db-subnet-group" + subnet_ids = module.vpc.private_subnets + + tags = { + Name = "${local.common_prefix}-db-subnet-group" + Fastcampus = "true" + } +} + +resource "aws_security_group" "rds_security_group" { + name = "${local.common_prefix}-security-group" + vpc_id = module.vpc.vpc_id + + ingress { + from_port = 5432 + to_port = 5432 + protocol = "tcp" + security_groups = [module.eks.cluster_security_group_id] + } + + tags = { + Name = "${local.common_prefix}-security-group" + Fastcampus = "true" + } +} + +resource "aws_ecr_repository" "application_images" { + name = "${local.common_prefix}-image" + + tags = { + Name = "${local.common_prefix}-image" + Fastcampus = "true" + } +} \ No newline at end of file diff --git a/infrastructure/output.tf b/infrastructure/output.tf new file mode 100644 index 0000000..b2cf65c --- /dev/null +++ b/infrastructure/output.tf @@ -0,0 +1,12 @@ +output "eks_endpoint" { + value = module.eks.cluster_endpoint +} + +output "rds_endpoint" { + description = "address:port" + value = resource.aws_db_instance.rds.endpoint +} + +output "ecr_url" { + value = resource.aws_ecr_repository.application_images.repository_url +} \ No newline at end of file diff --git a/infrastructure/variables.tf b/infrastructure/variables.tf new file mode 100644 index 0000000..f0d260f --- /dev/null +++ b/infrastructure/variables.tf @@ -0,0 +1,14 @@ +variable "aws_access_key" { + description = "AWS ACCESS KEY" + type = string +} + +variable "aws_secret_key" { + description = "AWS SECRET KEY" + type = string +} + +variable "rds_password" { + description = "RDS PASSWORD" + type = string +} \ No newline at end of file