OS Apps: Weather

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

/content/images/2024/04/weather-02.png

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:

/content/images/2024/04/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

/content/images/2024/04/weather-05.png

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

/content/images/2024/04/weather-03.png

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

/content/images/2024/04/weather-04.png

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:

/content/images/2024/04/weather-14.png

After debugging my container and the URLs it used, I saw the cause was actually on the API side

/content/images/2024/04/weather-15.png

and after a few refreshes with 502, I managed to get a dumping Fedora box

/content/images/2024/04/weather-16.png

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

/content/images/2024/04/weather-07.png

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

/content/images/2024/04/weather-08.png

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

/content/images/2024/04/weather-09.png

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

/content/images/2024/04/weather-10.png

Once there, we can generate or fetch our API keys.

/content/images/2024/04/weather-11.png

A quick check shows it’s working

/content/images/2024/04/weather-12.png

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

/content/images/2024/04/weather-13.png

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.

TWC Weather OpenWeather OpenSource Containers Kubernetes

Have something to add? Feedback? Try our new forums

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes