add release orchestrator docs and sprints gaps fills
This commit is contained in:
427
docs/modules/release-orchestrator/deployment/agentless.md
Normal file
427
docs/modules/release-orchestrator/deployment/agentless.md
Normal file
@@ -0,0 +1,427 @@
|
||||
# Agentless Deployment (SSH/WinRM)
|
||||
|
||||
> Agentless deployment using SSH and WinRM for remote execution without installing agents.
|
||||
|
||||
**Status:** Planned (not yet implemented)
|
||||
**Source:** [Architecture Advisory Section 10.4](../../../product/advisories/09-Jan-2026%20-%20Stella%20Ops%20Orchestrator%20Architecture.md)
|
||||
**Related Modules:** [Agents Module](../modules/agents.md), [Deploy Orchestrator](../modules/deploy-orchestrator.md)
|
||||
**Sprints:** [108_004 SSH Agent](../../../../implplan/SPRINT_20260110_108_004_AGENTS_ssh.md), [108_005 WinRM Agent](../../../../implplan/SPRINT_20260110_108_005_AGENTS_winrm.md)
|
||||
|
||||
## Overview
|
||||
|
||||
Agentless deployment enables deployment to targets without requiring a pre-installed agent. The orchestrator connects directly to targets using SSH (Linux/Unix) or WinRM (Windows) to execute deployment commands.
|
||||
|
||||
---
|
||||
|
||||
## SSH Remote Executor
|
||||
|
||||
### Capabilities
|
||||
|
||||
- SSH key-based authentication
|
||||
- File transfer via SFTP
|
||||
- Remote command execution
|
||||
- Docker operations over SSH
|
||||
- Script execution
|
||||
- Backup and rollback
|
||||
|
||||
### Connection Management
|
||||
|
||||
```typescript
|
||||
class SSHRemoteExecutor implements TargetExecutor {
|
||||
private ssh: SSHClient;
|
||||
|
||||
async connect(config: SSHConnectionConfig): Promise<void> {
|
||||
const privateKey = await this.secrets.getSecret(config.privateKeyRef);
|
||||
|
||||
this.ssh = new SSHClient();
|
||||
await this.ssh.connect({
|
||||
host: config.host,
|
||||
port: config.port || 22,
|
||||
username: config.username,
|
||||
privateKey: privateKey.value,
|
||||
readyTimeout: config.connectionTimeout || 30000,
|
||||
keepaliveInterval: 10000,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Deploy Task Flow
|
||||
|
||||
```typescript
|
||||
async deploy(task: DeployTaskPayload): Promise<DeployResult> {
|
||||
const { artifacts, config } = task;
|
||||
const deployDir = config.deploymentDirectory;
|
||||
|
||||
try {
|
||||
// 1. Ensure deployment directory exists
|
||||
await this.exec(`mkdir -p ${deployDir}`);
|
||||
await this.exec(`mkdir -p ${deployDir}/.stella-backup`);
|
||||
|
||||
// 2. Backup current deployment
|
||||
await this.exec(`cp -r ${deployDir}/* ${deployDir}/.stella-backup/ 2>/dev/null || true`);
|
||||
|
||||
// 3. Upload artifacts
|
||||
for (const artifact of artifacts) {
|
||||
const content = await this.fetchArtifact(artifact);
|
||||
const remotePath = path.join(deployDir, artifact.name);
|
||||
await this.uploadFile(content, remotePath);
|
||||
}
|
||||
|
||||
// 4. Run pre-deploy hook
|
||||
if (task.hooks?.preDeploy) {
|
||||
await this.runRemoteHook(task.hooks.preDeploy, deployDir);
|
||||
}
|
||||
|
||||
// 5. Execute deployment script
|
||||
const deployScript = artifacts.find(a => a.type === "deploy_script");
|
||||
if (deployScript) {
|
||||
const scriptPath = path.join(deployDir, deployScript.name);
|
||||
await this.exec(`chmod +x ${scriptPath}`);
|
||||
|
||||
const result = await this.exec(scriptPath, {
|
||||
cwd: deployDir,
|
||||
timeout: config.deploymentTimeout,
|
||||
env: config.environment,
|
||||
});
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new DeploymentError(`Deploy script failed: ${result.stderr}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Run post-deploy hook
|
||||
if (task.hooks?.postDeploy) {
|
||||
await this.runRemoteHook(task.hooks.postDeploy, deployDir);
|
||||
}
|
||||
|
||||
// 7. Health check
|
||||
if (config.healthCheck) {
|
||||
const healthy = await this.runHealthCheck(config.healthCheck);
|
||||
if (!healthy) {
|
||||
await this.rollback(task);
|
||||
throw new HealthCheckFailedError("Health check failed");
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Write version sticker
|
||||
await this.writeSticker(config.sticker, deployDir);
|
||||
|
||||
// 9. Cleanup backup
|
||||
await this.exec(`rm -rf ${deployDir}/.stella-backup`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
logs: this.getLogs(),
|
||||
durationMs: this.getDuration(),
|
||||
};
|
||||
|
||||
} finally {
|
||||
this.ssh.end();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Command Execution
|
||||
|
||||
```typescript
|
||||
private async exec(
|
||||
command: string,
|
||||
options?: ExecOptions
|
||||
): Promise<CommandResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = options?.timeout || 60000;
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
this.ssh.exec(command, { cwd: options?.cwd }, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
stream.close();
|
||||
reject(new TimeoutError(`Command timed out after ${timeout}ms`));
|
||||
}, timeout);
|
||||
|
||||
stream.on("data", (data: Buffer) => {
|
||||
stdout += data.toString();
|
||||
this.log(data.toString());
|
||||
});
|
||||
|
||||
stream.stderr.on("data", (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
this.log(`[stderr] ${data.toString()}`);
|
||||
});
|
||||
|
||||
stream.on("close", (code: number) => {
|
||||
clearTimeout(timer);
|
||||
resolve({ exitCode: code, stdout, stderr });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### File Upload via SFTP
|
||||
|
||||
```typescript
|
||||
private async uploadFile(content: Buffer | string, remotePath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.ssh.sftp((err, sftp) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const writeStream = sftp.createWriteStream(remotePath);
|
||||
writeStream.on("close", () => resolve());
|
||||
writeStream.on("error", reject);
|
||||
writeStream.end(content);
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Rollback
|
||||
|
||||
```typescript
|
||||
async rollback(task: RollbackTaskPayload): Promise<DeployResult> {
|
||||
const deployDir = task.config.deploymentDirectory;
|
||||
|
||||
// Restore from backup
|
||||
await this.exec(`rm -rf ${deployDir}/*`);
|
||||
await this.exec(`cp -r ${deployDir}/.stella-backup/* ${deployDir}/`);
|
||||
|
||||
// Re-run deployment from backup
|
||||
const deployScript = path.join(deployDir, "deploy.sh");
|
||||
await this.exec(deployScript, { cwd: deployDir });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
logs: this.getLogs(),
|
||||
durationMs: this.getDuration(),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WinRM Remote Executor
|
||||
|
||||
### Capabilities
|
||||
|
||||
- NTLM/Kerberos authentication
|
||||
- PowerShell script execution
|
||||
- File transfer via base64 encoding
|
||||
- Windows container operations
|
||||
- Windows service management
|
||||
|
||||
### Connection Management
|
||||
|
||||
```typescript
|
||||
class WinRMRemoteExecutor implements TargetExecutor {
|
||||
private winrm: WinRMClient;
|
||||
|
||||
async connect(config: WinRMConnectionConfig): Promise<void> {
|
||||
const credential = await this.secrets.getSecret(config.credentialRef);
|
||||
|
||||
this.winrm = new WinRMClient({
|
||||
host: config.host,
|
||||
port: config.port || 5986,
|
||||
username: credential.username,
|
||||
password: credential.password,
|
||||
protocol: config.useHttps ? "https" : "http",
|
||||
authentication: config.authType || "ntlm", // ntlm, kerberos, basic
|
||||
});
|
||||
|
||||
await this.winrm.openShell();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Deploy Task Flow
|
||||
|
||||
```typescript
|
||||
async deploy(task: DeployTaskPayload): Promise<DeployResult> {
|
||||
const { artifacts, config } = task;
|
||||
const deployDir = config.deploymentDirectory;
|
||||
|
||||
try {
|
||||
// 1. Ensure deployment directory exists
|
||||
await this.execPowerShell(`
|
||||
if (-not (Test-Path "${deployDir}")) {
|
||||
New-Item -ItemType Directory -Path "${deployDir}" -Force
|
||||
}
|
||||
if (-not (Test-Path "${deployDir}\\.stella-backup")) {
|
||||
New-Item -ItemType Directory -Path "${deployDir}\\.stella-backup" -Force
|
||||
}
|
||||
`);
|
||||
|
||||
// 2. Backup current deployment
|
||||
await this.execPowerShell(`
|
||||
Get-ChildItem "${deployDir}" -Exclude ".stella-backup" |
|
||||
Copy-Item -Destination "${deployDir}\\.stella-backup" -Recurse -Force
|
||||
`);
|
||||
|
||||
// 3. Upload artifacts
|
||||
for (const artifact of artifacts) {
|
||||
const content = await this.fetchArtifact(artifact);
|
||||
const remotePath = `${deployDir}\\${artifact.name}`;
|
||||
await this.uploadFile(content, remotePath);
|
||||
}
|
||||
|
||||
// 4. Run pre-deploy hook
|
||||
if (task.hooks?.preDeploy) {
|
||||
await this.runRemoteHook(task.hooks.preDeploy, deployDir);
|
||||
}
|
||||
|
||||
// 5. Execute deployment script
|
||||
const deployScript = artifacts.find(a => a.type === "deploy_script");
|
||||
if (deployScript) {
|
||||
const scriptPath = `${deployDir}\\${deployScript.name}`;
|
||||
|
||||
const result = await this.execPowerShell(`
|
||||
Set-Location "${deployDir}"
|
||||
& "${scriptPath}"
|
||||
exit $LASTEXITCODE
|
||||
`, { timeout: config.deploymentTimeout });
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new DeploymentError(`Deploy script failed: ${result.stderr}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Run post-deploy hook
|
||||
if (task.hooks?.postDeploy) {
|
||||
await this.runRemoteHook(task.hooks.postDeploy, deployDir);
|
||||
}
|
||||
|
||||
// 7. Health check
|
||||
if (config.healthCheck) {
|
||||
const healthy = await this.runHealthCheck(config.healthCheck);
|
||||
if (!healthy) {
|
||||
await this.rollback(task);
|
||||
throw new HealthCheckFailedError("Health check failed");
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Write version sticker
|
||||
await this.writeSticker(config.sticker, deployDir);
|
||||
|
||||
// 9. Cleanup backup
|
||||
await this.execPowerShell(`
|
||||
Remove-Item -Path "${deployDir}\\.stella-backup" -Recurse -Force
|
||||
`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
logs: this.getLogs(),
|
||||
durationMs: this.getDuration(),
|
||||
};
|
||||
|
||||
} finally {
|
||||
this.winrm.closeShell();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PowerShell Execution
|
||||
|
||||
```typescript
|
||||
private async execPowerShell(
|
||||
script: string,
|
||||
options?: ExecOptions
|
||||
): Promise<CommandResult> {
|
||||
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
||||
return this.winrm.runCommand(
|
||||
`powershell -EncodedCommand ${encoded}`,
|
||||
{ timeout: options?.timeout || 60000 }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### File Upload
|
||||
|
||||
```typescript
|
||||
private async uploadFile(content: Buffer | string, remotePath: string): Promise<void> {
|
||||
// Use PowerShell to write file content
|
||||
const base64Content = Buffer.from(content).toString("base64");
|
||||
|
||||
await this.execPowerShell(`
|
||||
$bytes = [Convert]::FromBase64String("${base64Content}")
|
||||
[IO.File]::WriteAllBytes("${remotePath}", $bytes)
|
||||
`);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### SSH Security
|
||||
|
||||
1. **Key-Based Authentication:** Always use SSH keys, never passwords
|
||||
2. **Key Rotation:** Regularly rotate SSH keys
|
||||
3. **Bastion Hosts:** Use jump hosts for network isolation
|
||||
4. **Connection Timeouts:** Enforce strict connection timeouts
|
||||
5. **Known Hosts:** Verify host fingerprints
|
||||
|
||||
### WinRM Security
|
||||
|
||||
1. **HTTPS Required:** Always use WinRM over HTTPS in production
|
||||
2. **Certificate Validation:** Validate server certificates
|
||||
3. **Kerberos Preferred:** Use Kerberos when available, NTLM as fallback
|
||||
4. **Credential Protection:** Store credentials in vault
|
||||
5. **Session Cleanup:** Always close sessions after use
|
||||
|
||||
---
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
### SSH Target Configuration
|
||||
|
||||
```yaml
|
||||
target:
|
||||
name: web-server-01
|
||||
type: ssh
|
||||
connection:
|
||||
host: 192.168.1.100
|
||||
port: 22
|
||||
username: deploy
|
||||
privateKeyRef: vault://ssh-keys/deploy-key
|
||||
deployment:
|
||||
directory: /opt/myapp
|
||||
healthCheck:
|
||||
command: curl -f http://localhost:8080/health
|
||||
timeout: 30
|
||||
```
|
||||
|
||||
### WinRM Target Configuration
|
||||
|
||||
```yaml
|
||||
target:
|
||||
name: windows-server-01
|
||||
type: winrm
|
||||
connection:
|
||||
host: 192.168.1.200
|
||||
port: 5986
|
||||
useHttps: true
|
||||
authType: kerberos
|
||||
credentialRef: vault://windows-creds/deploy-user
|
||||
deployment:
|
||||
directory: C:\Apps\MyApp
|
||||
healthCheck:
|
||||
command: Invoke-WebRequest -Uri http://localhost:8080/health -UseBasicParsing
|
||||
timeout: 30
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Agent-Based Deployment](agent-based.md)
|
||||
- [Agents Module](../modules/agents.md)
|
||||
- [Deployment Orchestrator](../modules/deploy-orchestrator.md)
|
||||
- [Security Overview](../security/overview.md)
|
||||
Reference in New Issue
Block a user