Deployment and Operations
Learning Objectives
Configure environments for development, staging, and production
Deploy XRPL integrations with zero-downtime strategies
Implement rollback procedures for failed deployments
Manage secrets and configuration across environments
Operate production systems with appropriate runbooks
// config/index.js
const config = {
development: {
xrpl: {
servers: ['wss://s.altnet.rippletest.net:51233'],
network: 'testnet'
},
wallets: {
hot: { address: 'rTestHotWallet...' }
},
limits: {
maxPayment: 10000,
dailyLimit: 100000
},
features: {
debugLogging: true,
mockExternalServices: true
}
},
staging: {
xrpl: {
servers: [
'wss://s.altnet.rippletest.net:51233',
'wss://s.devnet.rippletest.net:51233'
],
network: 'testnet'
},
wallets: {
hot: { address: 'rStagingHotWallet...' }
},
limits: {
maxPayment: 10000,
dailyLimit: 100000
},
features: {
debugLogging: false,
mockExternalServices: false
}
},
production: {
xrpl: {
servers: [
'wss://s1.ripple.com:51233',
'wss://s2.ripple.com:51233',
'wss://xrplcluster.com:51233'
],
network: 'mainnet'
},
wallets: {
hot: { address: process.env.HOT_WALLET_ADDRESS }
},
limits: {
maxPayment: parseInt(process.env.MAX_PAYMENT) || 1000,
dailyLimit: parseInt(process.env.DAILY_LIMIT) || 10000
},
features: {
debugLogging: false,
mockExternalServices: false
}
}
}
const env = process.env.NODE_ENV || 'development'
module.exports = config[env]
```
class NetworkVerifier {
constructor(client, expectedNetwork) {
this.client = client
this.expectedNetwork = expectedNetwork
}
async verify() {
const serverInfo = await this.client.request({ command: 'server_info' })
const networkId = serverInfo.result.info.network_id
// Network IDs: 0 = mainnet, 1 = testnet, 2 = devnet
const networks = {
0: 'mainnet',
1: 'testnet',
2: 'devnet'
}
const actualNetwork = networks[networkId] || 'unknown'
if (actualNetwork !== this.expectedNetwork) {
throw new Error(
Network mismatch! Expected ${this.expectedNetwork}, connected to ${actualNetwork}
)
}
console.log(Network verified: ${actualNetwork})
return true
}
}
// Startup check
async function startupChecks(config) {
const client = new xrpl.Client(config.xrpl.servers[0])
await client.connect()
// Verify correct network
const verifier = new NetworkVerifier(client, config.xrpl.network)
await verifier.verify()
// Verify hot wallet exists and has balance
if (config.xrpl.network === 'mainnet') {
const info = await client.request({
command: 'account_info',
account: config.wallets.hot.address
})
const balance = parseInt(info.result.account_data.Balance) / 1_000_000
console.log(Hot wallet balance: ${balance} XRP)
if (balance < 100) {
console.warn('WARNING: Low hot wallet balance!')
}
}
await client.disconnect()
}
```
# docker-compose.yml
version: '3.8'
services:
xrpl-service-blue:
image: xrpl-service:${BLUE_VERSION}
deploy:
replicas: 2
environment:
- NODE_ENV=production
- SERVICE_COLOR=blue
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]" target="_blank" rel="noopener noreferrer" class="text-cyan-400 hover:text-cyan-300 underline hover:no-underline transition-colors inline-flex items-center gap-1">http://localhost:3000/health%22%5D">http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3
xrpl-service-green:
image: xrpl-service:${GREEN_VERSION}
deploy:
replicas: 2
environment:
- NODE_ENV=production
- SERVICE_COLOR=green
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]" target="_blank" rel="noopener noreferrer" class="text-cyan-400 hover:text-cyan-300 underline hover:no-underline transition-colors inline-flex items-center gap-1">http://localhost:3000/health%22%5D">http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3
nginx:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- xrpl-service-blue
- xrpl-service-green
```
#!/bin/bash
# deploy.sh
set -e
NEW_VERSION=$1
CURRENT_COLOR=$(cat /var/run/current_color || echo "blue")
NEW_COLOR=$([ "$CURRENT_COLOR" = "blue" ] && echo "green" || echo "blue")
echo "Deploying version $NEW_VERSION to $NEW_COLOR..."
Deploy new version to inactive color
docker-compose up -d "xrpl-service-$NEW_COLOR"
-e "${NEW_COLOR^^}_VERSION=$NEW_VERSION"
Wait for health check
echo "Waiting for health check..."
for i in {1..30}; do
if curl -f "http://xrpl-service-$NEW_COLOR:3000/health" target="_blank" rel="noopener noreferrer" class="text-cyan-400 hover:text-cyan-300 underline hover:no-underline transition-colors inline-flex items-center gap-1">http://xrpl-service-$NEW_COLOR:3000/health">http://xrpl-service-$NEW_COLOR:3000/health " > /dev/null 2>&1; then
echo "New deployment healthy!"
break
fi
if [ $i -eq 30 ]; then
echo "Health check failed, rolling back..."
docker-compose stop "xrpl-service-$NEW_COLOR"
exit 1
fi
sleep 2
done
Run smoke tests
echo "Running smoke tests..."
./smoke-tests.sh "xrpl-service-$NEW_COLOR"
if [ $? -ne 0 ]; then
echo "Smoke tests failed, rolling back..."
docker-compose stop "xrpl-service-$NEW_COLOR"
exit 1
fi
Switch traffic
echo "Switching traffic to $NEW_COLOR..."
sed -i "s/xrpl-service-$CURRENT_COLOR/xrpl-service-$NEW_COLOR/g" /etc/nginx/nginx.conf
nginx -s reload
Update current color
echo "$NEW_COLOR" > /var/run/current_color
Stop old version after grace period
echo "Waiting for old connections to drain..."
sleep 30
docker-compose stop "xrpl-service-$CURRENT_COLOR"
echo "Deployment complete!"
```
app.get('/health', async (req, res) => {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
version: process.env.APP_VERSION,
checks: {}
}
// Check XRPL connection
try {
if (xrplClient.isConnected()) {
await xrplClient.request({ command: 'ping' })
health.checks.xrpl = { status: 'ok' }
} else {
health.checks.xrpl = { status: 'disconnected' }
health.status = 'degraded'
}
} catch (error) {
health.checks.xrpl = { status: 'error', message: error.message }
health.status = 'unhealthy'
}
// Check database
try {
await db.query('SELECT 1')
health.checks.database = { status: 'ok' }
} catch (error) {
health.checks.database = { status: 'error', message: error.message }
health.status = 'unhealthy'
}
// Check Redis
try {
await redis.ping()
health.checks.redis = { status: 'ok' }
} catch (error) {
health.checks.redis = { status: 'error', message: error.message }
health.status = 'degraded'
}
const statusCode = health.status === 'healthy' ? 200 :
health.status === 'degraded' ? 200 : 503
res.status(statusCode).json(health)
})
```
class DeploymentManager {
constructor(config) {
this.config = config
this.deploymentHistory = []
}
async deploy(version) {
const deployment = {
version,
timestamp: Date.now(),
status: 'deploying'
}
this.deploymentHistory.push(deployment)
try {
await this.performDeployment(version)
await this.runHealthChecks()
await this.runSmokeTests()
deployment.status = 'success'
console.log(Deployment of ${version} successful)
} catch (error) {
deployment.status = 'failed'
deployment.error = error.message
console.error(Deployment failed: ${error.message})
await this.rollback()
throw error
}
}
async rollback() {
const successful = this.deploymentHistory
.filter(d => d.status === 'success')
.sort((a, b) => b.timestamp - a.timestamp)
if (successful.length === 0) {
throw new Error('No successful deployment to rollback to')
}
const rollbackTo = successful[0]
console.log(Rolling back to version ${rollbackTo.version})
await this.performDeployment(rollbackTo.version)
await this.runHealthChecks()
console.log('Rollback complete')
}
async runHealthChecks() {
const maxAttempts = 30
for (let i = 0; i < maxAttempts; i++) {
try {
const response = await fetch(${this.config.serviceUrl}/health)
const health = await response.json()
if (health.status === 'healthy') return true
if (health.status === 'unhealthy') throw new Error('Service unhealthy')
} catch (error) {
if (i === maxAttempts - 1) throw error
}
await sleep(2000)
}
throw new Error('Health check timeout')
}
async runSmokeTests() {
// Test basic operations
const tests = [
this.testXRPLConnection,
this.testDatabaseConnection,
this.testPaymentQuery
]
for (const test of tests) {
await test.call(this)
}
}
}
```
class SecretRotator {
constructor(secretsManager, notifier) {
this.secretsManager = secretsManager
this.notifier = notifier
}
async rotateApiKey(secretName) {
console.log(Rotating secret: ${secretName})
// Generate new key
const newKey = crypto.randomBytes(32).toString('hex')
// Update in secrets manager
await this.secretsManager.putSecretValue({
SecretId: secretName,
SecretString: JSON.stringify({
key: newKey,
rotatedAt: new Date().toISOString()
})
})
// Notify services to refresh
await this.notifier.broadcast('secret_rotated', { secretName })
console.log(Secret ${secretName} rotated successfully)
}
async getSecret(secretName) {
const result = await this.secretsManager.getSecretValue({
SecretId: secretName
})
return JSON.parse(result.SecretString)
}
}
// Application refreshes secrets periodically
class SecretCache {
constructor(secretsClient, refreshInterval = 300000) {
this.secretsClient = secretsClient
this.cache = new Map()
this.refreshInterval = refreshInterval
this.startRefreshLoop()
}
async get(secretName) {
if (!this.cache.has(secretName)) {
await this.refresh(secretName)
}
return this.cache.get(secretName)
}
async refresh(secretName) {
const secret = await this.secretsClient.getSecret(secretName)
this.cache.set(secretName, secret)
}
startRefreshLoop() {
setInterval(async () => {
for (const secretName of this.cache.keys()) {
try {
await this.refresh(secretName)
} catch (error) {
console.error(Failed to refresh secret ${secretName}:, error)
}
}
}, this.refreshInterval)
}
}
```
# Operational Runbooks
- Configures environments (dev, staging, production)
- Implements health checks
- Supports blue-green deployment
- Includes rollback capability
- Manages secrets securely
Time Investment: 4-5 hours
End of Lesson 17
Key Takeaways
Verify network before operations:
Never accidentally operate on mainnet.
Deploy with rollback capability:
Blue-green enables instant rollback.
Health checks gate traffic:
Don't route to unhealthy instances.
Rotate secrets regularly:
Limit exposure from any single compromise.
Document procedures:
Runbooks enable consistent operations. ---