Published: Apr 25, 2024 by Isaac Johnson
I recently came across a fun TWC containerized open-source app that simulates The Weather Channel from 80s and 90s using real forecast data from api.weather.gov. They had removed the audio to avoid copyright issues, but still it is an amazing rendering. Once cloned, I show how to run locally then created a manifest to run in Kubernetes.
This inspired me to find other Weather apps in Github. I checked out a few options such as a dotnet written app but most of them didn’t work. However, I did find a python Flask app that also uses OpenWeatherMap API. I updated it to show temps in Fahrenheit as well as Celsius. Lastly, I updated to run in Kubernetes using a fresh API Key (so we aren’t using the original authors).
Let’s check them out (with running examples below).
WeatherStation4000 - local installation
Let’s first clone the repo and run locally
builder@LuiGi:~/Workspaces$ git clone https://github.com/netbymatt/ws4kp
Cloning into 'ws4kp'...
remote: Enumerating objects: 3472, done.
remote: Counting objects: 100% (668/668), done.
remote: Compressing objects: 100% (96/96), done.
remote: Total 3472 (delta 601), reused 594 (delta 572), pack-reused 2804
Receiving objects: 100% (3472/3472), 31.85 MiB | 9.49 MiB/s, done.
Resolving deltas: 100% (2123/2123), done.
builder@LuiGi:~/Workspaces$ cd ws4kp/
I can then install and run
builder@LuiGi:~/Workspaces/ws4kp$ nvm install 16.20.2
Downloading and installing node v16.20.2...
Downloading https://nodejs.org/dist/v16.20.2/node-v16.20.2-linux-x64.tar.xz...
################################################################################################################# 100.0%
Computing checksum with sha256sum
Checksums matched!
Now using node v16.20.2 (npm v8.19.4)
builder@LuiGi:~/Workspaces/ws4kp$ npm install
npm WARN deprecated urix@0.1.0: Please see https://github.com/lydell/urix#deprecated
npm WARN deprecated source-map-url@0.4.1: See https://github.com/lydell/source-map-url#deprecated
npm WARN deprecated source-map-resolve@0.5.3: See https://github.com/lydell/source-map-resolve#deprecated
npm WARN deprecated resolve-url@0.2.1: https://github.com/lydell/resolve-url#deprecated
npm WARN deprecated querystring@0.2.0: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
npm WARN deprecated chokidar@2.1.8: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies
added 818 packages, and audited 819 packages in 13s
105 packages are looking for funding
run `npm fund` for details
8 vulnerabilities (1 low, 1 moderate, 6 high)
To address issues that do not require attention, run:
npm audit fix
To address all issues (including breaking changes), run:
npm audit fix --force
Run `npm audit` for details.
npm notice
npm notice New major version of npm available! 8.19.4 -> 10.5.2
npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.5.2
npm notice Run npm install -g npm@10.5.2 to update!
npm notice
builder@LuiGi:~/Workspaces/ws4kp$ node index.js
It’s fun to watch it run:
I wanted it more realistic, so I cropped and saved the original TWC logo to the same size as Logo3.png:
I can also switch the Logo3.png to make it show TWC logo
builder@LuiGi:~/Workspaces/ws4kp$ node index.js
Server listening on port 8080
Docker
He does have a shared container image we could just use:
$ docker run -p 8080:8080 ghcr.io/netbymatt/ws4kp
However, I think it’s more interesting to build and run locally using the Dockerfile
builder@LuiGi:~/Workspaces/ws4kp$ docker build -t myweather .
[+] Building 32.0s (12/12) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 163B 0.0s
=> [internal] load metadata for docker.io/library/node:18-alpine 1.3s
=> [auth] library/node:pull token for registry-1.docker.io 0.0s
=> [internal] load .dockerignore 0.1s
=> => transferring context: 65B 0.0s
=> [1/6] FROM docker.io/library/node:18-alpine@sha256:80338ff3fc4e989c1d5264a23223cec1c6014e812e584e825e78d1a98d893381 7.6s
=> => resolve docker.io/library/node:18-alpine@sha256:80338ff3fc4e989c1d5264a23223cec1c6014e812e584e825e78d1a98d893381 0.1s
=> => sha256:2ace4c9b5d2891f3a15c288befd59a1b763a78173276bf5911e151c3f2f94515 450B / 450B 0.2s
=> => sha256:80338ff3fc4e989c1d5264a23223cec1c6014e812e584e825e78d1a98d893381 1.43kB / 1.43kB 0.0s
=> => sha256:05583a00b42e064e4f0bb05fb2392133aa2a8728f50d16bd95ccc705ddf96bc9 1.16kB / 1.16kB 0.0s
=> => sha256:b430f596d217b796bc51027d4af7766c6c6250e8c630c6bd2282b4ee05508710 7.18kB / 7.18kB 0.0s
=> => sha256:45a0166cf96b2a4f328191f78f73e68e0e340450a962ff6fc34013111c014d26 39.82MB / 39.82MB 5.2s
=> => sha256:1fed6d4dd8cab8acc89c68e39cb78cf930ed7e1ad8767a1424b8b4bd147bdb37 2.34MB / 2.34MB 1.2s
=> => extracting sha256:45a0166cf96b2a4f328191f78f73e68e0e340450a962ff6fc34013111c014d26 1.6s
=> => extracting sha256:1fed6d4dd8cab8acc89c68e39cb78cf930ed7e1ad8767a1424b8b4bd147bdb37 0.1s
=> => extracting sha256:2ace4c9b5d2891f3a15c288befd59a1b763a78173276bf5911e151c3f2f94515 0.0s
=> [internal] load build context 3.5s
=> => transferring context: 218.60MB 3.3s
=> [2/6] WORKDIR /app 0.4s
=> [3/6] COPY package.json . 0.1s
=> [4/6] COPY package-lock.json . 0.1s
=> [5/6] RUN npm ci 17.6s
=> [6/6] COPY . . 2.6s
=> exporting to image 1.9s
=> => exporting layers 1.9s
=> => writing image sha256:8a3ae5ca39b4eff0ee13aa826f978e56613a7e9255a6fcb0e624e7635b6eaa5f 0.0s
=> => naming to docker.io/library/myweather 0.0s
What's Next?
View a summary of image vulnerabilities and recommendations → docker scout quickview
Which I can now run
builder@LuiGi:~/Workspaces/ws4kp$ docker run -p 8080:8080 myweather
Server listening on port 8080
Kubernetes
Let’s start with a basic manifest that includes a Deployment and Service
$ cat manifest.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ws4kp-deployment
spec:
replicas: 1
selector:
matchLabels:
app: ws4kp
template:
metadata:
labels:
app: ws4kp
spec:
containers:
- name: ws4kp-container
image: ghcr.io/netbymatt/ws4kp
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: ws4kp-service
spec:
selector:
app: ws4kp
ports:
- protocol: TCP
port: 8080
targetPort: 8080
type: ClusterIP
Then deploy it
$ kubectl apply -f ./manifest.yaml
deployment.apps/ws4kp-deployment created
service/ws4kp-service created
I can now port-forward
$ kubectl port-forward svc/ws4kp-service 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
Let’s now create a DNS A Record
$ az account set --subscription "Pay-As-You-Go" && az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n weather
{
"ARecords": [
{
"ipv4Address": "75.73.224.240"
}
],
"TTL": 3600,
"etag": "9dbcc62c-2c10-440f-9388-52e75419bf9e",
"fqdn": "weather.tpk.pw.",
"id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/weather",
"name": "weather",
"provisioningState": "Succeeded",
"resourceGroup": "idjdnsrg",
"targetResource": {},
"trafficManagementProfile": {},
"type": "Microsoft.Network/dnszones/A"
}
Now let’s create an ingress
$ cat weather.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: azuredns-tpkpw
ingress.kubernetes.io/ssl-redirect: "true"
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.org/websocket-services: ws4kp-service
name: weather-ingress
spec:
rules:
- host: weather.tpk.pw
http:
paths:
- backend:
service:
name: ws4kp-service
port:
number: 8080
path: /
pathType: Prefix
tls:
- hosts:
- weather.tpk.pw
secretName: weather-tls
$ kubectl apply -f ./weather.yaml
ingress.networking.k8s.io/weather-ingress created
Which now serves up on https://weather.tpk.pw
Updates
I have noted the API that weather.gov provides does tend to go down. It’s basically a free service and thus has some downtime.
In checking the day before I posted this, I noted:
After debugging my container and the URLs it used, I saw the cause was actually on the API side
and after a few refreshes with 502, I managed to get a dumping Fedora box
If you see a similar error, it could just be that the Weather API is down for the moment.
Flask Weather App
I was searching for some options and came across this simple Flask based weather app.
It seems to be just someone named Yash’s project from a few years back.
I pulled and ran the built container which was fine, but rather simple
$ docker pull yash301998/flask-weather-app:latest
latest: Pulling from yash301998/flask-weather-app
b52ebda398ed: Pull complete
5ea3b77facf9: Pull complete
3cf88772a539: Pull complete
58e7c3cd0452: Pull complete
8b23ff4b1264: Pull complete
a071e9efeb52: Pull complete
099b221529f4: Pull complete
e503b851183f: Pull complete
618ab0081528: Pull complete
Digest: sha256:f32c613c9842ac41c5d5e1f9c38d099f7a85c22ac649e313df3a6a46c8214af1
Status: Downloaded newer image for yash301998/flask-weather-app:latest
docker.io/yash301998/flask-weather-app:latest
What's Next?
View a summary of image vulnerabilities and recommendations → docker scout quickview yash301998/flask-weather-app:latest
$ docker run -d -p 5000:5000 yash301998/flask-weather-app
267508e5c9d5b1f5174878726ac88eac6777098d7171e34df5e9922847aaf032
Updates
I liked it, but I really wanted to see temps in F not C, so I updated the code (as you can see int this PR.
Then built and ran
^Cbuilder@LuiGi:~/Workspaces/Weather-App$ docker build -t pyweatherapp .
[+] Building 2.4s (10/10) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 243B 0.0s
=> [internal] load metadata for docker.io/library/python:3.8.16-slim-buster 2.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/5] FROM docker.io/library/python:3.8.16-slim-buster@sha256:eb48d017c5e117d9fbcbe991b4dbc61339734e01578d8d350b38fe2033a67199 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 8.35kB 0.0s
=> CACHED [2/5] WORKDIR /python-docker 0.0s
=> CACHED [3/5] COPY requirements.txt requirements.txt 0.0s
=> CACHED [4/5] RUN pip3 install -r requirements.txt 0.0s
=> [5/5] COPY . . 0.1s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:4458b3584122bbaf2e343e5412947fb1dd3567aacf7f8e1ca68f21af8f1b23f5 0.0s
=> => naming to docker.io/library/pyweatherapp 0.0s
What's Next?
View a summary of image vulnerabilities and recommendations → docker scout quickview
builder@LuiGi:~/Workspaces/Weather-App$ docker run -p 5000:5000 pyweatherapp
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://172.17.0.2:5000
Press CTRL+C to quit
I created a fork to https://github.com/idjohnson/Weather-App
Kubernetes YAML
let’s look to serve this now with Kubernetes.
I’ll create an A Record in Azure DNS
$ az account set --subscription "Pay-As-You-Go" && az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n pyweather
{
"ARecords": [
{
"ipv4Address": "75.73.224.240"
}
],
"TTL": 3600,
"etag": "9fee1283-cabf-4e03-bb32-d20457d0e500",
"fqdn": "pyweather.tpk.pw.",
"id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/pyweather",
"name": "pyweather",
"provisioningState": "Succeeded",
"resourceGroup": "idjdnsrg",
"targetResource": {},
"type": "Microsoft.Network/dnszones/A"
}
Our deployment YAML should include a deployment block
apiVersion: apps/v1
kind: Deployment
metadata:
name: pyweatherapp-deployment
spec:
replicas: 1
selector:
matchLabels:
app: pyweatherapp
template:
metadata:
labels:
app: pyweatherapp
spec:
containers:
- name: pyweatherapp
image: idjohnson/pyweatherapp:latest
ports:
- containerPort: 5000
The service
apiVersion: v1
kind: Service
metadata:
name: pyweatherapp-service
spec:
selector:
app: pyweatherapp
ports:
- protocol: TCP
port: 80
targetPort: 5000
Lastly, we need an ingress as well
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: pyweatherapp-ingress
annotations:
cert-manager.io/cluster-issuer: azuredns-tpkpw
ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
kubernetes.io/tls-acme: "true"
kubernetes.io/ingress.class: nginx
spec:
tls:
- hosts:
- pyweather.tpk.pw
secretName: pyweather-tls
rules:
- host: pyweather.tpk.pw
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: pyweatherapp-service
port:
number: 80
I can now go ahead and launch it
$ kubectl apply -f ./manifest.yaml
deployment.apps/pyweatherapp-deployment created
service/pyweatherapp-service created
ingress.networking.k8s.io/pyweatherapp-ingress created
Once I see the cert is satisified
$ kubectl get cert | tail -n 1
pyweather-tls True pyweather-tls 7m37s
We now have a basic weather app running
There is a downside to this image, however.
I’m still using the API_KEY the original author left in their code:
$ cat app.py
from flask import Flask, request, render_template
from decimal import Decimal, ROUND_HALF_UP
import requests
import datetime as dt
app = Flask(__name__)
# Your API Key
API_KEY = '5f1348e8f6aa675ecc09c5bafd5cf2d3'
def kelvin_to_fahrenheit(kelvin):
fahrenheit = Decimal(kelvin * 1.8 - 459.67)
return fahrenheit.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
def kelvin_to_celsius(kelvin):
celsius = Decimal(kelvin - 273.15)
return celsius.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
@app.route('/', methods=["POST", "GET"])
def search_city():
if request.method == "POST":
city = request.form.get("city")
if city == "":
return render_template("error.html")
if len(city) <= 1:
return render_template("error.html")
url = f'http://api.openweathermap.org/data/2.5/weather?q={city}&APPID={API_KEY}'
response = requests.get(url).json()
country = response["sys"]["country"]
current_date = response["dt"]
m = dt.datetime.fromtimestamp(int(current_date)).strftime('%d-%m-%Y %H:%M:%S ')
new_city = city
temperature = kelvin_to_fahrenheit(response["main"]["temp"])
tempcelsius = kelvin_to_celsius(response["main"]["temp"])
description = response["weather"][0]["description"]
icon = response["weather"][0]["icon"]
humidity = response["main"]["humidity"]
return render_template("weather_new.html", city=new_city, temperature=temperature, tempcelsius=tempcelsius, description=description,
icon=icon, humidity=humidity, country=country, m=m)
return render_template("weather_new.html")
if __name__ == '__main__':
app.run()
It feels a bit dishonest to use their API key even if it’s still working.
I’ll change the app.py to fetch from an environment variable
$ git diff app.py
diff --git a/app.py b/app.py
index 5b8bc3a..520f908 100644
--- a/app.py
+++ b/app.py
@@ -1,12 +1,12 @@
from flask import Flask, request, render_template
from decimal import Decimal, ROUND_HALF_UP
+import os
import requests
import datetime as dt
app = Flask(__name__)
-# Your API Key
-API_KEY = '5f1348e8f6aa675ecc09c5bafd5cf2d3'
+API_KEY = os.environ.get('API_KEY')
def kelvin_to_fahrenheit(kelvin):
fahrenheit = Decimal(kelvin * 1.8 - 459.67)
I can test locally by first installing the requirements
builder@DESKTOP-QADGF36:~/Workspaces/Weather-App$ pip3 install -r requirements.txt
Collecting pytest~=7.2.0 (from -r requirements.txt (line 1))
Downloading pytest-7.2.2-py3-none-any.whl.metadata (7.8 kB)
Collecting requests~=2.28.1 (from -r requirements.txt (line 2))
Downloading requests-2.28.2-py3-none-any.whl.metadata (4.6 kB)
Collecting selenium~=4.7.2 (from -r requirements.txt (line 3))
Downloading selenium-4.7.2-py3-none-any.whl.metadata (7.1 kB)
Collecting Flask~=2.2.2 (from -r requirements.txt (line 4))
Downloading Flask-2.2.5-py3-none-any.whl.metadata (3.9 kB)
Collecting attrs>=19.2.0 (from pytest~=7.2.0->-r requirements.txt (line 1))
Downloading attrs-23.2.0-py3-none-any.whl.metadata (9.5 kB)
Collecting iniconfig (from pytest~=7.2.0->-r requirements.txt (line 1))
Downloading iniconfig-2.0.0-py3-none-any.whl.metadata (2.6 kB)
Collecting packaging (from pytest~=7.2.0->-r requirements.txt (line 1))
Downloading packaging-24.0-py3-none-any.whl.metadata (3.2 kB)
Collecting pluggy<2.0,>=0.12 (from pytest~=7.2.0->-r requirements.txt (line 1))
Downloading pluggy-1.4.0-py3-none-any.whl.metadata (4.3 kB)
Collecting charset-normalizer<4,>=2 (from requests~=2.28.1->-r requirements.txt (line 2))
Downloading charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (33 kB)
Collecting idna<4,>=2.5 (from requests~=2.28.1->-r requirements.txt (line 2))
Downloading idna-3.7-py3-none-any.whl.metadata (9.9 kB)
Collecting urllib3<1.27,>=1.21.1 (from requests~=2.28.1->-r requirements.txt (line 2))
Downloading urllib3-1.26.18-py2.py3-none-any.whl.metadata (48 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 48.9/48.9 kB 3.2 MB/s eta 0:00:00
Collecting certifi>=2017.4.17 (from requests~=2.28.1->-r requirements.txt (line 2))
Downloading certifi-2024.2.2-py3-none-any.whl.metadata (2.2 kB)
Collecting trio~=0.17 (from selenium~=4.7.2->-r requirements.txt (line 3))
Downloading trio-0.25.0-py3-none-any.whl.metadata (8.7 kB)
Collecting trio-websocket~=0.9 (from selenium~=4.7.2->-r requirements.txt (line 3))
Downloading trio_websocket-0.11.1-py3-none-any.whl.metadata (4.7 kB)
Collecting Werkzeug>=2.2.2 (from Flask~=2.2.2->-r requirements.txt (line 4))
Downloading werkzeug-3.0.2-py3-none-any.whl.metadata (4.1 kB)
Collecting Jinja2>=3.0 (from Flask~=2.2.2->-r requirements.txt (line 4))
Downloading Jinja2-3.1.3-py3-none-any.whl.metadata (3.3 kB)
Collecting itsdangerous>=2.0 (from Flask~=2.2.2->-r requirements.txt (line 4))
Downloading itsdangerous-2.2.0-py3-none-any.whl.metadata (1.9 kB)
Collecting click>=8.0 (from Flask~=2.2.2->-r requirements.txt (line 4))
Downloading click-8.1.7-py3-none-any.whl.metadata (3.0 kB)
Collecting MarkupSafe>=2.0 (from Jinja2>=3.0->Flask~=2.2.2->-r requirements.txt (line 4))
Downloading MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (3.0 kB)
Collecting sortedcontainers (from trio~=0.17->selenium~=4.7.2->-r requirements.txt (line 3))
Downloading sortedcontainers-2.4.0-py2.py3-none-any.whl.metadata (10 kB)
Collecting outcome (from trio~=0.17->selenium~=4.7.2->-r requirements.txt (line 3))
Downloading outcome-1.3.0.post0-py2.py3-none-any.whl.metadata (2.6 kB)
Collecting sniffio>=1.3.0 (from trio~=0.17->selenium~=4.7.2->-r requirements.txt (line 3))
Downloading sniffio-1.3.1-py3-none-any.whl.metadata (3.9 kB)
Collecting wsproto>=0.14 (from trio-websocket~=0.9->selenium~=4.7.2->-r requirements.txt (line 3))
Downloading wsproto-1.2.0-py3-none-any.whl.metadata (5.6 kB)
Collecting PySocks!=1.5.7,<2.0,>=1.5.6 (from urllib3[socks]~=1.26->selenium~=4.7.2->-r requirements.txt (line 3))
Downloading PySocks-1.7.1-py3-none-any.whl.metadata (13 kB)
Collecting h11<1,>=0.9.0 (from wsproto>=0.14->trio-websocket~=0.9->selenium~=4.7.2->-r requirements.txt (line 3))
Downloading h11-0.14.0-py3-none-any.whl.metadata (8.2 kB)
Downloading pytest-7.2.2-py3-none-any.whl (317 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 317.2/317.2 kB 3.4 MB/s eta 0:00:00
Downloading requests-2.28.2-py3-none-any.whl (62 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.8/62.8 kB 3.8 MB/s eta 0:00:00
Downloading selenium-4.7.2-py3-none-any.whl (6.3 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.3/6.3 MB 3.6 MB/s eta 0:00:00
Downloading Flask-2.2.5-py3-none-any.whl (101 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 101.8/101.8 kB 4.5 MB/s eta 0:00:00
Downloading attrs-23.2.0-py3-none-any.whl (60 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 60.8/60.8 kB 2.9 MB/s eta 0:00:00
Downloading certifi-2024.2.2-py3-none-any.whl (163 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 163.8/163.8 kB 4.2 MB/s eta 0:00:00
Downloading charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (140 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 140.3/140.3 kB 3.4 MB/s eta 0:00:00
Downloading click-8.1.7-py3-none-any.whl (97 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 97.9/97.9 kB 4.2 MB/s eta 0:00:00
Downloading idna-3.7-py3-none-any.whl (66 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 66.8/66.8 kB 3.8 MB/s eta 0:00:00
Downloading itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Downloading Jinja2-3.1.3-py3-none-any.whl (133 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 133.2/133.2 kB 3.4 MB/s eta 0:00:00
Downloading pluggy-1.4.0-py3-none-any.whl (20 kB)
Downloading trio-0.25.0-py3-none-any.whl (467 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 467.2/467.2 kB 4.2 MB/s eta 0:00:00
Downloading trio_websocket-0.11.1-py3-none-any.whl (17 kB)
Downloading urllib3-1.26.18-py2.py3-none-any.whl (143 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 143.8/143.8 kB 4.5 MB/s eta 0:00:00
Downloading werkzeug-3.0.2-py3-none-any.whl (226 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 226.8/226.8 kB 3.9 MB/s eta 0:00:00
Downloading iniconfig-2.0.0-py3-none-any.whl (5.9 kB)
Downloading packaging-24.0-py3-none-any.whl (53 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 53.5/53.5 kB 3.7 MB/s eta 0:00:00
Downloading MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (28 kB)
Downloading PySocks-1.7.1-py3-none-any.whl (16 kB)
Downloading sniffio-1.3.1-py3-none-any.whl (10 kB)
Downloading wsproto-1.2.0-py3-none-any.whl (24 kB)
Downloading outcome-1.3.0.post0-py2.py3-none-any.whl (10 kB)
Downloading sortedcontainers-2.4.0-py2.py3-none-any.whl (29 kB)
Downloading h11-0.14.0-py3-none-any.whl (58 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 58.3/58.3 kB 3.1 MB/s eta 0:00:00
Installing collected packages: sortedcontainers, urllib3, sniffio, PySocks, pluggy, packaging, MarkupSafe, itsdangerous, iniconfig, idna, h11, click, charset-normalizer, certifi, attrs, wsproto, Werkzeug, requests, pytest, outcome, Jinja2, trio, Flask, trio-websocket, selenium
Successfully installed Flask-2.2.5 Jinja2-3.1.3 MarkupSafe-2.1.5 PySocks-1.7.1 Werkzeug-3.0.2 attrs-23.2.0 certifi-2024.2.2 charset-normalizer-3.3.2 click-8.1.7 h11-0.14.0 idna-3.7 iniconfig-2.0.0 itsdangerous-2.2.0 outcome-1.3.0.post0 packaging-24.0 pluggy-1.4.0 pytest-7.2.2 requests-2.28.2 selenium-4.7.2 sniffio-1.3.1 sortedcontainers-2.4.0 trio-0.25.0 trio-websocket-0.11.1 urllib3-1.26.18 wsproto-1.2.0
[notice] A new release of pip is available: 23.3.1 -> 24.0
[notice] To update, run: python3.11 -m pip install --upgrade pip
Then launching
builder@DESKTOP-QADGF36:~/Workspaces/Weather-App$ $ export API_KEY=5f1348e8f6aa675ecc09c5bafd5cf2d3
builder@DESKTOP-QADGF36:~/Workspaces/Weather-App$ $ python3 app.py
* Serving Flask app 'app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
My next step is to use the signup URL to create my own OpenWeatherMap API Key
Once there, we can generate or fetch our API keys.
A quick check shows it’s working
Lastly, we need to build and push a container image
builder@DESKTOP-QADGF36:~/Workspaces/Weather-App$ docker build -t idjohnson/pyweatherapp:1.0.0 .
[+] Building 33.9s (11/11) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 249B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/python:3.8.16-slim-buster 21.6s
=> [auth] library/python:pull token for registry-1.docker.io 0.0s
=> [1/5] FROM docker.io/library/python:3.8.16-slim-buster@sha256:eb48d017c5e117d9fbcbe991b4dbc61339734e01578d8d3 2.5s
=> => resolve docker.io/library/python:3.8.16-slim-buster@sha256:eb48d017c5e117d9fbcbe991b4dbc61339734e01578d8d3 0.0s
=> => sha256:eb48d017c5e117d9fbcbe991b4dbc61339734e01578d8d350b38fe2033a67199 988B / 988B 0.0s
=> => sha256:13856bb0fadf6bc658125bae44d41ffd3cc42e423c399cfc7300d41a47f66237 1.37kB / 1.37kB 0.0s
=> => sha256:6876192207e1134f260ac84340cf31e294da116c3c8e0a7ed9addae657ff37f3 6.87kB / 6.87kB 0.0s
=> => sha256:99bf4787315b60d97d860ac6d006b7835b2241a601e93c2da4af6ca554be8704 27.14MB / 27.14MB 0.9s
=> => sha256:a8a848364b534984e122a8bff364f2cf36055a35950c378b336f878b75c8612a 2.78MB / 2.78MB 0.6s
=> => sha256:ca9f63e352d8a4d230e899c4c7c5cda9cc80984133e5a58c948028f54aaa2a0c 10.88MB / 10.88MB 0.7s
=> => sha256:b7c88b22ab235d8b80bfd51c76c767d54e5acb67821faaa0841be1e68434ddeb 245B / 245B 0.8s
=> => sha256:d5dd36a4520bc2d85d1f65a711c54923b55a55ca2ab614a16054af14d1567a80 3.20MB / 3.20MB 1.0s
=> => extracting sha256:99bf4787315b60d97d860ac6d006b7835b2241a601e93c2da4af6ca554be8704 0.6s
=> => extracting sha256:a8a848364b534984e122a8bff364f2cf36055a35950c378b336f878b75c8612a 0.1s
=> => extracting sha256:ca9f63e352d8a4d230e899c4c7c5cda9cc80984133e5a58c948028f54aaa2a0c 0.3s
=> => extracting sha256:b7c88b22ab235d8b80bfd51c76c767d54e5acb67821faaa0841be1e68434ddeb 0.0s
=> => extracting sha256:d5dd36a4520bc2d85d1f65a711c54923b55a55ca2ab614a16054af14d1567a80 0.2s
=> [internal] load build context 0.3s
=> => transferring context: 72.97kB 0.1s
=> [2/5] WORKDIR /python-docker 0.3s
=> [3/5] COPY requirements.txt requirements.txt 0.0s
=> [4/5] RUN pip3 install -r requirements.txt 8.8s
=> [5/5] COPY . . 0.0s
=> exporting to image 0.4s
=> => exporting layers 0.4s
=> => writing image sha256:12c270e6d092ea2ca20e397384ae3022fe91fc4df7306467c2e25dc767fa027f 0.0s
=> => naming to docker.io/idjohnson/pyweatherapp:1.0.0 0.0s
builder@DESKTOP-QADGF36:~/Workspaces/Weather-App$ docker push idjohnson/pyweatherapp:1.0.0
The push refers to repository [docker.io/idjohnson/pyweatherapp]
8b4628c16261: Pushed
cb2787c739c0: Pushed
f8bcbdca7c6c: Pushed
5883e00c645b: Pushed
c0212dc63007: Layer already exists
d8b06e4bca9a: Layer already exists
5f87a6d48234: Layer already exists
37b14643f733: Layer already exists
d85b356ec3b5: Layer already exists
1.0.0: digest: sha256:ac5521873f21ad05727da80a03dae74559c41625d7e74f3c68d3dd9a66cd3bfe size: 2205
I’ll want to now create a Kubernetes secret.
We can, of course, base64 our key and create it with a manifest YAML
apiVersion: v1
kind: Secret
metadata:
name: pyweather-api-key
type: Opaque
data:
API_KEY: YXNkZmFzZGY= # Base64-encoded value of "asdfasdf"
But I’ll do it directly instead
$ kubectl create secret generic pyweather-api-key --from-literal=API_KEY=a3*******************f
secret/pyweather-api-key created
I updated my manifest with the new version and told the deployment to fetch the API_KEY from the secret value
$ cat manifest.yaml
# apiVersion: v1
# kind: Secret
# metadata:
# name: pyweather-api-key
# type: Opaque
# data:
# API_KEY: YXNkZmFzZGY= # Base64-encoded value of "asdfasdf"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: pyweatherapp-deployment
spec:
replicas: 1
selector:
matchLabels:
app: pyweatherapp
template:
metadata:
labels:
app: pyweatherapp
spec:
containers:
- name: pyweatherapp
image: idjohnson/pyweatherapp:1.0.0
ports:
- containerPort: 5000
env:
- name: API_KEY
valueFrom:
secretKeyRef:
name: pyweather-api-key
key: API_KEY
---
apiVersion: v1
kind: Service
metadata:
name: pyweatherapp-service
spec:
selector:
app: pyweatherapp
ports:
- protocol: TCP
port: 80
targetPort: 5000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: pyweatherapp-ingress
annotations:
cert-manager.io/cluster-issuer: azuredns-tpkpw
ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
kubernetes.io/tls-acme: "true"
kubernetes.io/ingress.class: nginx
spec:
tls:
- hosts:
- pyweather.tpk.pw
secretName: pyweather-tls
rules:
- host: pyweather.tpk.pw
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: pyweatherapp-service
port:
number: 80
I applied
$ kubectl apply -f ./manifest.yaml
deployment.apps/pyweatherapp-deployment configured
service/pyweatherapp-service unchanged
ingress.networking.k8s.io/pyweatherapp-ingress unchanged
When I saw the replacement running
$ kubectl get pods -l app=pyweatherapp
NAME READY STATUS RESTARTS AGE
pyweatherapp-deployment-7466b8dd8d-pdv5x 1/1 Running 0 22s
pyweatherapp-deployment-64d8fffcd9-f9qgb 1/1 Terminating 0 27m
and saw it was still working
You can review the differences here or just browse the latest files here.
Summary
Today I wanted to explore a couple fun Open-Source weather apps. The first is reminiscent of the old Weather Channel (when indeed TWC was really just the weather channel). The original author stripped out the background music, but pointed out you can find the audio files here. This one certainly brings back memories.
The other option was just a really simple but functional flask option. We ran the authors original before updating to handle Fahrenheit. I then forked to my own forked repo where I first saved the temp update then finished with abstracting out the original API key to one I could provide in a Kubernetes manifest.