From 520206f3918d3dca0133f37eedf388c91e1fac02 Mon Sep 17 00:00:00 2001 From: fukunee Date: Thu, 14 May 2026 01:16:15 +0800 Subject: [PATCH] Add infinite-scroll pagination to home page and publish new blog posts --- content/_index.md | 2 - content/blog/_index.md | 1 - content/blog/cicd-github-actions.md | 98 +++++++++++++++++++++++++++ content/blog/database-indexing.md | 48 +++++++++++++ content/blog/home-network-nas.md | 49 ++++++++++++++ content/blog/linux-customization.md | 32 +++++++++ content/blog/python-async-guide.md | 67 ++++++++++++++++++ content/blog/rust-error-handling.md | 95 ++++++++++++++++++++++++++ content/blog/ssh-tunnel-guide.md | 53 +++++++++++++++ content/blog/svelte-first-look.md | 45 ++++++++++++ content/blog/tailwind-v4-migration.md | 64 +++++++++++++++++ content/blog/terraform-aws-ecs.md | 70 +++++++++++++++++++ layouts/hextra-home.html | 66 ++++++++++++------ 13 files changed, 667 insertions(+), 23 deletions(-) create mode 100644 content/blog/cicd-github-actions.md create mode 100644 content/blog/database-indexing.md create mode 100644 content/blog/home-network-nas.md create mode 100644 content/blog/linux-customization.md create mode 100644 content/blog/python-async-guide.md create mode 100644 content/blog/rust-error-handling.md create mode 100644 content/blog/ssh-tunnel-guide.md create mode 100644 content/blog/svelte-first-look.md create mode 100644 content/blog/tailwind-v4-migration.md create mode 100644 content/blog/terraform-aws-ecs.md diff --git a/content/_index.md b/content/_index.md index faf214a..9dea0ac 100644 --- a/content/_index.md +++ b/content/_index.md @@ -2,8 +2,6 @@ title: FuKun layout: hextra-home --- - - {{< hextra/hero-subtitle >}} Fukun.Net {{< /hextra/hero-subtitle >}} diff --git a/content/blog/_index.md b/content/blog/_index.md index 3bdbf6e..e5c36ae 100644 --- a/content/blog/_index.md +++ b/content/blog/_index.md @@ -1,6 +1,5 @@ --- --- - {{< hextra/hero-subtitle >}} Fukun.Net/blog {{< /hextra/hero-subtitle >}} diff --git a/content/blog/cicd-github-actions.md b/content/blog/cicd-github-actions.md new file mode 100644 index 0000000..e2ce138 --- /dev/null +++ b/content/blog/cicd-github-actions.md @@ -0,0 +1,98 @@ +--- +title: "CI/CD Patterns with GitHub Actions" +date: 2026-05-15T09:00:00+08:00 +draft: false +description: "Build matrices, caching strategies, environment-specific deployments, and reusable workflows. Patterns that scale from solo projects to team repos." +tags: + - CI/CD + - GitHub + - DevOps +--- + +GitHub Actions is now the default CI for most open-source projects. Build matrices save config lines, caching `node_modules` and Go modules saves minutes per run. Reusable workflows keep things DRY across repos. + +## Build Matrices + +Don't copy-paste job definitions for different Node versions or OS targets. Use a matrix: + +```yaml +jobs: + test: + strategy: + matrix: + node: [18, 20, 22] + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - run: npm test +``` + +This single definition expands to 6 parallel jobs. Add `fail-fast: false` if you want all runs to complete even when one fails — helpful for catching platform-specific bugs in one CI run. + +## Smart Caching + +Caching is the easiest way to cut CI minutes: + +```yaml +- uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + restore-keys: npm-${{ runner.os }}- +``` + +The `restore-keys` fallback is important: even a partial cache hit saves time. For Go modules, cache `~/go/pkg/mod`. For Docker builds, use BuildKit's registry cache (`--cache-from type=registry`) for layer caching across CI runs. + +## Environment-Specific Deployments + +Use environments to gate production deploys: + +```yaml +deploy-prod: + needs: [test, build] + environment: production + steps: + - run: ./deploy.sh production +``` + +The `environment: production` lets you set required reviewers, wait timers, and environment-specific secrets in the GitHub UI. No more accidentally deploying to prod from a feature branch. + +## Reusable Workflows + +When you maintain multiple repos, reusable workflows prevent drift: + +```yaml +# .github/workflows/deploy.yml in a shared repo +on: + workflow_call: + inputs: + environment: + required: true + type: string + secrets: + AWS_ROLE: + required: true +``` + +Call it from any repo: + +```yaml +jobs: + deploy: + uses: org/shared-workflows/.github/workflows/deploy.yml@main + with: + environment: staging + secrets: + AWS_ROLE: ${{ secrets.AWS_ROLE }} +``` + +Update the shared workflow once, and every repo benefits. This pattern scales well for orgs with 10+ services. + +## Practical Tips + +- Set `timeout-minutes` on every job — the default is 360 minutes, and you don't want a hung test burning your quota. +- Use `concurrency` to cancel redundant runs when pushing to the same PR multiple times. +- Pin action versions to SHA hashes for supply-chain security, not just tags. diff --git a/content/blog/database-indexing.md b/content/blog/database-indexing.md new file mode 100644 index 0000000..10a699d --- /dev/null +++ b/content/blog/database-indexing.md @@ -0,0 +1,48 @@ +--- +title: "数据库索引优化实战" +date: 2026-05-10T16:00:00+08:00 +draft: false +description: "慢查询分析、联合索引的最左前缀原则、覆盖索引与回表的性能差异。几个真实 SQL 优化案例,从几秒降到毫秒级。" +tags: + - 数据库 + - SQL + - 性能 +--- + +`EXPLAIN` 是 DBA 的眼睛。看 `type` 字段从 `ALL` 变成 `ref` 是最有成就感的事。 + +## 从慢查询开始 + +一切优化从定位问题开始。打开 MySQL 的慢查询日志(`slow_query_log`),设置 `long_query_time = 0.1`,跑一段时间后分析。PostgreSQL 用 `pg_stat_statements` 扩展,同样有效。 + +拿到慢查询,先 `EXPLAIN` 看执行计划。关注几个关键字段: + +| 字段 | 含义 | 目标 | +|------|------|------| +| type | 访问类型 | 至少到 range,最好到 ref/const | +| key | 使用的索引 | 不为 NULL | +| rows | 扫描行数 | 越小越好 | +| Extra | 额外信息 | 避免 Using filesort / Using temporary | + +## 最左前缀原则 + +联合索引是最容易出错的地方。假设索引是 `(a, b, c)`: + +- `WHERE a = 1` ✅ 用到索引 +- `WHERE a = 1 AND b = 2` ✅ 用到索引 +- `WHERE b = 2` ❌ 用不到 +- `WHERE a = 1 AND c = 3` ⚠️ 只用到了 a 列 + +排序也受这个规则约束。`ORDER BY a, b` 没问题,`ORDER BY b, c` 就需要额外排序。 + +## 覆盖索引 + +如果查询需要的所有列都在索引里,就不需要回表取数据。`Extra` 列显示 `Using index` 就是覆盖索引。对一个高频查询,覆盖索引能减少大量随机 I/O。 + +代价是索引变大了。不要为了覆盖索引把所有列都塞进索引——算一笔写入性能和存储空间的账。 + +## 真实案例 + +某个订单列表查询,原始 SQL 跑了 8 秒。`EXPLAIN` 一看,`type: ALL`,全表扫描 200 万行。加了 `(user_id, created_at)` 联合索引后,`type: ref`,扫描 20 行,耗时 12ms。 + +**索引不是越多越好**——每个索引都会拖慢写入速度并占用磁盘空间。建索引前问自己:这个查询的执行频率值得一个索引吗? diff --git a/content/blog/home-network-nas.md b/content/blog/home-network-nas.md new file mode 100644 index 0000000..f297e7a --- /dev/null +++ b/content/blog/home-network-nas.md @@ -0,0 +1,49 @@ +--- +title: "家庭网络与 NAS 搭建方案" +date: 2026-05-17T15:00:00+08:00 +draft: false +description: "软路由、Wi-Fi 6 AP、自建 NAS 的硬件选型和软件方案。Plex 媒体服务器 + Syncthing 文件同步 + Tailscale 远程访问,一套低成本高品质的家庭网络实践。" +tags: + - NAS + - 网络 + - 硬件 +--- + +折腾家庭网络的终点是克制。和 Linux 桌面美化一样,容易用力过猛。本文记录一套低成本但够用的家庭网络方案。 + +## 网络拓扑 + +``` +光猫 → 软路由(OpenWrt)→ 交换机 + ├── Wi-Fi 6 AP ×2 + ├── NAS(TrueNAS) + └── 有线设备 +``` + +软路由选 J4125 或 N100 这类低功耗 x86,刷 OpenWrt,负责拨号、DHCP、科学上网。AP 选 Wi-Fi 6 的,TP-Link XDR 系列性价比不错,刷 OpenWrt 或者用原厂固件都可以。 + +两个 AP 组 Mesh,不需要折腾 AC 控制器。家里 120 平米基本无死角覆盖。 + +## NAS 硬件与系统 + +旧电脑加几块硬盘就是一台 NAS。电费要算:不要拿老 i7 加独显当 NAS,待机功耗四五十瓦,一个月电费够买半块硬盘。 + +推荐方案:J4125 或 N100 小板 + 8GB 内存 + 2~4 块 4TB 硬盘组 RAID-Z1。系统选 TrueNAS Scale,基于 Linux,容器和虚拟机支持比 Core 版更好。待机功耗 15W 左右,一年电费不到一百块。 + +## 软件方案 + +三个核心应用: + +| 软件 | 用途 | 替代方案 | +|------|------|----------| +| **Plex** | 媒体服务器,自动刮削海报和元数据 | Jellyfin(开源免费) | +| **Syncthing** | 文件同步,多设备自动备份照片和文档 | Resilio Sync | +| **Tailscale** | 虚拟局域网,外面也能访问家里的服务 | ZeroTier、WireGuard 裸配 | + +Plex 装在 TrueNAS 的 App 里,显卡直通给 Plex 做硬件转码——手机上用较低码率看片时自动转码,画质损失不大。Syncthing 每台设备装一个,照片目录设为"仅发送",电脑上的文档文件夹设为"双向同步"。 + +Tailscale 是真正的体验质变。不需要公网 IP,不需要端口转发,不需要记 IP 地址。手机、笔记本、NAS 在同一个虚拟局域网里,出门在外访问 Plex 和 NAS 文件就像在家一样。免费版支持最多 100 台设备,个人用完全够。 + +## 总结 + +方案的核心思路:**够用即可**。不要想着一步到位,先从旧硬件开始跑起来,哪里不够补哪里。家庭网络的价值在于稳定无声地跑着,不是折腾本身。 diff --git a/content/blog/linux-customization.md b/content/blog/linux-customization.md new file mode 100644 index 0000000..747f3ed --- /dev/null +++ b/content/blog/linux-customization.md @@ -0,0 +1,32 @@ +--- +title: "Linux 桌面美化指北" +date: 2026-05-11T10:00:00+08:00 +draft: false +description: "从默认 Gnome 到一套顺手的桌面环境。GTK 主题、图标包、终端配色、窗口管理器的选择与配置思路。不追求花哨,追求每天盯着看不累。" +tags: + - Linux + - 桌面 + - 效率 +--- + +Linux 桌面环境的选择很多,但真正好用的配置往往很简单。本文记录从 Ubuntu 默认桌面开始,逐步调整到一套耐看、高效的配置。 + +## 主题选择 + +白天用亮色主题降低反光,晚上切暗色保护眼睛。推荐 **Nord** 和 **Catppuccin** 两套配色——前者冷峻克制,后者柔和温暖,每天盯着看八小时不累。 + +GTK 主题建议选择维护活跃的项目,避免 Gnome 大版本升级后样式错乱。Qt 应用在 Gnome 下可以用 `qt5ct` 统一风格。 + +## 字体配置 + +终端字体一定要等宽且清晰。推荐 **Fira Code** 做编程字体,连字(ligatures)让 `=>`、`!=`、`>=` 这类符号组合更易读。中文字体用 **Noto Sans CJK**,和西文搭配自然。 + +## 终端与 Shell + +终端模拟器推荐 **Alacritty**——GPU 加速,配置文件清晰。配合 zsh + **starship** 提示符,日常使用效率提升不少。Starship 的优势在于跨 shell 兼容,换到 fish 或 nu 不用重新配置。 + +## 窗口管理 + +Gnome 默认的三指滑动切换工作区已经很好用。如果追求更极致的键盘操作,可以试试 **Pop Shell** 平铺扩展,或者直接上 **i3/sway**。但对大多数人来说,Gnome 的默认交互已经够用。 + +关键原则:**不追求花哨,追求每天盯着看不累。** diff --git a/content/blog/python-async-guide.md b/content/blog/python-async-guide.md new file mode 100644 index 0000000..c768eca --- /dev/null +++ b/content/blog/python-async-guide.md @@ -0,0 +1,67 @@ +--- +title: "Python Async: Beyond the Basics" +date: 2026-05-12T11:00:00+08:00 +draft: false +description: "Understanding the event loop, writing proper async context managers, and avoiding the most common pitfalls with asyncio.gather and cancel scopes." +tags: + - Python + - Async + - Programming +--- + +Async Python is no longer optional if you're doing any kind of I/O-bound work. Beyond the basics of `async`/`await`, understanding the event loop helps debug elusive issues. + +## The Event Loop in Practice + +The event loop runs one coroutine at a time, switching at `await` points. This is cooperative multitasking — a coroutine that never awaits blocks everything else. If you have CPU-bound work inside an async function, offload it to a thread pool with `asyncio.to_thread()`. + +```python +import asyncio + +# Bad: blocks the event loop +async def bad(): + result = heavy_computation() # CPU-bound, no await + return result + +# Good: offloads to a thread +async def good(): + result = await asyncio.to_thread(heavy_computation) + return result +``` + +## Async Context Managers + +Writing proper async context managers is essential for managing connections and locks: + +```python +class AsyncConnection: + async def __aenter__(self): + self.conn = await create_connection() + return self.conn + + async def __aexit__(self, *args): + await self.conn.close() +``` + +The `contextlib.asynccontextmanager` decorator makes this even cleaner for simple cases. + +## Common Pitfalls with `asyncio.gather` + +`gather` runs tasks concurrently, but one failing task doesn't cancel the others by default. If you need all-or-nothing semantics, use `asyncio.TaskGroup` (Python 3.11+): + +```python +async with asyncio.TaskGroup() as tg: + t1 = tg.create_task(fetch(url1)) + t2 = tg.create_task(fetch(url2)) +# Both succeeded, or both were cancelled +``` + +## Cancel Scopes + +Cancellation in asyncio is cooperative: `asyncio.CancelledError` is raised at the next `await`. If your coroutine catches and suppresses it, the task becomes uncancellable — usually a bug. Use `asyncio.shield()` sparingly to protect critical cleanup code. + +## Structured Concurrency + +The trend across languages (Trio in Python, Kotlin coroutines, Swift concurrency) is toward structured concurrency: every task has a clear parent scope that determines its lifetime. `TaskGroup` is Python's step in this direction. Embrace it over bare `create_task()` calls. + +Async Python has matured significantly. Most of the old footguns have been addressed, but the fundamentals — understand the event loop, respect cancellation, and structure your concurrency — remain essential. diff --git a/content/blog/rust-error-handling.md b/content/blog/rust-error-handling.md new file mode 100644 index 0000000..c87d7c2 --- /dev/null +++ b/content/blog/rust-error-handling.md @@ -0,0 +1,95 @@ +--- +title: "Rust Error Handling: From unwrap to anyhow" +date: 2026-05-16T13:00:00+08:00 +draft: false +description: "The journey from panic-driven development to proper error types. When to use thiserror for library crates and anyhow for applications." +tags: + - Rust + - Error Handling + - Programming +--- + +Rust forces you to think about errors. Starting with `unwrap()` everywhere is fine for prototypes, but production code needs proper error types. + +## Stage 1: The Unwrap Phase + +```rust +let config = read_config().unwrap(); +let db = connect(&config).unwrap(); +let result = query(&db).unwrap(); +``` + +Every Rust beginner goes through this. It's fine for scripts and prototypes — if something fails, the panic message with a line number is good enough. The problem starts when you need graceful error handling or meaningful error messages for users. + +## Stage 2: Box + +The quick upgrade path: + +```rust +fn do_thing() -> Result<(), Box> { + let config = read_config()?; + let db = connect(&config)?; + query(&db)?; + Ok(()) +} +``` + +The `?` operator works with any error type that implements `From`. `Box` catches everything. But you lose type information — the caller can't match on specific error variants. + +## Stage 3: Proper Error Types with thiserror + +For libraries, define explicit error types: + +```rust +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AppError { + #[error("configuration error: {0}")] + Config(String), + #[error("database error: {0}")] + Database(#[from] sqlx::Error), + #[error("not found: {0}")] + NotFound(String), +} +``` + +The `#[from]` attribute auto-implements `From`, so `?` works seamlessly. Callers can match on variants: + +```rust +match do_thing().await { + Err(AppError::NotFound(id)) => { /* handle 404 */ } + Err(e) => { /* log and return 500 */ } + Ok(data) => { /* success */ } +} +``` + +## Stage 4: Application Errors with anyhow + +For binaries, `anyhow` provides flexible error handling without the ceremony: + +```rust +use anyhow::{Context, Result}; + +fn main() -> Result<()> { + let config = read_config() + .with_context(|| "failed to read config file")?; + let db = connect(&config) + .context("database connection failed")?; + Ok(()) +} +``` + +`context()` and `with_context()` attach human-readable messages at each fallible step. The error output is a chain of contexts, making debugging straightforward. + +## The Ecosystem Split + +| Crate | Use Case | Key Feature | +|-------|----------|-------------| +| `thiserror` | Library crates | Derive macro, typed errors | +| `anyhow` | Application binaries | Context chains, easy `?` | +| `eyre` | Applications (alternative) | Colorful error reports, custom hooks | + +The ecosystem has settled on this pragmatic split: **thiserror for libraries, anyhow for binaries**. Libraries expose typed errors so callers can react programmatically; applications benefit from rich, human-readable error chains. + +Remember: `unwrap()` and `expect()` still have their place — in tests, in examples, and when an invariant violation truly merits a panic. But for production code paths, make errors part of your type signature. diff --git a/content/blog/ssh-tunnel-guide.md b/content/blog/ssh-tunnel-guide.md new file mode 100644 index 0000000..e1a69a4 --- /dev/null +++ b/content/blog/ssh-tunnel-guide.md @@ -0,0 +1,53 @@ +--- +title: "SSH 隧道实用指南" +date: 2026-05-09T14:00:00+08:00 +draft: false +description: "本地端口转发、远程端口转发、动态端口转发三种模式的实际应用场景。从绕过防火墙访问内网服务,到把本地开发环境暴露给公网调试。" +tags: + - SSH + - 网络 + - 教程 +--- + +SSH 隧道是开发者的必备技能,但很多人只用过最基本的 `ssh user@host`。三种端口转发模式记清楚了,日常开发基本够用。 + +## 本地端口转发(-L) + +把远程内网的服务映射到本机。 + +```bash +ssh -L 3306:db.internal:3306 jump-host +``` + +这条命令让本地的 `localhost:3306` 流量通过跳板机转发到内网的数据库服务器。典型场景:你在家开发,公司的数据库只在内网可访问。连上跳板机后,`mysql -h 127.0.0.1 -P 3306` 就能直接操作数据库。 + +## 远程端口转发(-R) + +反过来,把本地端口暴露给远程服务器。 + +```bash +ssh -R 8080:localhost:3000 public-server +``` + +这个场景适合调试 webhook:你在本地起了服务,第三方服务需要回调你的端点。把本地的 3000 端口映射到公网服务器的 8080,webhook URL 填 `http://public-server:8080` 就能收到回调。 + +记得在 remote 服务器上设置 `GatewayPorts yes` 才能让外部访问。 + +## 动态端口转发(-D) + +临时搭建 SOCKS 代理。 + +```bash +ssh -D 1080 jump-host +``` + +然后浏览器或系统代理设置为 `socks5://localhost:1080`,流量就全走跳板机出去了。配合 SwitchyOmega 这类浏览器扩展,按域名自动切换代理规则,体验更好。 + +## 实用技巧 + +- 加 `-N` 不执行远程命令,纯隧道。 +- 加 `-f` 后台运行。 +- 加 `-C` 压缩传输,慢网络下有用。 +- 用 `~/.ssh/config` 给常用隧道起别名,避免每次敲长命令。 + +三种模式记清楚,SSH 隧道基本够用。 diff --git a/content/blog/svelte-first-look.md b/content/blog/svelte-first-look.md new file mode 100644 index 0000000..8ccff20 --- /dev/null +++ b/content/blog/svelte-first-look.md @@ -0,0 +1,45 @@ +--- +title: "Svelte 初体验:不一样的思路" +date: 2026-05-10T09:30:00+08:00 +draft: false +description: "试用了 Svelte 5 的 runes 语法和 SvelteKit 框架。没有虚拟 DOM,编译时直接把组件转成 DOM 操作代码,打包体积小得惊人。" +tags: + - Svelte + - 前端 + - JavaScript +--- + +用惯了 React 和 Vue,第一次接触 Svelte 的时候确实有种新鲜感。最直观的感受:**代码量少了很多**。 + +## Runes 语法 + +Svelte 5 引入了 runes 语法,让响应式声明更显式: + +- **`$state`** — 声明响应式变量,替代了之前靠赋值触发响应式的隐式规则。 +- **`$derived`** — 衍生值,类似 Vue 的 computed。 +- **`$effect`** — 副作用,自动追踪依赖并在变化时执行。 + +```svelte + +``` + +逻辑清晰,没有 `useEffect` 的依赖数组心智负担。 + +## 编译时魔法 + +Svelte 的核心理念:没有虚拟 DOM,编译器在构建时直接把组件转成高效的 DOM 操作代码。打包体积小得惊人,一个简单的组件可能只有几百字节。对移动端场景尤其友好。 + +## SvelteKit + +SvelteKit 是 Svelte 的全栈框架,文件路由设计简洁。约定优于配置——新建一个 `src/routes/about/+page.svelte`,就自动有了 `/about` 路由。内置 SSR、SSG、CSR 多种渲染模式,服务器端数据加载用 `load` 函数,类型安全做得不错。 + +## 总结 + +Svelte 适合想要减少样板代码、追求小打包体积的项目。React 和 Vue 的生态更成熟,但 Svelte 的思路值得关注。下一步打算用 SvelteKit 重写个人博客试试看。 diff --git a/content/blog/tailwind-v4-migration.md b/content/blog/tailwind-v4-migration.md new file mode 100644 index 0000000..cdb1cf9 --- /dev/null +++ b/content/blog/tailwind-v4-migration.md @@ -0,0 +1,64 @@ +--- +title: "Migrating to Tailwind CSS v4" +date: 2026-05-13T08:00:00+08:00 +draft: false +description: "CSS-first configuration, the new @theme directive, container queries, and what actually changed from v3. A practical migration log." +tags: + - CSS + - Tailwind + - Frontend +--- + +Tailwind v4 drops the JavaScript config file in favor of CSS-based configuration. The migration is mostly mechanical but there are gotchas with the `@apply` directive and custom variants. + +## CSS-First Configuration + +The biggest change: `tailwind.config.js` is replaced by CSS. Your design tokens live in the `@theme` directive: + +```css +@import "tailwindcss"; + +@theme { + --color-primary: #2563eb; + --color-primary-dark: #1d4ed8; + --font-sans: "Inter", sans-serif; + --spacing-container: 1280px; +} +``` + +VSCode IntelliSense still works — the Tailwind CSS extension reads theme values from your stylesheet. + +## The Migration Path + +1. Install v4: `npm install tailwindcss@next` +2. Run the automatic migration tool: `npx @tailwindcss/upgrade` +3. Manually check `@apply` usages — the tool handles most cases, but deeply nested `@apply` with custom utilities sometimes breaks. +4. Replace `theme.extend` in your old config with `@theme` blocks in CSS. + +## New Features Worth Using + +### Container Queries + +No more media queries for component-level responsiveness. Tailwind v4 ships container query utilities: + +```html +
+
...
+
+``` + +### Dynamic Utility Values + +Arbitrary values are simpler: `w-(--sidebar-width)` references a CSS custom property. No more bracket syntax for common cases. + +### Native Cascade Layers + +Tailwind v4 uses `@layer` to control specificity. Base, components, and utilities are layered in the correct order — `@apply` in component layer no longer risks specificity wars. + +## Gotchas + +- Custom variants defined in old JS config need to be re-registered via `@variant` in CSS. +- The `important` config option now needs an explicit CSS strategy. +- Some plugins haven't updated yet — check compatibility before migrating production projects. + +Overall, the migration took about two hours for a mid-sized project. The CSS-first approach feels more natural, and the build is noticeably faster thanks to the new Oxide engine. diff --git a/content/blog/terraform-aws-ecs.md b/content/blog/terraform-aws-ecs.md new file mode 100644 index 0000000..ad37907 --- /dev/null +++ b/content/blog/terraform-aws-ecs.md @@ -0,0 +1,70 @@ +--- +title: "Deploying to AWS ECS with Terraform" +date: 2026-05-14T10:00:00+08:00 +draft: false +description: "Infrastructure as code from scratch: VPC, subnets, security groups, ECR repository, ECS service with Fargate, and an ALB in front. Every resource explained." +tags: + - AWS + - Terraform + - DevOps +--- + +ECS with Fargate is the sweet spot between managing servers and going full serverless. Terraform makes it reproducible. + +## The Stack + +Here's what we need to provision: + +| Resource | Purpose | +|----------|---------| +| VPC | Isolated network | +| Public & private subnets | Multi-AZ placement | +| Security groups | Network access control | +| ECR repository | Container image storage | +| ECS cluster + service | Run containers | +| ALB + target group | Traffic distribution | + +## Security Groups: Getting Them Right + +The key is getting the security groups right — one misconfigured rule and your containers can't talk to each other, or worse, your database is exposed to the internet. + +```hcl +resource "aws_security_group" "ecs_service" { + name = "ecs-service-sg" + vpc_id = aws_vpc.main.id + + ingress { + from_port = 3000 + to_port = 3000 + protocol = "tcp" + security_groups = [aws_security_group.alb.id] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} +``` + +The principle: **allow the minimum**. The ALB security group allows inbound from the internet on 80/443. The ECS security group only allows inbound from the ALB. The database security group only allows inbound from ECS. + +## Fargate Configuration + +Fargate abstracts the EC2 instances. You specify CPU and memory, and AWS handles the rest. For most web services, 0.25 vCPU / 512 MB is a good starting point — scale up based on load testing, not guessing. + +## CI/CD Integration + +After Terraform creates the infrastructure, your CI pipeline needs to: + +1. Build and tag the Docker image +2. Push to ECR +3. Update the ECS service with the new task definition + +Use a Terraform backend with state locking — S3 + DynamoDB is the standard approach. Without state locking, two people running `terraform apply` at the same time can corrupt your infrastructure state. + +## Cost Notes + +Fargate is more expensive per vCPU-hour than raw EC2, but you save the operational cost of patching instances and managing capacity. For small-to-medium workloads, the difference is negligible compared to the engineering time saved. diff --git a/layouts/hextra-home.html b/layouts/hextra-home.html index 8735055..10af189 100644 --- a/layouts/hextra-home.html +++ b/layouts/hextra-home.html @@ -11,44 +11,70 @@ {{- $pages := where .Site.RegularPages "Section" "blog" }} {{- $pages = - $pages.ByDate.Reverse }} {{- $recent := first 6 $pages }} {{- $readMore - := (T "readMore") | default "Read more →" }} + $pages.ByDate.Reverse }} {{- $batch := 6 }} -
- {{- range $recent }} -
+
+ {{- range $i, $p := $pages }} +

{{ .Title }}{{ $p.Title }}

- {{ partial "utils/format-date" .Date }} + {{ partial "utils/format-date" $p.Date }}

- {{- partial "utils/page-description" . -}} + {{- partial "utils/page-description" $p -}}

- {{- end }} {{- if gt (len $pages) 6 }} -

- - {{ $readMore }} - -

{{- end }}
+ + {{- $total := len $pages }} {{- if gt $total $batch }} +
+ {{- end }}
-{{ end }} + +{{- if gt $total $batch }} + +{{- end }} {{ end }}