Share this result

Previews are deleted daily. Get a permanent share link sent to your inbox:
Script
<!doctype html> <html lang="es"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>sprint-demo.yml – FinTech Nova</title> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500..700&family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap"> <style> :root { --bg: #0B0F14; --panel: #11161D; --panel2: #161C25; --line: #232B36; --text: #E6EDF3; --muted: #8B98A8; --dim: #5B6675; --ok: #3FB950; --changed: #D29922; --skip: #39C5CF; --fail: #F85149; --key: #C792EA; --str: #A5D6A7; --accent: #F08A3C; --mono: 'JetBrains Mono', 'Courier New', monospace; --bash: 'Courier New', monospace; --sans: 'Inter', Arial, sans-serif; --display: 'Space Grotesk', Arial, sans-serif; } html, body { margin: 0; height: 100%; background: #05080B; overflow: hidden; } * { box-sizing: border-box; } #stage { position: absolute; left: 50%; top: 50%; width: 1920px; height: 1080px; transform-origin: center; background: var(--bg); color: var(--text); font-family: var(--sans); } h1, h2, h3, p, ul, ol, pre { margin: 0; } /* ventana de terminal */ .bar { height: 56px; display: flex; align-items: center; gap: 12px; padding: 0 24px; background: #0E131A; border-bottom: 1px solid var(--line); font: 500 20px var(--mono); color: var(--muted); } .dot { width: 16px; height: 16px; border-radius: 50%; } .bar .title { flex: 1; text-align: center; margin-right: 84px; } .body { position: absolute; top: 56px; left: 0; right: 0; bottom: 0; display: flex; } /* lateral: el playbook */ .side { width: 440px; flex: none; background: var(--panel); border-right: 1px solid var(--line); padding: 32px 0; display: flex; flex-direction: column; } .side-h { font: 700 20px var(--mono); color: var(--muted); padding: 0 32px 20px; letter-spacing: 1px; } .side-h b { color: var(--text); } #tasklist { list-style: none; padding: 0; display: flex; flex-direction: column; } #tasklist li { display: flex; gap: 14px; align-items: baseline; padding: 6px 32px; font: 400 19px/1.3 var(--mono); color: var(--dim); border-left: 4px solid transparent; cursor: pointer; } #tasklist li.ph { padding: 18px 32px 6px; font: 700 17px/1.3 var(--mono); color: var(--accent); letter-spacing: .5px; cursor: default; } #tasklist li.ph:first-child { padding-top: 0; } #tasklist li .st { width: 92px; flex: none; font-weight: 700; } #tasklist li.done { color: var(--muted); } #tasklist li.current { color: var(--text); background: var(--panel2); border-left-color: var(--accent); } #tasklist li.current .st { color: var(--accent); } .side-foot { margin-top: auto; padding: 0 32px; font: 400 18px/1.5 var(--mono); color: var(--dim); } /* slides */ #slides { position: relative; flex: 1; } .slide { position: absolute; inset: 0; padding: 48px 72px 120px; display: flex; flex-direction: column; gap: 32px; opacity: 0; pointer-events: none; transition: opacity .3s; } .slide.active { opacity: 1; pointer-events: auto; } .task { font: 500 26px var(--mono); color: var(--skip); white-space: nowrap; overflow: hidden; } .playline { font: 700 26px var(--mono); color: var(--accent); white-space: nowrap; overflow: hidden; line-height: 1.3; margin-bottom: -14px; } .lbl { font: 700 22px var(--mono); color: var(--muted); letter-spacing: .5px; } td.mono { font: 500 24px var(--mono); color: var(--str) !important; white-space: nowrap; } .task .stars { color: var(--dim); } .slide h2 { font: 700 60px/1.08 var(--display); letter-spacing: -1px; } .out { position: absolute; left: 72px; right: 72px; bottom: 44px; font: 500 26px var(--bash); padding-top: 18px; border-top: 1px dashed var(--line); white-space: nowrap; overflow: hidden; } .slide.active .out { animation: appear .35s .9s both; } .slide.active .reveal { animation: rise .45s both; } .slide.active .reveal:nth-child(2) { animation-delay: .1s; } .slide.active .reveal:nth-child(3) { animation-delay: .2s; } .slide.active .reveal:nth-child(4) { animation-delay: .3s; } .slide.active .reveal:nth-child(5) { animation-delay: .4s; } .slide.active .reveal:nth-child(6) { animation-delay: .5s; } .slide.active .reveal:nth-child(7) { animation-delay: .6s; } .slide.active .reveal:nth-child(8) { animation-delay: .7s; } @keyframes appear { from { opacity: 0; } to { opacity: 1; } } @keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } } .ok { color: var(--ok); } .changed { color: var(--changed); } .skip { color: var(--skip); } .fail { color: var(--fail); } .muted { color: var(--muted); } .acc { color: var(--accent); } /* bloques */ .code { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 24px 32px; font: 400 26px/1.6 var(--mono); white-space: pre; } .k { color: var(--key); } .s { color: var(--str); } .c { color: var(--dim); } .n { color: var(--accent); } .row { display: flex; gap: 24px; } .card { flex: 1; background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 32px; display: flex; flex-direction: column; gap: 12px; } .card .tag { font: 700 20px var(--mono); letter-spacing: 1px; color: var(--muted); } .card h3 { font: 700 34px/1.15 var(--display); } .card p { font-size: 26px; line-height: 1.4; color: #B9C4D0; } .card.hl { border-color: var(--accent); background: #1A140F; } .card.sm { padding: 24px 28px; gap: 10px; } .card.sm p { font-size: 24px; line-height: 1.4; } .card code { font: 500 22px var(--mono); color: var(--str); } .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; } .pill { display: inline-block; font: 700 20px var(--mono); padding: 4px 12px; border-radius: 6px; border: 1px solid currentColor; } /* portada */ .term-cmd { font: 500 32px var(--bash); } .prompt { color: var(--ok); } .caret { display: inline-block; width: 16px; height: 34px; background: var(--text); vertical-align: -6px; margin-left: 4px; animation: blink 1s steps(1) infinite; } @keyframes blink { 50% { opacity: 0; } } .after { opacity: 0; transition: opacity .5s; display: flex; flex-direction: column; gap: 32px; } .after.on { opacity: 1; } .play { font: 500 28px var(--mono); color: var(--text); } .cover-title { font: 700 104px/1.02 var(--display); letter-spacing: -2px; max-width: 1350px; } .cover-title em { font-style: normal; color: var(--accent); } /* loop del pipeline */ .loop { display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: 12px; overflow: hidden; } .loop .it { display: flex; align-items: center; gap: 24px; padding: 13px 28px; background: var(--panel); border-top: 1px solid var(--line); font-size: 26px; } .loop .it:first-child { border-top: none; } .loop .it .st { width: 130px; font: 700 24px var(--mono); } .loop .it .nm { width: 400px; flex: none; font: 500 26px var(--mono); } .loop .it .ds { flex: 1; color: #B9C4D0; } .loop .it.sec { background: #1A140F; } .loop .it .tg { font: 700 18px var(--mono); color: var(--accent); border: 1px solid var(--accent); border-radius: 6px; padding: 3px 10px; } .loop .it.pend .nm, .loop .it.pend .ds { color: var(--muted); } .facts3 { display: flex; gap: 24px; } .facts3 div { flex: 1; border-top: 3px solid var(--line); padding-top: 16px; } .facts3 h3 { font: 700 28px var(--display); margin-bottom: 6px; } .facts3 p { font-size: 24px; line-height: 1.4; color: #B9C4D0; } /* diff */ .diff { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 22px 32px; font: 400 28px/1.65 var(--mono); white-space: pre-wrap; } .diff .m { color: var(--fail); } .diff .p { color: var(--ok); } .diff .h { color: var(--dim); } table.t { width: 100%; border-collapse: collapse; font-size: 25px; } table.t th { text-align: left; font: 700 20px var(--mono); color: var(--muted); padding: 12px 18px; border-bottom: 1px solid var(--line); letter-spacing: 1px; } table.t td { padding: 16px 18px; border-bottom: 1px solid var(--line); vertical-align: top; line-height: 1.35; color: #C9D3DD; } table.t td:first-child { font: 500 24px var(--mono); color: var(--text); white-space: nowrap; } .big { font: 700 150px/1 var(--display); color: var(--accent); white-space: nowrap; } .recap { font: 500 26px/1.6 var(--mono); background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 24px 36px; white-space: pre; } #notes { position: fixed; left: 0; right: 0; bottom: 0; max-height: 35%; overflow: auto; background: rgba(5,8,11,.96); color: #E6EDF3; font: 18px/1.5 Inter, Arial, sans-serif; padding: 16px 24px; display: none; border-top: 2px solid #F08A3C; z-index: 5; } #notes.show { display: block; } #hud { position: fixed; right: 16px; bottom: 12px; color: #8B98A8; font: 14px Inter, Arial, sans-serif; opacity: .55; } #notes.show ~ #hud { display: none; } aside.note { display: none; } /* ansible: visuales */ .ico { width: 1em; height: 1em; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; flex: none; } .journey { display: grid; grid-template-columns: 200px 120px 330px 260px 1fr; align-items: stretch; } .stg { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 22px; display: flex; flex-direction: column; gap: 14px; } .stg .hd { display: flex; align-items: center; gap: 12px; } .stg .hd .ico { font-size: 40px; color: var(--accent); } .stg .nm { font: 700 28px/1.1 var(--display); } .stg .sub { font-size: 20px; color: var(--muted); line-height: 1.35; } .stg.empty { border-style: dashed; background: transparent; justify-content: center; align-items: center; text-align: center; } .stg.empty .ico { font-size: 80px; color: var(--dim); } .stg.live { border-color: var(--ok); } .stg.live .hd .ico { color: var(--ok); } .chips { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 10px; } .chips li { display: flex; align-items: center; gap: 12px; font-size: 21px; line-height: 1.25; color: #C9D3DD; background: var(--panel2); border: 1px solid var(--line); border-radius: 10px; padding: 10px 14px; } .chips li .ico { font-size: 26px; color: var(--skip); } .arr { display: flex; flex-direction: column; justify-content: center; gap: 12px; padding: 0 18px; } .arr .lb { font: 500 19px var(--mono); color: var(--accent); text-align: center; } .arr .ln { height: 3px; background: var(--accent); position: relative; margin-right: 8px; } .arr .ln::after { content: ''; position: absolute; right: -10px; top: -7px; border-left: 14px solid var(--accent); border-top: 8px solid transparent; border-bottom: 8px solid transparent; } .arr .chips li { font-size: 19px; padding: 8px 12px; } .arr .chips li .ico { font-size: 22px; color: var(--accent); } .tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; } .tile { border: 1px solid var(--line); border-radius: 8px; padding: 14px 2px; text-align: center; font: 500 16px var(--mono); color: var(--dim); background: var(--panel2); } .hrow { display: flex; align-items: center; justify-content: space-between; gap: 24px; } .runbtn { font: 500 22px var(--mono); color: var(--key); background: #1C1426; border: 1px solid var(--key); border-radius: 10px; padding: 12px 22px; cursor: pointer; white-space: nowrap; } .runbtn:hover { background: #2A1D3A; } .runbtn:disabled { color: var(--changed); border-color: var(--changed); background: #1E1A0F; cursor: default; } #s-deploy .ln { transform: scaleX(0); transform-origin: left; transition: transform .6s ease; } #s-deploy .ln.on { transform: none; } #s-deploy .prov li, #s-deploy .dep li, #s-deploy .checks div { opacity: .25; transition: opacity .3s, border-color .3s; } #s-deploy .on { opacity: 1 !important; } #s-deploy .prov li.on { border-color: var(--skip); } #s-deploy .dep li.on { border-color: var(--accent); } #s-deploy .prep .hd .ico, #s-deploy .stg.live .hd .ico { color: var(--dim); transition: color .4s; } #s-deploy .prep.on .hd .ico { color: var(--accent); } #s-deploy .stg.live { border-color: var(--line); transition: border-color .4s; } #s-deploy .stg.live.on { border-color: var(--ok); } #s-deploy .stg.live.on .hd .ico { color: var(--ok); } #s-deploy .tile { transition: color .3s, border-color .3s, background .3s; } #s-deploy .tile.on { color: var(--ok); border-color: var(--ok); background: #0F2016; } .checks { display: flex; flex-direction: column; gap: 12px; margin-top: auto; } .checks div { display: flex; align-items: center; gap: 12px; font-size: 21px; color: #C9D3DD; } .checks .ico { font-size: 28px; color: var(--ok); } .strip { display: flex; gap: 24px; } .strip > div { white-space: nowrap; background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 18px 24px; display: flex; align-items: center; gap: 18px; font-size: 22px; color: #C9D3DD; } .strip .tag { font: 700 18px var(--mono); letter-spacing: 1px; } .run { font: 500 20px var(--mono); padding: 4px 10px; border-radius: 6px; border: 1px solid currentColor; white-space: nowrap; } .dash { flex: 1; border-top: 3px dashed var(--changed); min-width: 60px; } .secrets { display: grid; grid-template-columns: 1fr 1.15fr 1fr; gap: 28px; align-items: stretch; } .vbox { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 26px; display: flex; flex-direction: column; gap: 16px; } .vbox .hd { display: flex; align-items: center; gap: 14px; font: 700 30px var(--display); } .vbox .hd .ico { font-size: 40px; color: var(--skip); } .vbox .sub { font-size: 21px; color: var(--muted); line-height: 1.35; } .vbox.zero { align-items: center; text-align: center; } .vbox.zero .hd .ico { color: var(--dim); } .zero-n { font: 700 120px/1 var(--display); color: var(--ok); } .nolist { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 10px; align-self: stretch; } .nolist li { display: flex; align-items: center; gap: 12px; font-size: 21px; color: var(--muted); text-decoration: line-through; text-decoration-color: var(--fail); } .nolist .ico { font-size: 24px; color: var(--fail); } .flowv { display: flex; flex-direction: column; } .node { display: flex; align-items: center; gap: 16px; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 16px 20px; font-size: 20px; color: var(--muted); line-height: 1.3; } .node b { display: block; font: 700 24px var(--display); color: var(--text); } .node .ico { font-size: 40px; color: var(--skip); } .node.hl { border-color: var(--accent); background: #1A140F; } .node.hl .ico { color: var(--accent); } .vstep { display: flex; align-items: center; gap: 16px; height: 60px; padding-left: 38px; font: 500 18px var(--mono); color: var(--accent); } .vstep i { align-self: stretch; border-left: 3px solid var(--accent); } .vstep.mem { color: var(--ok); } .vstep.mem i { border-left-style: dashed; border-left-color: var(--ok); } .ba { display: grid; grid-template-columns: 1fr 72px 1fr; row-gap: 16px; } .ba .colh { font: 700 22px var(--mono); letter-spacing: 1px; display: flex; align-items: baseline; gap: 16px; padding-left: 4px; } .ba .colh small { font: 400 21px var(--sans); letter-spacing: 0; color: var(--muted); } .ba .cell { display: flex; align-items: center; gap: 22px; border-radius: 12px; padding: 22px 28px; font-size: 25px; line-height: 1.35; } .ba .cell .ico { font-size: 42px; } .ba .was { background: #140F11; border: 1px solid #3A2327; color: #9AA5B1; } .ba .was .ico { color: var(--fail); opacity: .85; } .ba .now { background: var(--panel); border: 1px solid var(--line); border-left: 4px solid var(--ok); color: #C9D3DD; } .ba .now .ico { color: var(--ok); } .ba .now b { display: block; font: 700 28px var(--display); color: var(--text); margin-bottom: 2px; } .ba .to { display: flex; align-items: center; justify-content: center; font: 700 34px var(--mono); color: var(--accent); } .crash-hd { display: flex; align-items: center; justify-content: space-between; gap: 16px; } .crashbtn { font-size: 20px; padding: 10px 16px; } .crashbtn:disabled { color: var(--dim) !important; border-color: var(--line) !important; background: var(--panel) !important; } .crash-st { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .crash-st.run { font: 500 20px var(--mono); letter-spacing: 0; } .crash-st.bad { color: var(--fail); } .crash-st.chg { color: var(--changed); } .crash-st.good { color: var(--ok); } .ctile.err { color: var(--fail); border-color: var(--fail); background: #22100F; border-style: dashed; } .runbtn.bad:disabled { color: var(--fail); border-color: var(--fail); background: #22100F; } .runbtn.good:disabled { color: var(--ok); border-color: var(--ok); background: #0F2016; } .ctiles { display: grid; grid-template-columns: repeat(8, 1fr); gap: 6px; } .ctile { border: 1px solid var(--ok); border-radius: 8px; padding: 12px 0; text-align: center; font: 500 15px var(--mono); color: var(--ok); background: #0F2016; transition: color .3s, border-color .3s, background .3s; } .ctile.down { color: var(--fail); border-color: var(--fail); background: #22100F; animation: shake .4s; } .ctile.starting { color: var(--changed); border-color: var(--changed); background: #1E1A0F; animation: blinkst .8s infinite; } @keyframes shake { 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } } @keyframes blinkst { 50% { opacity: .45; } } .shield { transition: border-color .3s, background .3s; } .shield.lit { border-color: var(--ok); background: #0F2016; } .shields { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } .shield { display: flex; gap: 16px; align-items: center; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 18px 20px; } .shield .ico { font-size: 42px; color: var(--ok); } .shield b { display: block; font: 700 24px var(--display); margin-bottom: 2px; } .shield span { font-size: 19px; color: var(--muted); } .calc { display: flex; align-items: center; gap: 10px; } .cbox { flex: 1; border: 1px dashed var(--accent); border-radius: 10px; padding: 12px 8px; text-align: center; } .cbox small { display: block; font-size: 17px; color: var(--muted); line-height: 1.2; margin-bottom: 6px; } .cbox b { font: 700 26px var(--display); } .fld { display: flex; align-items: baseline; justify-content: center; gap: 6px; font: 700 24px var(--display); color: var(--muted); } .fld input { width: 100%; min-width: 0; flex: 1; background: transparent; border: none; border-bottom: 2px solid var(--line); color: var(--text); font: 700 26px var(--display); text-align: center; padding: 2px 0; outline: none; -moz-appearance: textfield; } .fld input:focus { border-bottom-color: var(--accent); } .fld input::placeholder { color: var(--dim); } .fld input::-webkit-outer-spin-button, .fld input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .op { font: 700 30px var(--mono); color: var(--accent); } .total { font: 700 60px/1 var(--display); color: var(--accent); } .spof { display: flex; align-items: center; gap: 24px; } .spof .now, .spof .next { display: flex; align-items: center; gap: 22px; border-radius: 14px; padding: 16px 26px; } .spof .now { border: 1px solid var(--fail); background: #1C1012; } .spof .next { border: 2px dashed var(--line); color: var(--muted); flex: 1; } .srv { display: flex; flex-direction: column; align-items: center; gap: 4px; font: 500 16px var(--mono); } .srv .ico { font-size: 46px; } .spof .txt { font-size: 21px; line-height: 1.35; } .spof .txt b { display: block; font: 700 18px var(--mono); letter-spacing: 1px; } </style> </head> <body> <svg width="0" height="0" style="position:absolute" aria-hidden="true"> <symbol id="i-server" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="7" rx="1.5"/><rect x="3" y="14" width="18" height="7" rx="1.5"/><path d="M7 6.5h.01M7 17.5h.01"/></symbol> <symbol id="i-box" viewBox="0 0 24 24"><path d="M12 2.5 3.5 7v10l8.5 4.5 8.5-4.5V7z"/><path d="M3.5 7 12 11.5 20.5 7M12 11.5v10"/></symbol> <symbol id="i-lock" viewBox="0 0 24 24"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></symbol> <symbol id="i-cloud" viewBox="0 0 24 24"><path d="M7 19h10a4 4 0 0 0 .6-7.96A6 6 0 0 0 6.2 9.6 4.7 4.7 0 0 0 7 19z"/></symbol> <symbol id="i-user" viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></symbol> <symbol id="i-check" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9.5"/><path d="m7.5 12 3 3 6-6"/></symbol> <symbol id="i-ban" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9.5"/><path d="m5.5 5.5 13 13"/></symbol> <symbol id="i-x" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></symbol> <symbol id="i-file" viewBox="0 0 24 24"><path d="M6 2.5h8l4 4v15H6z"/><path d="M14 2.5v4h4"/></symbol> <symbol id="i-key" viewBox="0 0 24 24"><circle cx="8" cy="15" r="4.5"/><path d="M11.2 11.8 20 3M16.5 6.5l3 3M14.5 8.5l2 2"/></symbol> <symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 2.5 4 5.5v6c0 5 3.4 8.4 8 10 4.6-1.6 8-5 8-10v-6z"/><path d="m8.5 12 2.5 2.5 4.5-5"/></symbol> <symbol id="i-play" viewBox="0 0 24 24"><path d="M7 4.5v15l12-7.5z"/></symbol> <symbol id="i-pin" viewBox="0 0 24 24"><path d="M9 3h6M10 3v6l-3 4h10l-3-4V3M12 13v8"/></symbol> <symbol id="i-refresh" viewBox="0 0 24 24"><path d="M20 12a8 8 0 1 1-2.6-5.9"/><path d="M20 3.5v5h-5"/></symbol> <symbol id="i-hourglass" viewBox="0 0 24 24"><path d="M6 3h12M6 21h12M7.5 3v3.5L12 12l-4.5 5.5V21M16.5 3v3.5L12 12l4.5 5.5V21"/></symbol> <symbol id="i-gauge" viewBox="0 0 24 24"><path d="M3.5 18a8.5 8.5 0 1 1 17 0"/><path d="m12 18 4.5-6"/></symbol> <symbol id="i-rewind" viewBox="0 0 24 24"><path d="M11 6 4 12l7 6zM20 6l-7 6 7 6z"/></symbol> <symbol id="i-alert" viewBox="0 0 24 24"><path d="M12 3 2 20.5h20z"/><path d="M12 10v4.5M12 17.5h.01"/></symbol> <symbol id="i-git" viewBox="0 0 24 24"><circle cx="6" cy="5.5" r="2.5"/><circle cx="6" cy="18.5" r="2.5"/><circle cx="18" cy="8" r="2.5"/><path d="M6 8v8M18 10.5c0 4-6 3.5-10.3 6.4"/></symbol> <symbol id="i-repo" viewBox="0 0 24 24"><path d="M3 6.5a1 1 0 0 1 1-1h5l2 2h9a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/></symbol> <symbol id="i-split" viewBox="0 0 24 24"><path d="M12 3v6M12 9l-7 6M12 9l7 6"/><circle cx="12" cy="3" r="1"/><circle cx="5" cy="17" r="2"/><circle cx="19" cy="17" r="2"/></symbol> <symbol id="i-question" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9.5"/><path d="M9.5 9.2a2.6 2.6 0 1 1 3.6 2.4c-.7.3-1.1.9-1.1 1.6v.6M12 17h.01"/></symbol> <symbol id="i-tag" viewBox="0 0 24 24"><path d="M3 12.5V3.5h9l9 9-9 9z"/><path d="M7.5 7.5h.01"/></symbol> <symbol id="i-bell" viewBox="0 0 24 24"><path d="M6 16V11a6 6 0 0 1 12 0v5l2 2H4z"/><path d="M10 21h4"/></symbol> </svg> <div id="stage"> <div class="bar"> <span class="dot" style="background:#F85149"></span><span class="dot" style="background:#D29922"></span><span class="dot" style="background:#3FB950"></span> <span class="title">grupo-15@fintech-nova: ~/tp-2 — ansible-playbook sprint-demo.yml</span> </div> <div class="body"> <div class="side"> <div class="side-h"><b>sprint-demo.yml</b> · tasks</div> <ol id="tasklist"></ol> <div class="side-foot">plays: 3 · tiempo máx: 10 min</div> </div> <main id="slides"> <!-- ===================== PLAY 0 · SPRINT DEMO ===================== --> <!-- 1 · PORTADA --> <section class="slide" data-play="Sprint Demo" data-title="Gathering Facts" data-status="ok"> <div class="term-cmd"><span class="prompt">grupo-15@fintech-nova:~/tp-2$</span> <span id="typed"></span><span class="caret"></span></div> <div class="after" id="coverAfter"> <div class="play">PLAY [Sprint Demo · FinTech Nova] <span class="muted">*******************************************</span></div> <h1 class="cover-title">De un deploy a mano a un pipeline <em>automático y seguro</em></h1> <div class="code" style="font-size:24px"><span class="skip">TASK [Gathering Facts]</span> <span class="k">equipo:</span> <span class="s">Grupo 15</span> <span class="k">audiencia:</span> <span class="s">CTO de FinTech Nova</span> <span class="k">plays:</span> <span class="n">1</span> <span class="s">terraform</span> <span class="c">→</span> Ghibaudo, Alan Yair <span class="n">2</span> <span class="s">ansible</span> <span class="c">→</span> Sidorowicz, Sebastián Pablo <span class="n">3</span> <span class="s">pipeline</span> <span class="c">→</span> Lambertucci, Juan Pablo</div> </div> <div class="out"><span class="ok">ok: [cto-fintech-nova]</span></div> <aside class="note">Presentarse en una frase: somos el grupo 15. La presentación está armada como un playbook con tres plays, en el mismo orden en que se construye la solución: primero Terraform crea la infraestructura, después Ansible prepara el servidor y despliega, y por último el pipeline de GitLab valida, escanea y publica cada cambio. Cada integrante presenta un play, en el orden que muestra la portada. Tiempo total: 10 minutos.</aside> </section> <!-- 2 · VALOR --> <section class="slide" data-title="Qué cambió" data-status="changed"> <div class="task">TASK [Qué cambió para el negocio] <span class="stars">************************************</span></div> <h2>Qué cambió para el negocio</h2> <div class="ba reveal"> <div class="colh fail">ANTES <small>un script desde una computadora</small></div><div></div> <div class="colh ok">AHORA <small>infraestructura, servidor e imágenes como código</small></div> <div class="cell was"><svg class="ico"><use href="#i-user"/></svg>Una persona, su computadora y un script</div> <div class="to">→</div> <div class="cell now"><svg class="ico"><use href="#i-refresh"/></svg><div><b>Repetible</b>Siempre el mismo camino, sin pasos que alguien se olvide</div></div> <div class="cell was"><svg class="ico"><use href="#i-alert"/></svg>Si esa persona o su máquina falla, no hay deploy</div> <div class="to">→</div> <div class="cell now"><svg class="ico"><use href="#i-shield"/></svg><div><b>Seguro</b>Una imagen con vulnerabilidades críticas conocidas no llega a producción</div></div> <div class="cell was"><svg class="ico"><use href="#i-question"/></svg>No se sabe qué versión está corriendo</div> <div class="to">→</div> <div class="cell now"><svg class="ico"><use href="#i-tag"/></svg><div><b>Trazable</b>Versión y commit en cada imagen: sabemos qué corre y podemos volver atrás</div></div> </div> <div class="out"><span class="changed">changed: [fintech-nova] =&gt; deploy: "manual" → "automático"</span></div> <aside class="note">Responde la pregunta del CTO: qué ganamos. Leer cada fila de izquierda a derecha: cada problema de antes tiene su respuesta ahora. Antes un integrante corría un script desde su computadora y el resultado dependía de esa persona y esa máquina; ahora la infraestructura, el servidor y cada imagen se arman con código. Tres beneficios concretos: repetible, seguro y trazable. A continuación cada integrante muestra su parte en el orden en que se arma todo: infraestructura, servidor y pipeline.</aside> </section> <!-- ===================== PLAY 1 · TERRAFORM ===================== --> <!-- 3 · TERRAFORM: DE CLOUDFORMATION A TERRAFORM --> <section class="slide" data-play="Terraform · infraestructura" data-title="De CloudFormation a Terraform" data-status="changed"> <div class="playline">PLAY [Terraform · infraestructura] <span class="muted">*********************************</span></div> <div class="task">TASK [Pasar de CloudFormation a Terraform] <span class="stars">**************************</span></div> <h2>De CloudFormation a Terraform</h2> <div class="diff reveal"><span class="h">--- antes · CloudFormation</span> <span class="h">+++ ahora · Terraform</span> <span class="m">- Plantilla propia de AWS; ver el impacto exigía armar change sets aparte.</span> <span class="p">+ terraform plan muestra qué se crea, cambia o destruye, antes de aplicar.</span> <span class="p">+ terraform apply ejecuta ese plan: solo cambia lo que difiere.</span></div> <div class="row"> <div class="card reveal"><span class="tag skip">¿PARA QUÉ SIRVE?</span><p>Describimos la infraestructura en archivos de código y Terraform la crea en AWS: red, firewall y servidor.</p></div> <div class="card reveal"><span class="tag acc">PLAN</span><p>Una vista previa legible de los cambios. Se revisa antes de aplicar, sin sorpresas.</p></div> <div class="card reveal"><span class="tag ok">APPLY</span><p>Ejecuta lo que mostró el plan. Repetirlo no duplica recursos: es repetible.</p></div> </div> <div class="out"><span class="changed">changed: [aws] =&gt; iac: "CloudFormation" → "Terraform"</span></div> <aside class="note">Contar de dónde venimos: en la Parte 1 la infraestructura estaba en una plantilla de CloudFormation. La pasamos a Terraform. Terraform sirve para describir la infraestructura como código y crearla en AWS de forma repetible: red, firewall y servidor salen de unos archivos versionados. El beneficio que más valoramos son dos comandos: plan, que muestra qué se va a crear, cambiar o destruir antes de tocar nada, y apply, que ejecuta ese plan y solo cambia lo que difiere. Si preguntan: CloudFormation también tiene change sets para previsualizar, pero hay que armarlos aparte; en Terraform el plan es el flujo normal de trabajo.</aside> </section> <!-- 4 · TERRAFORM: QUÉ CREA --> <section class="slide" data-title="Crear infraestructura" data-status="ok"> <div class="task">TASK [Crear la infraestructura en AWS] <span class="stars">******************************</span></div> <h2>Infraestructura como código</h2> <div class="row" style="align-items:stretch"> <div class="code reveal" style="flex:6;font-size:24px"><span class="c">Terraform will perform the following actions:</span> <span class="ok">+ red propia con una subred pública</span> <span class="ok">+ salida a internet</span> <span class="ok">+ firewall: solo el 443 abierto al público</span> <span class="ok">+ administración por SSH desde una sola IP</span> <span class="ok">+ servidor con rol para leer sus secretos</span> <span class="ok">+ llave SSH del equipo</span> <span class="ok">+ el estado vive de terraform en S3</span> <span class="k">Plan:</span> <span class="n">8</span> to add, <span class="n">0</span> to change, <span class="n">0</span> to destroy.</div> <div style="flex:5;display:flex;flex-direction:column;gap:24px"> <div class="card hl reveal"><span class="tag acc">DECISIÓN</span><h3>Terraform crea, Ansible configura</h3><p>Antes Docker se instalaba en el arranque del servidor, una sola vez y sin verificación. Ahora lo hace Ansible: se puede repetir y comprobar.</p></div> <div class="card reveal"><span class="tag skip">ENTREGA AL PASO SIGUIENTE</span><p>Terraform genera el inventario de Ansible con la IP de la sesión.</p></div> </div> </div> <div class="code reveal" style="font-size:24px;padding:16px 28px"><span class="c"># sandbox de AWS Academy: sesiones de 2 horas, IP y credenciales nuevas cada vez</span> <span class="k">al_iniciar_sesion:</span> <span class="s">terraform apply y actualizar las variables del pipeline</span></div> <div class="out"><span class="ok">ok: [aws] =&gt; recursos: 8 · público: 443 · puerto 80: cerrado</span></div> <aside class="note">Terraform reemplaza a la plantilla de CloudFormation de la Parte 1. Crea la red, el firewall con solo el 443 abierto al público, la administración por SSH desde una sola IP (la variable se valida y no acepta abierto a todo internet) y el servidor con el rol que le permite leer sus secretos sin credenciales guardadas. Decisión importante: sacamos la instalación de Docker del arranque del servidor, porque corría una sola vez y si fallaba no nos enterábamos; ahora la hace Ansible. Terraform deja listo el inventario para el paso siguiente. Contar el procedimiento de cada sesión del sandbox: [completar].</aside> </section> <!-- 5 · ERROR (Terraform): estado local desactualizado --> <section class="slide" data-title="Error: estado desactualizado" data-status="rescued"> <div class="task">TASK [Aplicar Terraform entre sesiones del lab] <span class="stars">**************************</span></div> <div class="code reveal" style="border-color:var(--fail);font-size:24px;padding:16px 28px"><span class="fail">fatal: [tfstate]: FAILED! =&gt; {"msg": "el estado recordaba recursos que ya no existían"}</span></div> <h2>El estado local quedó desactualizado</h2> <div class="grid2"> <div class="card sm reveal"><span class="tag fail">SÍNTOMA</span><p><code>terraform state list</code> mostraba VPC, subnets, Internet Gateway y route table de una sesión anterior. En la consola de AWS solo existía la VPC por defecto del lab.</p></div> <div class="card sm reveal"><span class="tag changed">CAUSA</span><p>El sandbox elimina los recursos al terminar la sesión, pero el <code>terraform.tfstate</code> local conserva el registro de lo que se creó.</p></div> <div class="card sm reveal"><span class="tag skip">RESOLUCIÓN</span><p>Como en AWS no quedaba nada, descartamos el estado y empezamos desde cero. El siguiente <code>plan</code> propuso crear todo.</p></div> <div class="card sm hl reveal"><span class="tag acc">LECCIÓN</span><p>En AWS Academy el estado es efímero: <code>terraform destroy</code> antes de cerrar el lab, o descartar el estado al iniciar. Mejora futura: backend remoto (estado en GitLab).</p></div> </div> <div class="out"><span class="ok">ok: [terraform] =&gt; rescued=1 · estado descartado · plan: crear todo</span></div> <aside class="note">El error clave que tuvimos al aplicar Terraform. Síntoma: terraform state list mostraba la VPC, las subnets, el Internet Gateway y la route table de una sesión anterior, pero en la consola de AWS solo existía la VPC por defecto del lab. Causa: el sandbox elimina los recursos al terminar la sesión, pero el terraform.tfstate local conserva el registro de lo que se creó. Resolución: como en AWS no quedaba nada, descartamos el estado y empezamos de cero; el siguiente plan propuso crear todo. Lección: en AWS Academy el estado es efímero, conviene ejecutar terraform destroy antes de cerrar el lab o descartar el estado al iniciar una sesión nueva. Mejora futura: un backend remoto, con el estado en GitLab.</aside> </section> <!-- ===================== PLAY 2 · ANSIBLE ===================== --> <!-- 5 · ANSIBLE --> <section class="slide" id="s-deploy" data-play="Ansible · configuración y despliegue" data-title="Preparar y desplegar" data-status="ok"> <div class="playline">PLAY [Ansible · configuración y despliegue] <span class="muted">************************</span></div> <div class="task">TASK [Preparar el servidor y desplegar] <span class="stars">*****************************</span></div> <div class="hrow"><h2>De servidor vacío a app funcionando</h2><button class="runbtn" id="runDeploy">▶ ansible-playbook site.yml</button></div> <div class="journey"> <div class="stg empty reveal"> <svg class="ico"><use href="#i-server"/></svg> <div class="nm">Servidor vacío</div> <div class="sub">recién creado por Terraform</div> </div> <div class="arr reveal"><div class="lb">provision<br>.yml</div><div class="ln"></div></div> <div class="stg prep reveal"> <div class="hd"><svg class="ico"><use href="#i-server"/></svg><div class="nm">Servidor listo</div></div> <ul class="chips prov"> <li><svg class="ico"><use href="#i-box"/></svg>Docker</li> <li><svg class="ico"><use href="#i-shield"/></svg>Certificados</li> <li><svg class="ico"><use href="#i-cloud"/></svg>Cliente de AWS</li> <li><svg class="ico"><use href="#i-pin"/></svg>Compose con versión fija y verificada</li> <li><svg class="ico"><use href="#i-user"/></svg>Usuario con acceso a Docker</li> </ul> </div> <div class="arr reveal"> <div class="lb">deploy.yml</div><div class="ln"></div> <ul class="chips dep"> <li><svg class="ico"><use href="#i-file"/></svg>Copia solo lo necesario</li> <li><svg class="ico"><use href="#i-shield"/></svg>Configuración y certificado</li> <li><svg class="ico"><use href="#i-key"/></svg>Secretos en AWS</li> <li><svg class="ico"><use href="#i-play"/></svg>Levanta y espera a que esté sano</li> </ul> </div> <div class="stg live reveal"> <div class="hd"><svg class="ico"><use href="#i-server"/></svg><div class="nm">App funcionando</div></div> <div class="tiles"> <div class="tile">db</div><div class="tile">traefik</div><div class="tile">web</div><div class="tile">clientes</div> <div class="tile">pagos</div><div class="tile">facturas</div><div class="tile">transacc.</div><div class="tile">tarjetas</div> </div> <div class="checks"> <div><svg class="ico"><use href="#i-check"/></svg>Web y 5 APIs responden por HTTPS</div> <div><svg class="ico"><use href="#i-ban"/></svg>Puerto 80 cerrado</div> </div> </div> </div> <div class="strip"> <div class="reveal" style="flex:3"><span class="tag ok">IDEMPOTENTE</span><span class="run changed">1ª: instala</span>→<span class="run ok">2ª: mismo resultado</span></div> <div class="reveal" style="flex:2"><span class="tag changed">HOY</span><span class="run skip">pipeline</span><span class="dash"></span><span class="run acc">ansible</span><span class="muted">a integrar</span></div> </div> <div class="out"><span class="ok">ok: [ec2-fintech-nova] =&gt; tareas: 21 · failed: 0</span></div> <aside class="note">Ansible era una mejora pendiente de la Parte 1: reemplaza a los scripts por SSH. Contarlo de izquierda a derecha como la historia del servidor. Terraform nos deja un servidor vacío. El primer playbook, provision.yml, lo deja listo: Docker, certificados, el cliente de AWS, Compose con versión fija y verificada, y el usuario con acceso a Docker. El segundo, deploy.yml, despliega: copia solo los archivos de ejecución, sin código fuente; genera la configuración y el certificado si no existe; prepara los secretos; levanta todo y espera a que cada servicio esté sano (los 8 cuadros que se prenden). Al final verifica por HTTPS la web y las 5 APIs y confirma que el puerto 80 está cerrado. Abajo: es idempotente, correrlo dos veces deja el mismo resultado. Aclarar con honestidad si ya está integrado al pipeline o si todavía se corre aparte.</aside> </section> <!-- 6 · SECRETOS --> <section class="slide" data-title="Gestión de secretos" data-status="ok"> <div class="task">TASK [Gestión de secretos] <span class="stars">*******************************************</span></div> <h2>Ninguna contraseña vive en el repositorio</h2> <div class="secrets"> <div class="vbox reveal"> <div class="hd"><svg class="ico"><use href="#i-git"/></svg>GitLab</div> <div class="sub">Variables del pipeline, enmascaradas y protegidas</div> <ul class="chips"> <li><svg class="ico"><use href="#i-key"/></svg>Acceso al registry</li> <li><svg class="ico"><use href="#i-key"/></svg>Llave del servidor</li> </ul> </div> <div class="flowv reveal"> <div class="node"><svg class="ico"><use href="#i-server"/></svg><div><b>El servidor</b>genera las claves de la base</div></div> <div class="vstep"><i></i>las guarda cifradas</div> <div class="node hl"><svg class="ico"><use href="#i-lock"/></svg><div><b>AWS SSM</b>Parameter Store, cifrado</div></div> <div class="vstep mem"><i></i>en memoria · nunca a disco</div> <div class="node"><svg class="ico"><use href="#i-box"/></svg><div><b>Contenedores</b>las reciben en cada deploy</div></div> </div> <div class="vbox zero reveal"> <div class="hd"><svg class="ico"><use href="#i-repo"/></svg>Repositorio</div> <div class="zero-n">0</div> <div class="sub">secretos versionados</div> <ul class="nolist"> <li><svg class="ico"><use href="#i-x"/></svg>contraseñas</li> <li><svg class="ico"><use href="#i-x"/></svg>llaves</li> <li><svg class="ico"><use href="#i-x"/></svg>certificados</li> <li><svg class="ico"><use href="#i-x"/></svg>estado de la infraestructura</li> </ul> </div> </div> <div class="code reveal" style="font-size:22px;padding:14px 26px"><span class="skip">TASK [Guardar las claves de la base en SSM]</span> <span class="c"># no_log: true</span> <span class="ok">ok: [ec2-fintech-nova] =&gt; {"censored": "the output has been hidden..."}</span></div> <div class="out"><span class="ok">ok: [ec2-fintech-nova] =&gt; secretos en el repo: 0 · SSM: único camino</span></div> <aside class="note">Tres lugares, tres reglas. A la izquierda, GitLab: todo lo sensible del pipeline son variables enmascaradas y protegidas. En el centro, el recorrido de las claves de la base: el playbook de deploy hace que la propia instancia las genere y las guarde cifradas en Parameter Store; en cada deploy se leen con el rol de la instancia y viajan solo en memoria hasta los contenedores, nunca se escriben en disco. A la derecha, el repositorio: cero secretos. Abajo, no_log: la opción de Ansible que evita que un secreto aparezca en la salida, así se ve en pantalla. En la Parte 1 esto era una variante opcional, ahora es el único camino. Autenticación de la EC2 contra el registry: [completar].</aside> </section> <!-- 7 · CAÍDA --> <section class="slide" id="s-crash" data-title="Si un servicio se cae" data-status="ok"> <div class="task">TASK [Qué pasa si un servicio se cae] <span class="stars">********************************</span></div> <h2>Qué pasa si un servicio se cae</h2> <div class="row" style="align-items:stretch;gap:28px"> <div class="reveal" style="flex:6;display:flex;flex-direction:column;gap:14px"> <div class="crash-hd"><span class="lbl crash-st" id="crashSt">SI UN CONTENEDOR FALLA</span><button class="runbtn crashbtn" id="runCrash">api-pagos</button><button class="runbtn crashbtn" id="runCrashDb">base de datos</button></div> <div class="ctiles"> <div class="ctile" data-svc="db">db</div><div class="ctile">traefik</div><div class="ctile">web</div><div class="ctile" data-svc="api">clientes</div> <div class="ctile" data-svc="api" id="crashTarget">pagos</div><div class="ctile" data-svc="api">facturas</div><div class="ctile" data-svc="api">transacc.</div><div class="ctile" data-svc="api">tarjetas</div> </div> <div class="shields"> <div class="shield" id="shieldRestart"><svg class="ico"><use href="#i-refresh"/></svg><div><b>Se reinicia solo</b><span>reinicio automático del contenedor</span></div></div> <div class="shield"><svg class="ico"><use href="#i-hourglass"/></svg><div><b>Arranque ordenado</b><span>las APIs esperan a que la base esté lista</span></div></div> <div class="shield"><svg class="ico"><use href="#i-gauge"/></svg><div><b>Recursos acotados</b><span>límites de CPU y memoria por servicio</span></div></div> <div class="shield"><svg class="ico"><use href="#i-rewind"/></svg><div><b>Vuelta atrás</b><span>a la versión anterior por su número</span></div></div> </div> </div> <div class="card hl reveal" style="flex:5;gap:18px"><span class="tag acc">IMPACTO DE 1 HORA CAÍDOS</span> <div class="calc"> <div class="cbox"><small>transacciones por hora</small><div class="fld"><input id="impTx" type="number" min="0" step="any" inputmode="decimal" placeholder="___"></div></div><span class="op">×</span> <div class="cbox"><small>monto promedio</small><div class="fld">USD<input id="impMonto" type="number" min="0" step="any" inputmode="decimal" placeholder="___"></div></div><span class="op">×</span> <div class="cbox"><small>comisión</small><div class="fld"><input id="impCom" type="number" min="0" step="any" inputmode="decimal" placeholder="__">%</div></div> </div> <div style="display:flex;align-items:baseline;gap:16px"><span class="op">=</span><span class="total" id="impTotal">USD ___</span><span class="muted" style="font-size:22px">por hora</span></div> <p style="font-size:21px">Estimación, sin contar reclamos ni pérdida de confianza.</p> </div> </div> <div class="spof reveal"> <div class="now"> <div class="srv fail"><svg class="ico"><use href="#i-server"/></svg>EC2</div> <div class="txt"><b class="fail">HOY</b>un solo servidor<br>punto único de falla</div> <svg class="ico fail" style="font-size:40px"><use href="#i-alert"/></svg> </div> <div class="next"> <div class="txt"><b>PRÓXIMO PASO</b>otra zona y alertas</div> <div class="srv"><svg class="ico"><use href="#i-split"/></svg>balanceador</div> <div class="srv"><svg class="ico"><use href="#i-server"/></svg>zona A</div> <div class="srv"><svg class="ico"><use href="#i-server"/></svg>zona B</div> <div class="srv"><svg class="ico"><use href="#i-bell"/></svg>alertas</div> </div> </div> <div class="out"><span class="ok">ok: [ec2-fintech-nova] =&gt; restart: always · rollback: versión anterior</span></div> <aside class="note">Dos casos. Botón api-pagos: se cae, se reinicia solo y vuelve, y el resto de los servicios nunca se entera. Botón base de datos: es el caso más delicado, porque las 5 APIs usan la misma base y mientras está caída todas dan error, aunque sigan corriendo. La base se reinicia sola y los datos no se pierden porque están en un volumen aparte; cuando vuelve, las APIs se reconectan. Si preguntan: es el único punto donde una caída afecta a todos, por eso los próximos pasos son backups y una base administrada con réplica. Ojo: depends_on solo ordena el arranque, no protege si la base se cae en el medio. A la izquierda, lo que ya nos protege, del lado de Docker Compose: el contenedor se reinicia solo (restart: always), las APIs arrancan recién cuando la base está sana (healthcheck y depends_on), cada servicio tiene límites de CPU y memoria, y volver atrás es desplegar la versión anterior: cada imagen aprobada tiene su número. Ansible acompaña en el deploy: espera a que todo esté sano y lo verifica por HTTPS. A la derecha, el impacto: escribir valores de ejemplo en los tres casilleros (Enter o Esc para salir) y el total se calcula solo. Aclarar que es una estimación con supuestos. Abajo, ser honestos: hoy todo corre en un solo servidor, es un punto único de falla. El próximo paso es una segunda instancia en otra zona con balanceador y alertas de monitoreo.</aside> </section> <!-- ===================== PLAY 3 · PIPELINE ===================== --> <!-- 8 · PIPELINE --> <section class="slide" data-play="Pipeline · CI/CD y seguridad" data-title="Etapas del pipeline" data-status="ok"> <div class="playline">PLAY [Pipeline · CI/CD y seguridad] <span class="muted">********************************</span></div> <div class="task">TASK [Cómo llega un cambio a producción] <span class="stars">*****************************</span></div> <h2>Cómo llega un cambio a producción</h2> <div class="loop"> <div class="it reveal"><span class="st ok">ok:</span><span class="nm">(item=validar)</span><span class="ds">Lint de cada Dockerfile</span></div> <div class="it sec reveal"><span class="st ok">ok:</span><span class="nm">(item=revisar_seguridad)</span><span class="ds">Checkov revisa cada Dockerfile antes de construir</span><span class="tg">SEGURIDAD</span></div> <div class="it reveal"><span class="st ok">ok:</span><span class="nm">(item=construir)</span><span class="ds">Una imagen por servicio, etiquetada con su commit</span></div> <div class="it sec reveal"><span class="st ok">ok:</span><span class="nm">(item=escanear)</span><span class="ds">Trivy analiza cada imagen construida</span><span class="tg">SEGURIDAD</span></div> <div class="it reveal"><span class="st ok">ok:</span><span class="nm">(item=publicar)</span><span class="ds">Solo las aprobadas reciben versión (ej. 1.0.0)</span></div> <div class="it pend reveal"><span class="st skip">pending:</span><span class="nm">(item=desplegar)</span><span class="ds">Llamar a los playbooks de Ansible · a integrar</span></div> </div> <div class="facts3"> <div><h3>En paralelo</h3><p>Los 8 servicios avanzan cada uno por su cuenta: si uno falla, no frena al resto.</p></div> <div><h3>Cuándo corre</h3><p>Con cambios en dev y main. A main solo se llega por merge request desde dev.</p></div> <div><h3>Runner</h3><p>Runner compartido del laboratorio, sin Docker-in-Docker. [confirmar por qué]</p></div> </div> <div class="out"><span class="ok">ok: [gitlab-pipeline] =&gt; etapas: 6 · servicios en paralelo: 8</span></div> <aside class="note">Se lee como el loop de una tarea: cada línea es una etapa del pipeline. Las dos con borde naranja son controles de seguridad. Los ocho servicios corren en paralelo, así que el tiempo total es el del más lento y no la suma. La última etapa conecta con el play anterior: el despliegue es llamar a los playbooks de Ansible, y está marcada como pendiente hasta que el job de deploy esté integrado; si ya lo está al momento de presentar, cambiar a ok.</aside> </section> <!-- 9 · SEGURIDAD --> <section class="slide" data-title="Controles de seguridad" data-status="ok"> <div class="task">TASK [Controles de seguridad en cada corrida] <span class="stars">************************</span></div> <h2>Cuatro controles en cada corrida</h2> <div class="row"> <div class="card reveal"><span class="tag skip">role: hadolint</span><h3>Buenas prácticas en los Dockerfiles</h3><p><b>Encontró:</b> chequeos de salud mal escritos en 5 servicios. Corregido.</p></div> <div class="card hl reveal"><span class="tag acc">role: checkov</span><h3>Seguridad del Dockerfile antes de construir</h3><p><b>Encontró:</b> la base de datos no avisaba cuándo estaba lista. Corregido.</p></div> </div> <div class="row"> <div class="card hl reveal"><span class="tag acc">role: trivy</span><h3>Vulnerabilidades y secretos en cada imagen</h3><p><b>Encontró:</b> 12 vulnerabilidades críticas con parche en 4 de las 8 imágenes.</p></div> <div class="card reveal"><span class="tag skip">role: reporte</span><h3>Un resumen descargable por corrida</h3><p>Todos los hallazgos, por servicio, quedan como artifact del pipeline.</p></div> </div> <div class="out"><span class="ok">ok: [gitlab-pipeline] =&gt; controles: 4 · se ejecutan en cada corrida</span></div> <aside class="note">Cada herramienta es un role con una sola responsabilidad. Presentarlas con su rol en una frase, sin teoría. Checkov fue una mejora que agregamos por nuestra cuenta: revisa la seguridad antes de gastar tiempo construyendo. Trivy revisa la imagen ya construida. Todo termina en un reporte descargable.</aside> </section> <!-- 10 · POLÍTICA --> <section class="slide" data-title="Política de promoción" data-status="skipping"> <div class="task">TASK [Promover imagen a producción] <span class="stars">*********************************</span></div> <h2>Si una imagen tiene una vulnerabilidad crítica con parche, <span class="acc">no llega a producción</span></h2> <div class="code reveal"><span class="c"># la condición que decide si una imagen recibe su versión</span> <span class="k">when:</span> - <span class="s">trivy.criticas_con_parche</span> == <span class="n">0</span> - <span class="s">checkov.chequeos_fallidos</span> == <span class="n">0</span></div> <div class="row"> <div class="card reveal"><span class="tag">1</span><p>La imagen se construye y se guarda con su commit, para poder revisarla.</p></div> <div class="card reveal"><span class="tag changed">2</span><p>El pipeline queda en advertencia, visible para todo el equipo.</p></div> <div class="card hl reveal"><span class="tag acc">3</span><p>No recibe versión: producción sigue con la última aprobada.</p></div> </div> <div class="out"><span class="skip">skipping: [imagen-con-criticas] =&gt; producción sigue con la última imagen aprobada</span></div> <aside class="note">Es la decisión consciente que pide la consigna, expresada como un when de Ansible: si la condición no se cumple, la promoción se saltea. No cortamos todo el pipeline: la imagen queda publicada con su commit para analizarla, pero no recibe número de versión, que es lo que usa el despliegue. Lo que no es crítico se informa en el reporte. Los riesgos aceptados se documentan con motivo y fecha de revisión.</aside> </section> <!-- 11 · HALLAZGOS --> <section class="slide" data-title="Corregir críticas" data-status="changed"> <div class="task">TASK [Corregir vulnerabilidades críticas] <span class="stars">****************************</span></div> <h2>Qué encontramos y cómo lo resolvimos</h2> <div class="row" style="gap:56px;align-items:flex-start"> <div style="width:470px;flex:none;display:flex;flex-direction:column;gap:16px" class="reveal"> <div class="big">12 → 0</div> <p style="font-size:28px;line-height:1.4;color:#B9C4D0">vulnerabilidades críticas con parche, en 4 de las 8 imágenes</p> <p style="font-size:26px"><span class="pill ok">excepciones: 0</span></p> </div> <table class="t reveal"> <tr><th style="width:22%">SERVICIO</th><th style="width:40%">QUÉ TENÍA</th><th>QUÉ HICIMOS</th></tr> <tr><td>clientes</td><td>Librería de base de datos con riesgo de inyección SQL</td><td>Actualizada a la versión corregida</td></tr> <tr><td>facturas</td><td>9 críticas en el framework web</td><td>Migrada a la nueva versión mayor del framework</td></tr> <tr><td>db</td><td>Un componente que no usamos, con una falla de TLS</td><td>Eliminado de la imagen</td></tr> <tr><td>web</td><td>Librería de compresión desactualizada</td><td>Paquetes de la imagen base actualizados</td></tr> </table> </div> <div class="out"><span class="changed">changed: [4 imágenes] =&gt; críticas con parche: 12 → 0 · sin excepciones</span></div> <aside class="note">El escaneo marcó 12 vulnerabilidades críticas que ya tenían solución, en cuatro imágenes; las otras cuatro no tenían ninguna. La más interesante fue facturas: una solo estaba corregida en la versión mayor nueva del framework, así que migramos en lugar de aceptar el riesgo. En la base eliminamos un componente que nunca se ejecuta. [Confirmar el 0 con la última corrida del pipeline antes de presentar.]</aside> </section> <!-- 12 · VERSIONADO --> <section class="slide" data-title="Versionado" data-status="changed"> <div class="task">TASK [Versionar cada imagen] <span class="stars">*****************************************</span></div> <h2>Cada imagen tiene su número de versión</h2> <table class="t reveal"> <tr><th style="width:18%">RAMA</th><th style="width:30%">ETIQUETA</th><th style="width:22%">EJEMPLO</th><th>PARA QUÉ</th></tr> <tr><td>todas</td><td>El commit que la generó</td><td class="mono">:a1b2c3d</td><td>Saber exactamente de qué código salió</td></tr> <tr><td>dev</td><td>Versión de prueba</td><td class="mono">:1.0.0-dev.57</td><td>Probar antes de liberar</td></tr> <tr><td>main</td><td>Versión liberada</td><td class="mono">:1.0.0</td><td>Lo que corre en producción</td></tr> </table> <div class="row"> <div class="card reveal"><span class="tag ok">SOLO LAS APROBADAS</span><p>Una imagen que no pasa los controles queda solo con su commit.</p></div> <div class="card reveal"><span class="tag skip">NO SE PISA</span><p>Una versión liberada nunca se reemplaza: para un cambio, se sube el número.</p></div> <div class="card hl reveal"><span class="tag acc">VUELTA ATRÁS</span><p>Volver a producción anterior es desplegar la versión anterior por su número.</p></div> </div> <div class="out"><span class="changed">changed: [imágenes] =&gt; latest: ya no se usa · versión: 1.0.0</span></div> <aside class="note">La consigna pide una estrategia de versionado y no depender de latest. Usamos versionado semántico con un archivo VERSION en el repositorio, una sola versión para las ocho imágenes. Toda imagen lleva el commit; en dev además recibe una versión de prueba con el número de pipeline, y en main la versión liberada. Solo las imágenes que pasan los controles reciben versión, y una versión liberada nunca se pisa. Eso nos da trazabilidad y vuelta atrás por nombre.</aside> </section> <!-- ===================== CIERRE ===================== --> <!-- 13 · MEJORAS --> <section class="slide" data-play="Cierre" data-title="Mejoras de la Parte 1" data-status="changed"> <div class="task">TASK [Aplicar mejoras pendientes de la Parte 1] <span class="stars">***********************</span></div> <h2>Qué resolvimos de la Parte 1</h2> <div class="loop"> <div class="it reveal"><span class="st changed">changed:</span><span class="ds">Plantilla de CloudFormation → <b>Terraform</b></span></div> <div class="it reveal"><span class="st changed">changed:</span><span class="ds">Scripts por SSH desde una computadora → <b>Ansible</b>, con playbooks idempotentes</span></div> <div class="it reveal"><span class="st changed">changed:</span><span class="ds">Secretos en Parameter Store como variante → <b>único camino</b></span></div> <div class="it reveal"><span class="st changed">changed:</span><span class="ds">Base sin chequeo de salud → <b>las APIs esperan</b> a que esté lista</span></div> <div class="it reveal"><span class="st changed">changed:</span><span class="ds">Sin escaneo automático → <b>Checkov y Trivy</b> en cada corrida</span></div> <div class="it reveal"><span class="st changed">changed:</span><span class="ds">Imágenes solo con latest → <b>versión semántica</b> que no se pisa</span></div> <div class="it pend reveal"><span class="st skip">pending:</span><span class="ds">Un usuario de base de datos por microservicio</span></div> <div class="it pend reveal"><span class="st skip">pending:</span><span class="ds">Más de un servidor</span></div> </div> <div class="out"><span class="changed">changed: [parte-1] =&gt; resueltas: 6 · pendientes: 2</span></div> <aside class="note">Lo que en la Parte 1 dejamos como oportunidad de mejora y ahora está resuelto, en el mismo orden de la presentación: Terraform, Ansible, secretos y salud de la base, y del pipeline el escaneo y el versionado. Dos quedan pendientes y lo decimos sin rodeos: un usuario de base por servicio y salir de una sola instancia. [Ajustar si el equipo resolvió algo más.]</aside> </section> <!-- 14 · CIERRE --> <section class="slide" data-title="PLAY RECAP" data-status="ok"> <div class="task" style="color:var(--text)">PLAY RECAP <span class="stars">*******************************************************************</span></div> <div class="recap reveal"><span class="ok">cto-fintech-nova</span> : <span class="ok">ok=3</span> <span class="changed">changed=2</span> <span class="fail">failed=0</span> <span class="skip">skipped=0</span> rescued=0 <span class="ok">aws</span> : <span class="ok">ok=2</span> changed=0 <span class="fail">failed=0</span> <span class="skip">skipped=0</span> <span class="changed">rescued=1</span> <span class="ok">ec2-fintech-nova</span> : <span class="ok">ok=3</span> changed=0 <span class="fail">failed=0</span> <span class="skip">skipped=0</span> rescued=0 <span class="ok">gitlab-pipeline</span> : <span class="ok">ok=4</span> <span class="changed">changed=2</span> <span class="fail">failed=0</span> <span class="skip">skipped=1</span> rescued=0</div> <div class="row" style="gap:48px; align-items:center; margin-top: 10px;"> <div style="flex:1; display:flex; flex-direction:column; gap:12px" class="reveal"> <div class="lbl">PRÓXIMAS TASKS</div> <div class="loop"> <div class="it"><span class="st skip">pending:</span><span class="ds">Desplegar desde el pipeline llamando a Ansible</span></div> <div class="it"><span class="st skip">pending:</span><span class="ds">Registry de GitLab con token de solo lectura</span></div> <div class="it"><span class="st skip">pending:</span><span class="ds">Estado de Terraform fuera de las computadoras</span></div> <div class="it"><span class="st skip">pending:</span><span class="ds">Segunda instancia y alertas de monitoreo</span></div> </div> <div style="display:flex; align-items:baseline; gap:24px; margin-top:12px;"> <div style="font:700 80px/1 var(--display); letter-spacing:-2px;">¡Muchas Gracias!</div> <p style="font-size:28px; color:#B9C4D0;">¿Preguntas?</p> </div> </div> <div style="flex:1.15; text-align:center;" class="reveal"> <img src="La_Banda_Ancha_Chiste_Final.png" alt="Chiste final de La Banda Ancha" style="max-width:100%; max-height:460px; object-fit:contain; border-radius:14px; border:1px solid var(--line); box-shadow:0 8px 24px rgba(0,0,0,0.5);"> </div> </div> <div class="out"><span class="muted">grupo-15@fintech-nova:~/tp-2$</span> <span class="caret" style="height:28px;width:14px"></span></div> <aside class="note">Cerrar con el resumen como si terminara el playbook, una línea por cada play: ninguno falló, Terraform se recuperó de un error (el que contamos) y el pipeline salteó una promoción a propósito (la política de seguridad). Leer los próximos pasos en una frase cada uno, rematar con el guiño de La Banda Ancha y abrir a preguntas. Este cierre debería empezar antes del minuto 9.</aside> </section> </main> </div> </div> <div id="notes"></div> <div id="hud">← → avanzar · N notas · F pantalla completa</div> <script> (function () { var stage = document.getElementById('stage'); var slides = Array.prototype.slice.call(document.querySelectorAll('.slide')); var list = document.getElementById('tasklist'); var notes = document.getElementById('notes'); var LABEL = { ok: 'ok', changed: 'changed', skipping: 'skipping', rescued: 'rescued' }; var COLOR = { ok: 'var(--ok)', changed: 'var(--changed)', skipping: 'var(--skip)', rescued: 'var(--changed)' }; var items = []; slides.forEach(function (s, k) { var play = s.getAttribute('data-play'); if (play) { var ph = document.createElement('li'); ph.className = 'ph'; ph.textContent = 'PLAY [' + play.split(' · ')[0] + ']'; ph.addEventListener('click', function (e) { e.stopPropagation(); }); list.appendChild(ph); } var li = document.createElement('li'); li.innerHTML = '<span class="st"></span><span>' + s.getAttribute('data-title') + '</span>'; li.addEventListener('click', function (e) { e.stopPropagation(); show(k); }); list.appendChild(li); items.push(li); }); var cmd = 'ansible-playbook sprint-demo.yml'; var typedEl = document.getElementById('typed'), after = document.getElementById('coverAfter'), timer; function typeCover() { clearInterval(timer); typedEl.textContent = ''; after.classList.remove('on'); var n = 0; timer = setInterval(function () { typedEl.textContent = cmd.slice(0, ++n); if (n >= cmd.length) { clearInterval(timer); setTimeout(function () { after.classList.add('on'); }, 300); } }, 45); } var i = 0; var h = parseInt((location.hash.match(/slide-(\d+)/) || [])[1], 10); if (h >= 1 && h <= slides.length) i = h - 1; function fit() { var s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080); stage.style.transform = 'translate(-50%, -50%) scale(' + s + ')'; } function show(n) { i = Math.max(0, Math.min(slides.length - 1, n)); slides.forEach(function (s, k) { s.classList.toggle('active', k === i); var li = items[k], st = li.querySelector('.st'); li.className = k < i ? 'done' : (k === i ? 'current' : ''); if (k < i) { var v = s.getAttribute('data-status'); st.textContent = LABEL[v]; st.style.color = COLOR[v]; } else if (k === i) { st.textContent = 'running'; st.style.color = ''; } else { st.textContent = '·'; st.style.color = ''; } }); if (i === 0) typeCover(); else { clearInterval(timer); after.classList.add('on'); typedEl.textContent = cmd; } var a = slides[i].querySelector('aside.note'); notes.textContent = (i + 1) + '/' + slides.length + ' — ' + (a ? a.textContent : ''); history.replaceState(null, '', '#slide-' + (i + 1)); } document.addEventListener('keydown', function (e) { if (e.target.closest && e.target.closest('input')) { if (e.key === 'Enter' || e.key === 'Escape') e.target.blur(); return; } if (['ArrowRight', 'PageDown', ' ', 'Enter'].indexOf(e.key) >= 0) { e.preventDefault(); show(i + 1); } else if (['ArrowLeft', 'PageUp', 'Backspace'].indexOf(e.key) >= 0) { e.preventDefault(); show(i - 1); } else if (e.key === 'Home') show(0); else if (e.key === 'End') show(slides.length - 1); else if (e.key === 'n' || e.key === 'N') notes.classList.toggle('show'); else if (e.key === 'f' || e.key === 'F') { if (document.fullscreenElement) document.exitFullscreen(); else document.documentElement.requestFullscreen(); } }); document.addEventListener('click', function (e) { if (e.target.closest('#notes, input, button, .cbox')) return; show(e.clientX < window.innerWidth / 3 ? i - 1 : i + 1); }); window.addEventListener('resize', fit); fit(); show(i); })(); // Ansible: simulación de site.yml en la diapositiva "Preparar y desplegar" (function () { var sec = document.getElementById('s-deploy'), btn = document.getElementById('runDeploy'); if (!sec || !btn) return; var q = function (s) { return Array.prototype.slice.call(sec.querySelectorAll(s)); }; var lns = q('.ln'), timers = []; var LABEL = '▶ ansible-playbook site.yml'; function reset() { timers.forEach(clearTimeout); timers = []; q('.on').forEach(function (e) { e.classList.remove('on'); }); btn.disabled = false; btn.textContent = LABEL; } function at(ms, fn) { timers.push(setTimeout(fn, ms)); } function on(e) { e.classList.add('on'); } function run() { reset(); btn.disabled = true; btn.textContent = '⏳ provision.yml…'; var t = 0; at(t, function () { on(lns[0]); }); t += 600; q('.prov li').forEach(function (e) { at(t, function () { on(e); }); t += 380; }); at(t, function () { on(sec.querySelector('.prep')); btn.textContent = '⏳ deploy.yml…'; on(lns[1]); }); t += 700; q('.dep li').forEach(function (e) { at(t, function () { on(e); }); t += 450; }); at(t, function () { on(sec.querySelector('.stg.live')); }); t += 300; q('.tile').forEach(function (e) { at(t, function () { on(e); }); t += 200; }); t += 250; q('.checks div').forEach(function (e) { at(t, function () { on(e); }); t += 400; }); at(t, function () { btn.disabled = false; btn.textContent = '↻ volver a correr'; }); } btn.addEventListener('click', function (e) { e.stopPropagation(); btn.blur(); run(); }); new MutationObserver(function () { if (!sec.classList.contains('active')) reset(); }) .observe(sec, { attributes: true, attributeFilter: ['class'] }); reset(); })(); // Caída: simulación de restart: always y calculadora de impacto (function () { var sec = document.getElementById('s-crash'), btn = document.getElementById('runCrash'), btnDb = document.getElementById('runCrashDb'); if (!sec || !btn || !btnDb) return; var tile = document.getElementById('crashTarget'), db = sec.querySelector('[data-svc="db"]'); var apis = Array.prototype.slice.call(sec.querySelectorAll('[data-svc="api"]')); var st = document.getElementById('crashSt'), shield = document.getElementById('shieldRestart'), timers = []; var IDLE = 'SI UN CONTENEDOR FALLA'; function at(ms, fn) { timers.push(setTimeout(fn, ms)); } function say(text, cls) { st.textContent = text; st.className = cls ? 'crash-st run ' + cls : 'lbl crash-st'; } function set(els, cls) { els.forEach(function (e) { e.className = 'ctile' + (cls ? ' ' + cls : ''); }); } function busy(b) { btn.disabled = b; btnDb.disabled = b; } function reset() { timers.forEach(clearTimeout); timers = []; set([db].concat(apis), ''); shield.classList.remove('lit'); busy(false); say(IDLE); } function runApi() { reset(); busy(true); set([tile], 'down'); say('✗ api-pagos se cayó', 'bad'); at(1600, function () { set([tile], 'starting'); shield.classList.add('lit'); say('↻ restart: always', 'chg'); }); at(3600, function () { set([tile], ''); say('✓ volvió solo · el resto siguió', 'good'); }); at(5200, function () { shield.classList.remove('lit'); busy(false); }); } function runDb() { reset(); busy(true); set([db], 'down'); say('✗ la base se cayó', 'bad'); at(800, function () { set(apis, 'err'); say('✗ las 5 APIs dan error', 'bad'); }); at(2800, function () { set([db], 'starting'); shield.classList.add('lit'); say('↻ restart: always', 'chg'); }); at(4600, function () { set([db], ''); set(apis, 'starting'); say('↻ las APIs se reconectan', 'chg'); }); apis.forEach(function (e, k) { at(5600 + k * 220, function () { set([e], ''); }); }); at(6900, function () { say('✓ volvió · datos intactos', 'good'); }); at(8200, function () { shield.classList.remove('lit'); busy(false); }); } btn.addEventListener('click', function (e) { e.stopPropagation(); btn.blur(); runApi(); }); btnDb.addEventListener('click', function (e) { e.stopPropagation(); btnDb.blur(); runDb(); }); new MutationObserver(function () { if (!sec.classList.contains('active')) reset(); }) .observe(sec, { attributes: true, attributeFilter: ['class'] }); reset(); var tx = document.getElementById('impTx'), monto = document.getElementById('impMonto'), com = document.getElementById('impCom'), total = document.getElementById('impTotal'); function calc() { var a = parseFloat(tx.value), b = parseFloat(monto.value), c = parseFloat(com.value); if (isNaN(a) || isNaN(b) || isNaN(c)) { total.textContent = 'USD ___'; return; } var r = a * b * c / 100; total.textContent = 'USD ' + r.toLocaleString('es-AR', { maximumFractionDigits: r < 100 ? 2 : 0 }); } [tx, monto, com].forEach(function (el) { el.addEventListener('input', calc); }); })(); </script> </body> </html>
Landing Page
This ad does not have a landing page available
Network Timeline
Performance Summary

6

Requests

3

Domains

171KB

Transfer Size

186KB

Content Size

306.0ms

Dom Content Loaded

324.0ms

First Paint

432.0ms

Load Time
Domain Breakdown
Transfer Size (bytes)
Loading...
Content Size (bytes)
Loading...
Header Size (bytes)
Loading...
Requests
Loading...
Timings (ms)
Loading...
Total Time
Loading...
Content Breakdown
Transfer Size (bytes)
Loading...
Content Size (bytes)
Loading...
Header Size (bytes)
Loading...
Requests
Loading...