├── .env_example ├── .github └── workflows │ └── workflow.yaml ├── .gitignore ├── Dockerfile ├── README.md ├── app ├── __init__.py ├── crud.py ├── database.py ├── main.py ├── models.py └── schemas.py ├── docker-compose.yaml ├── kubernetes ├── cluster-issuer.yaml ├── deployment.yaml ├── ingress.yaml ├── secret.yaml └── service.yaml └── requirements.txt /.env_example: -------------------------------------------------------------------------------- 1 | POSTGRES_USER=demo 2 | POSTGRES_PASSWORD=demo 3 | POSTGRES_DB=demo 4 | POSTGRES_PORT=5432 5 | POSTGRES_SERVER=db # this is the name of the service in docker-compose.yml -------------------------------------------------------------------------------- /.github/workflows/workflow.yaml: -------------------------------------------------------------------------------- 1 | name: kubernetes-demo-app 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ main, development ] 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build-and-push: 11 | runs-on: ubuntu-latest 12 | env: 13 | DOCKER_IMAGE: valonjanuzaj/kubernetes-demo:${{ github.sha }} 14 | steps: 15 | - name: Set up QEMU 16 | uses: docker/setup-qemu-action@v2 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v2 19 | - name: Login to Docker Hub 20 | uses: docker/login-action@v2 21 | with: 22 | username: ${{ secrets.DOCKERHUB_USERNAME }} 23 | password: ${{ secrets.DOCKERHUB_TOKEN }} 24 | - name: Build and push 25 | uses: docker/build-push-action@v3 26 | with: 27 | push: true 28 | tags: ${{ env.DOCKER_IMAGE }} 29 | 30 | - name: Checkout manifest repo 31 | uses: actions/checkout@v2 32 | with: 33 | repository: vjanz/kubernetes-demo-gitops 34 | token: ${{ secrets.GH_PAT }} 35 | 36 | ref: main 37 | - name: Update dev manifest 38 | if: ${{ github.event_name == 'push' && 39 | contains(' 40 | refs/heads/development 41 | ', github.ref)}} 42 | env: 43 | K8S_YAML_DIR: ./apps/fastapi-service 44 | IMAGE_NAME: ${{ env.DOCKER_IMAGE }} 45 | run: | 46 | ls -la 47 | cd $K8S_YAML_DIR/overlays/development 48 | curl -s -o kustomize --location https://github.com/kubernetes-sigs/kustomize/releases/download/v3.1.0/kustomize_3.1.0_linux_amd64 49 | chmod u+x ./kustomize 50 | ./kustomize edit set image example-image=$IMAGE_NAME 51 | 52 | - name: Push the changes to git 53 | run: | 54 | git config user.name github-actions 55 | git config user.email github-actions@github.com 56 | git add . 57 | git commit -m "Update image version" 58 | git push -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | .idea/ 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/#use-with-ide 112 | .pdm.toml 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | 3 | WORKDIR /app 4 | 5 | ENV PYTHONUNBUFFERED 1 6 | 7 | COPY requirements.txt ./ 8 | RUN pip install -r requirements.txt 9 | 10 | COPY . . 11 | 12 | CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes-demo-application 2 | See the application GitOps repository: [kubernetes-demo-gitops](https://github.com/vjanz/kubernetes-demo-gitops) 3 | 4 | 5 | ## Project structure: 6 | 7 | ``` 8 | 9 | ├── app # Base directory 10 | │ ├── crud.py # Crud Logic 11 | │ ├── database.py # Database engine 12 | │ ├── main.py # API endpoints and FastAPI initialization 13 | │ ├── models.py # Database models 14 | │ └── schemas.py # Schemas used to exchange data in API 15 | ├── docker-compose.yaml # docker-compose with web and postgres 16 | ├── Dockerfile # Dockerfile for FastAPI application 17 | └── requirements.txt # Requirements for project 18 | ``` 19 | 20 | ## Install and run: 21 | 22 | Clone the project: 23 | 24 | ```bash 25 | git clone git@github.com:vjanz/kubernetes-demo-app.git 26 | ``` 27 | 28 | Copy .env.example to .env 29 | 30 | ```bash 31 | cp .env.example .env 32 | ``` 33 | 34 | Start the project with docker-compose: 35 | 36 | ```bash 37 | docker-compose up -d --build 38 | ``` 39 | 40 | ### Visit the application: 41 | 42 | http://localhost:8000/docs 43 | 44 | #### Create a user 45 | 46 | ``` 47 | curl -X 'POST' \ 48 | 'http://localhost:8000/users/' \ 49 | -H 'Content-Type: application/json' \ 50 | -d '{ 51 | "email": "test@demo.com", 52 | "password": "somepassword" 53 | }' 54 | ------------------------------------------------------------ 55 | {"email":"test@demo.com","id":1,"is_active":true,"items":[]} 56 | ``` 57 | 58 | #### Create an item for user 59 | 60 | ``` 61 | curl -X 'POST' \ 62 | 'http://0.0.0.0:8000/users/1/items/' \ 63 | -H 'Content-Type: application/json' \ 64 | -d '{ 65 | "title": "Item1", 66 | "description": "Some Description" 67 | }' 68 | 69 | Excepted response: 70 | {"title":"Item1","description":"Some Description","id":1,"owner_id":1} 71 | ``` 72 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vjanz/kubernetes-demo-app/aef6f67d25d1188ce405dba25933623021506168/app/__init__.py -------------------------------------------------------------------------------- /app/crud.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from app import models 4 | from app import schemas 5 | 6 | def get_user(db: Session, user_id: int): 7 | return db.query(models.User).filter(models.User.id == user_id).first() 8 | 9 | 10 | def get_user_by_email(db: Session, email: str): 11 | return db.query(models.User).filter(models.User.email == email).first() 12 | 13 | 14 | def get_users(db: Session, skip: int = 0, limit: int = 100): 15 | return db.query(models.User).offset(skip).limit(limit).all() 16 | 17 | 18 | def create_user(db: Session, user: schemas.UserCreate): 19 | fake_hashed_password = f"{user.password}notreallyhashed" 20 | db_user = models.User(email=user.email, hashed_password=fake_hashed_password) 21 | db.add(db_user) 22 | db.commit() 23 | db.refresh(db_user) 24 | return db_user 25 | 26 | 27 | def get_items(db: Session, skip: int = 0, limit: int = 100): 28 | return db.query(models.Item).offset(skip).limit(limit).all() 29 | 30 | 31 | def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int): 32 | db_item = models.Item(**item.dict(), owner_id=user_id) 33 | db.add(db_item) 34 | db.commit() 35 | db.refresh(db_item) 36 | return db_item 37 | -------------------------------------------------------------------------------- /app/database.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from sqlalchemy.orm import sessionmaker 6 | 7 | # SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" 8 | POSTGRES_USER = os.getenv("POSTGRES_USER") 9 | POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD") 10 | POSTGRES_SERVER = os.getenv("POSTGRES_SERVER") 11 | POSTGRES_DB = os.getenv("POSTGRES_DB") 12 | 13 | SQLALCHEMY_DATABASE_URL = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_SERVER}:5432/{POSTGRES_DB}" 14 | 15 | engine = create_engine( 16 | SQLALCHEMY_DATABASE_URL 17 | ) 18 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 19 | 20 | Base = declarative_base() 21 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, FastAPI, HTTPException, Request, Response 2 | from sqlalchemy.orm import Session 3 | 4 | from app import crud 5 | from app import models 6 | from app import schemas 7 | from app.database import SessionLocal, engine 8 | 9 | models.Base.metadata.create_all(bind=engine) 10 | 11 | app = FastAPI() 12 | 13 | 14 | @app.middleware("http") 15 | async def db_session_middleware(request: Request, call_next): 16 | response = Response("Internal server error", status_code=500) 17 | try: 18 | request.state.db = SessionLocal() 19 | response = await call_next(request) 20 | finally: 21 | request.state.db.close() 22 | return response 23 | 24 | 25 | # Dependency 26 | def get_db(request: Request): 27 | return request.state.db 28 | 29 | 30 | @app.post("/users/", response_model=schemas.User) 31 | def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): 32 | db_user = crud.get_user_by_email(db, email=user.email) 33 | if db_user: 34 | raise HTTPException(status_code=400, detail="Email already registered") 35 | return crud.create_user(db=db, user=user) 36 | 37 | 38 | @app.get("/users/", response_model=list[schemas.User]) 39 | def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): 40 | users = crud.get_users(db, skip=skip, limit=limit) 41 | return users 42 | 43 | 44 | @app.get("/users/{user_id}", response_model=schemas.User) 45 | def read_user(user_id: int, db: Session = Depends(get_db)): 46 | db_user = crud.get_user(db, user_id=user_id) 47 | if db_user is None: 48 | raise HTTPException(status_code=404, detail="User not found") 49 | return db_user 50 | 51 | 52 | @app.post("/users/{user_id}/items/", response_model=schemas.Item) 53 | def create_item_for_user( 54 | user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db) 55 | ): 56 | return crud.create_user_item(db=db, item=item, user_id=user_id) 57 | 58 | 59 | @app.get("/items/", response_model=list[schemas.Item]) 60 | def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): 61 | items = crud.get_items(db, skip=skip, limit=limit) 62 | return items 63 | 64 | 65 | @app.get("/health") 66 | def health(): 67 | return {"status": "App is running!!!!"} -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, ForeignKey, Integer, String 2 | from sqlalchemy.orm import relationship 3 | 4 | from app.database import Base 5 | 6 | 7 | class User(Base): 8 | __tablename__ = "users" 9 | 10 | id = Column(Integer, primary_key=True, index=True) 11 | email = Column(String, unique=True, index=True) 12 | hashed_password = Column(String) 13 | is_active = Column(Boolean, default=True) 14 | 15 | items = relationship("Item", back_populates="owner") 16 | 17 | 18 | class Item(Base): 19 | __tablename__ = "items" 20 | 21 | id = Column(Integer, primary_key=True, index=True) 22 | title = Column(String, index=True) 23 | description = Column(String, index=True) 24 | owner_id = Column(Integer, ForeignKey("users.id")) 25 | 26 | owner = relationship("User", back_populates="items") 27 | -------------------------------------------------------------------------------- /app/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class ItemBase(BaseModel): 7 | title: str 8 | description: Union[str, None] = None 9 | 10 | 11 | class ItemCreate(ItemBase): 12 | pass 13 | 14 | 15 | class Item(ItemBase): 16 | id: int 17 | owner_id: int 18 | 19 | class Config: 20 | orm_mode = True 21 | 22 | 23 | class UserBase(BaseModel): 24 | email: str 25 | 26 | 27 | class UserCreate(UserBase): 28 | password: str 29 | 30 | 31 | class User(UserBase): 32 | id: int 33 | is_active: bool 34 | items: list[Item] = [] 35 | 36 | class Config: 37 | orm_mode = True 38 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | app: 5 | build: . 6 | ports: 7 | - "8000:8000" 8 | env_file: 9 | - .env 10 | volumes: 11 | - ./:/app 12 | depends_on: 13 | - db 14 | db: 15 | image: postgres:14-alpine 16 | volumes: 17 | - postgres_data_dir:/var/lib/postgresql/data/ 18 | env_file: .env 19 | ports: 20 | - "5432:5432" 21 | volumes: 22 | postgres_data_dir: 23 | -------------------------------------------------------------------------------- /kubernetes/cluster-issuer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: ClusterIssuer 3 | metadata: 4 | name: letsencrypt-cluster-issuer 5 | spec: 6 | acme: 7 | # Enter your email here 8 | email: valon.januzaj98@gmail.com 9 | # The ACME server URL 10 | server: https://acme-v02.api.letsencrypt.org/directory 11 | privateKeySecretRef: 12 | # Name of a secret used to store the ACME account private key 13 | name: letsencrypt-private-key 14 | # Add a single challenge solver, HTTP01 using nginx 15 | solvers: 16 | - http01: 17 | ingress: 18 | class: nginx -------------------------------------------------------------------------------- /kubernetes/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: kubernetes-demo 5 | namespace: kubernetes-demo 6 | spec: 7 | replicas: 2 8 | selector: 9 | matchLabels: 10 | app: kubernetes-demo 11 | template: 12 | metadata: 13 | labels: 14 | app: kubernetes-demo 15 | spec: 16 | containers: 17 | - name: kubernetes-demo 18 | image: valonjanuzaj/kubernetes-demo:latest 19 | ports: 20 | - containerPort: 8000 21 | envFrom: 22 | - secretRef: 23 | name: demo-secrets 24 | -------------------------------------------------------------------------------- /kubernetes/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: kubernetes-demo-ingress 5 | namespace: kubernetes-demo 6 | # Add the following annotations to the ingress 7 | annotations: 8 | cert-manager.io/cluster-issuer: letsencrypt-cluster-issuer 9 | spec: 10 | # Add tls section 11 | tls: 12 | - hosts: 13 | - "demo.vjanztutorials.tech" 14 | secretName: kubernetes-demo-tls 15 | ingressClassName: nginx 16 | rules: 17 | - host: "demo.vjanztutorials.tech" 18 | http: 19 | paths: 20 | - backend: 21 | service: 22 | name: kubernetes-demo 23 | port: 24 | number: 80 25 | path: / 26 | pathType: Prefix 27 | 28 | -------------------------------------------------------------------------------- /kubernetes/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: demo-secrets 5 | namespace: kubernetes-demo 6 | type: Opaque 7 | data: 8 | POSTGRES_USER: cG9zdGdyZXM= 9 | POSTGRES_PASSWORD: TDVYT3lacTViUg== 10 | POSTGRES_PORT: NTQzMg== 11 | POSTGRES_DB: a3ViZXJuZXRlcy1kZW1v 12 | POSTGRES_SERVER: cG9zdGdyZXMtcG9zdGdyZXNxbC5wb3N0Z3Jlcw== 13 | -------------------------------------------------------------------------------- /kubernetes/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: kubernetes-demo 5 | namespace: kubernetes-demo 6 | spec: 7 | ports: 8 | - name: http 9 | protocol: TCP 10 | port: 80 11 | targetPort: 8000 12 | selector: 13 | app: kubernetes-demo -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.6.2 2 | click==8.1.3 3 | fastapi==0.88.0 4 | greenlet==2.0.1 5 | h11==0.14.0 6 | idna==3.4 7 | psycopg2-binary==2.9.5 8 | pydantic==1.10.4 9 | sniffio==1.3.0 10 | SQLAlchemy==1.4.46 11 | starlette==0.22.0 12 | typing_extensions==4.4.0 13 | uvicorn==0.20.0 14 | --------------------------------------------------------------------------------