├── .flake8 ├── .gitignore ├── README.md ├── authentication_api ├── .dockerignore ├── Dockerfile ├── README.md ├── alembic.ini ├── alembic │ ├── README │ ├── env.py │ ├── script.py.mako │ └── versions │ │ └── a9f7b29eaeb9_add_account_table.py ├── app │ ├── __init__.py │ ├── authenticator.py │ ├── config.py │ ├── core.py │ ├── crud.py │ ├── db.py │ ├── main.py │ ├── models.py │ ├── routers │ │ ├── __init__.py │ │ └── accounts.py │ ├── schemas.py │ └── session_db.py └── requirements.txt ├── helloworld_api ├── Dockerfile ├── README.md ├── app │ └── main.py └── requirements.txt └── kubernetes ├── authentication_api ├── app-config.yaml ├── db-config.yaml └── db-secrets.yaml ├── helloworld ├── deployment.yaml └── service.yaml ├── kube-dash ├── admin-user.yaml └── cluster-role.yaml └── minikube-ingress.yaml /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 125 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated files 2 | __pycache__ 3 | .DS_Store 4 | 5 | # VSCode 6 | *code-workspace 7 | .vscode 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Experiments 2 | 3 | This repository contains code that I'm using to learn and experiment with Kubernetes. 4 | 5 | # 1. Environment setup 6 | 7 | - minikube 8 | - kubectl 9 | - docker (for macOS) 10 | - python 3.7 (via Anaconda) 11 | 12 | ## 1.1 Install `minikube` and `kubectl` CLI tools via gcloud: 13 | 14 | I use GCP for production deployments, so I'm installing kubectl and minikube from gcloud to avoid version discrepancies. 15 | 16 | Homebrew can also be used to install kubectl and minikube. 17 | 18 | ```shell 19 | # Install kubectl and minikube via gcloud 20 | gcloud components install kubectl; 21 | gcloud components install minikube; 22 | 23 | # Make sure we have the latest versions of kubectl and minikube 24 | gcloud components update; 25 | ``` 26 | 27 | ## 1.2 Install Docker for macOS: 28 | 29 | Follow the instructions here: https://docs.docker.com/docker-for-mac/install/ 30 | 31 | ## 1.3 Start minikube 32 | 33 | ```shell 34 | # Starts a node Kubernete cluster using Virtualbox/VMWareFusion 35 | minikube start; 36 | 37 | # Enables a plugin that allows minikube to use a local docker repository, 38 | # so we don't have to use DockerHub or GCR. 39 | minikube addons enable registry; 40 | 41 | # Enables an nginx frontend that will route incoming requests to different services 42 | minikube addons enable ingress; 43 | ``` 44 | 45 | ## 1.4 Create a new Anaconda environment 46 | 47 | ```shell 48 | conda create --name kubernetes-experiment python=3.7; 49 | conda activate kubernetes-experiment; 50 | ``` 51 | 52 | # 2. Set up Kubernetes Dashboard Web UI 53 | 54 | Kubernetes comes with an admin dashboard that shows information about the cluster, such as what 55 | pods are running and what jobs have run. 56 | 57 | ## 2.1 Install web UI 58 | 59 | ```shell 60 | kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-rc5/aio/deploy/recommended.yaml; 61 | ``` 62 | 63 | ## 2.2 Create admin user with cluster admin role 64 | 65 | In order to access the dashboard, we need to create an admin account and give the account the cluster-admin role. 66 | 67 | ```shell 68 | kubectl apply -f kubernetes/kube-dash/admin-user.yaml; 69 | kubectl apply -f kubernetes/kube-dash/cluster-role.yaml; 70 | ``` 71 | 72 | ## 2.3 Viewing the web UI 73 | 74 | To view the dashboard, we need to run this command in a separate terminal: 75 | 76 | ```shell 77 | kubectl proxy 78 | ``` 79 | 80 | Now we can view the dashboard at: http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/ 81 | 82 | ## 2.4 Logging in to the web UI 83 | 84 | To log in to the web UI we need to get an access token, which can be done with this command: 85 | 86 | ```shell 87 | kubectl -n kubernetes-dashboard describe secret $(kubectl -n kubernetes-dashboard get secret | grep admin-user | awk '{print $1}') 88 | ``` 89 | 90 | Copy and paste this token into the web UI. 91 | 92 | ## 2.5 Resources: 93 | 94 | - https://github.com/kubernetes/dashboard 95 | - https://github.com/kubernetes/dashboard/blob/master/docs/user/access-control/creating-sample-user.md 96 | -------------------------------------------------------------------------------- /authentication_api/.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore generated files 2 | .DS_Store 3 | **/__pycache__ 4 | -------------------------------------------------------------------------------- /authentication_api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 2 | 3 | COPY ./requirements.txt /tmp/requirements.txt 4 | RUN pip install -r /tmp/requirements.txt 5 | 6 | COPY ./app /app/app 7 | 8 | -------------------------------------------------------------------------------- /authentication_api/README.md: -------------------------------------------------------------------------------- 1 | # Authentication API 2 | 3 | This exercise sets up an API for authenticating users. We will use the following software: 4 | 5 | 1. Fast API 6 | - Web app for authenticating users 7 | 2. Postgres 8 | - Store user accounts 9 | 3. Redis 10 | - Store access tokens 11 | 4. Kubernetes 12 | - Configures an authentication api that uses Fast API, Postgres, and Redis 13 | 14 | - The Kubernetes configuration is based off this tutorial: [https://testdriven.io/blog/running-flask-on-kubernetes](https://testdriven.io/blog/running-flask-on-kubernetes). 15 | - The Authentication API is based off the Fast API tutorials, which are very detailed and helpful: 16 | - [SQL (Relational) Databases](https://fastapi.tiangolo.com/tutorial/sql-databases/) 17 | - [Security](https://fastapi.tiangolo.com/tutorial/security/) 18 | - [CORS](https://fastapi.tiangolo.com/tutorial/cors/) 19 | 20 | ## 1. The authentication API 21 | 22 | - The authentication API will be used a centralized authentication service for other APIs. 23 | - It will return an access token when the user logs in, which will allow them to make authorized 24 | requests to the API 25 | - The access token will be stored in Redis so that the other APIs can verify that the 26 | access token is valid 27 | 28 | This is the barebones version of the authentication API. There are a lot of logistics that I'm trying 29 | to figure out in terms of account and app authorization. So there will likely be large changes here. 30 | 31 | Since the focus is on Kubernetes, I'm not going to go indepth on the API implementation, but there 32 | are some things worth highlighting. 33 | 34 | ### 1.1 Local and Minikube development 35 | 36 | This app is simple enough that I can test and develop the API locally using: 37 | 38 | ```shell 39 | uvicorn app.main:app --reload --port 8000; 40 | ``` 41 | 42 | We can easily install the dependencies using our Anaconda environment, which will allow 43 | us to run this app without having to push to the Minikube Docker repository every time. 44 | 45 | We also need to access to Postgres and Redis. We could install local versions, but that will get messy, 46 | We could also create a Docker compose configuration, but that that is one more thing to manage. 47 | 48 | A better option is to expose the Postgres and Redis Pods through the NodePort service. This is easy to do, 49 | and all we need to do is update our config.py to use the right host and port. 50 | 51 | With NodePort the service, we can connect to Postgres and Redis using the Minikube IP and the node port, 52 | which is between 30000-32767. 53 | 54 | I haven't tried [Skaffold](https://github.com/GoogleContainerTools/skaffold), but that seems like another option for 55 | developing apps with Kubernetes. 56 | 57 | ### 1.2 Alembic 58 | 59 | For database migrations, I'm using Alembic. I had a few problems getting it set up. 60 | 61 | I needed to edit the `env.py` and make the following adjustments: 62 | 63 | The right path needs to be added so that imports will work. 64 | 65 | ```python 66 | parent_dir = os.path.abspath(os.path.join(os.getcwd())) 67 | sys.path.append(parent_dir) 68 | 69 | from app.config import AUTH_DB_URL # noqa 70 | from app.db import Base # noqa 71 | import app.models # noqa 72 | ``` 73 | 74 | Set the database url to use: 75 | 76 | ```python 77 | config.set_main_option("sqlalchemy.url", AUTH_DB_URL) 78 | ``` 79 | 80 | Add our SQL Alchemy metadata to enable auto generation. You will also need to import the models 81 | otherwise auto generation will not work. I'm not sure why importing `Base.metadata` is not 82 | enough. 83 | 84 | ```python 85 | target_metadata = Base.metadata 86 | ``` 87 | 88 | ### 1.3 authenticator.py 89 | 90 | The authenticator.py file is where the authentication occurs. This differs slightly from the 91 | FastAPI security tutorial since I decided not to use JWTs here. I keep reading a lot of reasons 92 | not to use JWTs. 93 | 94 | Instead I am generating access tokens and then storing the hashed token in Redis with an expiration 95 | time. 96 | 97 | The will be generated when the user logs in with their credentials. Then we they make a request to 98 | an private endpoint, we will check the token in Redis. 99 | 100 | This is only a basic implementation for now. I plan to implement something similar to what's described in 101 | this article: 102 | 103 | - 104 | 105 | We will generate our token using the secrets module. Then hash the token. I considered using passlib here, 106 | but I couldn't figure out how to retrieve passlib hashed token from Redis since we won't have an id or email 107 | to use as a key. 108 | 109 | So I settled on using SHA-256 hash with a salt. This way we will at least stored the hashed tokens in Redis. 110 | 111 | The metadata will be stored using the hash data type, which would allow us to store other information aside 112 | from email, such as APIs that the user has access to or their roles. Granted that information is undecided. 113 | 114 | ```python 115 | def create_access_token(*, account: Account, expires_delta: timedelta = None): 116 | if not expires_delta: 117 | expires_delta = timedelta(minutes=15) 118 | 119 | token = secrets.token_hex() 120 | 121 | hashed_token = hash_token(token, SESSION_DB_TOKEN_KEY) 122 | session_db = get_session_db() 123 | session_db.hmset(hashed_token, {"email": account.email}) 124 | session_db.expire(hashed_token, expires_delta) 125 | 126 | return token 127 | 128 | 129 | def get_account(token: str = Depends(oauth2_scheme), db: DbSession = Depends(get_db)): 130 | session_db = get_session_db() 131 | hashed_token = hash_token(token, SESSION_DB_TOKEN_KEY) 132 | data = session_db.hgetall(hashed_token) 133 | 134 | if data is None: 135 | raise CREDENTIALS_EXCEPTION 136 | 137 | account = crud.get_account_by_email(db, data["email"]) 138 | if account is None: 139 | raise CREDENTIALS_EXCEPTION 140 | return account 141 | ``` 142 | 143 | ### 1.4 CORS 144 | 145 | Since the we're using a centralized authentication API, we will need to enable CORS 146 | to include the domains of other APIs that depend on this API. Here there are no 147 | other domains, since I haven't created any other APIs yet. 148 | 149 | CORS can be added using the middleware pattern. FastAPI provides a CORSMiddleware class. 150 | 151 | ```python 152 | origins = [] 153 | 154 | 155 | app.add_middleware( 156 | CORSMiddleware, 157 | allow_origins=origins, 158 | allow_credentials=True, 159 | allow_methods=["*"], 160 | allow_headers=["*"], 161 | ) 162 | ``` 163 | 164 | ## 2. Running the API on Kubernetes 165 | 166 | Make sure push up the auth-api image as `localhost:5000/auth-api` to the Minikube Docker registry. 167 | 168 | Then run the following commands: 169 | 170 | ```shell 171 | # Store Postgres DB user and password 172 | kubectl apply -f kubernetes/authentication_api/db-secrets.yaml; 173 | 174 | # Configure Redis and Postgres (persistent volumes and claims, services, and deployments) 175 | kubectl apply -f kubernetes/authentication_api/db-config.yaml; 176 | 177 | # Connect to the postres database (auth db) container 178 | kubectl get pods; 179 | kubectl exec -it -- psql -U postgresl 180 | 181 | # For this experiment, we'll just manually create the auth database inside psql; 182 | psql> create database auth; 183 | 184 | # Configure authentication API (service and deployment) 185 | kubectl apply -f kubernetes/authentication_api/app-config.yaml; 186 | 187 | # Update ingress to allow the API to be accessed at auth. 188 | kubectl kubernetes/minikube-ingress.yaml 189 | 190 | # Update hosts file with Minikube IP and auth.books.test 191 | sudo vi /etc/hosts 192 | ``` 193 | 194 | Now the Swaggers Docs for the authentication API should be accessible at http://auth.books.test/docs. 195 | 196 | ## 3. Kubernetes configuration details 197 | 198 | To run the authentication API we will need the following objects: 199 | 200 | - PersistentVolume 201 | - Needed for Postgres data persistence 202 | - PersistentVolumeClaim 203 | - I don't fully understand claims, but they seem similar to services in that they allow access 204 | to a persistent volume. In addition they seem to provide settings for the volume such as access 205 | modes (ReadWriteOnce, ReadOnlyMany, ReadWriteMany) and disk size. 206 | - NodePort Services 207 | - We will need NodePorts for both Redis and Postgres 208 | - Normally we'd use the ClusterIP service, but for development it's nice to be able to access these 209 | data stores directly. 210 | - Deployments 211 | - We will also need the Pods to run the Postgres and Redis containers 212 | - Secrets 213 | - We use this to store the postgres user and password 214 | - I have a lot of questions still about how to use this securely 215 | 216 | ### 3.1 Persistent volume and claim 217 | 218 | The configuration is pretty straightforward for Minikube. I imagine it's a bit more complicated 219 | for a production deployment where we would want to to configure GCE persistent disk. 220 | 221 | The `ReadWriteOnce` mode allows the disk to be connected to only one container. This makes sense 222 | since having multiple writers for one disk seems problematic in terms of concurrency. Also GCE 223 | persistent disks not allow `ReadWriteMany`. 224 | 225 | As previously mentioned I don't fully understand volume claims yet aside from we need them 226 | similar to how Services connect Pods, which make sense to have that extra layer. 227 | 228 | I'm a bit confused why we need to add the specs to the claim? Is it so additional volumes 229 | with the same spec/metadata could be used? 230 | 231 | Which leads me to the question of how persistent volumes are connected to claims. At 232 | first I assumed `volumeName`, but now I think that is not correct. 233 | 234 | ```yml 235 | apiVersion: v1 236 | kind: PersistentVolume 237 | metadata: 238 | name: auth-db-pv 239 | labels: 240 | type: local 241 | spec: 242 | capacity: 243 | storage: 128M 244 | storageClassName: standard 245 | accessModes: 246 | - ReadWriteOnce 247 | hostPath: 248 | path: "/data/auth-db-pv" 249 | 250 | apiVersion: v1 251 | kind: PersistentVolumeClaim 252 | metadata: 253 | name: auth-db-pvc 254 | labels: 255 | type: local 256 | spec: 257 | accessModes: 258 | - ReadWriteOnce 259 | resources: 260 | requests: 261 | storage: 128M 262 | volumeName: auth-db-pv 263 | storageClassName: standard 264 | ``` 265 | 266 | ### 3.2 Postgres configuration 267 | 268 | As mentioned earlier, we use the NodePort service for development purposes since that 269 | allows us to access the postgres database directly. 270 | 271 | For the postgres Deployment Pod, we add a PersistentVolume using the PersistentVolumeClaim. 272 | Then we also use the Secrets object to get postgres user credentials to use as environment 273 | variables in the postgres container. 274 | 275 | ```yaml 276 | apiVersion: v1 277 | kind: Service 278 | metadata: 279 | name: auth-db 280 | spec: 281 | selector: 282 | service: auth-db 283 | type: NodePort 284 | ports: 285 | - port: 5432 286 | nodePort: 30001 287 | 288 | --- 289 | 290 | apiVersion: apps/v1 291 | kind: Deployment 292 | metadata: 293 | name: auth-db 294 | labels: 295 | name: database 296 | spec: 297 | replicas: 1 298 | selector: 299 | matchLabels: 300 | service: auth-db 301 | template: 302 | metadata: 303 | labels: 304 | service: auth-db 305 | spec: 306 | containers: 307 | - name: auth-db 308 | image: postgres:12-alpine 309 | env: 310 | - name: POSTGRES_USER 311 | valueFrom: 312 | secretKeyRef: 313 | name: auth-db-credentials 314 | key: user 315 | - name: POSTGRES_PASSWORD 316 | valueFrom: 317 | secretKeyRef: 318 | name: auth-db-credentials 319 | key: password 320 | volumeMounts: 321 | - name: auth-db-volume-mount 322 | mountPath: /var/lib/postgresql/data 323 | volumes: 324 | - name: auth-db-volume-mount 325 | persistentVolumeClaim: 326 | claimName: auth-db-pvc 327 | restartPolicy: Always 328 | ``` 329 | 330 | ### 3.3 Redis configuration 331 | 332 | There's not much to add about the Redis configuration since it's 333 | pretty basic, which is great. 334 | 335 | ```yaml 336 | apiVersion: v1 337 | kind: Service 338 | metadata: 339 | name: session-db 340 | spec: 341 | selector: 342 | service: session-db 343 | type: NodePort 344 | ports: 345 | - port: 6379 346 | nodePort: 30002 347 | 348 | --- 349 | 350 | apiVersion: apps/v1 351 | kind: Deployment 352 | metadata: 353 | name: session-db 354 | spec: 355 | replicas: 1 356 | selector: 357 | matchLabels: 358 | service: session-db 359 | template: 360 | metadata: 361 | labels: 362 | service: session-db 363 | spec: 364 | containers: 365 | - name: session-db 366 | image: redis:5.0-alpine 367 | ports: 368 | - containerPort: 6379 369 | ``` 370 | 371 | ### 3.4 Secrets configuration 372 | 373 | The secrets are base64 encoded, which isn't secure, so I'm confused on how to add the 374 | secrets configuration to git. 375 | 376 | I have read that secrets can be paired with HashiCorp's Vault, which sounds 377 | interesting but I haven't looked into it yet. 378 | 379 | ```yaml 380 | apiVersion: v1 381 | kind: Secret 382 | metadata: 383 | name: auth-db-credentials 384 | type: Opaque 385 | data: 386 | user: cG9zdGdyZXM= 387 | password: cGFzc3dvcmQ= 388 | ``` 389 | 390 | ### 3.5 Auth API configuration 391 | 392 | Setting up the auth API is straightforward. Similar to the postgres Pod, 393 | we add our postgres credentials as environment variables. 394 | 395 | ```yaml 396 | apiVersion: v1 397 | kind: Service 398 | metadata: 399 | name: auth-api 400 | labels: 401 | service: auth-api 402 | spec: 403 | selector: 404 | app: auth-api 405 | ports: 406 | - port: 80 407 | 408 | --- 409 | 410 | apiVersion: apps/v1 411 | kind: Deployment 412 | metadata: 413 | name: auth-api 414 | spec: 415 | replicas: 1 416 | selector: 417 | matchLabels: 418 | app: auth-api 419 | template: 420 | metadata: 421 | labels: 422 | app: auth-api 423 | spec: 424 | containers: 425 | - name: auth-api 426 | image: localhost:5000/auth-api:latest 427 | env: 428 | - name: AUTH_DB_USER 429 | valueFrom: 430 | secretKeyRef: 431 | name: auth-db-credentials 432 | key: user 433 | - name: AUTH_DB_PASSWORD 434 | valueFrom: 435 | secretKeyRef: 436 | name: auth-db-credentials 437 | key: password 438 | ports: 439 | - containerPort: 80 440 | ``` 441 | 442 | ### 3.6 Ingress configuration 443 | 444 | The ingress configuration is also straightforward. 445 | 446 | ```yaml 447 | apiVersion: extensions/v1beta1 448 | kind: Ingress 449 | metadata: 450 | name: minikube-ingress 451 | annotations: 452 | spec: 453 | rules: 454 | - host: helloworld.test 455 | http: 456 | paths: 457 | - path: / 458 | backend: 459 | serviceName: helloworld 460 | servicePort: 80 461 | - host: auth.books.test 462 | http: 463 | paths: 464 | - path: / 465 | backend: 466 | serviceName: auth-api 467 | servicePort: 80 468 | ``` 469 | 470 | ## 4. Questions 471 | 472 | Overall, setting up this simple authentication API was straightforward. Kubernetes is a lot more 473 | powerful than Docker Compose. But it's nice that we can create a similar set up in Kubernetes 474 | and Minikube with relatively low complexity. 475 | 476 | There are some things I will need to look into more: 477 | 478 | 1. PersistentVolumeClaims 479 | 2. Securely using secrets 480 | 3. How to replace or use Ansible for certain use cases: 481 | - Setting up configuration files for different environments 482 | - Setting up persistent disks (folders and files) 483 | 4. GKE deployment differences 484 | 5. Scaling 485 | 486 | There's a lot more stuff that I have questions about, but those are the ones that come to mind. 487 | -------------------------------------------------------------------------------- /authentication_api/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # timezone to use when rendering the date 11 | # within the migration file as well as the filename. 12 | # string value is passed to dateutil.tz.gettz() 13 | # leave blank for localtime 14 | # timezone = 15 | 16 | # max length of characters to apply to the 17 | # "slug" field 18 | # truncate_slug_length = 40 19 | 20 | # set to 'true' to run the environment during 21 | # the 'revision' command, regardless of autogenerate 22 | # revision_environment = false 23 | 24 | # set to 'true' to allow .pyc and .pyo files without 25 | # a source .py file to be detected as revisions in the 26 | # versions/ directory 27 | # sourceless = false 28 | 29 | # version location specification; this defaults 30 | # to alembic/versions. When using multiple version 31 | # directories, initial revisions must be specified with --version-path 32 | # version_locations = %(here)s/bar %(here)s/bat alembic/versions 33 | 34 | # the output encoding used when revision files 35 | # are written from script.py.mako 36 | # output_encoding = utf-8 37 | 38 | sqlalchemy.url = driver://user:pass@localhost/dbname 39 | 40 | 41 | [post_write_hooks] 42 | # post_write_hooks defines scripts or Python functions that are run 43 | # on newly generated revision scripts. See the documentation for further 44 | # detail and examples 45 | 46 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 47 | # hooks=black 48 | # black.type=console_scripts 49 | # black.entrypoint=black 50 | # black.options=-l 79 51 | 52 | # Logging configuration 53 | [loggers] 54 | keys = root,sqlalchemy,alembic 55 | 56 | [handlers] 57 | keys = console 58 | 59 | [formatters] 60 | keys = generic 61 | 62 | [logger_root] 63 | level = WARN 64 | handlers = console 65 | qualname = 66 | 67 | [logger_sqlalchemy] 68 | level = WARN 69 | handlers = 70 | qualname = sqlalchemy.engine 71 | 72 | [logger_alembic] 73 | level = INFO 74 | handlers = 75 | qualname = alembic 76 | 77 | [handler_console] 78 | class = StreamHandler 79 | args = (sys.stderr,) 80 | level = NOTSET 81 | formatter = generic 82 | 83 | [formatter_generic] 84 | format = %(levelname)-5.5s [%(name)s] %(message)s 85 | datefmt = %H:%M:%S 86 | -------------------------------------------------------------------------------- /authentication_api/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /authentication_api/alembic/env.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from logging.config import fileConfig 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy import pool 8 | 9 | from alembic import context 10 | 11 | parent_dir = os.path.abspath(os.path.join(os.getcwd())) 12 | sys.path.append(parent_dir) 13 | 14 | from app.config import AUTH_DB_URL # noqa 15 | from app.db import Base # noqa 16 | import app.models # noqa 17 | 18 | 19 | # this is the Alembic Config object, which provides 20 | # access to the values within the .ini file in use. 21 | config = context.config 22 | 23 | # Interpret the config file for Python logging. 24 | # This line sets up loggers basically. 25 | fileConfig(config.config_file_name) 26 | config.set_main_option("sqlalchemy.url", AUTH_DB_URL) 27 | 28 | # add your model's MetaData object here 29 | # for 'autogenerate' support 30 | # from myapp import mymodel 31 | # target_metadata = mymodel.Base.metadata 32 | target_metadata = Base.metadata 33 | 34 | # other values from the config, defined by the needs of env.py, 35 | # can be acquired: 36 | # my_important_option = config.get_main_option("my_important_option") 37 | # ... etc. 38 | 39 | 40 | def run_migrations_offline(): 41 | """Run migrations in 'offline' mode. 42 | 43 | This configures the context with just a URL 44 | and not an Engine, though an Engine is acceptable 45 | here as well. By skipping the Engine creation 46 | we don't even need a DBAPI to be available. 47 | 48 | Calls to context.execute() here emit the given string to the 49 | script output. 50 | 51 | """ 52 | url = config.get_main_option("sqlalchemy.url") 53 | context.configure( 54 | url=url, 55 | target_metadata=target_metadata, 56 | literal_binds=True, 57 | dialect_opts={"paramstyle": "named"}, 58 | ) 59 | 60 | with context.begin_transaction(): 61 | context.run_migrations() 62 | 63 | 64 | def run_migrations_online(): 65 | """Run migrations in 'online' mode. 66 | 67 | In this scenario we need to create an Engine 68 | and associate a connection with the context. 69 | 70 | """ 71 | connectable = engine_from_config( 72 | config.get_section(config.config_ini_section), 73 | prefix="sqlalchemy.", 74 | poolclass=pool.NullPool, 75 | ) 76 | 77 | with connectable.connect() as connection: 78 | context.configure( 79 | connection=connection, target_metadata=target_metadata 80 | ) 81 | 82 | with context.begin_transaction(): 83 | context.run_migrations() 84 | 85 | 86 | if context.is_offline_mode(): 87 | run_migrations_offline() 88 | else: 89 | run_migrations_online() 90 | -------------------------------------------------------------------------------- /authentication_api/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /authentication_api/alembic/versions/a9f7b29eaeb9_add_account_table.py: -------------------------------------------------------------------------------- 1 | """Add account table 2 | 3 | Revision ID: a9f7b29eaeb9 4 | Revises: 5 | Create Date: 2020-02-22 19:11:19.579203 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'a9f7b29eaeb9' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('account', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('email', sa.String(), nullable=True), 24 | sa.Column('password', sa.String(), nullable=True), 25 | sa.Column('active', sa.Boolean(), nullable=True), 26 | sa.PrimaryKeyConstraint('id'), 27 | sa.UniqueConstraint('email') 28 | ) 29 | op.create_index(op.f('ix_account_id'), 'account', ['id'], unique=False) 30 | # ### end Alembic commands ### 31 | 32 | 33 | def downgrade(): 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_index(op.f('ix_account_id'), table_name='account') 36 | op.drop_table('account') 37 | # ### end Alembic commands ### 38 | -------------------------------------------------------------------------------- /authentication_api/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richard-to/kubernetes-experiments/60affd763efa08ffdb18f0fc07ba7783f387fe6c/authentication_api/app/__init__.py -------------------------------------------------------------------------------- /authentication_api/app/authenticator.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | import hashlib 3 | import secrets 4 | 5 | from fastapi import Depends, HTTPException 6 | from sqlalchemy.orm import Session as DbSession 7 | from starlette.status import HTTP_401_UNAUTHORIZED 8 | 9 | from . import crud 10 | from .config import SESSION_DB_TOKEN_KEY 11 | from .core import oauth2_scheme, pwd_context 12 | from .db import get_db 13 | from .models import Account 14 | from .session_db import get_session_db 15 | 16 | 17 | CREDENTIALS_EXCEPTION = HTTPException( 18 | status_code=HTTP_401_UNAUTHORIZED, 19 | detail="Could not validate credentials", 20 | headers={"WWW-Authenticate": "Bearer"}, 21 | ) 22 | 23 | LOGIN_EXCEPTION = HTTPException( 24 | status_code=HTTP_401_UNAUTHORIZED, 25 | detail="Incorrect username or password", 26 | headers={"WWW-Authenticate": "Bearer"}, 27 | ) 28 | 29 | 30 | def hash_token(token, secret_key): 31 | return hashlib.sha256(secret_key + token).hexdigest() 32 | 33 | 34 | def authenticate_account(db: DbSession, email: str, password: str): 35 | account = crud.get_account_by_email(db, email) 36 | if not account: 37 | return False 38 | if not pwd_context.verify(password, account.password): 39 | return False 40 | return account 41 | 42 | 43 | def create_access_token(*, account: Account, expires_delta: timedelta = None): 44 | if not expires_delta: 45 | expires_delta = timedelta(minutes=15) 46 | 47 | token = secrets.token_hex() 48 | 49 | hashed_token = hash_token(token, SESSION_DB_TOKEN_KEY) 50 | session_db = get_session_db() 51 | session_db.hmset(hashed_token, {"email": account.email}) 52 | session_db.expire(hashed_token, expires_delta) 53 | 54 | return token 55 | 56 | 57 | def get_account(token: str = Depends(oauth2_scheme), db: DbSession = Depends(get_db)): 58 | session_db = get_session_db() 59 | hashed_token = hash_token(token, SESSION_DB_TOKEN_KEY) 60 | data = session_db.hgetall(hashed_token) 61 | 62 | if data is None: 63 | raise CREDENTIALS_EXCEPTION 64 | 65 | account = crud.get_account_by_email(db, data["email"]) 66 | if account is None: 67 | raise CREDENTIALS_EXCEPTION 68 | return account 69 | -------------------------------------------------------------------------------- /authentication_api/app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Auth DB (Postgres) 4 | AUTH_DB_NAME = "auth" 5 | AUTH_DB_USER = os.environ.get("AUTH_DB_USER") 6 | AUTH_DB_PASSWORD = os.environ.get("AUTH_DB_PASSWORD") 7 | AUTH_DB_URL = f"postgres://{AUTH_DB_USER}:{AUTH_DB_PASSWORD}@auth-db:5432/{AUTH_DB_NAME}" 8 | 9 | # Session DB (Redis) 10 | SESSION_DB_NAME = 0 11 | SESSION_DB_HOST = "session-db" 12 | SESSION_DB_PORT = 6379 13 | SESSION_DB_TOKEN_KEY = "secret" 14 | 15 | # OAuth2 16 | ACCESS_TOKEN_EXPIRE_MINUTES = 10 17 | TOKEN_URL = "http://auth.books.test/accounts/token" 18 | -------------------------------------------------------------------------------- /authentication_api/app/core.py: -------------------------------------------------------------------------------- 1 | from fastapi.security import OAuth2PasswordBearer 2 | from passlib.context import CryptContext 3 | 4 | from .config import TOKEN_URL 5 | 6 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 7 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl=TOKEN_URL) 8 | -------------------------------------------------------------------------------- /authentication_api/app/crud.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | 3 | from . import models as m, schemas as s 4 | from .core import pwd_context 5 | from .db import Base 6 | 7 | 8 | def create_account(db: Session, account: s.AccountCreate): 9 | account_dict = { 10 | **account.dict(), 11 | **{ 12 | "password": pwd_context.hash(account.password), 13 | }, 14 | } 15 | return _create(db, m.Account(**account_dict)) 16 | 17 | 18 | def get_account(db: Session, account_id: int): 19 | return _fetch_by_id(db, m.Account, account_id) 20 | 21 | 22 | def get_account_by_email(db: Session, email: str): 23 | return _fetch_by_id(db, m.Account, email, db_col="email") 24 | 25 | 26 | def _fetch_by_id(db: Session, db_model, db_id: int, db_col: str = "id"): 27 | return db.query(db_model).filter(getattr(db_model, db_col) == db_id).first() 28 | 29 | 30 | def _create(db: Session, db_model: Base): 31 | db.add(db_model) 32 | db.commit() 33 | db.refresh(db_model) 34 | return db_model 35 | -------------------------------------------------------------------------------- /authentication_api/app/db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker 4 | 5 | from .config import AUTH_DB_URL 6 | 7 | engine = create_engine(AUTH_DB_URL) 8 | 9 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 10 | 11 | Base = declarative_base() 12 | 13 | 14 | def get_db(): 15 | try: 16 | db = SessionLocal() 17 | yield db 18 | finally: 19 | db.close() 20 | -------------------------------------------------------------------------------- /authentication_api/app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from starlette.middleware.cors import CORSMiddleware 3 | 4 | from . import models 5 | from .db import engine 6 | from .routers import accounts 7 | 8 | 9 | models.Base.metadata.create_all(bind=engine) 10 | 11 | 12 | app = FastAPI() 13 | 14 | 15 | origins = [] 16 | 17 | 18 | app.add_middleware( 19 | CORSMiddleware, 20 | allow_origins=origins, 21 | allow_credentials=True, 22 | allow_methods=["*"], 23 | allow_headers=["*"], 24 | ) 25 | 26 | 27 | app.include_router( 28 | accounts.router, 29 | prefix="/accounts", 30 | tags=["accounts"], 31 | ) 32 | -------------------------------------------------------------------------------- /authentication_api/app/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, Integer, String 2 | 3 | from .db import Base 4 | 5 | 6 | class Account(Base): 7 | __tablename__ = "account" 8 | 9 | id = Column(Integer, primary_key=True, index=True) 10 | email = Column(String, unique=True) 11 | password = Column(String) 12 | active = Column(Boolean, default=True) 13 | -------------------------------------------------------------------------------- /authentication_api/app/routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richard-to/kubernetes-experiments/60affd763efa08ffdb18f0fc07ba7783f387fe6c/authentication_api/app/routers/__init__.py -------------------------------------------------------------------------------- /authentication_api/app/routers/accounts.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from fastapi import APIRouter, Depends 4 | from fastapi.security import OAuth2PasswordRequestForm 5 | from sqlalchemy.orm import Session 6 | 7 | from .. import authenticator, crud, models as m, schemas as s 8 | from ..db import get_db 9 | from ..config import ACCESS_TOKEN_EXPIRE_MINUTES 10 | 11 | router = APIRouter() 12 | 13 | 14 | @router.post("/", response_model=s.Account) 15 | def create_account(account: s.AccountCreate, db: Session = Depends(get_db)): 16 | return crud.create_account(db, account) 17 | 18 | 19 | @router.get("/me", response_model=s.Account) 20 | def read_account_me(account: m.Account = Depends(authenticator.get_account)): 21 | return account 22 | 23 | 24 | @router.post("/token", response_model=s.Token) 25 | async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): 26 | account = authenticator.authenticate_account(db, form_data.username, form_data.password) 27 | if not account: 28 | raise authenticator.LOGIN_EXCEPTION 29 | access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) 30 | access_token = authenticator.create_access_token( 31 | account=account, 32 | expires_delta=access_token_expires, 33 | ) 34 | return {"access_token": access_token, "token_type": "bearer"} 35 | -------------------------------------------------------------------------------- /authentication_api/app/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class Token(BaseModel): 5 | access_token: str 6 | token_type: str 7 | 8 | 9 | class TokenData(BaseModel): 10 | username: str = None 11 | 12 | 13 | class AccountBase(BaseModel): 14 | email: str 15 | 16 | 17 | class AccountCreate(AccountBase): 18 | password: str 19 | 20 | 21 | class Account(AccountBase): 22 | id: int 23 | active: bool 24 | 25 | class Config: 26 | orm_mode = True 27 | -------------------------------------------------------------------------------- /authentication_api/app/session_db.py: -------------------------------------------------------------------------------- 1 | import redis 2 | 3 | from .config import SESSION_DB_NAME, SESSION_DB_HOST, SESSION_DB_PORT 4 | 5 | 6 | def get_session_db(): 7 | return redis.Redis(host=SESSION_DB_HOST, port=SESSION_DB_PORT, db=SESSION_DB_NAME, decode_responses=True) 8 | -------------------------------------------------------------------------------- /authentication_api/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.4.0 2 | email-validator==1.0.5 3 | fastapi==0.45.0 4 | passlib[bcrypt]==1.7.2 5 | psycopg2-binary==2.8.4 6 | python-multipart==0.0.5 7 | redis==3.4.1 8 | sqlalchemy==1.3.12 9 | uvicorn==0.11.1 10 | -------------------------------------------------------------------------------- /helloworld_api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 2 | 3 | COPY ./app /app 4 | -------------------------------------------------------------------------------- /helloworld_api/README.md: -------------------------------------------------------------------------------- 1 | # Hello World API 2 | 3 | The goal of this exercise is to deploy a FastAPI app using Kubernetes. 4 | 5 | # 1. Hello World app 6 | 7 | This is the example app from the FastAPI documentation. It basically returns `{"Hello": "World"}`. 8 | 9 | ```python 10 | from fastapi import FastAPI 11 | 12 | app = FastAPI() 13 | 14 | 15 | @app.get("/") 16 | def read_root(): 17 | return {"Hello": "World"} 18 | ``` 19 | 20 | # 2. Dockerfile 21 | 22 | The Dockerfile is based off a prebuilt docker image that integrates Uvicorn, Gunicorn, and FastAPI. 23 | 24 | - [Uvicorn](https://www.uvicorn.org/) 25 | - ASGI server (different from WSGI) 26 | - [Gunicorn](https://gunicorn.org/) 27 | - Used to manage Uvicorn processes 28 | - [FastAPI](https://fastapi.tiangolo.com/) 29 | - ASGI web framework 30 | 31 | ```docker 32 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 33 | 34 | COPY ./app /app 35 | ``` 36 | 37 | # 3. Creating the Docker image 38 | 39 | ```shell 40 | docker build -t helloworld .; 41 | docker tag helloworld localhost:5000/helloworld; 42 | ``` 43 | 44 | In order for minikube to use this image, we need to push it to the minkube registry. To do this, 45 | we'll need to redirect the `docker push` command to push to the minikube registry. 46 | 47 | This can be done with this command. So long as this image is running, all `docker push` commands will 48 | push images to the minikube registry. 49 | 50 | ```shell 51 | docker run --rm -it --network=host alpine ash -c "apk add socat && socat TCP-LISTEN:5000,reuseaddr,fork TCP:$(minikube ip):5000"; 52 | ``` 53 | 54 | In another terminal, you now can push the image: 55 | 56 | ``` 57 | docker push localhost:5000/helloworld; 58 | ``` 59 | 60 | ## 3.1 Alternate method to push Docker image to Minikube registry 61 | 62 | After deleting my Minikube cluster and creating new one, I noticed that the above 63 | approach stopped working. I'm not sure what happened, but I did notice it used Hyperkit instead 64 | VirtualBox this time. 65 | 66 | Instead I had to do the following: 67 | 68 | In terminal 1: 69 | 70 | ``` 71 | kubectl port-forward --namespace kube-system [your-registry-pod-name] 5000:5000 72 | ``` 73 | 74 | In terminal 2: 75 | 76 | ``` 77 | docker run --rm -it --network=host alpine ash -c "apk add socat && socat TCPISTEN:5000,reuseaddr,fork TCP:host.docker.internal:5000" 78 | ``` 79 | 80 | In terminal 3: 81 | 82 | ``` 83 | docker push localhost:5000/helloworld; 84 | ``` 85 | 86 | This is the approach for Windows: 87 | 88 | - [https://minikube.sigs.k8s.io/docs/tasks/registry/insecure/](https://minikube.sigs.k8s.io/docs/tasks/registry/insecure/) 89 | 90 | To get the name of your registry pod, you can use this command: `kubectl get pods --namespace kube-system`. 91 | 92 | You can also use the approach used here: 93 | 94 | - [https://minikube.sigs.k8s.io/docs/tasks/docker_registry/](https://minikube.sigs.k8s.io/docs/tasks/docker_registry/) 95 | 96 | The problem with this approach is that image must be tagged with the Minikube IP instead of localhost, which 97 | makes it unconvenient to use with the Kubernetes declarative approach. 98 | 99 | # 4. Deploying the Helloworld API 100 | 101 | ```shell 102 | # Register a service will provide internal network access to the Helloworld API 103 | kubectl create -f ./kubernetes/helloworld/service.yaml; 104 | 105 | # Deploy the Helloworld API to a pod 106 | kubectl create -f ./kubernetes/helloworld/deployment.yaml; 107 | 108 | # Provide external access to the Helloworld API using the ingress method 109 | # With minikube, requests will be routed by Nginx to the appropriate service. 110 | kubectl apply -f ./kubernetes/minikube-ingress.yml; 111 | ``` 112 | 113 | In order to view the Helloworld API, we'll need to update our `/etc/hosts` file. 114 | 115 | You can get the IP of your minikube VM installation using `minikube ip`. My IP is `192.168.99.101`. 116 | 117 | ``` 118 | 192.168.99.101 helloworld.test 119 | ``` 120 | 121 | Now you should be able to view the api at http://helloworld.test 122 | 123 | ## 4.1 Service file 124 | 125 | This file configures a `helloworld` service that provides access to 126 | the `helloworld` app via port `80`. 127 | 128 | The `targetPort` is the port exposed by the `helloworld` app container, which 129 | is also `80`. Since this is the same `port`, we don't have to specify a `targetPort` since 130 | it will default to the value specified by `port`. 131 | 132 | ```yml 133 | apiVersion: v1 134 | kind: Service 135 | metadata: 136 | name: helloworld 137 | labels: 138 | service: helloworld 139 | spec: 140 | selector: 141 | app: helloworld 142 | ports: 143 | - port: 80 144 | ``` 145 | 146 | ## 4.2 Deployment file 147 | 148 | This file deploys a stateless pod to the cluster. Here we specify our 149 | helloworld image to be run with only 1 instance (replica). 150 | 151 | ```yml 152 | apiVersion: apps/v1 153 | kind: Deployment 154 | metadata: 155 | name: helloworld 156 | spec: 157 | replicas: 1 158 | selector: 159 | matchLabels: 160 | app: helloworld 161 | template: 162 | metadata: 163 | labels: 164 | app: helloworld 165 | spec: 166 | containers: 167 | - name: helloworld 168 | image: localhost:5000/helloworld:latest 169 | ports: 170 | - containerPort: 80 171 | ``` 172 | 173 | ## 4.3 Ingress file 174 | 175 | This file configures access to our helloworld app at http://helloworld.test. 176 | 177 | Here we use the `.test` domain since it is a reserved domain. I didn't use `.local` since Bonjour uses that domain. 178 | 179 | Basically we specify a host and link it to the helloworld service and port. Note that servicePort is set to 80. 180 | This is the port to access the helloworld app internally. Externally access to the app is also configured for port 80. In 181 | this case they just happen to be the same. 182 | 183 | ```yml 184 | apiVersion: extensions/v1beta1 185 | kind: Ingress 186 | metadata: 187 | name: minikube-ingress 188 | annotations: 189 | spec: 190 | rules: 191 | - host: helloworld.test 192 | http: 193 | paths: 194 | - path: / 195 | backend: 196 | serviceName: helloworld 197 | servicePort: 80 198 | ``` 199 | -------------------------------------------------------------------------------- /helloworld_api/app/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | 6 | @app.get("/") 7 | def read_root(): 8 | return {"Hello": "World"} 9 | -------------------------------------------------------------------------------- /helloworld_api/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.45.0 2 | uvicorn==0.11.1 3 | -------------------------------------------------------------------------------- /kubernetes/authentication_api/app-config.yaml: -------------------------------------------------------------------------------- 1 | # Configure a ClusterIP service that will give us internal acess to the authentication api. 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: auth-api 6 | labels: 7 | service: auth-api 8 | spec: 9 | selector: 10 | app: auth-api 11 | ports: 12 | - port: 80 13 | 14 | --- 15 | 16 | # Configure a deployment for the authentication api 17 | apiVersion: apps/v1 18 | kind: Deployment 19 | metadata: 20 | name: auth-api 21 | spec: 22 | replicas: 1 23 | selector: 24 | matchLabels: 25 | app: auth-api 26 | template: 27 | metadata: 28 | labels: 29 | app: auth-api 30 | spec: 31 | containers: 32 | - name: auth-api 33 | image: localhost:5000/auth-api:latest 34 | env: 35 | - name: AUTH_DB_USER 36 | valueFrom: 37 | secretKeyRef: 38 | name: auth-db-credentials 39 | key: user 40 | - name: AUTH_DB_PASSWORD 41 | valueFrom: 42 | secretKeyRef: 43 | name: auth-db-credentials 44 | key: password 45 | ports: 46 | - containerPort: 80 47 | -------------------------------------------------------------------------------- /kubernetes/authentication_api/db-config.yaml: -------------------------------------------------------------------------------- 1 | # Configure a persistent volume for postgres since we want to keep this data even if the postgres 2 | # pod is deleted. 3 | apiVersion: v1 4 | kind: PersistentVolume 5 | metadata: 6 | name: auth-db-pv 7 | labels: 8 | type: local 9 | spec: 10 | capacity: 11 | storage: 128M 12 | storageClassName: standard 13 | accessModes: 14 | - ReadWriteOnce 15 | hostPath: 16 | path: "/data/auth-db-pv" 17 | 18 | --- 19 | 20 | # I don't full understand PersistentVolumeClaims, but it seems to describe how a PersistentVolume can be 21 | # used, such as access mode and storage. In addition the PersistentVolumeClaim seems to work like 22 | # Services, which provide access to a Pod. 23 | apiVersion: v1 24 | kind: PersistentVolumeClaim 25 | metadata: 26 | name: auth-db-pvc 27 | labels: 28 | type: local 29 | spec: 30 | accessModes: 31 | - ReadWriteOnce 32 | resources: 33 | requests: 34 | storage: 128M 35 | volumeName: auth-db-pv 36 | storageClassName: standard 37 | 38 | --- 39 | 40 | # Configure a NodePort service that will give us access to the Postgres Pod. 41 | # 42 | # We are using NodePort for development purposes, since this service exposes an external port 43 | # via nodePort, which will give us access to the database. 44 | # 45 | # In production we'd probably user a ClusterIP service which allows internal access only. 46 | apiVersion: v1 47 | kind: Service 48 | metadata: 49 | name: auth-db 50 | spec: 51 | selector: 52 | service: auth-db 53 | type: NodePort 54 | ports: 55 | - port: 5432 56 | nodePort: 30001 57 | 58 | --- 59 | 60 | # Configure a deployment for our postgres database. Uses secrets to retrieve the 61 | # postgres user and password, 62 | apiVersion: apps/v1 63 | kind: Deployment 64 | metadata: 65 | name: auth-db 66 | labels: 67 | name: database 68 | spec: 69 | replicas: 1 70 | selector: 71 | matchLabels: 72 | service: auth-db 73 | template: 74 | metadata: 75 | labels: 76 | service: auth-db 77 | spec: 78 | containers: 79 | - name: auth-db 80 | image: postgres:12-alpine 81 | env: 82 | - name: POSTGRES_USER 83 | valueFrom: 84 | secretKeyRef: 85 | name: auth-db-credentials 86 | key: user 87 | - name: POSTGRES_PASSWORD 88 | valueFrom: 89 | secretKeyRef: 90 | name: auth-db-credentials 91 | key: password 92 | volumeMounts: 93 | - name: auth-db-volume-mount 94 | mountPath: /var/lib/postgresql/data 95 | volumes: 96 | - name: auth-db-volume-mount 97 | persistentVolumeClaim: 98 | claimName: auth-db-pvc 99 | restartPolicy: Always 100 | 101 | --- 102 | 103 | # Configure a NodePort service that will give us access to the Redis Pod, which 104 | # will use to store sessions. 105 | apiVersion: v1 106 | kind: Service 107 | metadata: 108 | name: session-db 109 | spec: 110 | selector: 111 | service: session-db 112 | type: NodePort 113 | ports: 114 | - port: 6379 115 | nodePort: 30002 116 | 117 | --- 118 | 119 | # Configure a deployment for our session db (Redis) 120 | apiVersion: apps/v1 121 | kind: Deployment 122 | metadata: 123 | name: session-db 124 | spec: 125 | replicas: 1 126 | selector: 127 | matchLabels: 128 | service: session-db 129 | template: 130 | metadata: 131 | labels: 132 | service: session-db 133 | spec: 134 | containers: 135 | - name: session-db 136 | image: redis:5.0-alpine 137 | ports: 138 | - containerPort: 6379 139 | -------------------------------------------------------------------------------- /kubernetes/authentication_api/db-secrets.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: auth-db-credentials 5 | type: Opaque 6 | data: 7 | user: cG9zdGdyZXM= 8 | password: cGFzc3dvcmQ= 9 | -------------------------------------------------------------------------------- /kubernetes/helloworld/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: helloworld 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: helloworld 10 | template: 11 | metadata: 12 | labels: 13 | app: helloworld 14 | spec: 15 | containers: 16 | - name: helloworld 17 | image: localhost:5000/helloworld:latest 18 | ports: 19 | - containerPort: 80 20 | -------------------------------------------------------------------------------- /kubernetes/helloworld/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: helloworld 5 | labels: 6 | service: helloworld 7 | spec: 8 | selector: 9 | app: helloworld 10 | ports: 11 | - port: 80 12 | 13 | -------------------------------------------------------------------------------- /kubernetes/kube-dash/admin-user.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: admin-user 5 | namespace: kubernetes-dashboard 6 | -------------------------------------------------------------------------------- /kubernetes/kube-dash/cluster-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: admin-user 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: cluster-admin 9 | subjects: 10 | - kind: ServiceAccount 11 | name: admin-user 12 | namespace: kubernetes-dashboard 13 | -------------------------------------------------------------------------------- /kubernetes/minikube-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: minikube-ingress 5 | annotations: 6 | spec: 7 | rules: 8 | - host: helloworld.test 9 | http: 10 | paths: 11 | - path: / 12 | backend: 13 | serviceName: helloworld 14 | servicePort: 80 15 | - host: auth.books.test 16 | http: 17 | paths: 18 | - path: / 19 | backend: 20 | serviceName: auth-api 21 | servicePort: 80 22 | --------------------------------------------------------------------------------