This guide will walk you through the process of setting up and deploying a full-stack TypeScript application on a local Kubernetes (k3s) cluster, using Terraform for infrastructure management and Ansible for automation.
Ensure you have the following installed on your local machine:
Create the following directory structure:
project-root/
├── frontend/
├── backend/
├── terraform/
├── ansible/
└── scripts/
curl -sfL https://get.k3s.io | sh -
mkdir ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $USER:$USER ~/.kube/config
npx create-react-app frontend --template typescript
cd frontend
npm install axios
src/App.tsx with:import React, { useState, useEffect } from 'react';
import axios from 'axios';
interface Message {
text: string;
}
function App() {
const [message, setMessage] = useState<string>('');
const [inputText, setInputText] = useState<string>('');
useEffect(() => {
fetchMessage();
}, []);
const fetchMessage = async () => {
try {
const response = await axios.get<Message>('http://localhost:3000/api/message');
setMessage(response.data.text);
} catch (error) {
console.error('Error fetching message:', error);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await axios.post<Message>('http://localhost:3000/api/message', { text: inputText });
fetchMessage();
setInputText('');
} catch (error) {
console.error('Error submitting message:', error);
}
};
return (
<div>
<h1>K3s React App with TypeScript</h1>
<p>Message from server: {message}</p>
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Enter a new message"
/>
<button type="submit">Submit</button>
</form>
</div>
);
}
export default App;
mkdir backend && cd backend
npm init -y
npm install express pg cors
npm install --save-dev typescript @types/express @types/pg @types/cors ts-node
npx tsc --init
tsconfig.json:{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
src/server.ts:import express, { Request, Response } from 'express';
import { Pool } from 'pg';
import cors from 'cors';
const app = express();
app.use(cors());
app.use(express.json());
const pool = new Pool({
user: 'postgres',
host: 'postgres',
database: 'myapp',
password: 'password',
port: 5432,
});
app.get('/api/message', async (req: Request, res: Response) => {
try {
const result = await pool.query('SELECT text FROM messages ORDER BY id DESC LIMIT 1');
res.json({ text: result.rows[0]?.text || 'No messages yet' });
} catch (error) {
console.error('Error fetching message:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
app.post('/api/message', async (req: Request, res: Response) => {
try {
const { text } = req.body;
await pool.query('INSERT INTO messages (text) VALUES ($1)', [text]);
res.status(201).json({ message: 'Message saved successfully' });
} catch (error) {
console.error('Error saving message:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
package.json scripts:"scripts": {
"start": "node dist/server.js",
"build": "tsc",
"dev": "ts-node src/server.ts"
}
We’ll use a PostgreSQL container in our k3s cluster. The setup will be handled by Terraform and Ansible in later steps.
Dockerfile in the frontend directory:FROM node:20-alpine as build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Dockerfile in the backend directory:FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
terraform directory, create main.tf:terraform {
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.0"
}
}
}
provider "kubernetes" {
config_path = "~/.kube/config"
}
resource "kubernetes_namespace" "myapp" {
metadata {
name = "myapp"
}
}
resource "kubernetes_deployment" "postgres" {
metadata {
name = "postgres"
namespace = kubernetes_namespace.myapp.metadata[0].name
}
spec {
replicas = 1
selector {
match_labels = {
app = "postgres"
}
}
template {
metadata {
labels = {
app = "postgres"
}
}
spec {
container {
image = "postgres:13"
name = "postgres"
env {
name = "POSTGRES_DB"
value = "myapp"
}
env {
name = "POSTGRES_PASSWORD"
value = "password"
}
port {
container_port = 5432
}
}
}
}
}
}
resource "kubernetes_service" "postgres" {
metadata {
name = "postgres"
namespace = kubernetes_namespace.myapp.metadata[0].name
}
spec {
selector = {
app = kubernetes_deployment.postgres.spec[0].template[0].metadata[0].labels.app
}
port {
port = 5432
target_port = 5432
}
}
}
resource "kubernetes_deployment" "backend" {
metadata {
name = "backend"
namespace = kubernetes_namespace.myapp.metadata[0].name
}
spec {
replicas = 1
selector {
match_labels = {
app = "backend"
}
}
template {
metadata {
labels = {
app = "backend"
}
}
spec {
container {
image = "backend:latest"
name = "backend"
image_pull_policy = "Never"
port {
container_port = 3000
}
}
}
}
}
}
resource "kubernetes_service" "backend" {
metadata {
name = "backend"
namespace = kubernetes_namespace.myapp.metadata[0].name
}
spec {
selector = {
app = kubernetes_deployment.backend.spec[0].template[0].metadata[0].labels.app
}
port {
port = 3000
target_port = 3000
}
}
}
resource "kubernetes_deployment" "frontend" {
metadata {
name = "frontend"
namespace = kubernetes_namespace.myapp.metadata[0].name
}
spec {
replicas = 1
selector {
match_labels = {
app = "frontend"
}
}
template {
metadata {
labels = {
app = "frontend"
}
}
spec {
container {
image = "frontend:latest"
name = "frontend"
image_pull_policy = "Never"
port {
container_port = 80
}
}
}
}
}
}
resource "kubernetes_service" "frontend" {
metadata {
name = "frontend"
namespace = kubernetes_namespace.myapp.metadata[0].name
}
spec {
type = "NodePort"
selector = {
app = kubernetes_deployment.frontend.spec[0].template[0].metadata[0].labels.app
}
port {
port = 80
target_port = 80
node_port = 30080
}
}
}
ansible directory, create playbook.yml:---
- hosts: localhost
connection: local
become: yes
tasks:
- name: Ensure Docker is installed
apt:
name: docker.io
state: present
when: ansible_os_family == "Debian"
- name: Ensure k3s is installed
shell: curl -sfL https://get.k3s.io | sh -
args:
creates: /usr/local/bin/k3s
- name: Copy k3s config
copy:
src: /etc/rancher/k3s/k3s.yaml
dest: ~/.kube/config
remote_src: yes
owner: ""
group: ""
mode: '0600'
- name: Build frontend Docker image
docker_image:
name: frontend
build:
path: ../frontend
args:
NODE_ENV: production
source: build
force_source: yes
- name: Build backend Docker image
docker_image:
name: backend
build:
path: ../backend
args:
NODE_ENV: production
source: build
force_source: yes
- name: Apply Terraform configuration
terraform:
project_path: ../terraform
state: present
- name: Initialize PostgreSQL database
kubernetes:
definition:
apiVersion: v1
kind: Pod
metadata:
name: postgres-init
namespace: myapp
spec:
containers:
- name: postgres-init
image: postgres:13
command: ["/bin/sh", "-c"]
args:
- psql -h postgres -U postgres -d myapp -c "CREATE TABLE IF NOT EXISTS messages (id SERIAL PRIMARY KEY, text TEXT NOT NULL);"
env:
- name: PGPASSWORD
value: password
register: postgres_init
- name: Wait for PostgreSQL initialization
kubernetes:
api_version: v1
kind: Pod
name: postgres-init
namespace: myapp
wait: yes
wait_condition:
type: Complete
status: "True"
when: postgres_init.changed
Navigate to the project root directory.
Run the Ansible playbook:
ansible-playbook -i localhost, ansible/playbook.yml
This will:
Access the frontend:
Open a web browser and navigate to http://localhost:30080
Test the interaction:
If you encounter issues:
kubectl get pods -n myapp
kubectl logs -n myapp <pod-name>
kubectl port-forward -n myapp service/backend 3000:3000
Then, in another terminal:
curl http://localhost:3000/api/message
kubectl exec -it -n myapp <postgres-pod-name> -- psql -U postgres -d myapp -c "SELECT * FROM messages;"
cd terraform && terraform show
ansible-playbook -i localhost, ansible/playbook.yml --check --diff
cd frontend && npm run build
cd ../backend && npm run build
cat tsconfig.json