diff --git a/.github/workflows/container-builld-push.yaml b/.github/workflows/container-builld-push.yaml new file mode 100644 index 0000000..80f034c --- /dev/null +++ b/.github/workflows/container-builld-push.yaml @@ -0,0 +1,29 @@ +name: Build Docker Container & Push to Docker Hub + +on: + workflow_dispatch: + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build web container image + run: | + docker build -f Dockerfile \ + -t jmitchel3/tf-python:latest \ + -t jmitchel3/tf-python:${GITHUB_SHA::7}-${GITHUB_RUN_ID::5} \ + . + - name: Push container + run: | + docker push jmitchel3/tf-python --all-tags \ No newline at end of file diff --git a/.github/workflows/infra-destroy.yaml b/.github/workflows/infra-destroy.yaml new file mode 100644 index 0000000..aa9a131 --- /dev/null +++ b/.github/workflows/infra-destroy.yaml @@ -0,0 +1,51 @@ +name: Destroy Infrastructure via Terraform +on: + workflow_dispatch: + # push: + # branches: + # - main + # paths: + # - 'infra/**' + # - 'config/**' + # - '.github/workflows/infra-sync.yaml' + +jobs: + terraform: + name: Apply Terraform + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + # setup terraform + # Terraform Backend -> s3 bucket + # Terraform TFVars -> pat + # init terraform + # validate + # auto-apply + - name: Setup Terraform + uses: hashicorp/setup-terraform@v2 + with: + terraform_version: 1.4.6 + - name: Add Terraform Backend for S3 + run: | + cat << EOF > infra/backend + skip_credentials_validation=true + skip_region_validation=true + bucket="${{ secrets.LINODE_OBJECT_STORAGE_BUCKET }}" + key="tf-k8s.tfstate" + region="us-east-1" + endpoint="us-east-1.linodeobjects.com" + access_key="${{ secrets.LINODE_OBJECT_STORAGE_ACCESS_KEY }}" + secret_key="${{ secrets.LINODE_OBJECT_STORAGE_SECRET_KEY }}" + EOF + - name: Add Terraform TFVars + run: | + cat << EOF > infra/terraform.tfvars + linode_api_token="${{ secrets.LINODE_PA_TOKEN }}" + EOF + - name: Terraform Init + run: terraform -chdir=./infra init -backend-config=backend + - name: Terraform Validate + run: terraform -chdir=./infra validate -no-color + - name: Terraform Apply Changes + run: terraform -chdir=./infra apply -auto-approve -destroy diff --git a/.github/workflows/infra-sync.yaml b/.github/workflows/infra-sync.yaml new file mode 100644 index 0000000..73b940f --- /dev/null +++ b/.github/workflows/infra-sync.yaml @@ -0,0 +1,51 @@ +name: Sync Infrastructure via Terraform +on: + workflow_dispatch: + # push: + # branches: + # - main + # paths: + # - 'infra/**' + # - 'config/**' + # - '.github/workflows/infra-sync.yaml' + +jobs: + terraform: + name: Apply Terraform + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + # setup terraform + # Terraform Backend -> s3 bucket + # Terraform TFVars -> pat + # init terraform + # validate + # auto-apply + - name: Setup Terraform + uses: hashicorp/setup-terraform@v2 + with: + terraform_version: 1.4.6 + - name: Add Terraform Backend for S3 + run: | + cat << EOF > infra/backend + skip_credentials_validation=true + skip_region_validation=true + bucket="${{ secrets.LINODE_OBJECT_STORAGE_BUCKET }}" + key="tf-k8s.tfstate" + region="us-east-1" + endpoint="us-east-1.linodeobjects.com" + access_key="${{ secrets.LINODE_OBJECT_STORAGE_ACCESS_KEY }}" + secret_key="${{ secrets.LINODE_OBJECT_STORAGE_SECRET_KEY }}" + EOF + - name: Add Terraform TFVars + run: | + cat << EOF > infra/terraform.tfvars + linode_api_token="${{ secrets.LINODE_PA_TOKEN }}" + EOF + - name: Terraform Init + run: terraform -chdir=./infra init -backend-config=backend + - name: Terraform Validate + run: terraform -chdir=./infra validate -no-color + - name: Terraform Apply Changes + run: terraform -chdir=./infra apply -auto-approve \ No newline at end of file diff --git a/.github/workflows/k8s-apply.yaml b/.github/workflows/k8s-apply.yaml new file mode 100644 index 0000000..57e5584 --- /dev/null +++ b/.github/workflows/k8s-apply.yaml @@ -0,0 +1,48 @@ +name: Apply Kubectl +on: + workflow_dispatch: + +jobs: + apply_k8s: + name: Verify K8s Service Account + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - uses: azure/setup-kubectl@v3 + - name: Create/Verify `.kube` directory + run: mkdir -p ~/.kube/ + - name: Create kubectl config + run: | + cat << EOF >> ~/.kube/kubeconfig.yaml + ${{ secrets.KUBECONFIG }} + EOF + - name: Add Secret + run: | + if [ -f ops/0-tf-secret.yaml ]; then + rm ops/0-tf-secret.yaml + fi + cat << EOF >> ops/0-tf-secret.json + { + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "tf-python-secret" + }, + "stringData": { + "SECRET_MESSAGE": "${{ secrets.ENV_SECRET_MESSAGE }}" + } + } + EOF + - name: Apply Kubernetes Config + run: | + KUBECONFIG=~/.kube/kubeconfig.yaml kubectl apply -f ops/ + - name: Rollout tf-python + run: | + KUBECONFIG=~/.kube/kubeconfig.yaml kubectl rollout restart deployment/tf-python + - name: Echo deployments + run: | + KUBECONFIG=~/.kube/kubeconfig.yaml kubectl get deployments + - name: Echo Services + run: | + KUBECONFIG=~/.kube/kubeconfig.yaml kubectl get services diff --git a/.github/workflows/py-rnd.yml b/.github/workflows/py-rnd.yml index 7937750..a681a1f 100644 --- a/.github/workflows/py-rnd.yml +++ b/.github/workflows/py-rnd.yml @@ -1,7 +1,7 @@ # This workflow will install Python dependencies # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python -name: Python application +name: Test Python Application on: workflow_dispatch: diff --git a/.gitignore b/.gitignore index 68bc17f..df8b7e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +infra/backend +infra/terraform.tfvars +.kube/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8619119 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.11 + +COPY ./src /app +WORKDIR /app + +RUN pip install -r requirements.txt + +# CMD ["python", "-m", "http.server", "8080"] + +CMD ["gunicorn", "main:app", "--workers", "1", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8080"] \ No newline at end of file diff --git a/infra/main.tf b/infra/main.tf new file mode 100644 index 0000000..2dbafd9 --- /dev/null +++ b/infra/main.tf @@ -0,0 +1,30 @@ +terraform { + required_version = ">= 0.15" + required_providers { + linode = { + source = "linode/linode" + # version = "1.30.0" + } + } + backend "s3" {} # object storage +} + +provider "linode" { + token = var.linode_api_token +} + +variable "linode_api_token" { + description = "Your Linode API Personal Access Token. (required)" + sensitive = true +} + +resource "linode_lke_cluster" "terraform_k8s" { + k8s_version="1.26" + label="tf-k8s" + region="us-east" + tags=["tf-k8s"] + pool { + type = "g6-standard-4" + count = 3 + } +} \ No newline at end of file diff --git a/ops/0-tf-secret.yaml b/ops/0-tf-secret.yaml new file mode 100644 index 0000000..c3b4c46 --- /dev/null +++ b/ops/0-tf-secret.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Secret +metadata: + name: tf-python-secret +stringData: + SECRET_MESSAGE: Not a good secret \ No newline at end of file diff --git a/ops/1-tf-configmap.yaml b/ops/1-tf-configmap.yaml new file mode 100644 index 0000000..0b3fe28 --- /dev/null +++ b/ops/1-tf-configmap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: tf-python-cm +data: + ENV_MESSAGE: This is a configmap! + MG: abc \ No newline at end of file diff --git a/ops/2-tf-deployment.yaml b/ops/2-tf-deployment.yaml new file mode 100644 index 0000000..7e210c4 --- /dev/null +++ b/ops/2-tf-deployment.yaml @@ -0,0 +1,42 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tf-python +spec: + replicas: 3 + selector: + matchLabels: + app: tf-python + template: + metadata: + labels: + app: tf-python + spec: + containers: + - name: tf-python + image: jmitchel3/tf-python:latest + ports: + - containerPort: 8080 + env: + - name: PORT + value: "8080" + - name: VERSION + value: "1.0.0" + - name: KNATIVE_URL + value: "http://tf-python.apps.svc.cluster.local" + - name: ENV_MESSAGE + valueFrom: + configMapKeyRef: + name: tf-python-cm + key: ENV_MESSAGE + - name: SECRET_MESSAGE + valueFrom: + secretKeyRef: + name: tf-python-secret + key: SECRET_MESSAGE + # envFrom: + # - configMapRef: + # name: tf-python-cm + + + diff --git a/ops/3-tf-service.yaml b/ops/3-tf-service.yaml new file mode 100644 index 0000000..921a2f9 --- /dev/null +++ b/ops/3-tf-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: tf-python +spec: + type: ClusterIP # delete my nodebalancer from Linode + ports: + - name: http + port: 80 + targetPort: 8080 + protocol: TCP + selector: + app: tf-python \ No newline at end of file diff --git a/ops/4-tf-ingress.yaml b/ops/4-tf-ingress.yaml new file mode 100644 index 0000000..b7c2be6 --- /dev/null +++ b/ops/4-tf-ingress.yaml @@ -0,0 +1,31 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: tf-ingress + annotations: + kubernetes.io/ingress.class: nginx + # cert-manager.io/cluster-issuer: "letsencrypt" + # nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + # nginx.ingress.kubernetes.io/ssl-passthrough: "true" +spec: + rules: + - host: www.pythonkeras.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: tf-python + port: + name: http + - host: pythonkeras.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: tf-python + port: + name: http diff --git a/ops/5-tf-statefulset.yaml b/ops/5-tf-statefulset.yaml new file mode 100644 index 0000000..abb6b94 --- /dev/null +++ b/ops/5-tf-statefulset.yaml @@ -0,0 +1,52 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: tf-python +spec: + replicas: 1 + selector: + matchLabels: + app: tf-python + template: + metadata: + labels: + app: tf-python + spec: + containers: + - name: tf-python + image: jmitchel3/tf-python:latest + ports: + - containerPort: 8080 + env: + - name: PORT + value: "8080" + - name: ENV_MESSAGE + valueFrom: + configMapKeyRef: + name: tf-python-cm + key: ENV_MESSAGE + - name: SECRET_MESSAGE + valueFrom: + secretKeyRef: + name: tf-python-secret + key: SECRET_MESSAGE + volumeMounts: + - name: tf-volume + mountPath: /data + # initContainers: + # - name: delete-existing-data + # image: alpine:latest + # command: ["sh", "-c", "rm -rf /mnt/*"] + # volumeMounts: + # - name: tf-volume + # mountPath: /mnt + volumeClaimTemplates: + - metadata: + name: tf-volume + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + storageClassName: linode-block-storage diff --git a/ops/6-tf-redis.yaml b/ops/6-tf-redis.yaml new file mode 100644 index 0000000..7c22e13 --- /dev/null +++ b/ops/6-tf-redis.yaml @@ -0,0 +1,64 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: redis-statefulset + labels: + app: redis-statefulset +spec: + replicas: 1 + # serviceName: redis-service + selector: + matchLabels: + app: redis-statefulset + template: + metadata: + labels: + app: redis-statefulset + spec: + containers: + - name: redis-container + image: redis:latest + imagePullPolicy: IfNotPresent + command: + - redis-server + ports: + - name: redis-port + containerPort: 6379 + volumeMounts: + - name: redis-data + mountPath: /data + initContainers: + - name: delete-existing-data + image: alpine:latest + command: ["sh", "-c", "rm -rf /mnt/*"] + volumeMounts: + - name: redis-data + mountPath: /mnt + volumeClaimTemplates: + - metadata: + name: redis-data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Gi + storageClassName: linode-block-storage + +--- +apiVersion: v1 +kind: Service +metadata: + name: redis-db + labels: + app: redis-db +spec: + type: ClusterIP # + ports: + - protocol: TCP + port: 6379 + targetPort: redis-port + selector: + app: redis-statefulset + +# redis://redis-db.default.svc.cluster.local:6379 diff --git a/ops/7-tf-knative-service.yaml b/ops/7-tf-knative-service.yaml new file mode 100644 index 0000000..902a3dd --- /dev/null +++ b/ops/7-tf-knative-service.yaml @@ -0,0 +1,46 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: apps +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: tf-python-cm + namespace: apps +data: + ENV_MESSAGE: This is a configmap! + MG: abc + NEW_ONE: abc + +--- +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: tf-python + namespace: apps # tf-python.apps.svc.cluster.local +spec: + template: + spec: + containers: + - name: tf-py-container + image: jmitchel3/tf-python:latest + ports: + - containerPort: 8080 + env: + - name: VERSION + value: "1.0.1" + - name: ENV_MESSAGE + valueFrom: + configMapKeyRef: + name: tf-python-cm + key: ENV_MESSAGE + # securityContext: + # allowPrivilegeEscalation: false + # runAsNonRoot: false + # capabilities: + # drop: + # - ALL + # seccompProfile: + # type: RuntimeDefault + diff --git a/ops/8-tf-knative-service-demo.yaml b/ops/8-tf-knative-service-demo.yaml new file mode 100644 index 0000000..d68f8d5 --- /dev/null +++ b/ops/8-tf-knative-service-demo.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: demo +--- +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: tf-python + namespace: demo # tf-python.demo.svc.cluster.local # tf-python.demo.pythonkeras.com +spec: + template: + spec: + containers: + - name: cfe-nginx-c + image: codingforentrepreneurs/cfe-nginx:latest + ports: + - containerPort: 80 \ No newline at end of file diff --git a/ops/9-tf-knative-virtualservice.yaml b/ops/9-tf-knative-virtualservice.yaml new file mode 100644 index 0000000..79945aa --- /dev/null +++ b/ops/9-tf-knative-virtualservice.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.istio.io/v1alpha3 +kind: VirtualService +metadata: + name: tf-python-root + namespace: apps +spec: + gateways: + - knative-shared-gateway.knative-serving.svc.cluster.local + - knative-serving/knative-ingress-gateway + hosts: + - pythonkeras.com + - www.pythonkeras.com + http: + - name: http-route + match: + - uri: + prefix: "/" # http://tf-python.apps.pythonkeras.com/ + rewrite: + authority: tf-python.apps.pythonkeras.com + route: + - destination: + host: tf-python.apps.svc.cluster.local + port: + number: 80 + weight: 100 \ No newline at end of file diff --git a/reference.md b/reference.md new file mode 100644 index 0000000..08e4e2a --- /dev/null +++ b/reference.md @@ -0,0 +1,34 @@ +```dockerfile +FROM some_image:some_tag + +COPY ./from/local/path /container/dest/path +WORKDIR /container/dest/path + +# install anything +RUN apt-get install -y nginx + +CMD ["what", "command", "to", "run", "by", "default"] +``` + + + +``` +docker build -t tf-python -f Dockerfile . +``` + + +``` +docker run -p 8080:8080 --rm --name my-tf-python tf-python +``` + +``` +docker ps +``` + +``` +docker exec -it my-tf-python /bin/bash +``` + +``` +docker run -e ENV_MESSAGE="hello from the cli" -p 8080:8080 --rm --name my-tf-python tf-python +``` \ No newline at end of file diff --git a/scripts/install-knative.sh b/scripts/install-knative.sh new file mode 100755 index 0000000..b73bdd0 --- /dev/null +++ b/scripts/install-knative.sh @@ -0,0 +1,22 @@ +# Get version at https://knative.dev/docs/install/yaml-install/serving/install-serving-with-yaml/ +# +export KNATIVE_VERSION="v1.10.1" # ensure ISTIO install matches this version too + +# Install knative serving +# Ref: https://knative.dev/docs/install/yaml-install/serving/install-serving-with-yaml/#install-the-knative-serving-component +kubectl apply -f https://github.com/knative/serving/releases/download/knative-$KNATIVE_VERSION/serving-crds.yaml +kubectl apply -f https://github.com/knative/serving/releases/download/knative-$KNATIVE_VERSION/serving-core.yaml + + +# install istio +# Ref: https://knative.dev/docs/install/yaml-install/serving/install-serving-with-yaml/#install-a-networking-layer +kubectl apply -l knative.dev/crd-install=true -f https://github.com/knative/net-istio/releases/download/knative-$KNATIVE_VERSION/istio.yaml +kubectl apply -f https://github.com/knative/net-istio/releases/download/knative-$KNATIVE_VERSION/istio.yaml +kubectl apply -f https://github.com/knative/net-istio/releases/download/knative-$KNATIVE_VERSION/net-istio.yaml + +# Confirm installed: +kubectl --namespace istio-system get service istio-ingressgateway +export KNATIVE_INGRESS_IP=$(kubectl --namespace istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + +echo "Your IP Address is: $KNATIVE_INGRESS_IP" +echo "Add a cname record for your domain using the above IP address." \ No newline at end of file diff --git a/src/main.py b/src/main.py index b32a90f..b4ffe04 100644 --- a/src/main.py +++ b/src/main.py @@ -7,6 +7,10 @@ def get_env_message(): return os.environ.get("ENV_MESSAGE") or "Nothing to report" +def get_secret_message(): + return os.environ.get("SECRET_MESSAGE") or "Nothing lurking" + + @app.get("/") def home_view(): - return {"hello": "world", "cron": "smooth-cronjob", "watchtower": "working", "env-message": get_env_message()} + return {"hello": "world", "cron": "smooth-cronjob", "watchtower": "working", "env-message": get_env_message(), "secret-message": get_secret_message()} diff --git a/src/test_app.py b/src/test_app.py index 0da5d78..e54d7eb 100644 --- a/src/test_app.py +++ b/src/test_app.py @@ -3,7 +3,7 @@ import pytest # Assuming your FastAPI code is in a file named `main.py` -from .main import app, get_env_message +from .main import app, get_env_message, get_secret_message client = TestClient(app) @@ -16,16 +16,21 @@ def test_home_view(): "cron": "smooth-cronjob", "watchtower": "working", "env-message": get_env_message(), + "secret-message": get_secret_message(), } @pytest.fixture(autouse=True) def clear_env_message(monkeypatch): + monkeypatch.delenv("SECRET_MESSAGE", raising=False) monkeypatch.delenv("ENV_MESSAGE", raising=False) -def test_env_message_set(monkeypatch): +def test_messages_set(monkeypatch): monkeypatch.setenv("ENV_MESSAGE", "Test message") + monkeypatch.setenv("SECRET_MESSAGE", "Test secret message") response = client.get("/") assert response.status_code == 200 - assert response.json()["env-message"] == "Test message" - + data = response.json() + assert data["env-message"] == "Test message" + assert data["secret-message"] == "Test secret message" + diff --git a/src/test_gunicorn.py b/src/test_gunicorn.py index 1788a4c..4788168 100644 --- a/src/test_gunicorn.py +++ b/src/test_gunicorn.py @@ -18,6 +18,7 @@ def test_gunicorn_start(): "cron": "smooth-cronjob", "watchtower": "working", "env-message": os.environ.get("ENV_MESSAGE") or "Nothing to report", + "secret-message": os.environ.get("SECRET_MESSAGE") or "Nothing lurking" } finally: gunicorn_process.terminate() diff --git a/tf.code-workspace b/tf.code-workspace new file mode 100644 index 0000000..a0dbf82 --- /dev/null +++ b/tf.code-workspace @@ -0,0 +1,20 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "files.autoSave": "afterDelay", + "terminal.integrated.env.osx": { + "KUBECONFIG": "${workspaceFolder}/.kube/kubeconfig.yaml", + "KUBE_EDITOR": "nano", + }, + "terminal.integrated.env.windows": { + "KUBECONFIG": "${workspaceFolder}\\.kube\\kubeconfig.yaml" + }, + "terminal.integrated.env.linux": { + "KUBECONFIG": "${workspaceFolder}/.kube/kubeconfig.yaml" + }, + } +} \ No newline at end of file