├── .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 |
--------------------------------------------------------------------------------