diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..d70cebd23 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,48 @@ +name: Python CI + +on: + push: + branches: [lab3] + paths: + - "app_python/**" + - ".github/workflows/main.yaml" + +jobs: + build_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.10.7' + - name: Install dependencies + run: pip install -r app_python/requirements.txt + - name: Upgrade Flask and Werkzeug + run: pip install --upgrade Flask + - name: Run unit tests + run: python3 app_python/test_main.py + + deploy: + needs: build_test + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Log in to Docker registry + uses: docker/login-action@v1 + with: + registry: docker.io + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker image + uses: docker/build-push-action@v2 + with: + context: . + file: app_python/Dockerfile + push: true + tags: docker.io/haraphat01/moscowtime:tagname + \ No newline at end of file diff --git a/README.md b/README.md index 119379536..b5094c4a8 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,92 @@ -# Labs +# Current Time in Moscow -## Introduction +![Python CI](https://github.com/haraphat01/moscow_time/workflows/Python%20CI/badge.svg) -Welcome to DevOps course labs. All labs are practical and will be built on each other. You will implement simple application, containerize it, implement simple tests, prepare an infrastructure and CI/CD processes, collect metrics, logs, etc. -## Architecture +## Overview +This is a simple Python web application that displays the current time in Moscow. It uses the Flask web framework and the datetime module to get the current time in the Europe/Moscow timezone. The time is displayed in a user-friendly format on a web page. -This repository contains a master branch with introduction and one branch with instructions for each lab. +## Build +To build this application, you need to create a Python file with a main.py extension, for example, app.py. Then, copy the code in main.py to your file. Save the file and run it using the command python app.py in a terminal or command prompt. +The application will start running on your local machine. -## Rules +## Usage +To use the application, open a web browser and navigate to http://localhost:5000/. The current time in Moscow will be displayed on the web page. The time will be updated every time you refresh the page. -Each labs requires the participant to finish all previous labs, therefore **participants are required to submit each lab and get at least 6/10 points for each lab to pass the course**. +## To run the Dockerfile in your project, you can follow these steps: -Grading is based on PRs with your solution to the corresponding branch of this repository. This repository is read-only for all participants, therefore to be able to create a pull request, a participant should fork this repository to his own workspace and solve the lab there. It is recommended to build a solution of a lab N upon a solution of lab N-1, so choose workflow in your fork of this repository wisely. Structure of your repository will not affect your grade, only state of your repository from which the PR is created will be checked and graded (state after last commit in your PR on corresponding lab). +1. Make sure that you have Docker installed on your local machine. You can download Docker from the official website: https://www.docker.com/products/docker-desktop -### Recommended workflow +2. Open a terminal or command prompt and navigate to the directory that contains your Dockerfile. -#### For the first lab +3. Build the Docker image using the following command: + docker build -t . + Make sure to replace with the name that you want to give to your Docker image. The . at the end of the command specifies that the build context is the current directory. -1. Fork this repository. -2. Checkout to lab1 branch. -3. Complete lab1 tasks. -4. Push the code to your repository. -5. Create a PR to the lab1 branch on this repository from your fork's lab1 branch. -6. Create an archive with the current version of your code and submit a zip file to Moodle. -7. Create a team with with your classmates, 6 people max. -8. Each student must review PRs of all teammates. -9. Wait for your grade. +4. Once the build is complete, you can run the Docker container using the following command: + docker run -p : -## Grading +Make sure to replace with the port number on your local machine that you want to use to access the container, with the port number that your application is listening on inside the container, and with the name of the Docker image that you built in step 3. -### Points distribution for the course +For example, if your application is listening on port 80 inside the container and you want to access it on port 8080 on your local machine, you can use the following command: + docker run -p 8080:80 -``` -70 - labs -20 - final exam -10 - attendance on lectures -``` +5. Once the container is running, you can access your application by opening a web browser and navigating to http://localhost:. In the example above, you would navigate to http://localhost:8080. -### Grade ranges +## Unit tests -``` -[90;100] - A -[75;90) - B -[60;75) - C -[0;60) - D -``` +I have written unit tests for the Python web application that displays the current time in Moscow. These tests ensure that the application is working as expected and help us catch any bugs early in the development process. -### Labs grading +To run the unit tests, navigate to the directory where the `test_main.py` file is located and run the following command: +python test_main.py -Each lab is marked out of 10. All labs have a set of main tasks and a set of extra tasks. -Completing main tasks correctly will give you 10 points out of 10. Completing extra tasks correctly will give you some additional points, depends on the bonus task difficulty. Your points for main and extra tasks will be summed up and will help you to get a better grade. +Make sure that you have all the necessary dependencies installed, including Flask and any other modules that your application depends on. -If you finish all bonus tasks correctly the **permission to skip the exam will be granted to you + 10 extra points**. If you finish not all of them you will must pass the exam, but it can save you from the exam's failure. +If the tests pass successfully, you should see output similar to the following: -## Deadlines and labs distribution +Ran 1 test in 0.001s -Participants have 2 new labs every week simultaneously and 1 week to submit solutions. Moodle will contain presentations and deadlines. +OK -You are required to submit a zip file with your source code to corresponding assignment in moodle. This is required for the university as a proof of work. -## Submission policy +## Git Actions CI -**Submitting results after the deadline will result in maximum of 6 points for the corresponding lab. As stated before, all labs must be submitted to pass the course.** +I have set up a continuous integration (CI) workflow using Git Actions to build and test our Python web application. The workflow has three steps: installing dependencies, running linters, and running unit tests. + +To run the workflow, push changes to the `main` branch of the repository. The workflow will automatically be triggered and the steps will be run in order. Make sure that you have all the necessary dependencies installed, including Flask and any other modules that your application depends on. + +I have also added Docker-related steps to our workflow to log in to a Docker registry, build a Docker image, and push the image to the registry. To use these steps, you will need to set up the `DOCKER_USERNAME` and `DOCKER_PASSWORD` secrets in your repository settings. + +By following these best practices and including a Git Actions CI workflow in your project, I can ensure that my application is thoroughly tested and working as expected. + +# Terraform AWS Example + +This Terraform configuration sets up an AWS infrastructure with a single EC2 instance running in the `us-west-2` region. The instance is tagged with the name "ExampleAppServerInstance". + +## Prerequisites + +- Terraform >= 1.2.0 +- AWS CLI +- An AWS account with the necessary permissions + +## Configuration + +The `main.tf` file contains the following resources: + +1. **Terraform block**: Specifies the required providers and their versions. +2. **AWS provider block**: Configures the AWS provider with the `us-west-2` region. +3. **AWS instance resource**: Creates an EC2 instance with the specified AMI and instance type. + + +## Usage + +1. Install Terraform and the AWS CLI. +2. Configure your AWS credentials using `aws configure` or by setting the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables. +3. Run `terraform init` to initialize the Terraform working directory. +4. Run `terraform apply` to create the infrastructure. Confirm the changes when prompted. +5. To destroy the infrastructure when you're done, run `terraform destroy`. + +## Customization + +You can customize the configuration by modifying the `main.tf` file. For example, you can change the instance type, region, or add additional resources as needed. \ No newline at end of file diff --git a/ansible/deploy.yml b/ansible/deploy.yml new file mode 100644 index 000000000..6a285b016 --- /dev/null +++ b/ansible/deploy.yml @@ -0,0 +1,5 @@ +- name: Deploy Docker image to Docker Hub + hosts: all + become: true + roles: + - role: docker_deploy diff --git a/ansible/roles/docker_deploy/defaults/main.yml b/ansible/roles/docker_deploy/defaults/main.yml new file mode 100644 index 000000000..182797b8a --- /dev/null +++ b/ansible/roles/docker_deploy/defaults/main.yml @@ -0,0 +1,8 @@ +docker_image_name: myapp +docker_image_tag: latest +docker_username: myusername +docker_password: mypassword +docker_email: myemail@example.com +app_path: /path/to/app +http_proxy: http://proxy.example.com:8080 +https_proxy: http://proxy.example.com:8080 diff --git a/ansible/roles/docker_deploy/tasks/main.yml b/ansible/roles/docker_deploy/tasks/main.yml new file mode 100644 index 000000000..9a954804f --- /dev/null +++ b/ansible/roles/docker_deploy/tasks/main.yml @@ -0,0 +1,30 @@ +- name: Login to Docker Hub + docker_login: + username: "{{ docker_username }}" + password: "{{ docker_password }}" + email: "{{ docker_email }}" + +- name: Pull Docker image + docker_image: + name: "{{ docker_image_name }}:{{ docker_image_tag }}" + source: pull + +- name: Stop and remove existing container + docker_container: + name: "{{ docker_image_name }}" + state: stopped + force_kill: yes + remove_volumes: yes + remove_image: yes + ignore_errors: yes + +- name: Run Docker container + docker_container: + name: "{{ docker_image_name }}" + image: "{{ docker_image_name }}:{{ docker_image_tag }}" + state: started + ports: + - "80:80" + env: + - "HTTP_PROXY={{ http_proxy }}" + - "HTTPS_PROXY={{ https_proxy }}" diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 000000000..755aea5e0 --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3-alpine +USER root +WORKDIR /main + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8081 + +CMD ["python3", "main.py"] + + diff --git a/app_python/main.py b/app_python/main.py new file mode 100644 index 000000000..00c8b1f82 --- /dev/null +++ b/app_python/main.py @@ -0,0 +1,17 @@ +from datetime import datetime + +from flask import Flask + +app = Flask(__name__) + +def get_current_time(): + now = datetime.now() + current_time = now.strftime("Current Time in Moscow: %Y-%m-%d %H:%M:%S") + return current_time + +@app.route('/') +def home(): + return get_current_time() + +if __name__ == '__main__': + app.run(host="0.0.0.0", debug=True) \ No newline at end of file diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 000000000..496a52930 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,2 @@ +pytz +Flask==2.0.1 diff --git a/app_python/test_main.py b/app_python/test_main.py new file mode 100644 index 000000000..676849e5e --- /dev/null +++ b/app_python/test_main.py @@ -0,0 +1,14 @@ +import unittest +from main import app + +class TestApp(unittest.TestCase): + def setUp(self): + self.app = app.test_client() + + def test_homepage(self): + response = self.app.get('/') + self.assertEqual(response.status_code, 200) + self.assertIn(b'Current Time in Moscow', response.data) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/deployment.yml b/deployment.yml new file mode 100644 index 000000000..b92332ef0 --- /dev/null +++ b/deployment.yml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: moscow +spec: + replicas: 3 + selector: + matchLabels: + app: moscow + template: + metadata: + labels: + app: moscow + spec: + containers: + - name: moscow + image: haraphat01/moscowtime + resources: + limits: + memory: "128Mi" + cpu: "500m" + ports: + - containerPort: 8081 diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 000000000..5eb1b5218 --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,48 @@ +## kubectl get pods +NAME READY STATUS RESTARTS AGE +moscow-6489c859b-v86nb 1/1 Running 1 (7m28s ago) 12m + + +## kubectl get svc +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +kubernetes ClusterIP 10.96.0.1 443/TCP 128m +moscow NodePort 10.106.170.135 8000:32134/TCP 5m28s + +## kubectl get pods +NAME READY STATUS RESTARTS AGE +moscow-78bccd8f65-b2s8h 1/1 Running 2 (3m7s ago) 3m36s +moscow-78bccd8f65-fskbx 1/1 Running 1 (3m18s ago) 3m36s +moscow-78bccd8f65-nlq6q 1/1 Running 1 (3m18s ago) 3m36s + +## kubectl get svc +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +kubernetes ClusterIP 10.96.0.1 443/TCP 4m10s +moscow ClusterIP 10.106.88.220 8081/TCP 99s + +## minikube service --all +W0701 11:43:34.451733 1771362 main.go:291] Unable to resolve the current Docker CLI context "default": context "default" does not exist +|-----------|------------|-------------|--------------| +| NAMESPACE | NAME | TARGET PORT | URL | +|-----------|------------|-------------|--------------| +| default | kubernetes | | No node port | +|-----------|------------|-------------|--------------| +😿 service default/kubernetes has no node port +|-----------|--------|-------------|---------------------------| +| NAMESPACE | NAME | TARGET PORT | URL | +|-----------|--------|-------------|---------------------------| +| default | moscow | 8081 | http://192.168.49.2:30000 | +|-----------|--------|-------------|---------------------------| +🎉 Opening service default/moscow in default browser... + +![Screenshot](./k8s/screen.png) + +## kubectl get pods,svc +NAME READY STATUS RESTARTS AGE +pod/moscow-c4bdbf657-42bdt 1/1 Running 0 36h +pod/moscow-c4bdbf657-c5hl8 1/1 Running 0 36h +pod/moscow-c4bdbf657-sqjjh 1/1 Running 0 36h +pod/moscow1-85bdcdc774-w4557 0/1 CrashLoopBackOff 5 (38s ago) 4m18s + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/kubernetes ClusterIP 10.96.0.1 443/TCP 36h +service/moscow1 ClusterIP 10.109.250.251 8081/TCP 4m19s \ No newline at end of file diff --git a/k8s/deployment.yml b/k8s/deployment.yml new file mode 100644 index 000000000..273d77f7f --- /dev/null +++ b/k8s/deployment.yml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: moscow +spec: + replicas: 3 + selector: + matchLabels: + app: moscow + template: + metadata: + labels: + app: moscow + spec: + containers: + - name: moscow + image: haraphat01/moscowtime + resources: + limits: + memory: "128Mi" + cpu: "250m" + ports: + - containerPort: 8081 diff --git a/k8s/moscow/.helmignore b/k8s/moscow/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/k8s/moscow/.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/k8s/moscow/Chart.yaml b/k8s/moscow/Chart.yaml new file mode 100644 index 000000000..905a530c6 --- /dev/null +++ b/k8s/moscow/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: moscow +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/k8s/moscow/templates/NOTES.txt b/k8s/moscow/templates/NOTES.txt new file mode 100644 index 000000000..3a228e6cb --- /dev/null +++ b/k8s/moscow/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "moscow.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "moscow.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "moscow.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "moscow.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/k8s/moscow/templates/_helpers.tpl b/k8s/moscow/templates/_helpers.tpl new file mode 100644 index 000000000..e768019f0 --- /dev/null +++ b/k8s/moscow/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "moscow.name" -}} +{{- default .Chart.Name .Values.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 "moscow.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.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 "moscow.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "moscow.labels" -}} +helm.sh/chart: {{ include "moscow.chart" . }} +{{ include "moscow.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "moscow.selectorLabels" -}} +app.kubernetes.io/name: {{ include "moscow.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "moscow.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "moscow.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/k8s/moscow/templates/deployment.yaml b/k8s/moscow/templates/deployment.yaml new file mode 100644 index 000000000..b0ee9f9ca --- /dev/null +++ b/k8s/moscow/templates/deployment.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "moscow.fullname" . }} + labels: + {{- include "moscow.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "moscow.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "moscow.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "moscow.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/k8s/moscow/templates/hpa.yaml b/k8s/moscow/templates/hpa.yaml new file mode 100644 index 000000000..8e2813ee7 --- /dev/null +++ b/k8s/moscow/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "moscow.fullname" . }} + labels: + {{- include "moscow.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "moscow.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/k8s/moscow/templates/ingress.yaml b/k8s/moscow/templates/ingress.yaml new file mode 100644 index 000000000..30c3b199d --- /dev/null +++ b/k8s/moscow/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "moscow.fullname" . -}} +{{- $svcPort := .Values.service.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 "moscow.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/k8s/moscow/templates/service.yaml b/k8s/moscow/templates/service.yaml new file mode 100644 index 000000000..042584ffe --- /dev/null +++ b/k8s/moscow/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "moscow.fullname" . }} + labels: + {{- include "moscow.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "moscow.selectorLabels" . | nindent 4 }} diff --git a/k8s/moscow/templates/serviceaccount.yaml b/k8s/moscow/templates/serviceaccount.yaml new file mode 100644 index 000000000..e22dc1bbf --- /dev/null +++ b/k8s/moscow/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "moscow.serviceAccountName" . }} + labels: + {{- include "moscow.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/k8s/moscow/templates/tests/test-connection.yaml b/k8s/moscow/templates/tests/test-connection.yaml new file mode 100644 index 000000000..d50ee4246 --- /dev/null +++ b/k8s/moscow/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "moscow.fullname" . }}-test-connection" + labels: + {{- include "moscow.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "moscow.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/k8s/moscow/values.yaml b/k8s/moscow/values.yaml new file mode 100644 index 000000000..f3360b99a --- /dev/null +++ b/k8s/moscow/values.yaml @@ -0,0 +1,82 @@ +# Default values for moscow. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: haraphat01/moscowtime + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 8081 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/k8s/screen.png b/k8s/screen.png new file mode 100644 index 000000000..f2486d8a3 Binary files /dev/null and b/k8s/screen.png differ diff --git a/k8s/service.yml b/k8s/service.yml new file mode 100644 index 000000000..f543c3bbc --- /dev/null +++ b/k8s/service.yml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: moscow +spec: + type: NodePort + ports: + - port: 8081 + targetPort: 8081 + nodePort: 30000 + selector: + app: moscow \ No newline at end of file diff --git a/monitoring/LOGGING.md b/monitoring/LOGGING.md new file mode 100644 index 000000000..e6fff5ef8 --- /dev/null +++ b/monitoring/LOGGING.md @@ -0,0 +1,62 @@ +## Introduction + +In this lab, I set up a logging stack using Promtail, Loki, and Grafana. I also prepared a docker-compose.yml file to set up the stack and my application. + +## Setting Up the Files + +To set up the files for the logging stack, I followed these steps: + +1. Created a Dockerfile for my application in the app_python directory. +2. Created a docker-compose.yml file in the root directory of my project. +3. Created a promtail directory in the root directory of my project. +4. Created a config.yaml file inside the promtail directory. +5. Created a monitoring directory in the root directory of my project. +6. Created a LOGGING.md file inside the monitoring directory. + +## Docker Compose File + +Here's the docker-compose.yml file I used to set up the logging stack and my application: + version: '3' +services: + app: + build: ../app_python + ports: + - "5000:5000" + loki: + image: grafana/loki:latest + ports: + - "3100:3100" + promtail: + image: grafana/promtail:latest + volumes: + - ./promtail:/etc/promtail + command: -config.file=/etc/promtail/config.yaml + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" + +This file sets up four services: my application, Loki, Promtail, and Grafana. + +## Promtail Configuration + +Here's the config.yaml file I used to configure Promtail: + +This configuration sets up Promtail to listen on port 9080 and to push logs to Loki. + +## Testing + +To test the logging stack, I ran the following command in the terminal: + +This command started all the services defined in the docker-compose.yml file. I then navigated to http://localhost:5000 in my web browser to test my application. I also navigated to http://localhost:3000 to check the logs in Grafana. + +## Screenshots + +Here are some screenshots of my success: + +- ![Screenshot of my application running](/graffana.png) + + +## Conclusion + +In conclusion, I was able to successfully set up a logging stack using Promtail, Loki, and Grafana. I also prepared a docker-compose.yml file to set up the stack and my application. diff --git a/monitoring/METRICS.md b/monitoring/METRICS.md new file mode 100644 index 000000000..e69de29bb diff --git a/monitoring/docker-compose.yml b/monitoring/docker-compose.yml new file mode 100644 index 000000000..9fc465584 --- /dev/null +++ b/monitoring/docker-compose.yml @@ -0,0 +1,69 @@ +version: '3' +services: + app: + build: ../app_python/main.py + ports: + - "5000:5000" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/health"] + interval: 30s + timeout: 10s + retries: 5 + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 512M + loki: + image: grafana/loki:latest + ports: + - "3100:3100" + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 256M + promtail: + image: grafana/promtail:latest + volumes: + - ./promtail:/etc/promtail + command: -config.file=/etc/promtail/config.yaml + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 128M + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" + prometheus: + image: prom/prometheus:v2.30.3 + ports: + - "9090:9090" + volumes: + - ./prometheus:/etc/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 256M \ No newline at end of file diff --git a/monitoring/graffana.png b/monitoring/graffana.png new file mode 100644 index 000000000..fa2cbea0d Binary files /dev/null and b/monitoring/graffana.png differ diff --git a/monitoring/metrics.png b/monitoring/metrics.png new file mode 100644 index 000000000..a406ab1df Binary files /dev/null and b/monitoring/metrics.png differ diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml new file mode 100644 index 000000000..82f6e523e --- /dev/null +++ b/monitoring/prometheus/prometheus.yml @@ -0,0 +1,14 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + - job_name: 'loki' + static_configs: + - targets: ['loki:3100'] + - job_name: 'promtail' + static_configs: + - targets: ['promtail:9080'] + \ No newline at end of file diff --git a/promtail/config.yml b/promtail/config.yml new file mode 100644 index 000000000..c2bcbfe04 --- /dev/null +++ b/promtail/config.yml @@ -0,0 +1,18 @@ +server: + http_listen_port: 9080 + grpc_listen_port: 0 + +positions: + filename: /tmp/positions.yaml + +clients: + - url: http://loki:3100/loki/api/v1/push + +scrape_configs: +- job_name: system + static_configs: + - targets: + - localhost + labels: + job: varlogs + __path__: /var/log/*log \ No newline at end of file diff --git a/service.yml b/service.yml new file mode 100644 index 000000000..936c126a9 --- /dev/null +++ b/service.yml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: moscow +spec: + selector: + app: moscow + ports: + - port: 8081 + targetPort: 8081 + diff --git a/terraform/TF.md b/terraform/TF.md new file mode 100644 index 000000000..c959d4200 --- /dev/null +++ b/terraform/TF.md @@ -0,0 +1,73 @@ +# Output of Terraform show +docker_container.nginx: +resource “docker_container” “nginx” { attach = false command = [ “nginx”, “-g”, “daemon off;”, ] container_read_refresh_timeout_milliseconds = +15000 cpu_shares = 0 entrypoint = [ “/docker-entrypoint.sh”, ] env = [] hostname = “21dca2656095” id = +“21dca2656095460e1391309cc5904b687acdaee6500a047df9493911addb239c” image = +“sha256:f9c14fe76d502861ba0939bc3189e642c02e257f06f4c0214b1f8ca329326cda” init = false ipc_mode = “private” log_driver = “json-file” logs = +false max_retry_count = 0 memory = 0 memory_swap = 0 must_run = true name = “tutorial” network_data = [ { gateway = “172.17.0.1” +global_ipv6_address = “” global_ipv6_prefix_length = 0 ip_address = “172.17.0.2” ip_prefix_length = 16 ipv6_gateway = “” mac_address = “02:42:ac +11:00:02” network_name = “bridge” }, ] network_mode = “default” privileged = false publish_all_ports = false read_only = false remove_volumes = +true restart = “no” rm = false runtime = “runc” security_opts = [] shm_size = 64 start = true stdin_open = false stop_signal = “SIGQUIT” stop_timeout += 0 tty = false wait = false wait_timeout = 60 +ports { +external = 8000 +internal = 80 +ip = "0.0.0.0" +protocol = "tcp" +} + +} +docker_image.nginx: +resource “docker_image” “nginx” { id = “sha256:f9c14fe76d502861ba0939bc3189e642c02e257f06f4c0214b1f8ca329326cdanginx:latest” image_id = +“sha256:f9c14fe76d502861ba0939bc3189e642c02e257f06f4c0214b1f8ca329326cda” keep_locally = false name = “nginx:latest” repo_digest = +“nginx@sha256:af296b188c7b7df99ba960ca614439c99cb7cf252ed7bbc23e90cfda59092305” } + +# Output of terraform state list +docker_container.nginx docker_image.nginx +aws_instance.app_server: +resource “aws_instance” “app_server” { ami = “ami-08d70e59c07c61a3a” arn = “arn:aws:ec2:us-west-2:354156216263:instance +i-0b39be7dc8d0a893a” associate_public_ip_address = true availability_zone = “us-west-2a” cpu_core_count = 1 cpu_threads_per_core = 1 +disable_api_stop = false disable_api_termination = false ebs_optimized = false get_password_data = false hibernation = false id = +“i-0b39be7dc8d0a893a” instance_initiated_shutdown_behavior = “stop” instance_state = “running” instance_type = “t2.micro” ipv6_address_count = +0 ipv6_addresses = [] monitoring = false placement_partition_number = 0 primary_network_interface_id = “eni-0cc62b162b9c9df4c” private_dns = +“ip-172-31-16-27.us-west-2.compute.internal” private_ip = “172.31.16.27” public_dns = “ec2-35-93-162-232.us-west-2.compute.amazonaws.com” +public_ip = “35.93.162.232” secondary_private_ips = [] security_groups = [ “default”, ] source_dest_check = true subnet_id = +“subnet-039b0778d5eaf2eb4” tags = { “Name” = “ExampleAppServerInstance” } tags_all = { “Name” = “ExampleAppServerInstance” } tenancy = +“default” user_data_replace_on_change = false vpc_security_group_ids = [ “sg-0849ce7988dae5104”, ] +capacity_reservation_specification { +capacity_reservation_preference = "open" +} +cpu_options { +core_count = 1 +threads_per_core = 1 +} +credit_specification { +cpu_credits = "standard" +} +enclave_options { +enabled = false +} +maintenance_options { +auto_recovery = "default" +}metadata_options { +http_endpoint = "enabled" +http_put_response_hop_limit = 1 +http_tokens = "optional" +instance_metadata_tags = "disabled" +} +private_dns_name_options { +enable_resource_name_dns_a_record = false +enable_resource_name_dns_aaaa_record = false +hostname_type = "ip-name" +} +root_block_device { +delete_on_termination = true +device_name = "/dev/sda1" +encrypted = false +iops = 100 +tags = {} +throughput = 0 +volume_id = "vol-0403cb7cdf11b3fb7" +volume_size = 8 +volume_type = "gp2" +} diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 000000000..68d9bef50 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,24 @@ +terraform { + required_providers { + docker = { + source = "kreuzwerker/docker" + version = "~> 3.0.1" + } + } +} + +provider "docker" {} + +resource "docker_image" "nginx" { + name = "nginx:latest" + keep_locally = false +} + +resource "docker_container" "nginx" { + image = docker_image.nginx.image_id + name = "tutorial" + ports { + internal = 80 + external = 8000 + } +}