From d6513a93afd4f979dcd5b157578cb39097bf0678 Mon Sep 17 00:00:00 2001 From: Yuneui Jeong Date: Mon, 12 Aug 2024 08:26:35 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=207?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yaml | 50 ++++++ .gitignore | 3 +- COE.md | 16 ++ Dockerfile | 11 +- README.md | 76 ++++++-- application/config/settings/base.py | 17 +- application/config/settings/prod.py | 30 +++- application/config/urls.py | 9 +- .../lectures/migrations/0001_initial.py | 106 +----------- application/lectures/models.py | 3 +- application/lectures/views.py | 73 +++++++- application/poetry.lock | 53 +++++- application/pyproject.toml | 2 + blunder.sql | 12 ++ chart/templates/deployment.yaml | 13 +- chart/templates/hpa.yaml | 4 +- chart/templates/ingress.yaml | 31 +--- chart/templates/secret.yaml | 9 - chart/templates/service.yaml | 2 +- chart/templates/serviceaccount.yaml | 2 +- chart/values.yaml | 8 +- helm-prod-values.yaml | 27 +-- helm-redis-values.yaml | 29 ++++ infrastructure/.terraform.lock.hcl | 91 ++++++++-- infrastructure/eks.tf | 163 ++++++++++++++++++ infrastructure/local.tf | 15 -- infrastructure/locals.tf | 14 ++ infrastructure/main.tf | 114 ++---------- infrastructure/output.tf | 5 - infrastructure/rds.tf | 57 ++++++ infrastructure/variables.tf | 5 - infrastructure/vpc.tf | 31 ++++ load-test.js | 18 ++ 33 files changed, 751 insertions(+), 348 deletions(-) create mode 100644 .github/workflows/deploy.yaml create mode 100644 COE.md create mode 100644 blunder.sql delete mode 100644 chart/templates/secret.yaml create mode 100644 helm-redis-values.yaml create mode 100644 infrastructure/eks.tf delete mode 100644 infrastructure/local.tf create mode 100644 infrastructure/locals.tf create mode 100644 infrastructure/rds.tf create mode 100644 infrastructure/vpc.tf create mode 100644 load-test.js diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..e6b294c --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,50 @@ +name: Deploy + +on: + release: + types: + - published + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + with: + mask-password: "true" + + - name: Build and Push Container Image + env: + REGISTRY: ${{ steps.login-ecr.outputs.registry }} + IMAGE_NAME: fastcampus-app-image + run: | + SHA_SHORT=$(git rev-parse --short HEAD) + docker build -t $REGISTRY/$IMAGE_NAME:$SHA_SHORT . + docker push $REGISTRY/$IMAGE_NAME:$SHA_SHORT + + - name: Create kube-config and install Helm + run: | + aws eks --region ap-northeast-2 update-kubeconfig --name fastcampus-infra-cluster + curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + + - name: Install production Helm chart + env: + DATABASE_HOST: ${{ secrets.DATABASE_HOST }} + run: | + helm upgrade --install fastcampus-prod chart \ + -f helm-prod-values.yaml \ + --set workload.image.tag="$(git rev-parse --short HEAD)" \ + --set workload.databaseHost="${{ secrets.DATABASE_HOST }}" \ + --set common.awsAccount="$(aws sts get-caller-identity --query Account --output text)" diff --git a/.gitignore b/.gitignore index 79d0438..fc91597 100644 --- a/.gitignore +++ b/.gitignore @@ -209,4 +209,5 @@ terraform.rc # End of https://www.toptal.com/developers/gitignore/api/django,terraform -.DS_Store \ No newline at end of file +.DS_Store +.vscode diff --git a/COE.md b/COE.md new file mode 100644 index 0000000..cdb4f69 --- /dev/null +++ b/COE.md @@ -0,0 +1,16 @@ +# 장애 보고서 (타임라인까지) + +## 장애 상황 요약 + +## 장애의 영향 범위 + +### 고객 영향 + +### 비기능적 요구사항 + +- 가용성 조건 +- 비용 조건 + +## 장애 상황 타임라인 + +- [00:00] \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 81f4ffa..8623454 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,8 +6,12 @@ ENV PYTHONBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 RUN apt-get update && apt-get install -y \ + gcc \ + g++ \ + pkg-config \ build-essential \ - libpq-dev \ + default-libmysqlclient-dev \ + default-mysql-client \ && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -33,7 +37,4 @@ 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 +CMD [ "/bin/bash", "-c", "python manage.py migrate && gunicorn config.wsgi:application -w 4 -b 0.0.0.0:8000"] diff --git a/README.md b/README.md index 388083e..e1f4170 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,70 @@ -# Fastcampus - System Designs with Anti-Patterns +# Fast Campus - 실전 장애 케이스 8가지 실습과 보고서 작성 -## Instruction +> [!CAUTION] +> 본 프로젝트의 시스템 구성에는 교육 목적을 위한 의도적인 결함이 존재합니다. +> This project presents a system with deliberate flaws for educational purposes. -### 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: +1. AWS 액세스 키와 시크릿 키를 발급받아 안전하게 보관하세요. +2. `secrets.tfvars` 라는 이름의 파일을 `infrastructure` 디렉토리 안에 생성하세요. +3. 해당 파일에 아래와 같이 채워 주세요. ``` aws_access_key = "YOURAWSACCESSKEY" aws_secret_key = "YOURAWSSECRETACCESSKEY" ``` -4. Move inside the `infrastructure` directory and enter `terraform init` in your terminal. +4. 터미널에서 `infrastructure` 디렉토리로 이동한 뒤 `terraform init`을 입력하세요. -### 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`. +- 테라폼 코드를 플래닝하여 인프라가 어떻게 생성될지 보려면, `terraform plan -var-file=secrets.tfvars` 커맨드를 `infrastructure` 디렉토리에서 실행하세요. +- 실제로 테라폼 코드를 적용하여 인프라를 생성 또는 수정하고자 한다면, `terraform apply -var-file=secrets.tfvars` 커맨드를 `infrastructure` 디렉토리에서 실행하세요. + - 약 20분 내외 소요됩니다. -### Access the EKS cluster +## EKS 클러스터 접속 -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` +1. AWS CLI, `kubectl`, `helm`, `k9s`를 설치하세요. +2. `aws configure --profile fastcampus` 커맨드를 실행하여 AWS 액세스 키와 시크릿 키를 AWS CLI에서 사용하도록 설정하세요. +3. 터미널에서 `aws eks --region ap-northeast-2 update-kubeconfig --name fastcampus-infra-cluster --profile fastcampus`를 실행하여 `kubectl`을 통해 생성한 클러스터에 접근할 수 있도록 합니다. +4. 터미널에서 `kubectl config use-context arn:aws:eks:ap-northeast-2:1234567890:cluster/fastcampus-infra-cluster`을 실행해서 현재 쿠버네티스 컨텍스트를 생성한 클러스터를 바라보게 합니다. (`1234567890`을 본인의 계정 ID로 바꿔주세요.) +5. `kubectl get pods -n kube-system` 커맨드를 실행해 파드들이 정상적으로 출력되는지 확인해 주세요. -### 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 +1. 쿠버네티스 클러스터에 설치된 모든 자원을 제거하기 위해서, `helm uninstall fastcampus-prod` 커맨드를 입력해 주세요. +2. 프로비저닝된 모든 인프라를 제거하기 위해서, 터미널에서 `terraform destroy -var-file=secrets.tfvars` 커맨드를 입력해 주세요. + - 약 10분 내외 소요됩니다. +3. RDS 콘솔에서 스냅샷과 PITR로 생성된 데이터베이스 인스턴스를 수동으로 삭제해 주세요. +4. RDS 콘솔 > Snapshots > Manual 에서 RDS 스냅샷을 삭제해 주세요. +5. 로컬 `kubectl` 설정 파일을 정리하기 위해서, 아래의 커맨드를 입력해 주세요. + - `kubectl config delete-context arn:aws:eks:ap-northeast-2:1234567890:cluster/fastcampus-infra-cluster` (`1234567890`을 본인의 계정 ID로 바꿔주세요.) + - `kubectl config delete-cluster arn:aws:eks:ap-northeast-2:1234567890:cluster/fastcampus-infra-cluster` + - `kubectl config delete-user arn:aws:eks:ap-northeast-2:1234567890:cluster/fastcampus-infra-cluster` + +## 문제 해결 + +### VPC가 마지막에 다음과 같이 정확하게 삭제되지 않는 경우가 있습니다. + +실습 도중 테라폼으로 보안그룹이 추적되지 않은 경우가 발생했을 때 이런 현상이 발생할 수 있습니다. + +![problem-vpc-terraform](https://assets.uniglot.dev/images/problem-vpc-terraform.png) + +이럴 때는 콘솔에서 VPC를 아래와 같이 수동으로 삭제해 주시면 됩니다. + +![problem-vpc-console](https://assets.uniglot.dev/images/problem-vpc-console.png) + +### 테라폼 제거 과정에서 Helm 관련하여 에러가 뜨면서 제거가 되지 않습니다. + +아래와 같이 쿠버네티스 서비스 어카운트나 헬름 관련으로 테라폼 에러가 발생하는 경우가 있습니다. + +![problem-eks-terraform](https://assets.uniglot.dev/images/problem-eks-terraform.png) + +이 경우 테라폼 관리 대상에서 쿠버네티스와 헬름을 제거하면 됩니다. 테라폼으로 쿠버네티스나 헬름 자원을 제거하지 않더라도 쿠버네티스 클러스터가 제거되면서 자연스럽게 함께 제거됩니다. + +- `infrastructure/eks.tf`에서 `resource "kubernetes_service_account"`와 `resource "helm_release"`로 되어 있는 블럭을 모두 주석으로 처리해 주세요. +- 주석으로 처리한 리소스 타입과 이름을 참조해서 아래와 같이 모든 자원의 상태를 제거해 주세요. + ```bash + terraform state rm kubernetes_service_account.lb_controller_sa + terraform state rm helm_release.aws_load_balancer_controller + ``` +- 그 다음 다시 `terraform destroy -var-file=secrets.tfvars`를 입력해 주세요. \ No newline at end of file diff --git a/application/config/settings/base.py b/application/config/settings/base.py index 231cfef..25255b8 100644 --- a/application/config/settings/base.py +++ b/application/config/settings/base.py @@ -25,7 +25,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ["*"] # Application definition @@ -50,6 +50,11 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 10, +} + ROOT_URLCONF = 'config.urls' TEMPLATES = [ @@ -81,6 +86,16 @@ } } +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://localhost:6379/0", + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + }, + } +} + # Password validation # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators diff --git a/application/config/settings/prod.py b/application/config/settings/prod.py index 303f563..c8ffad1 100644 --- a/application/config/settings/prod.py +++ b/application/config/settings/prod.py @@ -1,15 +1,31 @@ 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"), + "ENGINE": "django.db.backends.mysql", + "NAME": "sample", + "USER": "fastcampus", + "PASSWORD": "supersecretpassword", "HOST": os.environ.get("DATABASE_HOST"), + "PORT": "3306", + "CONN_MAX_AGE": 300, + } +} + +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": [ + "redis://redis-master.default.svc.cluster.local:6379/0", + "redis://redis-replicas.default.svc.cluster.local:6379/0", + ], + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + "PASSWORD": "supersecretpassword", + "SOCKET_TIMEOUT": 3, + "SOCKET_CONNECT_TIMEOUT": 3, + }, } -} \ No newline at end of file +} diff --git a/application/config/urls.py b/application/config/urls.py index 4f17e71..627c791 100644 --- a/application/config/urls.py +++ b/application/config/urls.py @@ -17,10 +17,17 @@ from django.contrib import admin from django.urls import path -from lectures.views import LectureView, LectureRegisterView +from lectures.views import ( + LectureDetailView, + LectureView, + LectureRegisterView, + RandomLectureRegisterView, +) urlpatterns = [ path('admin/', admin.site.urls), path('lectures/', LectureView.as_view()), + path('lectures//', LectureDetailView.as_view()), path('lectures/register/', LectureRegisterView.as_view()), + path('lectures/random/', RandomLectureRegisterView.as_view()), ] diff --git a/application/lectures/migrations/0001_initial.py b/application/lectures/migrations/0001_initial.py index 99a4939..2af554e 100644 --- a/application/lectures/migrations/0001_initial.py +++ b/application/lectures/migrations/0001_initial.py @@ -21,7 +21,7 @@ def create_initial_records(apps, schema_editor): departments = Department.objects.all() people = [] - family_names = ["김", "이", "박", "최", "정", "강", "조", "윤", "장", "임", "한", "오", "서", "신", "황", "안", "송", "전", "홍"] + family_names = ["김", "이", "박", "최", "정", "강"] given_names = [ "민준", "서윤", @@ -32,99 +32,9 @@ def create_initial_records(apps, schema_editor): "도윤", "하준", "수아", - "지민", - "지호", - "지안", - "예린", - "지윤", - "시우", - "하윤", - "주원", - "지원", - "윤서", - "지후", - "하율", - "하빈", - "유진", - "지아", - "수현", - "은우", - "도연", - "연우", - "수빈", - "예빈", - "시윤", - "태윤", - "윤아", - "서진", - "유나", - "예지", - "서현", - "하람", - "민서", - "다은", - "소윤", - "지율", - "다인", - "준우", - "서하", - "서영", - "민성", - "윤호", - "유준", - "은서", - "하연", - "지웅", - "소연", - "다윤", - "현우", - "시현", - "수호", - "은수", - "승우", - "지훈", - "시영", - "하람", - "예나", - "민호", - "유빈", - "시완", - "주하", - "하은", - "은채", - "하온", - "민아", - "수아", - "도현", - "도영", - "시은", - "하린", - "수정", - "지수", - "예슬", - "하온", - "서율", - "다영", - "하영", - "도율", - "소은", - "채원", - "예나", - "준서", - "지유", - "채은", - "은성", - "시안", - "도경", - "은하", - "주현", - "연서", - "세빈", - "주영", - "하늘", - "시영", - "채윤", - "유나", + "서연", + "예준", + "지우", ] count = 0 @@ -145,8 +55,8 @@ def create_initial_records(apps, schema_editor): professors = Professor.objects.all() lectures = [] - prefixes = ["기본 ", "고급 ", "현대 ", "고대 ", "중세 ", "중급 ", "실무 ", "실용 "] - topics = ["진", "케이틀린", "애쉬", "징크스", "럭스", "타릭", "녹턴", "벡스", "소나", "케넨"] + prefixes = ["기본", "고급", "현대", "고대", "중세"] + topics = ["진", "케이틀린", "애쉬", "징크스", "럭스"] suffixes = ["의 이해", "의 역사", "학 개론", "학 입문", "의 응용"] count = 0 @@ -155,7 +65,7 @@ def create_initial_records(apps, schema_editor): for suffix in suffixes: lectures.append( Lecture( - name=prefix+topic+suffix, + name=f"{prefix} {topic}{suffix}", code=topic+str(count), professor=professors[count%6], register_limit=30, @@ -205,7 +115,7 @@ class Migration(migrations.Migration): 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)), + ('code', models.CharField(max_length=20)), ('credit', models.IntegerField(default=3)), ('register_limit', models.IntegerField()), ('professor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='lectures.professor')), diff --git a/application/lectures/models.py b/application/lectures/models.py index 484b6b9..653da15 100644 --- a/application/lectures/models.py +++ b/application/lectures/models.py @@ -27,9 +27,10 @@ class Professor(models.Model): 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) + code = models.CharField(max_length=20) professor = models.ForeignKey(Professor, on_delete=models.CASCADE) credit = models.IntegerField(default=3) register_limit = models.IntegerField() diff --git a/application/lectures/views.py b/application/lectures/views.py index 64028d9..2515a7c 100644 --- a/application/lectures/views.py +++ b/application/lectures/views.py @@ -1,4 +1,8 @@ +import random + +from django.core.cache import cache from django.db import transaction +from rest_framework.pagination import PageNumberPagination from rest_framework.response import Response from rest_framework.views import APIView @@ -7,18 +11,48 @@ class LectureView(APIView): - def get(self, *args, **kwargs): - lectures = self._filter() + def get(self, request, *args, **kwargs): + paginator = PageNumberPagination() + lectures = paginator.paginate_queryset(self._filter(), request) serializer = LectureSerializer(lectures, many=True) - return Response(serializer.data) + return paginator.get_paginated_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 LectureDetailView(APIView): + def get(self, request, *args, **kwargs): + cache_key = f"lecture:{kwargs['lecture_id']}" + lecture_data = cache.get(cache_key) + + if not lecture_data: + lecture_data = self.get_lecture_data(kwargs["lecture_id"]) + if lecture_data: + cache.set(cache_key, lecture_data, timeout=2) + else: + return Response({"message": "존재하지 않는 강의입니다."}, status=400) + + return Response(lecture_data, status=200) + + def get_lecture_data(self, lecture_id): + try: + lecture = ( + Lecture.objects + .select_related('professor', 'professor__department') + .prefetch_related('students') + .get(id=lecture_id) + ) + serializer = LectureSerializer(lecture, many=False) + return serializer.data + except Lecture.DoesNotExist: + return None + + class LectureRegisterView(APIView): @transaction.atomic def post(self, *args, **kwargs): @@ -48,3 +82,36 @@ def post(self, *args, **kwargs): student.save() return Response({"message": "수강 신청에 성공했습니다."}, status=204) + + +class RandomLectureRegisterView(APIView): + def post(self, *args, **kwargs): + students = list(Person.objects.all()) + lectures = list(Lecture.objects.all()) + + random.shuffle(lectures) + + lecture_capacity = {lecture.id: lecture.register_limit for lecture in lectures} + + with transaction.atomic(): + for student in students: + num_lectures = random.randint(4, 8) + + available_lectures = [lecture for lecture in lectures if lecture_capacity[lecture.id] > 0] + + if not available_lectures: + continue + + selected_lectures = random.sample(available_lectures, min(num_lectures, len(available_lectures))) + + for lecture in selected_lectures: + lecture.students.add(student) + lecture_capacity[lecture.id] -= 1 + student.total_credit += 3 + + student.save() + + for lecture in selected_lectures: + lecture.save() + + return Response({"message": "수강 신청에 성공했습니다."}, status=204) \ No newline at end of file diff --git a/application/poetry.lock b/application/poetry.lock index c7981d0..c6965ca 100644 --- a/application/poetry.lock +++ b/application/poetry.lock @@ -34,6 +34,24 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-redis" +version = "5.4.0" +description = "Full featured redis cache backend for Django." +optional = false +python-versions = ">=3.6" +files = [ + {file = "django-redis-5.4.0.tar.gz", hash = "sha256:6a02abaa34b0fea8bf9b707d2c363ab6adc7409950b2db93602e6cb292818c42"}, + {file = "django_redis-5.4.0-py3-none-any.whl", hash = "sha256:ebc88df7da810732e2af9987f7f426c96204bf89319df4c6da6ca9a2942edd5b"}, +] + +[package.dependencies] +Django = ">=3.2" +redis = ">=3,<4.0.0 || >4.0.0,<4.0.1 || >4.0.1" + +[package.extras] +hiredis = ["redis[hiredis] (>=3,!=4.0.0,!=4.0.1)"] + [[package]] name = "djangorestframework" version = "3.15.1" @@ -69,6 +87,24 @@ setproctitle = ["setproctitle"] testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] tornado = ["tornado (>=0.2)"] +[[package]] +name = "mysqlclient" +version = "2.2.4" +description = "Python interface to MySQL" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mysqlclient-2.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:ac44777eab0a66c14cb0d38965572f762e193ec2e5c0723bcd11319cc5b693c5"}, + {file = "mysqlclient-2.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:329e4eec086a2336fe3541f1ce095d87a6f169d1cc8ba7b04ac68bcb234c9711"}, + {file = "mysqlclient-2.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:e1ebe3f41d152d7cb7c265349fdb7f1eca86ccb0ca24a90036cde48e00ceb2ab"}, + {file = "mysqlclient-2.2.4-cp38-cp38-win_amd64.whl", hash = "sha256:3c318755e06df599338dad7625f884b8a71fcf322a9939ef78c9b3db93e1de7a"}, + {file = "mysqlclient-2.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:9d4c015480c4a6b2b1602eccd9846103fc70606244788d04aa14b31c4bd1f0e2"}, + {file = "mysqlclient-2.2.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d43987bb9626096a302ca6ddcdd81feaeca65ced1d5fe892a6a66b808326aa54"}, + {file = "mysqlclient-2.2.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4e80dcad884dd6e14949ac6daf769123223a52a6805345608bf49cdaf7bc8b3a"}, + {file = "mysqlclient-2.2.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9d3310295cb682232cadc28abd172f406c718b9ada41d2371259098ae37779d3"}, + {file = "mysqlclient-2.2.4.tar.gz", hash = "sha256:33bc9fb3464e7d7c10b1eaf7336c5ff8f2a3d3b88bab432116ad2490beb3bf41"}, +] + [[package]] name = "packaging" version = "24.0" @@ -80,6 +116,21 @@ files = [ {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] +[[package]] +name = "redis" +version = "5.0.8" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"}, + {file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"}, +] + +[package.extras] +hiredis = ["hiredis (>1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + [[package]] name = "sqlparse" version = "0.5.0" @@ -109,4 +160,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "f079b044558bca5194056629f876d288dd8a9537931370e6ca4a434fa96a1382" +content-hash = "0293cf8645e4f06fdd382064624c13ac0557fdf8769d015132560466c6de2c1d" diff --git a/application/pyproject.toml b/application/pyproject.toml index 19f9d3c..7740347 100644 --- a/application/pyproject.toml +++ b/application/pyproject.toml @@ -10,6 +10,8 @@ python = "^3.12" django = "~4.2" djangorestframework = "^3.15" gunicorn = "~22.0" +mysqlclient = "^2.2.4" +django-redis = "^5.4.0" [build-system] diff --git a/blunder.sql b/blunder.sql new file mode 100644 index 0000000..f023489 --- /dev/null +++ b/blunder.sql @@ -0,0 +1,12 @@ +DELETE FROM lectures_lecture_students +WHERE person_id IN (1, 2, 3, 4, 5) +OR lecture_id IN ( + SELECT id FROM lectures_lecture + WHERE professor_id IN ( + SELECT id FROM lectures_professor + WHERE department_id IN ( + SELECT id FROM lectures_department + WHERE name = '컴퓨터공학과' + ) + ) +); \ No newline at end of file diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index 9f8d0ab..968fcba 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -2,7 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "chart.fullname" . }} + name: {{ include "chart.fullname" . }}-deployment labels: {{- include "chart.labels" . | nindent 4 }} spec: @@ -28,7 +28,7 @@ spec: imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} - serviceAccountName: {{ include "chart.serviceAccountName" . }} + serviceAccountName: {{ include "chart.serviceAccountName" . }}-sa securityContext: {{- toYaml .Values.workload.podSecurityContext | nindent 8 }} containers: @@ -63,10 +63,11 @@ spec: {{- end }} resources: {{- toYaml .Values.workload.resources | nindent 12 }} - {{- if .Values.workload.envFrom }} - envFrom: - {{- toYaml .Values.workload.envFrom | nindent 12 }} - {{- end }} + env: + - name: DATABASE_HOST + value: "{{ .Values.workload.databaseHost }}" + - name: DJANGO_SETTINGS + value: {{ .Values.workload.deployStage }} {{- with .Values.workload.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/chart/templates/hpa.yaml b/chart/templates/hpa.yaml index a91f61b..0e80a19 100644 --- a/chart/templates/hpa.yaml +++ b/chart/templates/hpa.yaml @@ -2,14 +2,14 @@ apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: - name: {{ include "chart.fullname" . }} + name: {{ include "chart.fullname" . }}-hpa labels: {{- include "chart.labels" . | nindent 4 }} spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment - name: {{ include "chart.fullname" . }} + name: {{ include "chart.fullname" . }}-deployment minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml index 6d5a71a..c91c100 100644 --- a/chart/templates/ingress.yaml +++ b/chart/templates/ingress.yaml @@ -2,31 +2,17 @@ {{- $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 }} + name: {{ $fullName }}-ingress labels: {{- include "chart.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} annotations: - {{- toYaml . | nindent 4 }} - {{- end }} + kubernetes.io/ingress.class: {{ .Values.ingress.className }} + alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": {{ $svcPort }} }]' + alb.ingress.kubernetes.io/scheme: {{ .Values.ingress.scheme }} 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 }} @@ -44,19 +30,12 @@ spec: 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 }} + name: {{ $fullName }}-service port: number: {{ $svcPort }} - {{- else }} - serviceName: {{ $fullName }} - servicePort: {{ $svcPort }} - {{- end }} {{- end }} {{- end }} {{- end }} diff --git a/chart/templates/secret.yaml b/chart/templates/secret.yaml deleted file mode 100644 index bc0b175..0000000 --- a/chart/templates/secret.yaml +++ /dev/null @@ -1,9 +0,0 @@ -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 index 3bb7cc1..878bbaa 100644 --- a/chart/templates/service.yaml +++ b/chart/templates/service.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Service metadata: - name: {{ include "chart.fullname" . }} + name: {{ include "chart.fullname" . }}-service labels: {{- include "chart.labels" . | nindent 4 }} spec: diff --git a/chart/templates/serviceaccount.yaml b/chart/templates/serviceaccount.yaml index 1df9350..4de85a3 100644 --- a/chart/templates/serviceaccount.yaml +++ b/chart/templates/serviceaccount.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: ServiceAccount metadata: - name: {{ include "chart.serviceAccountName" . }} + name: {{ include "chart.serviceAccountName" . }}-sa labels: {{- include "chart.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} diff --git a/chart/values.yaml b/chart/values.yaml index 3ce8659..eaf5a2a 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -34,15 +34,12 @@ workload: path: / conditions: {} resources: {} - envFrom: [] + databaseHost: "" + deployStage: "" nodeSelector: {} tolerations: [] affinity: {} -secret: - name: "" - body: {} - service: type: ClusterIP ports: @@ -54,6 +51,7 @@ service: ingress: enabled: false className: "" + scheme: internet-facing annotations: {} hosts: - host: "" diff --git a/helm-prod-values.yaml b/helm-prod-values.yaml index 93f125b..21f814c 100644 --- a/helm-prod-values.yaml +++ b/helm-prod-values.yaml @@ -14,10 +14,10 @@ workload: repository: fastcampus-app-image resources: requests: - cpu: 100m + cpu: 300m memory: 256Mi limits: - cpu: 500m + cpu: 600m memory: 512Mi containerPort: 8000 probes: @@ -25,7 +25,7 @@ workload: path: /lectures/ conditions: initialDelaySeconds: 20 - timeoutSeconds: 10 + timeoutSeconds: 15 periodSeconds: 10 successThreshold: 1 failureThreshold: 3 @@ -33,24 +33,17 @@ workload: path: /lectures/ conditions: initialDelaySeconds: 20 - timeoutSeconds: 10 + timeoutSeconds: 15 periodSeconds: 10 successThreshold: 1 failureThreshold: 3 - envFrom: - - secretRef: - name: fastcampus-app-secret - -secret: - name: fastcampus-app-secret - body: - key1: asdf - key2: fdsa + deployStage: prod autoscaling: enabled: false service: + type: NodePort ports: - port: 80 targetPort: 8000 @@ -59,10 +52,8 @@ service: ingress: enabled: true - annotations: - Kubernetes.io/ingress.class: nginx + className: alb hosts: - - host: fastcampus.uniglot.dev - paths: + - paths: - path: / - pathType: Prefix \ No newline at end of file + pathType: Prefix diff --git a/helm-redis-values.yaml b/helm-redis-values.yaml new file mode 100644 index 0000000..de5808b --- /dev/null +++ b/helm-redis-values.yaml @@ -0,0 +1,29 @@ +global: + redis: + password: supersecretpassword + +master: + resources: + requests: + memory: 128Mi + cpu: 300m + limits: + memory: 256Mi + cpu: 600m + persistence: + enabled: false + +replica: + replicaCount: 1 + resources: + requests: + memory: 128Mi + cpu: 100m + limits: + memory: 256Mi + cpu: 200m + persistence: + enabled: false + +sentinel: + enabled: false \ No newline at end of file diff --git a/infrastructure/.terraform.lock.hcl b/infrastructure/.terraform.lock.hcl index a965271..6117a8d 100644 --- a/infrastructure/.terraform.lock.hcl +++ b/infrastructure/.terraform.lock.hcl @@ -2,25 +2,25 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/aws" { - version = "5.51.1" - constraints = "~> 5.0, >= 5.30.0" + version = "5.56.1" + constraints = ">= 4.33.0, >= 5.30.0, >= 5.40.0, ~> 5.40" hashes = [ - "h1:KY/uPHIa+bHgMOAqoA2BnjIlIDuFRFwbLjLkf1gbeDk=", - "zh:03d524b70ab300d90dc4dccad0c28b18d797b8986722b7a93e40a41500450eaa", - "zh:04dbcb7ab52181a784877c409f6c882df34bda686d8c884d511ebd4abf493f0c", - "zh:2b068f7838e0f3677829258df05d8b9d73fe6434a1a809f8710956cc1c01ea03", - "zh:41a4b1e4adbf7c90015ebff17a719fc08133b8a2c4dcefd2fa281552126e59a8", - "zh:48b1adf57f695a72c88c598f99912171ef7067638fd63fb0c6ad3fa397b3f7c3", - "zh:5c2fb26ecb83adac90d06dcf5f97edbc944824c2821816b1653e1a2b9d37b3c4", - "zh:93df05f53702df829d9b9335e559ad8b313808dbd2fad8b2ff14f176732e693d", + "h1:LRmSNnudFVTkMSnEXJSKCojpknVVYEnls2UTeoxCxtc=", + "zh:0fff674596251d3f46b5a9e242220871d6c634f7cf69f2741d1c3c8f4baa708c", + "zh:1495d0f71bbd849ad286e7afa9d531a45217e6af7e3d165a447809dab364bd9b", + "zh:3eab136bd5b6c58a99f5cb588220819c70061b48da98f2b40061ebabfcbe1957", + "zh:3faa780ae84db4751d32ce3e7c4797711c9b5c537b67884037f0951a2f93c1ee", + "zh:47455bd243986893cc79f3d884633961244faeeef678fd64a37fcc77f3dabe24", + "zh:4a26df74f018ea25f3b543e9bc9d5763c7adc0cec647fc1cb1acec47cc331953", + "zh:592cebca964f297f569dc86e99789bfcc301904a9c26cd7294dab99e106acf59", + "zh:75d5ed50f1f56c484f7fcb1bd1c4ad33e2679ed249cc8db05e561233f8f5781f", + "zh:7ec8cce722a91ba141a3b2db0e833acc3be91e4eec6abb41f012bc9d641ca24e", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:b5da39898602e44551b56e2803a42d92ea7115e35b1792efbf6649da37ef597b", - "zh:b7ab7f743f864ed8d479a7cb04fd3ce00c376f867ee5b53c4c1acaef6e286c54", - "zh:e7e7b2d8ee486415481a25ac7bdded20bd2897d5dd0790741798f31935b9528d", - "zh:e8008e3f5ef560fd9004d1ed1738f0f53e99b0ce961d967e95fc7c02e5954e4e", - "zh:f1296f648b8608ffa930b52519b00ed01eebedde9fdaf94205b365536e6c3916", - "zh:f8539960fd978a54990740ee984c6f7f743c9c32c7734e2601e92abfe54367e9", - "zh:fd182e6e20bb52982752a5d8c4b16887565f413a9d50d9d394d2c06eea8a195e", + "zh:cba68f518f794e695b0448be4ff90906a7817f65ca5e4d987720e37fbeea7c90", + "zh:e29712ab48d6527253ae4aef3851bd8e831b7b0bb57b5097bef16cbb69af6e85", + "zh:ef34bd8ff4e1fb8cc222b78217df917d4833361ea514465e7dae9122a7c7cf7a", + "zh:fece9ac372653ab3195630cc9d817ad0f81cce1d2880bec03ffc624591f3702b", + "zh:ffd1c3b3e4fa447dd2a78f6696d0dac969cb2996d640e3efbf2a96c49892d298", ] } @@ -44,6 +44,63 @@ provider "registry.terraform.io/hashicorp/cloudinit" { ] } +provider "registry.terraform.io/hashicorp/helm" { + version = "2.14.0" + hashes = [ + "h1:8Vt9264v3UE6mHLRG8yiteVl5h8ZSTkJXf1xdVLa7GA=", + "zh:087a475fda3649e4b6b9aeb5f21704972f5d85c10d0bf334289b0a1b8c1a5575", + "zh:1877991d976491d4e2a653a89491bd3b92123a00f442f15aa62caea8902677c7", + "zh:233d9e550b900be8bbf62871322964239bb4827b3500b77d7e2652a8bae6a106", + "zh:6ed09d405ade276dfc6ec591d113ca328ea3fe423405d4bc1116f7a06dfd86ec", + "zh:9039de4cbee5ae006d9cbf27f40f0a285feb02c3b00901535a1112853de55b5f", + "zh:aea6311b0f29edddefa21b8c7953314459caeace77d72d60588d1277f1723c54", + "zh:bd6a4fea3461c2751527f1c4e4c2c160e72f5b5a3b5cfbfe051adf61badd5ead", + "zh:c5f12a2ea4c3b62d9dd2d8f62c9918ef77b1f9dd4d6ccf1758a2a24139ab5319", + "zh:cd84d7258f263c3bd24138e7633b022451fdc1935a11e34932b63f71bbe6059f", + "zh:e637d01ee4dc2e5702d62c158399ab0d0ba3269e71f5db38db922ff05505ae2a", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:fbf9c9936ae547b75a81170b7bd20f72bc5538e015efcf7d12f822358d758f57", + ] +} + +provider "registry.terraform.io/hashicorp/http" { + version = "3.4.3" + hashes = [ + "h1:Ep4kCumou6eEyPkFJFAfuzd7IAsYM7xMAdDaFTwdDZ8=", + "zh:001e12b8079955a9fa7f8fcd515ae665b2e1087107fd337c4b872e88a86d540b", + "zh:0874fb3f870b2ac24c967a9685f2da641079589024109340389694696301a85b", + "zh:3b5e533c3d2859575945568aad0aac66b71bfc709706231fc2de94e01ca76d7f", + "zh:622ee28d42ed9d4b1399dde377db515e62cac08bd65bb2455068621f7a42d90d", + "zh:6dea688d78840a3f678e06ee602d37c766ce2ee625dcdce0c6658116ebcbde8e", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7f57a1436a464bc2e1698457b402ff0fd98ef9e7dcf6707d6bd0debc67fad164", + "zh:829d89d82e6fc3c89714950dc8afa51d622bb8e4f4bd5c73037505fb55a67834", + "zh:e453202d09b62531ed3278926307d315276e05784e7c6448a2c21c6a2da6e48f", + "zh:e76edc035240b4ad9334b4a0282b44a086e001df3007a2fc51f6262c4db032d1", + "zh:eeb0379da9093e155a193f666079de6baf8ed02855bf2a443448903f7cfef378", + "zh:fcb00eeb665ccae383645173d8e10c3071946396629a7797db39c798997f21b0", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "2.31.0" + hashes = [ + "h1:ZlKkkHJrjF4AiMueI2yA+abBc1c37cfwjyxURdLKhEw=", + "zh:0d16b861edb2c021b3e9d759b8911ce4cf6d531320e5dc9457e2ea64d8c54ecd", + "zh:1bad69ed535a5f32dec70561eb481c432273b81045d788eb8b37f2e4a322cc40", + "zh:43c58e3912fcd5bb346b5cb89f31061508a9be3ca7dd4cd8169c066203bcdfb3", + "zh:4778123da9206918a92dfa73cc711475d2b9a8275ff25c13a30513c523ac9660", + "zh:8bfa67d2db03b3bfae62beebe6fb961aee8d91b7a766efdfe4d337b33dfd23dd", + "zh:9020bb5729db59a520ade5e24984b737e65f8b81751fbbd343926f6d44d22176", + "zh:90431dbfc5b92498bfbce38f0b989978c84421a6c33245b97788a46b563fbd6e", + "zh:b71a061dda1244f6a52500e703a9524b851e7b11bbf238c17bbd282f27d51cb2", + "zh:d6232a7651b834b89591b94bf4446050119dcde740247e6083a4d55a2cefd28a", + "zh:d89fba43e699e28e2b5e92fff2f75fc03dbc8de0df9dacefe1a8836f8f430753", + "zh:ef85c0b744f5ba1b10dadc3c11e331ba4225c45bb733e024d7218c24b02b0512", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + provider "registry.terraform.io/hashicorp/null" { version = "3.2.2" constraints = ">= 3.0.0" diff --git a/infrastructure/eks.tf b/infrastructure/eks.tf new file mode 100644 index 0000000..f776f97 --- /dev/null +++ b/infrastructure/eks.tf @@ -0,0 +1,163 @@ +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) + + cluster_security_group_additional_rules = { + postgres = { + description = "MySQL" + protocol = "tcp" + from_port = 3306 + to_port = 3306 + type = "ingress" + source_node_security_group = true + } + } + + 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" + } +} + +data "http" "lb_iam_policy" { + url = "https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.7.2/docs/install/iam_policy.json" +} + +resource "aws_iam_role" "lb_controller" { + name = "${local.common_prefix}-lb-controller-role" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + "Federated" : "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${module.eks.oidc_provider}" + } + Action = "sts:AssumeRoleWithWebIdentity" + Condition = { + StringEquals = { + "${module.eks.oidc_provider}:aud" : "sts.amazonaws.com" + "${module.eks.oidc_provider}:sub" : "system:serviceaccount:kube-system:aws-load-balancer-controller" + } + } + } + ] + }) + + tags = { + Fastcampus = "true" + } +} + +resource "aws_iam_policy" "lb_controller" { + name = "AWSLoadBalancerControllerIAMPolicy" + policy = data.http.lb_iam_policy.response_body +} + +resource "aws_iam_role_policy_attachment" "lb_controller" { + role = aws_iam_role.lb_controller.name + policy_arn = aws_iam_policy.lb_controller.arn +} + +resource "kubernetes_service_account" "lb_controller_sa" { + metadata { + name = "aws-load-balancer-controller" + namespace = "kube-system" + annotations = { + "eks.amazonaws.com/role-arn" = aws_iam_role.lb_controller.arn + } + } + + depends_on = [aws_iam_role.lb_controller, module.eks] +} + +resource "helm_release" "aws_load_balancer_controller" { + name = "aws-load-balancer-controller" + chart = "aws-load-balancer-controller" + namespace = "kube-system" + repository = "https://aws.github.io/eks-charts" + + dynamic "set" { + for_each = [ + { name = "clusterName", value = module.eks.cluster_name }, + { name = "serviceAccount.create", value = "false" }, + { name = "serviceAccount.name", value = "aws-load-balancer-controller" }, + { name = "region", value = local.region }, + { name = "vpcId", value = module.vpc.vpc_id } + ] + + content { + name = set.value.name + value = set.value.value + } + } + + depends_on = [kubernetes_service_account.lb_controller_sa] +} + +resource "helm_release" "metrics_server" { + name = "metrics-server" + chart = "metrics-server" + namespace = "kube-system" + repository = "https://kubernetes-sigs.github.io/metrics-server" + + depends_on = [ module.eks ] +} + +resource "helm_release" "prometheus" { + name = "prometheus" + chart = "kube-prometheus-stack" + namespace = "monitoring" + repository = "https://prometheus-community.github.io/helm-charts" + create_namespace = true + + set { + name = "grafana.adminPassword" + value = "adminadmin" + } + + set { + name = "grafana.sidecar.datasources.defaultDatasourceScrapeInterval" + value = "10s" + } + + depends_on = [module.eks, helm_release.aws_load_balancer_controller] +} + +resource "helm_release" "redis" { + name = "redis" + chart = "redis" + namespace = "default" + repository = "https://charts.bitnami.com/bitnami" + values = [file("../helm-redis-values.yaml")] +} diff --git a/infrastructure/local.tf b/infrastructure/local.tf deleted file mode 100644 index 278798a..0000000 --- a/infrastructure/local.tf +++ /dev/null @@ -1,15 +0,0 @@ -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/locals.tf b/infrastructure/locals.tf new file mode 100644 index 0000000..ccf299a --- /dev/null +++ b/infrastructure/locals.tf @@ -0,0 +1,14 @@ +locals { + common_prefix = "fastcampus-infra" + region = "ap-northeast-2" + + eks_cluster_version = "1.29" + eks_ng_min_size = 1 + eks_ng_max_size = 1 + eks_ng_desired_size = 1 + eks_instance_type = "t3.medium" + + rds_engine_version = "8.0.35" + rds_instance_class = "db.t3.micro" + rds_allocated_storage = 20 +} diff --git a/infrastructure/main.tf b/infrastructure/main.tf index 6a7c815..e1e40e7 100644 --- a/infrastructure/main.tf +++ b/infrastructure/main.tf @@ -17,114 +17,26 @@ provider "aws" { 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" - } +provider "kubernetes" { + host = module.eks.cluster_endpoint + token = data.aws_eks_cluster_auth.cluster.token + cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) } -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" +provider "helm" { + kubernetes { + host = module.eks.cluster_endpoint + token = data.aws_eks_cluster_auth.cluster.token + cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) } } -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 +data "aws_caller_identity" "current" {} - db_name = local.rds_db_name - username = local.common_prefix - password = var.rds_password - port = "5432" +data "aws_eks_cluster_auth" "cluster" { + name = module.eks.cluster_name - 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" - } + depends_on = [module.eks] } resource "aws_ecr_repository" "application_images" { diff --git a/infrastructure/output.tf b/infrastructure/output.tf index b2cf65c..9501741 100644 --- a/infrastructure/output.tf +++ b/infrastructure/output.tf @@ -2,11 +2,6 @@ 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/rds.tf b/infrastructure/rds.tf new file mode 100644 index 0000000..faf3217 --- /dev/null +++ b/infrastructure/rds.tf @@ -0,0 +1,57 @@ +resource "aws_db_instance" "rds" { + identifier = "${local.common_prefix}-db" + db_subnet_group_name = aws_db_subnet_group.rds_subnet_group.id + parameter_group_name = aws_db_parameter_group.rds_parameter_group.name + vpc_security_group_ids = [module.eks.cluster_security_group_id] + + engine = "mysql" + engine_version = local.rds_engine_version + instance_class = local.rds_instance_class + + storage_type = "gp3" + allocated_storage = local.rds_allocated_storage + + db_name = "sample" + username = "fastcampus" + password = "supersecretpassword" + port = "3306" + + backup_retention_period = 7 + + iam_database_authentication_enabled = false + skip_final_snapshot = true + deletion_protection = false + publicly_accessible = false + + tags = { + Name = "${local.common_prefix}-db" + Fastcampus = "true" + } +} + + +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_db_parameter_group" "rds_parameter_group" { + name = "${local.common_prefix}-db-parameter-group" + family = "mysql8.0" + + parameter { + name = "binlog_format" + value = "ROW" + } + + tags = { + Name = "${local.common_prefix}-db-parameter-group" + Fastcampus = "true" + } +} \ No newline at end of file diff --git a/infrastructure/variables.tf b/infrastructure/variables.tf index f0d260f..fc91bec 100644 --- a/infrastructure/variables.tf +++ b/infrastructure/variables.tf @@ -7,8 +7,3 @@ variable "aws_secret_key" { description = "AWS SECRET KEY" type = string } - -variable "rds_password" { - description = "RDS PASSWORD" - type = string -} \ No newline at end of file diff --git a/infrastructure/vpc.tf b/infrastructure/vpc.tf new file mode 100644 index 0000000..aa67149 --- /dev/null +++ b/infrastructure/vpc.tf @@ -0,0 +1,31 @@ +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 + + public_subnet_tags = { + "kubernetes.io/role/elb" = "1" + "kubernetes.io/role/alb-ingress" = "1" + } + + private_subnet_tags = { + "kubernetes.io/role/internal-elb" = "1" + } + + tags = { + Fastcampus = "true" + "kubernetes.io/cluster/${local.common_prefix}-cluster" = "shared" + } +} \ No newline at end of file diff --git a/load-test.js b/load-test.js new file mode 100644 index 0000000..20a4e31 --- /dev/null +++ b/load-test.js @@ -0,0 +1,18 @@ +import http from 'k6/http'; +import { sleep, check } from 'k6'; +import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.1.0/index.js'; + +export let options = { + vus: 100, + duration: "10m", +}; + +export default function () { + let lectureId = randomIntBetween(1, 100); + let res = http.get(`http://localhost:8000/lectures/${lectureId}/`); + check(res, { + 'status is 200': (r) => r.status === 200, + }); + sleep(0.1); +} +