Files
test-repo/docs/.vitepress/theme/components/appendix/infrastructure-as-code/ConfigDriftDemo.vue
T

361 lines
11 KiB
Vue
Raw Normal View History

<template>
<div class="config-drift-demo">
<div class="demo-label">交互演示 配置漂移无声的定时炸弹</div>
<div class="timeline">
<div class="timeline-track">
<div
v-for="(event, i) in events"
:key="i"
:class="['timeline-node', event.type, { active: step >= i }]"
@click="goToStep(i)"
>
<div class="node-dot"></div>
<div class="node-label">{{ event.label }}</div>
</div>
</div>
</div>
<div class="scene-area">
<div class="infra-visual">
<div class="server-group">
<div class="group-title">期望状态代码定义</div>
<div class="server-cards">
<div v-for="s in expectedServers" :key="s.name" class="server-card expected">
<div class="server-icon">🖥</div>
<div class="server-name">{{ s.name }}</div>
<div class="server-config">{{ s.config }}</div>
</div>
</div>
</div>
<div class="drift-indicator">
<div :class="['drift-status', driftLevel]">
<span class="drift-icon">{{ driftIcon }}</span>
<span class="drift-text">{{ driftText }}</span>
</div>
</div>
<div class="server-group">
<div class="group-title">实际状态线上环境</div>
<div class="server-cards">
<div
v-for="s in actualServers"
:key="s.name"
:class="['server-card', 'actual', { drifted: s.drifted }]"
>
<div class="server-icon">{{ s.drifted ? '⚠️' : '🖥️' }}</div>
<div class="server-name">{{ s.name }}</div>
<div class="server-config">{{ s.config }}</div>
<div v-if="s.driftReason" class="drift-reason">{{ s.driftReason }}</div>
</div>
</div>
</div>
</div>
<div class="event-desc">
<div class="event-title">{{ events[step].title }}</div>
<p class="event-detail">{{ events[step].detail }}</p>
</div>
</div>
<div class="controls">
<button class="ctrl-btn" :disabled="step === 0" @click="goToStep(step - 1)"> 上一步</button>
<button class="ctrl-btn reset" @click="goToStep(0)">重置</button>
<button class="ctrl-btn primary" :disabled="step >= events.length - 1" @click="goToStep(step + 1)">
下一步
</button>
</div>
<div class="lesson-box">
<div class="lesson-title">关键教训</div>
<div class="lesson-items">
<div v-for="(lesson, i) in lessons" :key="i" class="lesson-item">
<span class="lesson-icon">{{ lesson.icon }}</span>
<span class="lesson-text">{{ lesson.text }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const step = ref(0)
const events = [
{
label: '初始部署',
type: 'good',
title: '第 0 步:通过 IaC 初始部署',
detail: '团队使用 Terraform 部署了 3 台 Web 服务器,配置完全一致:Nginx 1.24、端口 443、2GB 内存。代码和实际状态完美匹配。'
},
{
label: '手动修改',
type: 'warn',
title: '第 1 步:深夜紧急手动修改',
detail: '凌晨 3 点,Server-B 出现性能问题。值班工程师直接 SSH 登录,手动将内存从 2GB 升级到 4GB,并修改了 Nginx 配置。没有更新 IaC 代码。'
},
{
label: '又一次修改',
type: 'warn',
title: '第 2 步:另一位同事的"临时"调整',
detail: '一周后,另一位工程师为了调试,在 Server-C 上开放了 22 端口(SSH),并安装了调试工具。同样没有更新代码。'
},
{
label: '漂移加剧',
type: 'bad',
title: '第 3 步:配置漂移已经失控',
detail: '此时 3 台"相同"的服务器实际配置已经各不相同。代码描述的状态和线上真实状态严重脱节,没有人能说清楚线上到底是什么配置。'
},
{
label: 'IaC 检测',
type: 'fix',
title: '第 4 步:terraform plan 发现漂移',
detail: '运行 terraform plan 后,Terraform 对比 State 文件和实际资源,清晰列出所有差异。团队决定将手动变更回退,统一通过代码管理。'
}
]
const expectedServers = [
{ name: 'Server-A', config: 'Nginx 1.24 | 443 | 2GB' },
{ name: 'Server-B', config: 'Nginx 1.24 | 443 | 2GB' },
{ name: 'Server-C', config: 'Nginx 1.24 | 443 | 2GB' }
]
const actualServers = computed(() => {
if (step.value === 0) {
return [
{ name: 'Server-A', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-B', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-C', config: 'Nginx 1.24 | 443 | 2GB', drifted: false }
]
}
if (step.value === 1) {
return [
{ name: 'Server-A', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-B', config: 'Nginx 1.25 | 443 | 4GB', drifted: true, driftReason: '手动升级内存和 Nginx' },
{ name: 'Server-C', config: 'Nginx 1.24 | 443 | 2GB', drifted: false }
]
}
if (step.value === 2 || step.value === 3) {
return [
{ name: 'Server-A', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-B', config: 'Nginx 1.25 | 443 | 4GB', drifted: true, driftReason: '手动升级内存和 Nginx' },
{ name: 'Server-C', config: 'Nginx 1.24 | 22+443 | 2GB', drifted: true, driftReason: '开放了 SSH 端口' }
]
}
// step 4: fix
return [
{ name: 'Server-A', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-B', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-C', config: 'Nginx 1.24 | 443 | 2GB', drifted: false }
]
})
const driftLevel = computed(() => {
if (step.value === 0 || step.value === 4) return 'ok'
if (step.value <= 2) return 'warning'
return 'danger'
})
const driftIcon = computed(() => {
if (driftLevel.value === 'ok') return '✅'
if (driftLevel.value === 'warning') return '⚠️'
return '🔥'
})
const driftText = computed(() => {
if (step.value === 0) return '状态一致'
if (step.value === 4) return '漂移已修复'
if (step.value === 1) return '1 台漂移'
if (step.value === 2) return '2 台漂移'
return '严重漂移!'
})
const lessons = [
{ icon: '🚫', text: '禁止手动修改线上环境,所有变更必须通过代码' },
{ icon: '🔍', text: '定期运行 terraform plan 检测漂移' },
{ icon: '🔒', text: '限制生产环境的 SSH 权限,减少人为干预' },
{ icon: '📋', text: '建立变更审批流程(PR → Review → Merge → Apply' }
]
function goToStep(i) {
step.value = Math.max(0, Math.min(i, events.length - 1))
}
</script>
<style scoped>
.config-drift-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
text-align: center;
}
.timeline { margin-bottom: 1rem; overflow-x: auto; }
.timeline-track {
display: flex;
align-items: flex-start;
gap: 0;
min-width: max-content;
position: relative;
padding: 0 0.5rem;
}
.timeline-node {
flex: 1;
min-width: 90px;
text-align: center;
cursor: pointer;
position: relative;
padding-top: 20px;
}
.timeline-node::before {
content: '';
position: absolute;
top: 8px;
left: 0;
right: 0;
height: 2px;
background: var(--vp-c-divider);
}
.timeline-node:first-child::before { left: 50%; }
.timeline-node:last-child::before { right: 50%; }
.node-dot {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
margin: 0 auto 4px;
position: relative;
z-index: 1;
transition: all 0.3s;
}
.timeline-node.active .node-dot { transform: scale(1.3); }
.timeline-node.active.good .node-dot { background: #10b981; border-color: #10b981; }
.timeline-node.active.warn .node-dot { background: #f59e0b; border-color: #f59e0b; }
.timeline-node.active.bad .node-dot { background: #ef4444; border-color: #ef4444; }
.timeline-node.active.fix .node-dot { background: #3b82f6; border-color: #3b82f6; }
.node-label { font-size: 0.68rem; color: var(--vp-c-text-3); }
.timeline-node.active .node-label { font-weight: 600; color: var(--vp-c-text-1); }
.scene-area { margin-bottom: 1rem; }
.infra-visual {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 0.8rem;
}
.group-title {
font-size: 0.72rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.3rem;
text-align: center;
}
.server-cards {
display: flex;
gap: 0.4rem;
justify-content: center;
flex-wrap: wrap;
}
.server-card {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem 0.6rem;
background: var(--vp-c-bg);
text-align: center;
min-width: 120px;
transition: all 0.3s;
font-size: 0.73rem;
}
.server-card.expected { border-color: #10b981; }
.server-card.drifted {
border-color: #ef4444;
background: #fef2f210;
box-shadow: 0 0 0 1px #fca5a540;
}
.server-icon { font-size: 1.2rem; }
.server-name { font-weight: 600; font-size: 0.75rem; }
.server-config { font-size: 0.68rem; color: var(--vp-c-text-2); }
.drift-reason {
font-size: 0.62rem;
color: #ef4444;
margin-top: 2px;
font-style: italic;
}
.drift-indicator { text-align: center; }
.drift-status {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 14px;
border-radius: 16px;
font-size: 0.78rem;
font-weight: 600;
}
.drift-status.ok { background: #d1fae5; color: #065f46; }
.drift-status.warning { background: #fef3c7; color: #92400e; }
.drift-status.danger { background: #fee2e2; color: #991b1b; }
:root.dark .drift-status.ok { background: #022c2240; color: #6ee7b7; }
:root.dark .drift-status.warning { background: #451a0340; color: #fcd34d; }
:root.dark .drift-status.danger { background: #450a0a40; color: #fca5a5; }
.event-desc {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.8rem;
background: var(--vp-c-bg);
}
.event-title { font-weight: 600; font-size: 0.88rem; margin-bottom: 4px; }
.event-detail { font-size: 0.8rem; color: var(--vp-c-text-2); line-height: 1.6; margin: 0; }
.controls {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.ctrl-btn {
padding: 6px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.78rem;
transition: all 0.2s;
}
.ctrl-btn:disabled { opacity: 0.4; cursor: default; }
.ctrl-btn.primary { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
.ctrl-btn.reset { color: var(--vp-c-text-3); }
.lesson-box {
border: 1px solid #3b82f640;
border-radius: 6px;
padding: 0.8rem;
background: #dbeafe10;
}
.lesson-title {
font-weight: 600;
font-size: 0.82rem;
margin-bottom: 0.5rem;
color: #2563eb;
}
:root.dark .lesson-title { color: #93c5fd; }
.lesson-items { display: flex; flex-direction: column; gap: 0.3rem; }
.lesson-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.78rem;
}
</style>