feat: migrate from Heroku to Google Cloud Platform

This commit is contained in:
Josh Sandlin
2025-09-04 15:41:17 -04:00
parent 4366d098e0
commit 59fbd0ebbe
29 changed files with 540 additions and 3369 deletions

View File

@@ -3,11 +3,6 @@ version: 2
updates:
# # Npm
- package-ecosystem: "npm"
directory: "/cloudflare"
schedule:
interval: "daily"
- package-ecosystem: "npm"
directory: "/hooks"
schedule:

View File

@@ -1,57 +0,0 @@
name: Deploy to Heroku
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
env:
HEROKU_API_KEY: ${{secrets.HEROKU_API_KEY}}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: install heroku cli
run: curl https://cli-assets.heroku.com/install-ubuntu.sh | sh
- name: heroku registry login
run: heroku container:login
- name: build registry
run: docker build -t registry.heroku.com/ttlsh/web registry
- name: push registry
run: docker push registry.heroku.com/ttlsh/web
- name: release registry
run: heroku container:release -a ttlsh web
- name: build nginx
run: docker build -t registry.heroku.com/ttlsh-router/web nginx
- name: push nginx
run: docker push registry.heroku.com/ttlsh-router/web
- name: release nginx
run: heroku container:release -a ttlsh-router web
- name: build hooks
run: docker build -f hooks/Dockerfile.hooks -t registry.heroku.com/ttlsh-hooks/web hooks
- name: push hooks
run: docker push registry.heroku.com/ttlsh-hooks/web
- name: release hooks
run: heroku container:release -a ttlsh-hooks web
- name: build reaper
run: docker build -f hooks/Dockerfile.reap -t registry.heroku.com/ttlsh-hooks/reap hooks
- name: push reaper
run: docker push registry.heroku.com/ttlsh-hooks/reap
- name: release reaper
run: heroku container:release -a ttlsh-hooks reap

2
.gitignore vendored
View File

@@ -1 +1,3 @@
deploy
terraform/staging/knowledge/*
.terraform

38
DEPLOYING.md Normal file
View File

@@ -0,0 +1,38 @@
# Deploying ttl.sh manually
## Hetzner Resources via Terraform
1. Doppler is installed and authenticated for use with the `ttl-sh` project
a. Have a valid `DOPPLER_TOKEN` from `ttl-sh` set
b. `echo $DOPPLER_TOKEN | doppler configure set token --scope /`
2. Export necessary doppler values to ENVs
a. `export HCLOUD_TOKEN=$(doppler secrets get HCLOUD_TOKEN --plain)`
b. `export AWS_ACCESS_KEY_ID=$(doppler secrets get HCLOUD_S3_ACCESS_KEY --plain)`
c. `export AWS_SECRET_ACCESS_KEY=$(doppler secrets get HCLOUD_S3_SECRET_KEY --plain)`
3. `cd terraform`
4. `terraform plan`
a. Make sure plan looks as expected
5. `terraform apply`
a. approve with `yes`
6. Successful run states `Apply complete!`
**NOTE**: For extra keys to be associated they must be added in the ttl-sh project. This is crucial to enable proper execution of installers.
## Build ttl.sh Docker images
1. Docker and Docker Compose are installed
2. `gcloud` CLI is installed and authenticated
3. `gcloud auth configure-docker us-east4-docker.pkg.dev`
4. `./build-and-push.sh`
## Server Setup and ttl.sh workloads
1. Doppler is installed and authenticated for use with the `ttl-sh` project
a. Have a valid `DOPPLER_TOKEN` from `ttl-sh` set
b. `echo $DOPPLER_TOKEN | doppler configure set token --scope /`
2. `./install.sh`
3. Successful run states:
a. `Container ttlsh-redis Started`
b. `Container ttlsh-hooks Started`
b. `Container ttlsh-reaper Started`
c. `Container ttlsh-registry Started`
d. `Container ttlsh-nginx Started`

6
build-and-push.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# builds, tags, and pushes the images referenced in docker-compose.yaml
docker compose build
docker compose push

View File

@@ -1,2 +0,0 @@
dist/
node_modules

View File

@@ -1,22 +0,0 @@
.PHONY: deps
deps:
yarn global add node-gyp
yarn --silent --frozen-lockfile
.PHONY: prebuild
prebuild:
rm -rf build
mkdir -p build
.PHONY: lint
lint:
npx tslint --project ./tsconfig.json --fix
.PHONY: build
build: prebuild
`yarn bin`/webpack
.PHONY: preview
preview: build
`yarn bin`/workers-preview < dist/bundle.js

View File

@@ -1,30 +0,0 @@
{
"name": "ttlsh-cloudflare-worker",
"version": "0.0.1",
"description": "Cloudflare Workers for ttl.sh",
"main": "index.js",
"scripts": {
"test": "make test"
},
"repository": {
"type": "git",
"url": "git+https://github.com/replicatedhq/ttl.sh.git"
},
"author": "",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/replicatedhq/ttl.sh/issues"
},
"homepage": "https://github.com/replicatedhq/ttl.sh#readme",
"devDependencies": {
"awesome-typescript-loader": "^5.2.1",
"typescript": "^3.4.5",
"webpack": "^4.31.0",
"webpack-cli": "^3.3.2",
"workers-preview": "^1.0.6"
},
"dependencies": {
"moment": "^2.24.0",
"parse-duration": "^0.1.1"
}
}

View File

@@ -1,45 +0,0 @@
if (typeof addEventListener === 'function') {
addEventListener('fetch', (e: Event): void => {
// work around as strict typescript check doesn't allow e to be of type FetchEvent
const fe = e as FetchEvent
fe.respondWith(proxyRequest(fe.request))
});
}
async function proxyRequest(r: Request): Promise<Response> {
const url = new URL(r.url);
if (!isRegistryRequest(r)) {
return fetch(`https://friendly-goldstine-7897fc.netlify.com/${url.pathname}`);
}
if (isCatalogRequest(r)) {
return new Response(null, {
status: 401,
});
}
// Removing this code for now. It causes Docker to exit with a 1 exit code :(
// const tagManifestParams = await tryParsePutTagRequest(r.method, url);
// if (tagManifestParams) {
// return handleTagManifestRequest(r, tagManifestParams);
// }
return fetch(r);
}
function isRegistryRequest(r: Request): boolean {
const url = new URL(r.url);
return url.pathname.startsWith(`/v2`);
}
function isCatalogRequest(r: Request): boolean {
const url = new URL(r.url);
return url.pathname.endsWith(`v2/_catalog`);
}
interface FetchEvent extends Event {
request: Request;
respondWith(r: Promise<Response> | Response): Promise<Response>;
}

View File

@@ -1,79 +0,0 @@
import { TagManifestParams } from "./";
import * as parseDuration from "parse-duration";
import * as moment from "moment";
const durationRegex = /^(?=\d+[ywdhms])(( ?\d+y)?(?!\d))?(( ?\d+w)?(?!\d))?(( ?\d+d)?(?!\d))?(( ?\d+h)?(?!\d))?(( ?\d+m)?(?!\d))?(( ?\d+s)?(?!\d))?( ?\d+ms)?$/;
export function tryParsePutTagRequest(method: string, url: URL): TagManifestParams | void {
const split = url.pathname.split("/");
// v2/ns/image/manifests/tag <- namespaced images
// v2image/manifests/tag <- library images
if (method !== "PUT") {
return;
}
if (split.length === 6 && split[4] === "manifests") {
const tagManifestParams: TagManifestParams = {
namespace: split[2],
name: split[3],
tag: split[5],
};
return tagManifestParams;
}
if (split.length === 5 && split[3] === "manifests") {
const tagManifestParams: TagManifestParams = {
namespace: "",
name: split[2],
tag: split[4],
};
return tagManifestParams;
}
return;
}
export async function handleTagManifestRequest(r: Request, params: TagManifestParams): Promise<Response> {
r.headers["Accept"] = "application/vnd.docker.distribution.manifest.v2+json";
const upstreamResponse = await fetch(r);
if (upstreamResponse.status != 201) {
return upstreamResponse;
}
let imageName;
if (params.namespace === "") {
imageName = params.name;
} else {
imageName = `${params.namespace}/${params.name}`;
}
const teapotResponse = {
"errors": [
{
"message": `\r \nimage ttl.sh/${imageName} is available now and will be automatically deleted ${expirationFromTag(params.tag)}\n\ttl.sh is contributed by Replicated (www.replicated.com)`
}
]
};
return new Response(JSON.stringify(teapotResponse), {
status: 418, // This is what works /shrug
headers: {
"Content-Type": "application/json",
},
});
}
function expirationFromTag(tag: string): string {
let parsed = parseDuration(tag);
if (!durationRegex.test(tag)) {
// invalid duration, default to 24h
parsed = 24 * 60 * 60 * 1000;
}
const now = new Date();
const then = moment(now.getTime() + parsed);
return then.fromNow();
}

View File

@@ -1,2 +0,0 @@
export * from "./tag_manifest";
export * from "./handler";

View File

@@ -1,5 +0,0 @@
export interface TagManifestParams {
namespace: string;
name: string;
tag: string;
}

View File

@@ -1,18 +0,0 @@
import { tryParsePutTagRequest } from "./handler";
import * as chai from "chai";
const testTryParsePutRequest = async () => {
let result = await tryParsePutTagRequest("PUT", new URL("v2/random/more-random/manifests/2h"));
expect(result).to.be.defined();
expect(result.namespace).to.equal("random");
expect(result.name).to.equal("more-random");
expect(result.tag).to.equal("2h");
result = await tryParsePutTagRequest("PUT", new URL("v2/image-name/manifests/2h"));
expect(result).to.be.defined();
expect(result.namespace).to.equal("");
expect(result.name).to.equal("image-name");
expect(result.tag).to.equal("2h");
}

View File

@@ -1,27 +0,0 @@
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"lib": ["es6", "dom"],
"noImplicitAny": false,
"noEmitOnError": true,
"sourceMap": true,
"preserveConstEnums": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictNullChecks": true,
"skipLibCheck": true,
"allowJs": true,
"outDir": "build",
"pretty": true
},
"include": [
"src/**/*.js",
"src/**/*.ts"
],
"exclude": [
"node_modules",
"src/**/*_test.ts",
"src/test/**/*.ts"
]
}

View File

@@ -1,40 +0,0 @@
const path = require("path")
const webpack = require('webpack');
module.exports = {
entry: {
bundle: path.join(__dirname, "./src/index.ts"),
},
output: {
filename: "bundle.js",
path: path.join(__dirname, "dist"),
},
mode: process.env.NODE_ENV || "development",
plugins: [
// Ignore all locale files of moment.js
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],
watchOptions: {
ignored: /node_modules|dist|\.js/g,
},
devtool: "cheap-module-source-map",
resolve: {
extensions: [".ts", ".tsx", ".js", ".json"],
plugins: [],
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "awesome-typescript-loader",
},
],
},
}

File diff suppressed because it is too large Load Diff

94
docker-compose.yaml Normal file
View File

@@ -0,0 +1,94 @@
services:
registry:
image: us-east4-docker.pkg.dev/rgstry/ttl-sh-registry/registry:latest
build:
context: registry
dockerfile: Dockerfile
platforms:
- "linux/amd64"
container_name: ttlsh-registry
restart: always
pull_policy: always
env_file: ".env"
environment:
- PORT=5000
ports:
- "5000:5000"
- "5001:5001"
networks:
- ttlsh-network
hooks:
image: us-east4-docker.pkg.dev/rgstry/ttl-sh-hooks/hooks:latest
build:
context: hooks
dockerfile: Dockerfile.hooks
platforms:
- "linux/amd64"
container_name: ttlsh-hooks
restart: always
pull_policy: always
env_file: ".env"
environment:
- LOG_LEVEL=${LOG_LEVEL:-info}
- NODE_ENV=${NODE_ENV:-production}
- PORT=8000
ports:
- "8000:8000"
networks:
- ttlsh-network
reaper:
image: us-east4-docker.pkg.dev/rgstry/ttl-sh-reap/reaper:latest
build:
context: hooks
dockerfile: Dockerfile.reap
platforms:
- "linux/amd64"
container_name: ttlsh-reaper
restart: always
pull_policy: always
env_file: ".env"
environment:
- LOG_LEVEL=${LOG_LEVEL:-info}
- NODE_ENV=${NODE_ENV:-production}
networks:
- ttlsh-network
redis:
image: redis:7-alpine
container_name: ttlsh-redis
restart: always
pull_policy: always
env_file: ".env"
ports:
- "6379:6379"
networks:
- ttlsh-network
volumes:
- ./redis/data:/data
command: redis-server --requirepass ${REDIS_PASSWORD}
nginx:
image: us-east4-docker.pkg.dev/rgstry/ttl-sh-nginx/nginx:latest
build:
context: nginx
dockerfile: Dockerfile
platforms:
- "linux/amd64"
container_name: ttlsh-nginx
restart: always
pull_policy: always
environment:
- PORT=80
ports:
- "80:80"
- "443:443"
depends_on:
- registry
networks:
- ttlsh-network
networks:
ttlsh-network:
driver: bridge

27
install.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env bash
REMOTE_HOST=178.156.198.215
REMOTE_USER=root
INSTALL_DIR=/opt/ttlsh
function setup_docker_compose() {
ssh $REMOTE_USER@$REMOTE_HOST "mkdir -p $INSTALL_DIR"
scp docker-compose.yaml $REMOTE_USER@$REMOTE_HOST:$INSTALL_DIR/docker-compose.yaml
}
function setup_provisioner_script() {
scp provision.sh $REMOTE_USER@$REMOTE_HOST:$INSTALL_DIR/provision.sh
ssh $REMOTE_USER@$REMOTE_HOST "chmod a+x $INSTALL_DIR/provision.sh"
}
function run_provisioner_script() {
ssh $REMOTE_USER@$REMOTE_HOST "bash $INSTALL_DIR/provision.sh $1"
}
function install() {
setup_docker_compose
setup_provisioner_script
run_provisioner_script $DOPPLER_TOKEN
}
install

View File

@@ -3,11 +3,8 @@ http {
include mime.types;
default_type application/octet-stream;
proxy_ssl_server_name on;
resolver 8.8.8.8;
upstream upstream_ttlsh {
server ttlsh.herokuapp.com:443;
upstream registry {
server ttlsh-registry:5000;
}
server {
@@ -25,12 +22,17 @@ http {
location /v2 {
client_max_body_size 10000m;
set $upstream upstream_ttlsh;
proxy_pass https://$upstream;
proxy_ssl_name ttlsh.herokuapp.com;
proxy_set_header x-forwarded-host $host;
client_body_timeout 3600s;
proxy_connect_timeout 300s;
proxy_send_timeout 3600s;
proxy_read_timeout 3600s;
send_timeout 3600s;
proxy_pass http://registry;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host ttlsh.herokuapp.com;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_request_buffering off;
}
}

View File

@@ -170,7 +170,7 @@
<div class="container col-12">
<nav class="mb10"><a target="_blank" href="http://www.replicated.com/" class="nav-link secondary-color">Contributed by Replicated</a></nav>
<div><a href="https://twitter.com/replicatedhq" target="_blank" class="socialicons accent-bg twitter"></a><a href="https://github.com/replicatedhq/ttl.sh" target="_blank" class="socialicons accent-bg github"></a></div>
<div class="mt50"><span class="span secondary-color">©</span> <span class="span secondary-color">Replicated Inc 2022</span></div>
<div class="mt50"><span class="span secondary-color">©</span> <span class="span secondary-color">Replicated Inc 2025</span></div>
</div>
</footer>

94
provision.sh Normal file
View File

@@ -0,0 +1,94 @@
#!/usr/bin/env bash
DOPPLER_TOKEN=$1
function setup_doppler() {
# install for ubuntu: https://docs.doppler.com/docs/cli
sudo apt-get update && sudo apt-get install -y apt-transport-https ca-certificates curl gnupg
curl -sLf --retry 3 --tlsv1.2 --proto "=https" 'https://packages.doppler.com/public/cli/gpg.DE2A7741A397C129.key' | sudo gpg --dearmor -o /usr/share/keyrings/doppler-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/doppler-archive-keyring.gpg] https://packages.doppler.com/public/cli/deb/debian any-version main" | sudo tee /etc/apt/sources.list.d/doppler-cli.list
sudo apt-get update && sudo apt-get install doppler
# authenticate with service token
echo $DOPPLER_TOKEN | doppler configure set token --scope /
}
setup_docker() {
# Adapted from:
# * https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
# install necessary packages
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
}
setup_gcloud() {
# Adapted from:
# * https://cloud.google.com/sdk/docs/downloads-interactive
# * Artifact Registry setup commands
INSTALL_PATH="$(doppler secrets get GOOGLE_APPLICATION_CREDENTIALS --plain)"
INSTALL_EMAIL="$(doppler secrets get GOOGLE_APPLICATION_CREDENTIALS_EMAIL --plain)"
REGISTRY_URI="$(doppler secrets get GOOGLE_CLOUD_ARTIFACT_REGISTRY_URI --plain)"
mkdir -p "$(dirname "$INSTALL_PATH")"
doppler secrets get GOOGLE_APPLICATION_CREDENTIALS_JSON --plain >"$INSTALL_PATH"
chmod 600 "$INSTALL_PATH"
echo "export GOOGLE_APPLICATION_CREDENTIALS=\"$INSTALL_PATH\"" >> "$HOME/.bashrc"
# download the installer
curl https://sdk.cloud.google.com | bash
# Explicitly add gcloud to PATH for this session
export PATH="$HOME/google-cloud-sdk/bin:$PATH"
gcloud auth activate-service-account "$INSTALL_EMAIL" --key-file="$INSTALL_PATH" --quiet
gcloud auth configure-docker "$REGISTRY_URI" --quiet
}
setup_ttlsh_env() {
for VAR in \
GCS_KEY_ENCODED \
HOOK_TOKEN \
HOOK_URI \
REDIS_PASSWORD \
REDISCLOUD_URL \
REPLREG_HOST \
REPLREG_SECRET \
REGISTRY_URL
do
echo "$VAR=$(doppler secrets get "$VAR" --plain)" >> /opt/ttlsh/.env
done
chmod 600 "$ENV_FILE"
}
function run_ttlsh()
{
docker compose -f /opt/ttlsh/docker-compose.yaml pull
docker compose -f /opt/ttlsh/docker-compose.yaml up -d
}
function provision() {
setup_doppler
setup_docker
setup_gcloud
setup_ttlsh_env
run_ttlsh
}
provision

View File

@@ -2,7 +2,6 @@ FROM registry:2.7.1
ADD ./entrypoint.sh /entrypoint.sh
ADD ./config.yml /etc/docker/registry/config.yml
ADD ./garbage-collect.sh /garbage-collect.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/etc/docker/registry/config.yml"]

View File

@@ -2,6 +2,7 @@ version: 0.1
log:
level: info
formatter: text
storage:
delete:
@@ -16,14 +17,20 @@ http:
addr: 0.0.0.0:__PORT__
secret: __REPLREG_SECRET__
host: __REPLREG_HOST__
debug:
addr: 0.0.0.0:5001
prometheus:
enabled: false
notifications:
events:
includereferences: true
endpoints:
- name: rgstry-hooks
- name: registry-hooks
disabled: false
url: __HOOK_URI__
headers:
Authorization: ["Token __HOOK_TOKEN__"]
timeout: 200ms
threshold: 3
backoff: 5s
timeout: 1s
threshold: 10
backoff: 1s

View File

@@ -1,7 +0,0 @@
#!/bin/sh
while true; do
sleep 1m
echo "Starting garbage collection..."
registry garbage-collect /etc/docker/registry/config.yml || true
done

24
terraform/.terraform.lock.hcl generated Normal file
View File

@@ -0,0 +1,24 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hetznercloud/hcloud" {
version = "1.52.0"
constraints = "~> 1.45"
hashes = [
"h1:8Juiz/B0XWpSCJmIYLBoGqU14R0W9rudwVInfd7jBt0=",
"zh:1e9bb6b6a2ea5f441638dbae2d60fbe04ff455f58a18c740b8b7913e2197d875",
"zh:29c122e404ba331cfbadacc7f1294de5a31c9dfd60bdfe3e1b402271fc8e419c",
"zh:2bd0ae2f0bb9f16b7753f59a08e57ac7230f9c471278d7882f81406b9426c8c7",
"zh:4383206971873f6b5d81580a9a36e0158924f5816ebb6206b0cf2430e4e6a609",
"zh:47e2ca1cfa18500e4952ab51dc357a0450d00a92da9ea03e452f1f3efe6bbf75",
"zh:8e9fe90e3cea29bb7892b64da737642fc22b0106402df76c228a3cbe99663278",
"zh:a2d69350a69c471ddb63bcc74e105e585319a0fc0f4d1b7f70569f6d2ece5824",
"zh:a97abcc254e21c294e2d6b0fc9068acfd63614b097dda365f1c56ea8b0fd5f6b",
"zh:aba8d72d4fe2e89c922d5446d329e5c23d00b28227b4666e6486ba18ea2ec278",
"zh:ad36c333978c2d9e4bc43dcadcbff42fe771a8c5ef53d028bcacec8287bf78a7",
"zh:cdb1e6903b9d2f0ad8845d4eb390fbe724ee2435fb045baeab38d4319e637682",
"zh:df77b08757f3f36b8aadb33d73362320174047044414325c56a87983f48b5186",
"zh:e07513d5ad387247092b5ae1c87e21a387fc51873b3f38eee616187e38b090a7",
"zh:e2be02bdc59343ff4b9e26c3b93db7680aaf3e6ed13c8c4c4b144c74c2689915",
]
}

148
terraform/main.tf Normal file
View File

@@ -0,0 +1,148 @@
# Data Sources to query available locations
data "hcloud_locations" "all" {}
# Select a specific location (you can change the filter criteria)
data "hcloud_location" "main" {
name = var.location
}
# Query all available SSH keys in the account
data "hcloud_ssh_keys" "all_keys" {}
# Centralized label management
locals {
# Common labels for all resources
common_labels = merge({
service = var.service_name
environment = var.environment
}, var.common_labels)
# Compute tier labels (merged with common)
compute_labels = merge(local.common_labels, var.compute_tier_labels)
}
# Network Configuration
resource "hcloud_network" "main_network" {
name = "${var.service_name}-network"
ip_range = var.network_cidr
}
# Private Subnet
resource "hcloud_network_subnet" "main_subnet" {
network_id = hcloud_network.main_network.id
type = "cloud"
network_zone = data.hcloud_location.main.network_zone
ip_range = var.subnet_cidr
}
# Firewall Configuration
resource "hcloud_firewall" "main_firewall" {
name = "${var.service_name}-firewall"
# Allow ICMP (ping)
rule {
direction = "in"
protocol = "icmp"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# Allow HTTP
rule {
direction = "in"
protocol = "tcp"
port = "80"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# Allow HTTPS
rule {
direction = "in"
protocol = "tcp"
port = "443"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
# Allow SSH (optional but usually needed)
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}
}
# Compute Server Instance (can be scaled to multiple instances)
resource "hcloud_server" "instance1" {
name = "${var.service_name}-compute-1" # Named to indicate it's a compute node
image = var.server_image
server_type = var.server_type
location = data.hcloud_location.main.name
firewall_ids = [hcloud_firewall.main_firewall.id]
ssh_keys = data.hcloud_ssh_keys.all_keys.ssh_keys[*].id
labels = local.compute_labels
network {
network_id = hcloud_network.main_network.id
# IP will be auto-assigned from the subnet
}
public_net {
ipv4_enabled = true # Public IP for SSH management access
ipv6_enabled = false
}
depends_on = [
hcloud_network_subnet.main_subnet
]
}
# Outputs for debugging and DNS configuration
output "server_primary_ip" {
value = hcloud_server.instance1.ipv4_address
description = "The server's primary IPv4 address to use for ttl.sh DNS A record"
}
output "server_ipv6" {
value = hcloud_server.instance1.ipv6_address
description = "The server's IPv6 address"
}
output "selected_location" {
value = {
name = data.hcloud_location.main.name
description = data.hcloud_location.main.description
network_zone = data.hcloud_location.main.network_zone
}
description = "Currently selected location details"
}
output "ssh_keys_used" {
value = {
for key in data.hcloud_ssh_keys.all_keys.ssh_keys :
key.name => key.fingerprint
}
description = "SSH keys that will be added to the server"
}
output "server_details" {
value = {
name = hcloud_server.instance1.name
private_ip = hcloud_server.instance1.network[*].ip
labels = hcloud_server.instance1.labels
}
description = "Details of the deployed server instance"
}

10
terraform/provider.tf Normal file
View File

@@ -0,0 +1,10 @@
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.45"
}
}
}
provider "hcloud" {}

10
terraform/terraform.tf Normal file
View File

@@ -0,0 +1,10 @@
terraform {
backend "s3" {
bucket = "ttl-sh-tf-state"
key = "terraform.tfstate"
endpoint = "hel1.your-objectstorage.com"
region = "eu-central1"
skip_credentials_validation = true
skip_region_validation = true
}
}

62
terraform/variables.tf Normal file
View File

@@ -0,0 +1,62 @@
# Service Configuration
variable "service_name" {
description = "Name of the service"
type = string
default = "ttl-sh"
}
variable "environment" {
description = "Environment name (staging, production)"
type = string
default = "production"
}
# Network Configuration
variable "network_cidr" {
description = "CIDR range for the main network"
type = string
default = "10.0.0.0/16"
}
variable "subnet_cidr" {
description = "CIDR range for the subnet"
type = string
default = "10.0.0.0/24"
}
# Compute Configuration
variable "server_image" {
description = "Image to use for servers"
type = string
default = "ubuntu-24.04"
}
variable "server_type" {
description = "Server type for compute instances"
type = string
default = "cpx41"
}
# Location Configuration
variable "location" {
description = "Hetzner Cloud location"
type = string
default = "ash"
}
# Label Configuration
variable "common_labels" {
description = "Common labels to apply to all resources"
type = map(string)
default = {}
}
variable "compute_tier_labels" {
description = "Additional labels for compute tier resources"
type = map(string)
default = {
tier = "compute"
role = "worker"
}
}