├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── app.py ├── kube-hello-change.yaml ├── requirements.txt └── test_app.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.8-slim-buster 2 | 3 | # Working Directory 4 | WORKDIR /app 5 | 6 | # Copy source code to working directory 7 | COPY . app.py /app/ 8 | 9 | # Install packages from requirements.txt 10 | # hadolint ignore=DL3013 11 | RUN pip install --no-cache-dir --upgrade pip &&\ 12 | pip install --no-cache-dir --trusted-host pypi.python.org -r requirements.txt 13 | 14 | EXPOSE 8080 15 | 16 | ENTRYPOINT [ "python" ] 17 | 18 | CMD [ "app.py" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | pip install --upgrade pip &&\ 3 | pip install -r requirements.txt 4 | 5 | lint: 6 | docker run --rm -i hadolint/hadolint < Dockerfile 7 | pylint --disable=R,C,W1203,W0702 app.py 8 | 9 | test: 10 | python -m pytest -vv --cov=app test_app.py 11 | 12 | build: 13 | docker build -t flask-change:latest . 14 | 15 | run: 16 | docker run -p 8080:8080 flask-change 17 | 18 | invoke: 19 | curl http://127.0.0.1:8080/change/1/34 20 | 21 | run-kube: 22 | kubectl apply -f kube-hello-change.yaml 23 | 24 | all: install lint test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 🎓 Pragmatic AI Labs | Join 1M+ ML Engineers 2 | 3 | ### 🔥 Hot Course Offers: 4 | * 🤖 [Master GenAI Engineering](https://ds500.paiml.com/learn/course/0bbb5/) - Build Production AI Systems 5 | * 🦀 [Learn Professional Rust](https://ds500.paiml.com/learn/course/g6u1k/) - Industry-Grade Development 6 | * 📊 [AWS AI & Analytics](https://ds500.paiml.com/learn/course/31si1/) - Scale Your ML in Cloud 7 | * ⚡ [Production GenAI on AWS](https://ds500.paiml.com/learn/course/ehks1/) - Deploy at Enterprise Scale 8 | * 🛠️ [Rust DevOps Mastery](https://ds500.paiml.com/learn/course/ex8eu/) - Automate Everything 9 | 10 | ### 🚀 Level Up Your Career: 11 | * 💼 [Production ML Program](https://paiml.com) - Complete MLOps & Cloud Mastery 12 | * 🎯 [Start Learning Now](https://ds500.paiml.com) - Fast-Track Your ML Career 13 | * 🏢 Trusted by Fortune 500 Teams 14 | 15 | Learn end-to-end ML engineering from industry veterans at [PAIML.COM](https://paiml.com) 16 | 17 | # Kubernetes Hello World 18 | A Kubernetes Hello World Project for Python Flask. This project uses [a simple Flask app that returns correct change](https://github.com/noahgift/flask-change-microservice) as the base project and converts it to Kubernetes. 19 | ![kubernetes-load-balanced-cluster](https://user-images.githubusercontent.com/58792/111511557-3f45a280-8725-11eb-8e4a-5f5ef787796d.png) 20 | 21 | This recipe is in the book Practical MLOps. 22 | 23 | ![9781098103002](https://user-images.githubusercontent.com/58792/111000927-eb1b7680-8350-11eb-8e24-d41064590fc1.jpeg) 24 | 25 | 26 | ## Assets in Repo 27 | 28 | * `Makefile`: [Builds project](https://github.com/noahgift/kubernetes-hello-world-python-flask/blob/main/Makefile) 29 | * `Dockerfile`: [Container configuration](https://github.com/noahgift/kubernetes-hello-world-python-flask/blob/main/Dockerfile) 30 | * `app.py`: [Flask app](https://github.com/noahgift/kubernetes-hello-world-python-flask/blob/main/app.py) 31 | * `kube-hello-change.yaml`: [Kubernetes YAML Config](https://github.com/noahgift/kubernetes-hello-world-python-flask/blob/main/kube-hello-change.yaml) 32 | 33 | ## Get Started 34 | 35 | * Create Python virtual environment `python3 -m venv ~/.kube-hello && source ~/.kube-hello/bin/activate` 36 | * Run `make all` to install python libraries, lint project, including `Dockerfile` and run tests 37 | 38 | ## Build and Run Docker Container 39 | 40 | * Install [Docker Desktop](https://www.docker.com/products/docker-desktop) 41 | 42 | * To build the image locally do the following. 43 | 44 | `docker build -t flask-change:latest .` or run `make build` which has the same command. 45 | 46 | * To verify container run `docker image ls` 47 | 48 | * To run do the following: `docker run -p 8080:8080 flask-change` or run `make run` which has the same command 49 | 50 | * In a separate terminal invoke the web service via curl, or run `make invoke` which has the same command 51 | 52 | `curl http://127.0.0.1:8080/change/1/34` 53 | 54 | ```bash 55 | [ 56 | { 57 | "5": "quarters" 58 | }, 59 | { 60 | "1": "nickels" 61 | }, 62 | { 63 | "4": "pennies" 64 | } 65 | ] 66 | ``` 67 | 68 | * Stop the running docker container by using `control-c` command 69 | 70 | ## Running Kubernetes Locally 71 | 72 | * Verify Kubernetes is working via docker-desktop context 73 | 74 | ```bash 75 | (.kube-hello) ➜ kubernetes-hello-world-python-flask git:(main) kubectl get nodes 76 | NAME STATUS ROLES AGE VERSION 77 | docker-desktop Ready master 30d v1.19.3 78 | ``` 79 | 80 | * Run the application in Kubernetes using the following command which tells Kubernetes to setup the load balanced service and run it: 81 | 82 | `kubectl apply -f kube-hello-change.yaml` or run `make run-kube` which has the same command 83 | 84 | You can see from the config file that a load-balancer along with three nodes is the configured application. 85 | 86 | ```yaml 87 | apiVersion: v1 88 | kind: Service 89 | metadata: 90 | name: hello-flask-change-service 91 | spec: 92 | selector: 93 | app: hello-python 94 | ports: 95 | - protocol: "TCP" 96 | port: 8080 97 | targetPort: 8080 98 | type: LoadBalancer 99 | 100 | --- 101 | apiVersion: apps/v1 102 | kind: Deployment 103 | metadata: 104 | name: hello-python 105 | spec: 106 | selector: 107 | matchLabels: 108 | app: hello-python 109 | replicas: 3 110 | template: 111 | metadata: 112 | labels: 113 | app: hello-python 114 | spec: 115 | containers: 116 | - name: flask-change 117 | image: flask-change:latest 118 | imagePullPolicy: Never 119 | ports: 120 | - containerPort: 8080 121 | ``` 122 | 123 | * Verify the container is running 124 | 125 | `kubectl get pods` 126 | 127 | Here is the output: 128 | 129 | ```bash 130 | NAME READY STATUS RESTARTS AGE 131 | flask-change-7b7d7f467b-26htf 1/1 Running 0 8s 132 | flask-change-7b7d7f467b-fh6df 1/1 Running 0 7s 133 | flask-change-7b7d7f467b-fpsxr 1/1 Running 0 6s 134 | ``` 135 | 136 | * Describe the load balanced service: 137 | 138 | `kubectl describe services hello-python-service` 139 | 140 | You should see output similar to this: 141 | 142 | ```bash 143 | Name: hello-python-service 144 | Namespace: default 145 | Labels: 146 | Annotations: 147 | Selector: app=hello-python 148 | Type: LoadBalancer 149 | IP Families: 150 | IP: 10.101.140.123 151 | IPs: 152 | LoadBalancer Ingress: localhost 153 | Port: 8080/TCP 154 | TargetPort: 8080/TCP 155 | NodePort: 30301/TCP 156 | Endpoints: 10.1.0.27:8080,10.1.0.28:8080,10.1.0.29:8080 157 | Session Affinity: None 158 | External Traffic Policy: Cluster 159 | Events: 160 | ``` 161 | 162 | Invoke the endpoint to curl it: 163 | 164 | `make invoke` 165 | 166 | ```bash 167 | curl http://127.0.0.1:8080/change/1/34 168 | [ 169 | { 170 | "5": "quarters" 171 | }, 172 | { 173 | "1": "nickels" 174 | }, 175 | { 176 | "4": "pennies" 177 | } 178 | ] 179 | ``` 180 | 181 | To cleanup the deployment do the following: `kubectl delete deployment hello-python` 182 | 183 | ## References 184 | 185 | * Azure [Kubernetes deployment strategy](https://azure.microsoft.com/en-us/overview/kubernetes-deployment-strategy/) 186 | * Service [Cluster Config](https://kubernetes.io/docs/tasks/access-application-cluster/service-access-application-cluster/) YAML file 187 | * [Kubernetes.io Hello World](https://kubernetes.io/blog/2019/07/23/get-started-with-kubernetes-using-python/) 188 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask import jsonify 3 | app = Flask(__name__) 4 | 5 | def change(amount): 6 | # calculate the resultant change and store the result (res) 7 | res = [] 8 | coins = [1,5,10,25] # value of pennies, nickels, dimes, quarters 9 | coin_lookup = {25: "quarters", 10: "dimes", 5: "nickels", 1: "pennies"} 10 | 11 | # divide the amount*100 (the amount in cents) by a coin value 12 | # record the number of coins that evenly divide and the remainder 13 | coin = coins.pop() 14 | num, rem = divmod(int(amount*100), coin) 15 | # append the coin type and number of coins that had no remainder 16 | res.append({num:coin_lookup[coin]}) 17 | 18 | # while there is still some remainder, continue adding coins to the result 19 | while rem > 0: 20 | coin = coins.pop() 21 | num, rem = divmod(rem, coin) 22 | if num: 23 | if coin in coin_lookup: 24 | res.append({num:coin_lookup[coin]}) 25 | return res 26 | 27 | 28 | @app.route('/') 29 | def hello(): 30 | """Return a friendly HTTP greeting.""" 31 | print("I am inside hello world") 32 | return 'Hello World! I can make change at route: /change' 33 | 34 | @app.route('/change//') 35 | def changeroute(dollar, cents): 36 | print(f"Make Change for {dollar}.{cents}") 37 | amount = f"{dollar}.{cents}" 38 | result = change(float(amount)) 39 | return jsonify(result) 40 | 41 | 42 | if __name__ == '__main__': 43 | app.run(host='0.0.0.0', port=8080, debug=True) -------------------------------------------------------------------------------- /kube-hello-change.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: hello-flask-change-service 5 | spec: 6 | selector: 7 | app: hello-python 8 | ports: 9 | - protocol: "TCP" 10 | port: 8080 11 | targetPort: 8080 12 | type: LoadBalancer 13 | 14 | --- 15 | apiVersion: apps/v1 16 | kind: Deployment 17 | metadata: 18 | name: hello-python 19 | spec: 20 | selector: 21 | matchLabels: 22 | app: hello-python 23 | replicas: 3 24 | template: 25 | metadata: 26 | labels: 27 | app: hello-python 28 | spec: 29 | containers: 30 | - name: flask-change 31 | image: flask-change:latest 32 | imagePullPolicy: Never 33 | ports: 34 | - containerPort: 8080 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.1.2 2 | pytest==6.2.2 3 | pylint==2.6.2 4 | pytest-cov==2.11.1 -------------------------------------------------------------------------------- /test_app.py: -------------------------------------------------------------------------------- 1 | from app import change 2 | 3 | 4 | def test_change(): 5 | assert [{5: 'quarters'}, {1: 'nickels'}, {4: 'pennies'}] == change(1.34) --------------------------------------------------------------------------------