{"id":4969,"date":"2026-04-15T12:08:00","date_gmt":"2026-04-15T09:08:00","guid":{"rendered":"https:\/\/upcloud.com\/global\/?post_type=tutorial&#038;p=4969"},"modified":"2026-05-05T13:09:29","modified_gmt":"2026-05-05T12:09:29","slug":"deploying-edge-ready-kubernetes-cluster-cli","status":"publish","type":"tutorial","link":"https:\/\/upcloud.com\/global\/resources\/tutorials\/deploying-edge-ready-kubernetes-cluster-cli\/","title":{"rendered":"Deploying an Edge-Ready Kubernetes Cluster Using UpCloud Kubernetes Service (UKS) and CLI"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Kubernetes is evolving beyond traditional data centers. With the increasing use of IoT devices, 5G networks, and latency-sensitive workloads, teams need clusters that can operate closer to where data is generated while remaining simple to manage.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">UpCloud Kubernetes Service (UKS) offers a managed control plane that fits this direction. It lets you run lightweight, region-specific clusters with high-performance storage and private networking, which align with many edge patterns.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">While several cloud providers offer managed Kubernetes, UKS stands out for its simplicity and developer-friendly design. You don\u2019t need to provision multiple subresources or navigate complex networking setups; just define your network, pick a node plan, and start deploying.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A previous <a href=\"https:\/\/upcloud.com\/global\/docs\/guides\/get-started-managed-kubernetes\/\">guide<\/a> showed how to deploy a UKS cluster through the Control Panel. In this tutorial, you\u2019ll take a hands-on approach using the UpCloud CLI to build an edge-ready cluster from scratch. By the end, you would have deployed a full application stack, gained practical experience with Upcloud\u2019s edge-ready Kubernetes Architecture, and learnt how to troubleshoot real-world scenarios you\u2019re likely to face in distributed environments.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">UKS edge-aligned architecture overview<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This tutorial deploys a simple three-tier Todo application on UKS using the CLI. Each YAML file defines a key component of the stack, and the setup follows a standard microservice architecture where each component runs as a Kubernetes workload.&nbsp;<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The PostgreSQL database is deployed using a StatefulSet and configured with persistent storage.<\/li>\n\n\n\n<li>The Backend runs as a <a href=\"http:\/\/node.js\/Express\" target=\"_blank\" rel=\"noopener\">Node.js\/Express<\/a> API service that handles requests and communicates with the database.<\/li>\n\n\n\n<li>The Frontend runs as a lightweight NGINX web server exposed publicly to users.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">This environment demonstrates how UKS can host a self-contained, edge-style Kubernetes setup. All of the components run inside a private UpCloud network, storage remains local to the zone, and external traffic is routed through a single controlled entry point. This pattern mirrors how teams design clusters closer to their workloads or specific user regions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The key UKS components leveraged in this architecture include:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Servers (Nodes):<\/strong> Worker nodes provisioned by UKS to run your application pods.<\/li>\n\n\n\n<li><strong>Networking:<\/strong> A dedicated router and private network connect all cluster nodes securely, while an external Load Balancer routes incoming traffic to the frontend service.<\/li>\n\n\n\n<li><strong>Storage:<\/strong> MaxIOPS block storage attached to the PostgreSQL StatefulSet ensures low-latency data access and persistence across pod restarts.<\/li>\n\n\n\n<li><strong>Load Balancers:<\/strong> Managed by UpCloud, providing reliable external access and automatic traffic distribution across healthy frontend pods.<br><strong>Control Plane:<\/strong> Fully managed by UpCloud, maintaining the Kubernetes API server, etcd, and other control-plane components so you can focus on workloads.<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/upcloud-kubernetes-topology-1024x683.png\" alt=\"-\" class=\"wp-image-77003\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Architecture of the Todo application deployed on UpCloud Kubernetes Service (UKS), showing managed control plane, worker nodes, private networking, MaxIOPS storage, and external access through an UpCloud Load Balancer.<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This architecture is compact yet production-like, offering a practical foundation for running edge-ready workloads on UpCloud before extending similar configurations to smaller or on-premise environments.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Let\u2019s start by setting up the environment and the required tools.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before getting started, make sure the following requirements are in place:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/kubernetes.io\/docs\/tasks\/tools\/\" target=\"_blank\" rel=\"noopener\">Kubernetes CLI<\/a> <code>kubect<\/code> is installed locally.<\/li>\n\n\n\n<li><a href=\"https:\/\/upcloudltd.github.io\/upcloud-cli\/\" target=\"_blank\" rel=\"noopener\">UpCloud CLI<\/a> <code>upctl<\/code><strong> <\/strong>is installed and configured.<\/li>\n\n\n\n<li><a href=\"https:\/\/signup.upcloud.com\/\">An active UpCloud account<\/a>.<\/li>\n\n\n\n<li><a href=\"https:\/\/upcloud.com\/global\/docs\/guides\/getting-started-upcloud-api\/\">A subaccount with API access<\/a> (username and password) for CLI authentication.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The subaccount must have the following permissions enabled:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Servers<\/strong> \u2013 Provision and manage virtual machines used as Kubernetes control plane and worker nodes.<\/li>\n\n\n\n<li><strong>Storages<\/strong> \u2013 Create persistent volumes, manage block storage for nodes, and handle container image or data storage.<\/li>\n\n\n\n<li><strong>Load Balancers<\/strong> \u2013 Expose services externally and distribute traffic across multiple pods or nodes.<\/li>\n\n\n\n<li><strong>Private Networks<\/strong> \u2013 Create isolated networks for internal cluster communication and secure node-to-node traffic.<\/li>\n\n\n\n<li><strong>Routers<\/strong> \u2013 Route traffic between private networks and connect cluster networks to the internet.<\/li>\n\n\n\n<li><strong>Kubernetes<\/strong> \u2013 Create, manage, and delete Kubernetes clusters.<\/li>\n\n\n\n<li><strong>API Connections<\/strong> \u2013 Allow CLI access to UpCloud\u2019s API for resource creation. For this tutorial, you can allow access from all addresses. In production, always restrict API access to specific IP ranges for security.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Create and populate the folder and file structure<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Start by creating a new project directory and the necessary files<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Run:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">mkdir upcloud_kubernetes_service\ncd upcloud_kubernetes_service\nTouch postgres-statefulset.yaml backend-deployment.yaml frontend-deployment.yaml .env<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">After creating the files, your structure should look like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">upcloud_kubernetes_service\/\n\u251c\u2500 .env\n\u251c\u2500 backend-deployment.yaml\n\u251c\u2500 frontend-deployment.yaml\n\u251c\u2500 postgres-statefulset.yaml<\/code><\/pre>\n\n\n\n<ol start=\"2\" class=\"wp-block-list\">\n<li>Next, populate the files with code.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Postgres-statefulset.yaml<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Creates a PostgreSQL database using a StatefulSet (for stable storage) and a headless Service.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It stores data persistently on a 10 GB UpCloud volume and configures credentials for the database <code>tododb<\/code>, user <code>todouser<\/code>, and password <code>todopass123<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For real deployments, make sure to use strong passwords and store credentials securely (for example, in Kubernetes Secrets) instead of hardcoding them.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">apiVersion: v1\nkind: Service\nmetadata:\nname: postgres\nspec:\nports:\n- port: 5432\nclusterIP: None\nselector:\n  app: postgres\n---\napiVersion: apps\/v1\nkind: StatefulSet\nmetadata:\nname: postgres\nspec:\nserviceName: postgres\nreplicas: 1\nselector:\n  matchLabels:\n    app: postgres\ntemplate:\n  metadata:\n    labels:\n      app: postgres\n  spec:\n    containers:\n    - name: postgres\n      image: postgres:15-alpine\n      ports:\n      - containerPort: 5432\n      env:\n      - name: POSTGRES_DB\n        value: tododb\n      - name: POSTGRES_USER\n        value: todouser\n      - name: POSTGRES_PASSWORD\n        value: todopass123\n      volumeMounts:\n      - name: postgres-storage\n        mountPath: \/var\/lib\/postgresql\/data\n        subPath: postgres\nvolumeClaimTemplates:\n- metadata:\n    name: postgres-storage\n  spec:\n    accessModes: [\"ReadWriteOnce\"]\n    storageClassName: upcloud-block-storage-maxiops\n    resources:\n      requests:\n        storage: 10Gi<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Backend-deployment.yaml<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Deploys two replicas of a Node.js\/Express API server that handles the Todo app\u2019s CRUD operations.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The backend connects to PostgreSQL, initializes the todos table on startup, and exposes port 3001 via a ClusterIP Service (internal access only). The application code is also included in the chart to reduce file management<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">apiVersion: apps\/v1\nkind: Deployment\nmetadata:\nname: backend\nnamespace: todo-app\nspec:\nreplicas: 2\nselector:\n  matchLabels:\n    app: backend\ntemplate:\n  metadata:\n    labels:\n      app: backend\n  spec:\n    containers:\n    - name: backend\n      image: node:18-alpine\n      workingDir: \/app\n      command: [\"\/bin\/sh\", \"-c\"]\n      args:\n        - |\n          cat &gt; \/app\/server.js &lt;&lt; 'ENDJS'\n          const express = require('express');\n          const { Pool } = require('pg');\n          const cors = require('cors');\n         \n          const app = express();\n          app.use(cors());\n          app.use(express.json());\n         \n          const pool = new Pool({\n            host: 'postgres',\n            database: 'tododb',\n            user: 'todouser',\n            password: 'todopass123',\n            port: 5432,\n          });\n         \n          const initDB = async () =&gt; {\n            let retries = 5;\n            while (retries &gt; 0) {\n              try {\n                await pool.query(`\n                  CREATE TABLE IF NOT EXISTS todos (\n                    id SERIAL PRIMARY KEY,\n                    title TEXT NOT NULL,\n                    completed BOOLEAN DEFAULT FALSE,\n                    created_at TIMESTAMP DEFAULT NOW()\n                  )\n                `);\n                console.log('Database initialized');\n                break;\n              } catch (err) {\n                console.log('Waiting for database...', err.message);\n                retries--;\n                await new Promise(resolve =&gt; setTimeout(resolve, 2000));\n              }\n            }\n          };\n         \n          app.get('\/api\/todos', async (req, res) =&gt; {\n            try {\n              const result = await pool.query('SELECT * FROM todos ORDER BY id');\n              res.json(result.rows);\n            } catch (err) {\n              res.status(500).json({ error: err.message });\n            }\n          });\n         \n          app.post('\/api\/todos', async (req, res) =&gt; {\n            try {\n              const { title } = req.body;\n              const result = await pool.query(\n                'INSERT INTO todos (title) VALUES ($1) RETURNING *',\n                [title]\n              );\n              res.json(result.rows[0]);\n            } catch (err) {\n              res.status(500).json({ error: err.message });\n            }\n          });\n         \n          app.delete('\/api\/todos\/:id', async (req, res) =&gt; {\n            try {\n              await pool.query('DELETE FROM todos WHERE id = $1', [req.params.id]);\n              res.json({ success: true });\n            } catch (err) {\n              res.status(500).json({ error: err.message });\n            }\n          });\n         \n          app.get('\/health', (req, res) =&gt; {\n            res.json({ status: 'ok' });\n          });\n         \n          initDB().then(() =&gt; {\n            app.listen(3001, '0.0.0.0', () =&gt; {\n              console.log('Backend running on port 3001');\n            });\n          });\n          ENDJS\n         \n          cd \/app\n          npm init -y\n          npm install express pg cors\n          node server.js\n      ports:\n      - containerPort: 3001\n---\napiVersion: v1\nkind: Service\nmetadata:\nname: backend\nnamespace: todo-app\nspec:\ntype: ClusterIP\nselector:\n  app: backend\nports:\n- port: 3001\n  targetPort: 3001<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">frontend-deployment.yaml&nbsp;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Deploys two replicas of an Nginx web server with a lightweight HTML\/CSS\/JavaScript interface.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It also includes an Nginx config that proxies <code>\/api\/*<\/code> requests to the backend service and exposes port 80 through a LoadBalancer Service for external access.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: nginx-config\n namespace: todo-app\ndata:\n default.conf: |\n   server {\n       listen 80;\n       server_name _;\n      \n       location \/ {\n           root \/usr\/share\/nginx\/html;\n           index index.html;\n           try_files $uri $uri\/ \/index.html;\n       }\n      \n       location \/api\/ {\n           proxy_pass http:\/\/backend:3001\/api\/;\n           proxy_set_header Host $host;\n           proxy_set_header X-Real-IP $remote_addr;\n           proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n       }\n   }\n---\napiVersion: apps\/v1\nkind: Deployment\nmetadata:\n name: frontend\n namespace: todo-app\nspec:\n replicas: 2\n selector:\n   matchLabels:\n     app: frontend\n template:\n   metadata:\n     labels:\n       app: frontend\n   spec:\n     containers:\n     - name: frontend\n       image: nginx:alpine\n       ports:\n       - containerPort: 80\n       volumeMounts:\n       - name: html\n         mountPath: \/usr\/share\/nginx\/html\n       - name: nginx-config\n         mountPath: \/etc\/nginx\/conf.d\n     initContainers:\n     - name: setup\n       image: alpine\n       command: [\"\/bin\/sh\", \"-c\"]\n       args:\n         - |\n           cat &gt; \/html\/index.html &lt;&lt; 'HTMLEOF'\n           &lt;!DOCTYPE html&gt;\n           &lt;html lang=\"en\"&gt;\n           &lt;head&gt;\n               &lt;meta charset=\"UTF-8\"&gt;\n               &lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"&gt;\n               &lt;title&gt;UpCloud K8s Todo App&lt;\/title&gt;\n               &lt;style&gt;\n                   * { margin: 0; padding: 0; box-sizing: border-box; }\n                   body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; }\n                   .container { max-width: 600px; margin: 0 auto; background: white; border-radius: 15px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); padding: 30px; }\n                   h1 { color: #667eea; margin-bottom: 10px; text-align: center; }\n                   .subtitle { text-align: center; color: #666; margin-bottom: 30px; font-size: 14px; }\n                   .input-group { display: flex; gap: 10px; margin-bottom: 20px; }\n                   input { flex: 1; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 16px; }\n                   button { padding: 12px 24px; background: #667eea; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; transition: background 0.3s; }\n                   button:hover { background: #5568d3; }\n                   .todo-item { display: flex; justify-content: space-between; align-items: center; padding: 15px; background: #f8f9fa; margin-bottom: 10px; border-radius: 8px; animation: fadeIn 0.3s; }\n                   @keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }\n                   .delete-btn { background: #dc3545; padding: 8px 16px; font-size: 14px; }\n                   .delete-btn:hover { background: #c82333; }\n                   .empty { text-align: center; color: #999; padding: 40px; }\n                   .status { text-align: center; padding: 10px; margin-bottom: 20px; border-radius: 8px; display: none; }\n                   .status.error { background: #fee; color: #c00; display: block; }\n                   .status.success { background: #efe; color: #0a0; display: block; }\n               &lt;\/style&gt;\n           &lt;\/head&gt;\n           &lt;body&gt;\n               &lt;div class=\"container\"&gt;\n                   &lt;h1&gt;\ud83d\ude80 UpCloud K8s Todo App&lt;\/h1&gt;\n                   &lt;p class=\"subtitle\"&gt;Powered by Kubernetes on UpCloud&lt;\/p&gt;\n                   &lt;div id=\"status\" class=\"status\"&gt;&lt;\/div&gt;\n                   &lt;div class=\"input-group\"&gt;\n                       &lt;input type=\"text\" id=\"todoInput\" placeholder=\"Enter a new todo...\" onkeypress=\"if(event.key==='Enter') addTodo()\"&gt;\n                       &lt;button onclick=\"addTodo()\"&gt;Add Todo&lt;\/button&gt;\n                   &lt;\/div&gt;\n                   &lt;div id=\"todoList\"&gt;&lt;\/div&gt;\n               &lt;\/div&gt;\n               &lt;script&gt;\n                   const API_URL = '\/api';\n                  \n                   function showStatus(message, isError = false) {\n                       const status = document.getElementById('status');\n                       status.textContent = message;\n                       status.className = 'status ' + (isError ? 'error' : 'success');\n                       setTimeout(() =&gt; status.className = 'status', 3000);\n                   }\n                  \n                   async function loadTodos() {\n                       try {\n                           const res = await fetch(API_URL + '\/todos');\n                           if (!res.ok) throw new Error('Failed to load todos');\n                           const todos = await res.json();\n                           const list = document.getElementById('todoList');\n                           if (todos.length === 0) {\n                               list.innerHTML = '&lt;div class=\"empty\"&gt;\ud83d\udcdd No todos yet. Add one above!&lt;\/div&gt;';\n                           } else {\n                               list.innerHTML = todos.map(todo =&gt; `\n                                   &lt;div class=\"todo-item\"&gt;\n                                       &lt;span&gt;${escapeHtml(todo.title)}&lt;\/span&gt;\n                                       &lt;button class=\"delete-btn\" onclick=\"deleteTodo(${todo.id})\"&gt;Delete&lt;\/button&gt;\n                                   &lt;\/div&gt;\n                               `).join('');\n                           }\n                       } catch (err) {\n                           showStatus('\u274c Error loading todos: ' + err.message, true);\n                           document.getElementById('todoList').innerHTML = '&lt;div class=\"empty\"&gt;\u26a0\ufe0f Could not connect to backend&lt;\/div&gt;';\n                       }\n                   }\n                  \n                   function escapeHtml(text) {\n                       const div = document.createElement('div');\n                       div.textContent = text;\n                       return div.innerHTML;\n                   }\n                  \n                   async function addTodo() {\n                       const input = document.getElementById('todoInput');\n                       const title = input.value.trim();\n                       if (!title) return;\n                       try {\n                           const res = await fetch(API_URL + '\/todos', {\n                               method: 'POST',\n                               headers: { 'Content-Type': 'application\/json' },\n                               body: JSON.stringify({ title })\n                           });\n                           if (!res.ok) throw new Error('Failed to add todo');\n                           input.value = '';\n                           showStatus('\u2705 Todo added!');\n                           loadTodos();\n                       } catch (err) {\n                           showStatus('\u274c Error: ' + err.message, true);\n                       }\n                   }<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">.env<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Holds your UpCloud sub-account credentials used to authenticate the CLI.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">While this file is referenced in the tutorial for simplicity, note that the demo application hardcodes configuration values directly in the YAML files.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In production, always store credentials securely using Kubernetes Secrets or ConfigMaps.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">export UPCLOUD_USERNAME=\"Your_sub_account_username\"\nexport UPCLOUD_PASSWORD=\"Your_sub_account_password\"<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Authenticate the UpCloud CLI<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Make sure you are in the root of the project directory <code>upcloud_kubernetes_service<\/code>. Load your credentials into the shell:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">source .env<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: Create a router<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Run the following commands:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">upctl router create --name todo-app-router\nupctl router list<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Copy the router UUID from <code>upctl router list<\/code>. You will use it when creating the network.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: Create a private network<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Replace the router UUID with the one you created in Step 3.&nbsp;<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">upctl network create --name todo-app-network --zone de-fra1 --ip-network address=172.16.0.0\/24,dhcp=true, --router &lt;UUID&gt;\nupctl network list<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Copy the network UUID when after running <code>upctl network list<\/code> for the cluster step.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 5: Create a UKS cluster<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Replace the network UUID with the value from Step 4.&nbsp; After creation the cluster, run the kubernetes list command to show the cluster and its cluster UUID.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">upctl kubernetes create --name my-todo-app --network &lt;UUID&gt; --node-group count=2,name=my-todo-app-node-group,plan=2xCPU-4GB, --zone de-fra1\nupctl kubernetes list<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 6: Get cluster credentials and access<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You will need your cluster, node group identifiers, and a kubeconfig to talk to the running cluster.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Inspect the cluster and node group<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Use these to view details about the cluster, which includes the version, zones, operational status and also the node group.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">upctl kubernetes show my-todo-app\nupctl kubernetes node-group show &lt;cluster_UUID&gt; --name my-todo-app-node-group<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Make sure your cluster operational state indicates running and node groups are created before moving to the next step.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/upcloud-upctl-show-kubernetes-app.png\" alt=\"-\" class=\"wp-image-77004\" \/><\/figure>\n\n\n\n<ol start=\"2\" class=\"wp-block-list\">\n<li>Temporarily allow your IP to reach the Kubernetes API<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">For this tutorial, you\u2019ll temporarily allow access from any IP so you can fetch credentials from your current machine without restrictions. In a production environment, however, its best to restrict access to your office or VPN IP range.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">upctl kubernetes modify &lt;cluster_UUID&gt; --kubernetes-api-allow-ip 0.0.0.0\/0<\/code><\/pre>\n\n\n\n<ol start=\"3\" class=\"wp-block-list\">\n<li>Generate a kubeconfig for the cluster<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">This exports a context that <code>kubectl<\/code> can use. The outputed file from the command below includes the cluster endpoint, certificate data, and an authentication token.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">upctl kubernetes config &lt;cluster_UUID&gt;&nbsp; --output yaml --write .\/my_kubeconfig.yaml<\/code><\/pre>\n\n\n\n<ol start=\"4\" class=\"wp-block-list\">\n<li>Activate the context locally<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Replace your current kubeconfig temporarily so kubectl picks the UKS context by default.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">cp ~\/.kube\/config ~\/.kube\/config.backup\ncp my_kubeconfig.yaml ~\/.kube\/config<\/code><\/pre>\n\n\n\n<ol start=\"5\" class=\"wp-block-list\">\n<li>Verify connectivity<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Confirm you can talk to the control plane and list worker nodes.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl get nodes<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Confirm that the nodes are ready<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/upcloud-upctl-show-kubernetes-app.png\" alt=\"-\" class=\"wp-image-77004\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Step 7: Create and target the namespace<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl create namespace todo-app\nkubectl config set-context --current --namespace=todo-app<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 8: Deploy the application<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Apply the Postgres manifest&nbsp;<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Make sure Postgres is fully up before applying the backend to avoid connection retries stacking.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl apply -f postgres-statefulset.yaml &nbsp; \n# wait until Postgres is Running\nkubectl get StatefulSet -n todo-app<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Confirm that Postgres Ready status is 1\/1 before deploying the other workloads<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/upcloud-kubectl-get-statefulset.png\" alt=\"-\" class=\"wp-image-77005\" \/><\/figure>\n\n\n\n<ol start=\"2\" class=\"wp-block-list\">\n<li>Apply the backend and frontend manifest files.<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl apply -f backend-deployment.yaml\nkubectl apply -f frontend-deployment.yaml<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 9: Check status and get the app endpoint<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Once all deployments are applied, verify that the pods and services are running correctly:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl get pods -n todo-app\nkubectl get services -n todo-app   # look for the frontend Service external IP<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Look for the frontend Service in the list.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Its EXTERNAL-IP field shows the public load balancer address created by UpCloud.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It typically looks something like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">lb-0ab9bf7da911436eb0feae1fc329bcfc-1.upcloudlb.com<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Copy your unique address and open it in a browser to access the Todo application.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/upcloud-kubernetes-to-do-app-1024x217.png\" alt=\"-\" class=\"wp-image-77008\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Step 10: Clean up<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">After inspecting and testing the cluster, run the following commands to clean up the cluster.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\"># 1) Delete namespace (removes app resources)\nkubectl delete namespace todo-app\n\n# 2) Check if LoadBalancers are gone\nupctl load-balancer list\n# If any remain, delete manually:\nupctl load-balancer  delete &lt;LOADBALANCER_UUID&gt;\n\n# 3) Delete the cluster (nodes are removed automatically)\nupctl kubernetes delete my-todo-app\n\n# Wait till the kubernetes cluster has been terminated completely before running the other cleanup commands.\n# 4) Inspect leftover infrastructure\nupctl network list --zone de-fra1\nupctl router list\nupctl storage list\n\n# 5) Delete network, router, and any orphaned storage\nupctl network delete &lt;NETWORK_UUID&gt;\nupctl router delete &lt;ROUTER_UUID&gt;\nupctl storage delete &lt;STORAGE_UUID&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The tutorial above demonstrates how straightforward it is to spin up a Kubernetes cluster on UpCloud and deploy workloads within it. It also highlights how simple it is to clean up your environment when you\u2019re done.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Next, let\u2019s look at some troubleshooting tips you can use if you run into issues while setting up or managing your cluster.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting Guide<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">1. API Access Denied<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Error:<\/strong> UNAUTHORIZED_ADDRESS<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Cause:<\/strong> The base IP from the computer used for CLI maybe not be whitelisted for API access.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fix:<\/strong> Add your current IP in UpCloud \u2192 Account \u2192 Sub-accounts \u2192 API Access.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">curl -s https:\/\/api.ipify.org &nbsp; # find your IP<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">2. Network Creation Fails<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Error:<\/strong> ROUTER_INVALID <strong>or<\/strong> DHCP <strong>not<\/strong> enabled<br><strong>Cause:<\/strong> Your router UUID could be missing, or DHCP is disabled.<br><strong>Fix:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">upctl router create --name todo-app-router\nupctl network create --name todo-app-network --zone de-fra1 --ip-network address=172.16.0.0\/24,dhcp=yes, --router &lt;ROUTER_UUID&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">3. kubectl Connection Errors<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Error: localhost refused or EOF<br>Fix:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Regenerate kubeconfig:<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">upctl kubernetes config &lt;CLUSTER_UUID&gt; --output yaml --write .\/kubeconfig.yaml<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Ensure your IP is allowed:<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">upctl kubernetes modify &lt;CLUSTER_UUID&gt; --kubernetes-api-allow-ip 0.0.0.0\/0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">4.&nbsp; <strong>Backend deployment fails to connect to Database<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Error: relation &#8220;todos&#8221; does not exist<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fix:<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Restart backend after PostgreSQL is ready:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl rollout restart deployment\/backend -n todo-app<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Useful Debug Commands<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl get pods -n todo-app\nkubectl describe pod &lt;NAME&gt; -n todo-app\nkubectl logs &lt;NAME&gt; -n todo-app\nkubectl get svc -n todo-app\nkubectl get events -n todo-app --sort-by=.lastTimestamp<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Wrapping up<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">By this point, you\u2019ve gone from creating your own network and router to deploying a working application on an edge-ready UpCloud Kubernetes Service, all from the command line. You\u2019ve seen how flexible the UpCloud CLI can be for setting up clusters, managing workloads, and keeping your environment clean when you\u2019re done.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now that you\u2019ve had hands-on experience with UKS, it\u2019s easier to appreciate how straightforward it is to set up. Despite its simplicity, UKS lets you deploy clusters with smaller-sized worker nodes, connect to<a href=\"https:\/\/upcloud.com\/global\/docs\/products\/block-storage\/tiers\/\"> high-performance MaxIOPS storage<\/a>, and maintain data residency within your chosen region.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In our next article, we\u2019ll compare how UKS stacks up against other managed Kubernetes platforms in real-world edge and distributed scenarios.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you\u2019d like to keep experimenting, check out the<a href=\"https:\/\/upcloud.com\/global\/docs\/products\/managed-kubernetes\/\"> UpCloud Kubernetes documentation<\/a>.<\/p>\n","protected":false},"author":83,"featured_media":0,"comment_status":"open","ping_status":"closed","template":"","community-category":[223,229],"class_list":["post-4969","tutorial","type-tutorial","status-publish","hentry"],"acf":[],"_links":{"self":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial\/4969","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial"}],"about":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/types\/tutorial"}],"author":[{"embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/users\/83"}],"replies":[{"embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/comments?post=4969"}],"version-history":[{"count":6,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial\/4969\/revisions"}],"predecessor-version":[{"id":6689,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial\/4969\/revisions\/6689"}],"wp:attachment":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/media?parent=4969"}],"wp:term":[{"taxonomy":"community-category","embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/community-category?post=4969"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}