@arcote.tech/arc-cli 0.5.1 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2854 -639
- package/package.json +8 -7
- package/src/builder/module-builder.ts +35 -9
- package/src/commands/platform-deploy.ts +143 -0
- package/src/commands/platform-start.ts +4 -1
- package/src/deploy/ansible.ts +69 -0
- package/src/deploy/assets/ansible/site.yml +169 -0
- package/src/deploy/assets/terraform/main.tf +38 -0
- package/src/deploy/assets/terraform/variables.tf +35 -0
- package/src/deploy/assets.ts +282 -0
- package/src/deploy/bootstrap.ts +131 -0
- package/src/deploy/caddyfile.ts +59 -0
- package/src/deploy/compose.ts +73 -0
- package/src/deploy/config.ts +279 -0
- package/src/deploy/remote-state.ts +92 -0
- package/src/deploy/remote-sync.ts +202 -0
- package/src/deploy/ssh.ts +246 -0
- package/src/deploy/survey.ts +172 -0
- package/src/deploy/terraform.ts +109 -0
- package/src/index.ts +12 -0
- package/src/platform/deploy-api.ts +183 -0
- package/src/platform/server.ts +49 -25
- package/src/platform/shared.ts +45 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "CLI tool for Arc framework",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
"build": "bun build --target=bun ./src/index.ts --outdir=dist --external @arcote.tech/arc --external @arcote.tech/arc-ds --external @arcote.tech/arc-react --external @arcote.tech/platform && chmod +x dist/index.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@arcote.tech/arc": "^0.5.
|
|
16
|
-
"@arcote.tech/arc-ds": "^0.5.
|
|
17
|
-
"@arcote.tech/arc-react": "^0.5.
|
|
18
|
-
"@arcote.tech/arc-host": "^0.5.
|
|
19
|
-
"@arcote.tech/arc-adapter-db-sqlite": "^0.5.
|
|
20
|
-
"@arcote.tech/platform": "^0.5.
|
|
15
|
+
"@arcote.tech/arc": "^0.5.5",
|
|
16
|
+
"@arcote.tech/arc-ds": "^0.5.5",
|
|
17
|
+
"@arcote.tech/arc-react": "^0.5.5",
|
|
18
|
+
"@arcote.tech/arc-host": "^0.5.5",
|
|
19
|
+
"@arcote.tech/arc-adapter-db-sqlite": "^0.5.5",
|
|
20
|
+
"@arcote.tech/platform": "^0.5.5",
|
|
21
|
+
"@clack/prompts": "^0.9.0",
|
|
21
22
|
"commander": "^11.1.0",
|
|
22
23
|
"chokidar": "^3.5.3",
|
|
23
24
|
"glob": "^10.3.10",
|
|
@@ -7,12 +7,16 @@ import {
|
|
|
7
7
|
writeFileSync,
|
|
8
8
|
} from "fs";
|
|
9
9
|
import { dirname, join, relative } from "path";
|
|
10
|
+
import type { BuildManifest, ModuleDescriptor } from "@arcote.tech/platform";
|
|
10
11
|
import {
|
|
11
12
|
buildTypeDeclarations,
|
|
12
13
|
type DeclarationResult,
|
|
13
14
|
} from "../utils/build";
|
|
14
15
|
import { i18nExtractPlugin, finalizeTranslations } from "../i18n";
|
|
15
16
|
|
|
17
|
+
/** Re-export for internal CLI consumers (avoid direct platform dependency in consumers). */
|
|
18
|
+
export type { BuildManifest, ModuleDescriptor };
|
|
19
|
+
|
|
16
20
|
/** Clients that a context package is built for. */
|
|
17
21
|
const CONTEXT_CLIENTS = [
|
|
18
22
|
{ name: "server", target: "bun" as const, defines: { ONLY_SERVER: "true", ONLY_BROWSER: "false", ONLY_CLIENT: "false" } },
|
|
@@ -97,14 +101,28 @@ export interface WorkspacePackage {
|
|
|
97
101
|
packageJson: Record<string, any>;
|
|
98
102
|
}
|
|
99
103
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
/** @deprecated use ModuleDescriptor from @arcote.tech/platform */
|
|
105
|
+
export type ModuleEntry = ModuleDescriptor;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* sha256 hex of bytes. Uses Bun.CryptoHasher which is always available in the CLI runtime.
|
|
109
|
+
*/
|
|
110
|
+
export function sha256Hex(bytes: Uint8Array | ArrayBuffer | string): string {
|
|
111
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
112
|
+
hasher.update(bytes as any);
|
|
113
|
+
return hasher.digest("hex");
|
|
103
114
|
}
|
|
104
115
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
116
|
+
/** Hash of concatenated file contents in a directory (stable order). */
|
|
117
|
+
export function sha256OfFiles(paths: readonly string[]): string {
|
|
118
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
119
|
+
const sorted = [...paths].sort();
|
|
120
|
+
for (const p of sorted) {
|
|
121
|
+
if (!existsSync(p)) continue;
|
|
122
|
+
hasher.update(readFileSync(p));
|
|
123
|
+
hasher.update("\0");
|
|
124
|
+
}
|
|
125
|
+
return hasher.digest("hex");
|
|
108
126
|
}
|
|
109
127
|
|
|
110
128
|
/**
|
|
@@ -270,17 +288,25 @@ export async function buildPackages(
|
|
|
270
288
|
const { rmSync } = await import("fs");
|
|
271
289
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
272
290
|
|
|
273
|
-
// Build manifest
|
|
274
|
-
const moduleEntries:
|
|
291
|
+
// Build manifest — hash each module file for deploy diffing & client cache-bust
|
|
292
|
+
const moduleEntries: ModuleDescriptor[] = result.outputs
|
|
275
293
|
.filter((o) => o.kind === "entry-point")
|
|
276
294
|
.map((o) => {
|
|
277
295
|
const file = o.path.split("/").pop()!;
|
|
278
296
|
const safeName = file.replace(/\.js$/, "");
|
|
279
|
-
|
|
297
|
+
const bytes = readFileSync(o.path);
|
|
298
|
+
return {
|
|
299
|
+
file,
|
|
300
|
+
name: fileToName.get(safeName) ?? safeName,
|
|
301
|
+
hash: sha256Hex(bytes),
|
|
302
|
+
};
|
|
280
303
|
});
|
|
281
304
|
|
|
305
|
+
// shellHash / stylesHash are filled in by buildAll() after shell and styles are built.
|
|
282
306
|
const manifest: BuildManifest = {
|
|
283
307
|
modules: moduleEntries,
|
|
308
|
+
shellHash: "",
|
|
309
|
+
stylesHash: "",
|
|
284
310
|
buildTime: new Date().toISOString(),
|
|
285
311
|
};
|
|
286
312
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { bootstrap } from "../deploy/bootstrap";
|
|
4
|
+
import {
|
|
5
|
+
deployConfigExists,
|
|
6
|
+
loadDeployConfig,
|
|
7
|
+
saveDeployConfig,
|
|
8
|
+
} from "../deploy/config";
|
|
9
|
+
import { detectRemoteState } from "../deploy/remote-state";
|
|
10
|
+
import { syncEnv } from "../deploy/remote-sync";
|
|
11
|
+
import { runSurvey } from "../deploy/survey";
|
|
12
|
+
import {
|
|
13
|
+
buildAll,
|
|
14
|
+
err,
|
|
15
|
+
log,
|
|
16
|
+
ok,
|
|
17
|
+
resolveWorkspace,
|
|
18
|
+
} from "../platform/shared";
|
|
19
|
+
|
|
20
|
+
interface PlatformDeployOptions {
|
|
21
|
+
/** Optional env filter from positional arg. */
|
|
22
|
+
env?: string;
|
|
23
|
+
/** Skip local build if artifacts already exist. */
|
|
24
|
+
skipBuild?: boolean;
|
|
25
|
+
/** Force rebuild before deploy. */
|
|
26
|
+
rebuild?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Entry point for `arc platform deploy [env] [--skip-build] [--rebuild]`.
|
|
31
|
+
//
|
|
32
|
+
// High-level flow:
|
|
33
|
+
// 1. resolveWorkspace
|
|
34
|
+
// 2. load or survey deploy.arc.json
|
|
35
|
+
// 3. ensure local build (buildAll unless --skip-build)
|
|
36
|
+
// 4. detectRemoteState → bootstrap if needed
|
|
37
|
+
// 5. for each env (or the one passed as arg): syncEnv
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
export async function platformDeploy(
|
|
41
|
+
envArg: string | undefined,
|
|
42
|
+
options: PlatformDeployOptions = {},
|
|
43
|
+
): Promise<void> {
|
|
44
|
+
const ws = resolveWorkspace();
|
|
45
|
+
|
|
46
|
+
// 1. Load or survey config
|
|
47
|
+
let cfg;
|
|
48
|
+
if (!deployConfigExists(ws.rootDir)) {
|
|
49
|
+
log("No deploy.arc.json found — launching survey.");
|
|
50
|
+
cfg = await runSurvey();
|
|
51
|
+
saveDeployConfig(ws.rootDir, cfg);
|
|
52
|
+
ok("Saved deploy.arc.json");
|
|
53
|
+
}
|
|
54
|
+
cfg = loadDeployConfig(ws.rootDir);
|
|
55
|
+
|
|
56
|
+
// Filter envs if an arg was provided
|
|
57
|
+
const targetEnvs = envArg
|
|
58
|
+
? envArg in cfg.envs
|
|
59
|
+
? [envArg]
|
|
60
|
+
: (() => {
|
|
61
|
+
err(`Unknown env "${envArg}". Known: ${Object.keys(cfg.envs).join(", ")}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
})()
|
|
64
|
+
: Object.keys(cfg.envs);
|
|
65
|
+
|
|
66
|
+
// 2. Ensure local build
|
|
67
|
+
const manifestPath = join(ws.modulesDir, "manifest.json");
|
|
68
|
+
const needBuild = options.rebuild || !existsSync(manifestPath);
|
|
69
|
+
if (needBuild && !options.skipBuild) {
|
|
70
|
+
log("Building platform...");
|
|
71
|
+
await buildAll(ws);
|
|
72
|
+
ok("Build complete");
|
|
73
|
+
} else if (!existsSync(manifestPath)) {
|
|
74
|
+
err("No build found and --skip-build was set.");
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 3. Detect remote state
|
|
79
|
+
log("Inspecting remote server...");
|
|
80
|
+
const state = await detectRemoteState(cfg);
|
|
81
|
+
log(`Remote state: ${state.kind}`);
|
|
82
|
+
|
|
83
|
+
// 4. Bootstrap if needed
|
|
84
|
+
const cliVersion = readCliVersion();
|
|
85
|
+
const configHash = await hashDeployConfig(ws.rootDir);
|
|
86
|
+
if (state.kind !== "ready") {
|
|
87
|
+
await bootstrap({
|
|
88
|
+
cfg,
|
|
89
|
+
rootDir: ws.rootDir,
|
|
90
|
+
state,
|
|
91
|
+
cliVersion,
|
|
92
|
+
configHash,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 5. Sync each env
|
|
97
|
+
for (const env of targetEnvs) {
|
|
98
|
+
log(`Syncing env "${env}"...`);
|
|
99
|
+
const outcome = await syncEnv({
|
|
100
|
+
cfg,
|
|
101
|
+
env,
|
|
102
|
+
ws,
|
|
103
|
+
projectDir: ws.rootDir,
|
|
104
|
+
});
|
|
105
|
+
if (
|
|
106
|
+
outcome.changedModules.length === 0 &&
|
|
107
|
+
!outcome.shellChanged &&
|
|
108
|
+
!outcome.stylesChanged
|
|
109
|
+
) {
|
|
110
|
+
ok(`${env}: already up to date`);
|
|
111
|
+
} else {
|
|
112
|
+
const parts: string[] = [];
|
|
113
|
+
if (outcome.changedModules.length > 0) {
|
|
114
|
+
parts.push(`${outcome.changedModules.length} module(s): ${outcome.changedModules.join(", ")}`);
|
|
115
|
+
}
|
|
116
|
+
if (outcome.shellChanged) parts.push("shell");
|
|
117
|
+
if (outcome.stylesChanged) parts.push("styles");
|
|
118
|
+
ok(`${env}: updated ${parts.join(", ")}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Helpers
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
function readCliVersion(): string {
|
|
128
|
+
try {
|
|
129
|
+
const pkgPath = join(import.meta.dir, "..", "..", "package.json");
|
|
130
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
131
|
+
return pkg.version ?? "unknown";
|
|
132
|
+
} catch {
|
|
133
|
+
return "unknown";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function hashDeployConfig(rootDir: string): Promise<string> {
|
|
138
|
+
const p = join(rootDir, "deploy.arc.json");
|
|
139
|
+
const content = readFileSync(p);
|
|
140
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
141
|
+
hasher.update(content);
|
|
142
|
+
return hasher.digest("hex").slice(0, 16);
|
|
143
|
+
}
|
|
@@ -34,8 +34,10 @@ export async function platformStart(): Promise<void> {
|
|
|
34
34
|
log("No context — server endpoints skipped");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// Start server (production mode =
|
|
37
|
+
// Start server (production mode = aggressive caching, SSE only used for deploy hot-swap)
|
|
38
38
|
const arcEntries = collectArcPeerDeps(ws.packages);
|
|
39
|
+
const deployApi = process.env.ARC_DEPLOY_API === "1";
|
|
40
|
+
if (deployApi) ok("Deploy API enabled (/api/deploy/*)");
|
|
39
41
|
const platform = await startPlatformServer({
|
|
40
42
|
ws,
|
|
41
43
|
port,
|
|
@@ -44,6 +46,7 @@ export async function platformStart(): Promise<void> {
|
|
|
44
46
|
moduleAccess,
|
|
45
47
|
dbPath: join(ws.rootDir, ".arc", "data", "prod.db"),
|
|
46
48
|
devMode: false,
|
|
49
|
+
deployApi,
|
|
47
50
|
arcEntries,
|
|
48
51
|
});
|
|
49
52
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { spawn } from "bun";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { ASSETS, materializeAssets } from "./assets";
|
|
6
|
+
import type { DeployProvisionAnsible, DeployTarget } from "./config";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Runs Ansible from embedded assets. Inventory is generated on the fly,
|
|
10
|
+
// targeting the single host described by DeployTarget. Runs as root on the
|
|
11
|
+
// first bootstrap (via `ansible_user=root`); subsequent runs reuse the
|
|
12
|
+
// `deploy` user created by the playbook.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export interface AnsibleInputs {
|
|
16
|
+
target: DeployTarget;
|
|
17
|
+
ansible?: DeployProvisionAnsible;
|
|
18
|
+
/** On first bootstrap, the fresh VM only has root — so override username. */
|
|
19
|
+
asRoot?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runAnsible(inputs: AnsibleInputs): Promise<void> {
|
|
23
|
+
const workDir = join(tmpdir(), "arc-deploy", `ansible-${Date.now()}`);
|
|
24
|
+
mkdirSync(workDir, { recursive: true });
|
|
25
|
+
await materializeAssets(workDir, ASSETS.ansible);
|
|
26
|
+
|
|
27
|
+
const user = inputs.asRoot ? "root" : inputs.target.user;
|
|
28
|
+
const port = inputs.ansible?.sshPort ?? inputs.target.port;
|
|
29
|
+
|
|
30
|
+
const inventory = [
|
|
31
|
+
"[arc]",
|
|
32
|
+
`${inputs.target.host} ansible_user=${user} ansible_port=${port}`,
|
|
33
|
+
"",
|
|
34
|
+
"[arc:vars]",
|
|
35
|
+
"ansible_ssh_common_args='-o StrictHostKeyChecking=accept-new -o BatchMode=yes'",
|
|
36
|
+
"ansible_python_interpreter=/usr/bin/python3",
|
|
37
|
+
"",
|
|
38
|
+
].join("\n");
|
|
39
|
+
writeFileSync(join(workDir, "inventory.ini"), inventory);
|
|
40
|
+
|
|
41
|
+
const extraVars = [
|
|
42
|
+
`username=${inputs.target.user}`,
|
|
43
|
+
`ssh_port=${port}`,
|
|
44
|
+
];
|
|
45
|
+
if (inputs.ansible?.extraAllowedIps?.length) {
|
|
46
|
+
extraVars.push(
|
|
47
|
+
`extra_allowed_ips=${JSON.stringify(inputs.ansible.extraAllowedIps)}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const proc = spawn({
|
|
52
|
+
cmd: [
|
|
53
|
+
"ansible-playbook",
|
|
54
|
+
"-i",
|
|
55
|
+
"inventory.ini",
|
|
56
|
+
"site.yml",
|
|
57
|
+
"-e",
|
|
58
|
+
extraVars.join(" "),
|
|
59
|
+
],
|
|
60
|
+
cwd: workDir,
|
|
61
|
+
stdout: "inherit",
|
|
62
|
+
stderr: "inherit",
|
|
63
|
+
env: { ...process.env, ANSIBLE_HOST_KEY_CHECKING: "False" },
|
|
64
|
+
});
|
|
65
|
+
const exit = await proc.exited;
|
|
66
|
+
if (exit !== 0) {
|
|
67
|
+
throw new Error(`ansible-playbook failed (exit ${exit})`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Arc platform bootstrap playbook — minimal hardened Docker host.
|
|
3
|
+
# No Portainer / Watchtower / private registry; images travel via docker save|load.
|
|
4
|
+
- name: Bootstrap Arc host
|
|
5
|
+
hosts: all
|
|
6
|
+
become: true
|
|
7
|
+
gather_facts: true
|
|
8
|
+
vars:
|
|
9
|
+
deploy_user: "{{ username | default('deploy') }}"
|
|
10
|
+
ssh_port: "{{ ssh_port | default(22) }}"
|
|
11
|
+
extra_allowed_ips: "{{ extra_allowed_ips | default([]) }}"
|
|
12
|
+
|
|
13
|
+
tasks:
|
|
14
|
+
- name: Update apt cache
|
|
15
|
+
apt:
|
|
16
|
+
update_cache: true
|
|
17
|
+
cache_valid_time: 3600
|
|
18
|
+
|
|
19
|
+
- name: Install base packages
|
|
20
|
+
apt:
|
|
21
|
+
name:
|
|
22
|
+
- ca-certificates
|
|
23
|
+
- curl
|
|
24
|
+
- gnupg
|
|
25
|
+
- ufw
|
|
26
|
+
- fail2ban
|
|
27
|
+
- unattended-upgrades
|
|
28
|
+
- python3-docker
|
|
29
|
+
state: present
|
|
30
|
+
|
|
31
|
+
- name: Create deploy user
|
|
32
|
+
user:
|
|
33
|
+
name: "{{ deploy_user }}"
|
|
34
|
+
shell: /bin/bash
|
|
35
|
+
groups: sudo
|
|
36
|
+
append: true
|
|
37
|
+
create_home: true
|
|
38
|
+
state: present
|
|
39
|
+
|
|
40
|
+
- name: Copy SSH key from root to deploy user
|
|
41
|
+
shell: |
|
|
42
|
+
mkdir -p /home/{{ deploy_user }}/.ssh
|
|
43
|
+
cp /root/.ssh/authorized_keys /home/{{ deploy_user }}/.ssh/authorized_keys
|
|
44
|
+
chown -R {{ deploy_user }}:{{ deploy_user }} /home/{{ deploy_user }}/.ssh
|
|
45
|
+
chmod 700 /home/{{ deploy_user }}/.ssh
|
|
46
|
+
chmod 600 /home/{{ deploy_user }}/.ssh/authorized_keys
|
|
47
|
+
args:
|
|
48
|
+
creates: "/home/{{ deploy_user }}/.ssh/authorized_keys"
|
|
49
|
+
|
|
50
|
+
- name: Passwordless sudo for deploy user
|
|
51
|
+
copy:
|
|
52
|
+
dest: /etc/sudoers.d/99-{{ deploy_user }}
|
|
53
|
+
content: "{{ deploy_user }} ALL=(ALL) NOPASSWD:ALL\n"
|
|
54
|
+
mode: "0440"
|
|
55
|
+
validate: "visudo -cf %s"
|
|
56
|
+
|
|
57
|
+
- name: Harden sshd
|
|
58
|
+
lineinfile:
|
|
59
|
+
path: /etc/ssh/sshd_config
|
|
60
|
+
regexp: "{{ item.re }}"
|
|
61
|
+
line: "{{ item.line }}"
|
|
62
|
+
state: present
|
|
63
|
+
loop:
|
|
64
|
+
- { re: "^#?PermitRootLogin", line: "PermitRootLogin no" }
|
|
65
|
+
- { re: "^#?PasswordAuthentication", line: "PasswordAuthentication no" }
|
|
66
|
+
- { re: "^#?PubkeyAuthentication", line: "PubkeyAuthentication yes" }
|
|
67
|
+
- { re: "^#?MaxAuthTries", line: "MaxAuthTries 3" }
|
|
68
|
+
notify: restart ssh
|
|
69
|
+
|
|
70
|
+
- name: Install Docker via official convenience script
|
|
71
|
+
shell: |
|
|
72
|
+
curl -fsSL https://get.docker.com | sh
|
|
73
|
+
args:
|
|
74
|
+
creates: /usr/bin/docker
|
|
75
|
+
|
|
76
|
+
- name: Enable and start docker
|
|
77
|
+
systemd:
|
|
78
|
+
name: docker
|
|
79
|
+
enabled: true
|
|
80
|
+
state: started
|
|
81
|
+
|
|
82
|
+
- name: Add deploy user to docker group
|
|
83
|
+
user:
|
|
84
|
+
name: "{{ deploy_user }}"
|
|
85
|
+
groups: docker
|
|
86
|
+
append: true
|
|
87
|
+
|
|
88
|
+
- name: Configure docker log rotation
|
|
89
|
+
copy:
|
|
90
|
+
dest: /etc/docker/daemon.json
|
|
91
|
+
content: |
|
|
92
|
+
{
|
|
93
|
+
"log-driver": "json-file",
|
|
94
|
+
"log-opts": {
|
|
95
|
+
"max-size": "10m",
|
|
96
|
+
"max-file": "3"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
mode: "0644"
|
|
100
|
+
notify: restart docker
|
|
101
|
+
|
|
102
|
+
- name: Ensure /opt/arc exists
|
|
103
|
+
file:
|
|
104
|
+
path: /opt/arc
|
|
105
|
+
state: directory
|
|
106
|
+
owner: "{{ deploy_user }}"
|
|
107
|
+
group: "{{ deploy_user }}"
|
|
108
|
+
mode: "0755"
|
|
109
|
+
|
|
110
|
+
- name: Configure ufw defaults
|
|
111
|
+
ufw:
|
|
112
|
+
policy: "{{ item.policy }}"
|
|
113
|
+
direction: "{{ item.dir }}"
|
|
114
|
+
loop:
|
|
115
|
+
- { policy: deny, dir: incoming }
|
|
116
|
+
- { policy: allow, dir: outgoing }
|
|
117
|
+
|
|
118
|
+
- name: Open firewall ports
|
|
119
|
+
ufw:
|
|
120
|
+
rule: allow
|
|
121
|
+
port: "{{ item }}"
|
|
122
|
+
proto: tcp
|
|
123
|
+
loop:
|
|
124
|
+
- "{{ ssh_port }}"
|
|
125
|
+
- "80"
|
|
126
|
+
- "443"
|
|
127
|
+
|
|
128
|
+
- name: Enable ufw
|
|
129
|
+
ufw:
|
|
130
|
+
state: enabled
|
|
131
|
+
|
|
132
|
+
- name: Configure fail2ban for sshd
|
|
133
|
+
copy:
|
|
134
|
+
dest: /etc/fail2ban/jail.local
|
|
135
|
+
content: |
|
|
136
|
+
[sshd]
|
|
137
|
+
enabled = true
|
|
138
|
+
port = {{ ssh_port }}
|
|
139
|
+
maxretry = 5
|
|
140
|
+
findtime = 600
|
|
141
|
+
bantime = 3600
|
|
142
|
+
ignoreip = 127.0.0.1/8 ::1 {{ extra_allowed_ips | join(' ') }}
|
|
143
|
+
mode: "0644"
|
|
144
|
+
notify: restart fail2ban
|
|
145
|
+
|
|
146
|
+
- name: Enable unattended upgrades
|
|
147
|
+
copy:
|
|
148
|
+
dest: /etc/apt/apt.conf.d/20auto-upgrades
|
|
149
|
+
content: |
|
|
150
|
+
APT::Periodic::Update-Package-Lists "1";
|
|
151
|
+
APT::Periodic::Unattended-Upgrade "1";
|
|
152
|
+
APT::Periodic::AutocleanInterval "7";
|
|
153
|
+
mode: "0644"
|
|
154
|
+
|
|
155
|
+
handlers:
|
|
156
|
+
- name: restart ssh
|
|
157
|
+
systemd:
|
|
158
|
+
name: ssh
|
|
159
|
+
state: restarted
|
|
160
|
+
|
|
161
|
+
- name: restart docker
|
|
162
|
+
systemd:
|
|
163
|
+
name: docker
|
|
164
|
+
state: restarted
|
|
165
|
+
|
|
166
|
+
- name: restart fail2ban
|
|
167
|
+
systemd:
|
|
168
|
+
name: fail2ban
|
|
169
|
+
state: restarted
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
terraform {
|
|
2
|
+
required_providers {
|
|
3
|
+
hcloud = {
|
|
4
|
+
source = "hetznercloud/hcloud"
|
|
5
|
+
version = "~> 1.51"
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
provider "hcloud" {
|
|
11
|
+
token = var.hcloud_token
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
resource "hcloud_ssh_key" "deploy" {
|
|
15
|
+
name = "${var.server_name}-deploy"
|
|
16
|
+
public_key = file(var.ssh_public_key)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
resource "hcloud_server" "arc" {
|
|
20
|
+
name = var.server_name
|
|
21
|
+
image = var.server_image
|
|
22
|
+
server_type = var.server_type
|
|
23
|
+
location = var.server_location
|
|
24
|
+
ssh_keys = [hcloud_ssh_key.deploy.id]
|
|
25
|
+
|
|
26
|
+
public_net {
|
|
27
|
+
ipv4_enabled = true
|
|
28
|
+
ipv6_enabled = true
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
output "server_ip" {
|
|
33
|
+
value = hcloud_server.arc.ipv4_address
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
output "server_name" {
|
|
37
|
+
value = hcloud_server.arc.name
|
|
38
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
variable "hcloud_token" {
|
|
2
|
+
description = "Hetzner Cloud API token"
|
|
3
|
+
type = string
|
|
4
|
+
sensitive = true
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
variable "server_name" {
|
|
8
|
+
description = "Name of the Hetzner server (shown in the console)"
|
|
9
|
+
type = string
|
|
10
|
+
default = "arc-platform"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
variable "server_type" {
|
|
14
|
+
description = "Hetzner server type (cx22, cx32, cx42, ...)"
|
|
15
|
+
type = string
|
|
16
|
+
default = "cx32"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
variable "server_location" {
|
|
20
|
+
description = "Hetzner datacenter location (nbg1, fsn1, hel1, ...)"
|
|
21
|
+
type = string
|
|
22
|
+
default = "nbg1"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
variable "server_image" {
|
|
26
|
+
description = "OS image"
|
|
27
|
+
type = string
|
|
28
|
+
default = "ubuntu-22.04"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
variable "ssh_public_key" {
|
|
32
|
+
description = "Path to the public key uploaded to the server"
|
|
33
|
+
type = string
|
|
34
|
+
default = "~/.ssh/id_ed25519.pub"
|
|
35
|
+
}
|