홈서버 IaC 전환기 (1) [with OCI & Terraform]

홈서버 IaC 전환기 (1) [with OCI & Terraform]

서론

홈서버를 운영하며 LXC와 VM이 점점 늘어감에 따라 각각의 상세 스펙 및 설정을 다 기억하기엔 한계가 왔다. 어떤 서비스는 systemctl 을 통해 백그라운드 데몬으로 돌고 있고, 어떤 서비스는 Docker 기반으로 돌고 있고. 이걸 계속 잊어버려 매번 들어가서 이 명령어가 왜 없지 고민하곤 한다.

이를 해결하기 위해 문서화를 하는 것도 방법이 될 수 있겠지만, 이는 지속적으로 문서를 업데이트해야 하는 또 다른 피로감이 생기게 된다.

따라서 이번 글에서는 이러한 고충을 모두 해결할 수 있는, Terraform + Ansible 기반으로 홈서버 인프라를 IaC로 전환해보고자 한다.

보다 정확히는 인프라 전환을 위한 사전 준비를 하고자 한다.


본론

Gemini 가라사대,

IaC(Infrastructure as Code, 코드형 인프라)의 정의

IaC는 물리적 하드웨어 구성이나 대화형 구성 도구를 사용한 수동 프로세스가 아닌, 기계가 읽을 수 있는 정의 파일(코드)을 통해 IT 인프라를 관리하고 프로비저닝하는 방식이다.

Red Hat 등 주요 IT 기업의 공식 문서에 정의된 IaC의 핵심 개념과 특징은 다음과 같다.

- 코드를 통한 구성 관리: 인프라 사양이 포함된 구성 파일을 생성하여, 서버, 가상 머신, 네트워크 등의 인프라 리소스를 자동화된 방식으로 배포하고 관리한다.
- 일관성 보장 및 드리프트(Drift) 방지: 인프라의 최종 상태(Desired State)를 코드로 정의하고 문서화하므로, 문서화되지 않은 임의의 수동 설정 변경을 방지할 수 있다. 이를 통해 언제나 동일한 환경을 반복적으로 프로비저닝할 수 있다.
- 버전 제어 및 협업: 인프라를 일반적인 소프트웨어 애플리케이션 코드와 동일하게 취급한다. 따라서 Git과 같은 버전 관리 시스템을 사용하여 변경 사항을 추적하고, 동료와 협업하며, 문제 발생 시 이전의 안정적인 상태로 롤백하는 것이 가능하다.
- 자동화 파이프라인 통합: 애플리케이션 코드에 수행하는 테스트와 마찬가지로, 인프라 코드 역시 CI/CD 파이프라인 내에 통합하여 배포 전에 변경 사항을 검증할 수 있다.

결론적으로 IaC는 인프라의 설정과 배포를 재사용 가능한 코드 형태로 정의하여 프로비저닝 속도를 높이고 인적 오류를 최소화하는 DevOps 및 플랫폼 엔지니어링의 핵심 사례이다.


출처 | Google. (2026). Gemini 3.1 (PRO) [Large language model]. https://gemini.google.com

IaC 도입 목적

솔직히 말하면, 필자의 홈서버 규모가 IaC를 도입할 만큼 거대한 것은 아니다. Proxmox 위에 VM 몇 개, LXC 몇 개가 돌아가는 수준이다.

그럼에도 IaC를 도입하려는 이유는 단순하다. 기억력의 한계 때문이다.

pfSense에서 방화벽 규칙을 어떻게 설정했는지, Dockge LXC의 CPU 코어를 몇 개로 할당했는지, NPM에서 어떤 도메인을 어디로 프록시하고 있는지 — 이런 세부 사항들을 전부 기억하는 것은 (적어도 나에게는) 불가능하다. 물론 각 서비스의 웹 UI에 접속하면 현재 설정을 확인할 수 있지만, "왜 이렇게 설정했는지"는 내 기억력에 의존해야 한다.

혹은 '필요 없겠지' 하고 설정을 지웠다가 된통 당해보거나

IaC로 전환하면 이 모든 것이 코드로 남는다. 무엇이 어떻게 설정되어 있는지뿐만 아니라, Git 히스토리를 통해 언제, 왜 변경했는지까지 추적할 수 있다. 문서화가 자동으로 따라오는 셈이다.

IaC 도구 선정

IaC에는 다양한 도구가 존재한다. 그중에서 필자가 선택한 조합은 다음과 같다.

도구 역할
Terraform 인프라 정의 (무엇을 만들 것인가)
Ansible 인프라 설정 (어떻게 구성할 것인가)
Semaphore Ansible 실행 환경 (어디서 실행할 것인가)

보다 구체적으로 설명해 보자면, 각각 아래와 같은 역할을 담당한다.

  • Terraform: OCI 클라우드 리소스(VM, 네트워크, 보안 정책) 및 Proxmox VM/LXC 스펙 정의
  • Ansible: 정의된 인프라 위에 실제 서비스 배포 및 설정 관리
  • Semaphore: Ansible, Terraform의 실행 이력 관리, 스케줄링, 웹 UI 기반 조작

아키텍처 개요

여기서 한 가지 중요한 지점은, Semaphore를 어디에 띄울 것인가? 이다. 보통은 관리 대상인 홈서버(Proxmox) 내부에 두는 것이 편하다. 하지만 필자는 이를 외부 클라우드인 OCI(Oracle Cloud)에 배치하기로 했다.

이유는 단순하다. ‘닭과 달걀의 모순’을 피하기 위해서다.

홈서버가 완전히 날아가 하드웨어를 교체했다고 가정해 보자. 만약 복구를 자동화해 줄 도구(Semaphore)마저 홈서버 내부에 있었다면, "자동화 도구를 띄우기 위해 또다시 누군가 홈서버를 수동으로 세팅해야 하는" 피곤한 상황이 벌어진다. 복구 도구가 복구 대상과 함께 죽는 셈이다.

반면 외부에 독립된 제어 평면(Control Plane)이 버티고 있다면 이야기가 다르다. 관리자가 베어메탈에 최소한의 베이스 인프라(Proxmox + PBS)만 살려내면, 외부의 Semaphore가 접속해 나머지 설정들을 코드로 밀어 넣어 준다. 수동 복구(DR)의 영역과 자동화(IaC)의 경계를 명확히 나누는 것이다.

다행히도, 최근에 OCI(Oracle Cloud)에서 무료 VM을 제공한다는 놀라운 사실을 알게 되어 여기에 Semaphore를 띄우면 딱이겠다 싶었다.

물론 필자의 홈서버 구조상 넘어야 할 산이 하나 더 있다. 대외 서비스(포트폴리오, 블로그)를 제외하면 외부망의 접근을 원천 차단하기에 VPN(pfSense)이 복구되어야만 Semaphore가 홈서버 내부에 접근할 수 있다. 때문에 정리하자면 복구 시나리오는 다음과 같다.

  1. [수동 영역] Proxmox 재설치 → PBS 연동 → pfSense(VPN) 복구
  2. [자동 영역] 이후 외부 OCI(Semaphore)가 VPN을 타고 들어와 전체 인프라 자동 프로비저닝

물론 IaC가 아니더라도 PBS를 통한 백업 및 복원은 가능하다. 때문에 이번 작업은 완벽한 무인 자동 복구보다는, 인프라 자체를 코드로써 관리하고 추적할 수 있다는 점에 주된 의의를 두었다.

이렇게 다듬은 최종 배포 파이프라인은 아래와 같다.

CI/CD 파이프라인 구조 | 출처 : Generated by CodeRabbit

사전 준비

1. OCI 계정 생성 및 자격 증명

본격적으로 Terraform을 돌리기 전에, 인증에 필요한 값들을 수집해 GitHub Secrets에 등록해야 한다.
Terraform이 OCI API를 호출하려면 다섯 가지 인증 정보를 필요로 한다.

  • Tenancy OCID, User OCID, API Key Fingerprint, Private Key, Region

먼저 Oracle 홈페이지에서 Free Tier 계정을 생성하였다. 가입 시 선택한 Home Region은 이후에 변경할 수 없으니 신중하게 고르자. 필자는 한국에서 가까운 ap-chuncheon-1 (춘천)을 선택했다.

서울도 있다곤 하는데 인기가 너무 많아서인지 내가 가입할 땐 없더라.
참고로, 뭔 오류가 그리 많은지 계정 가입부터 그렇게 수월하진 않았다. 하지만 공짜니까 참았다.

OCI API는 RSA 키페어로 서명된 요청만 받아들인다. 즉 단순 토큰 인증이 아니라, 모든 API 호출을 본인의 private key로 서명해야 한다. 이 키페어를 발급받는 과정에서 API 호출에 필요한 OCID, Fingerprint 등을 모두 조회할 수 있다.

User profile > API Keys > Add API Key > Generate API Key Pair 로 진입하면 아래와 같은 화면을 조회할 수 있다. 이 화면에서 private key 를 다운받아야 한다.

OCI API Key 생성 | 출처 : OCI

이후 아래와 같이 API 호출에 필요한 인증 정보들을 모두 조회할 수 있다.

OCI 인증 정보 | 출처 : OCI

최종적으로 아래와 같은 5개의 시크릿을 확보할 수 있다.

TF_VAR_tenancy_ocid     = ocid1.tenancy.oc1..aaaa...
TF_VAR_user_ocid        = ocid1.user.oc1..aaaa...
TF_VAR_fingerprint      = 00:3f:33:...
TF_VAR_private_key      = -----BEGIN PRIVATE KEY-----
                          MIIEvgIBADANBgkqhkiG9w0BAQEFAASC...
                          -----END PRIVATE KEY-----
TF_VAR_region           = ap-chuncheon-1

2. WireGuard 자격 증명

추후 자세히 서술하겠지만, 필자의 OCI VM은 Ingress를 허용하지 않고 VPN을 통해서만 통신할 예정이다. 때문에 로컬에서 키페어를 생성해 주었다.

wg genkey | tee oci_private.key | wg pubkey > oci_public.key

생성한 OCI public key를 pfSense의 WireGuard 설정에 peer로 등록하고, OCI private key는 Terraform 변수로 보관한다. 반대로 pfSense의 public key도 미리 복사해 둔다

pfSense Peer 정보 | 출처 : 내 pfSense

마찬가지로, VPN 연결을 위한 5개의 시크릿을 확보할 수 있다.

TF_VAR_wg_private_key       = <생성한 OCI private key>
TF_VAR_wg_address           = <생성한 OCI가 사용할 IP>
TF_VAR_wg_peer_public_key   = <pfSense의 public key>
TF_VAR_wg_peer_endpoint     = <내 공유기 공인 IP:Port>
TF_VAR_wg_peer_allowed_ips  = <내 홈서버 LAN IP 대역>

3. Backblaze B2 자격 증명

Terraform은 기본적으로 .tfstate 파일을 통해 생성한 인프라의 정보를 관리한다. 때문에 VM이 정상적으로 생성되었어도 수행 결과를 나타내는 .tfstate 파일이 없다면 다시 VM 생성을 시도한다. 반대로 말하면 Terraform을 재설치해도 .tfstate 파일만 제대로 있다면 중복 생성을 방지할 수 있다.

보다 자세히는 아래 글을 참고하면 좋을듯 하다.

[terraform] 테라폼 backend, tfstate란
이번게시글에서는 테라폼의 backend, tfstate에 대해 정리합니다. 1. 테라폼의 동작순서와 상태 먼저 테라폼의 동작순서와 .tfstate​ 에 대해 정리합니다. (1). terraform init tf파일(provider가 지정되어 있어 어떤 인프라를 관리할지 정해져있는)이 있는곳에서 terraform init 명령어를 입력하면 provider, module, state 설정을 진행하며 테라폼을 시작하기 위한 준비를 진행합니다. 표면적으로는 .terraform 디렉토리가 생성되며 관련 테라폼 관련 라이브러리 모듈 등을 가져옵니다.(내부에 .tfstate에 정의된 내용도 포함) 인프라 관련 동시성처리를 안전하게 해줄 .terraform.lock.hcl 파일도 생성됩니다. (2). terraform…

때문에 이전 PBS 백업 시스템을 구축할 때 만들어 둔 Backblaze B2를 원격 저장소로 활용하여 .tfstate 파일을 백업하고자 한다.

동일하게 Bucket을 생성하고 나면 아래 2개 정보를 확인할 수 있다.

AWS_ACCESS_KEY_ID     = <keyID>
AWS_SECRET_ACCESS_KEY = <applicationKey>

4. Github Secrets 등록

이렇게 모은 자격 증명들을 Github Actions 파이프라인에서 활용할 수 있도록 Secrets로 등록해 주었다.

참고로 Github Secrets로 등록된 값들은 평문 조회가 불가능하니 필요하다면 따로 기록해두자
Github Actions Secrets | 출처 : 내 Github Repo

아키텍처 구성

OCI에 띄울 리소스는 다음과 같다.

  • VM: Semaphore 호스트 (VM.Standard.A1.Flex, ARM 4 OCPU + 24GB RAM, Free Tier)
  • Network: VCN, Subnet, Internet Gateway, Default Route, Security List
  • Budget: 1원이라도 과금 시 메일 알림

디렉토리는 다음과 같이 분리하였다. 추후 OCI를 통해 Proxmox VM을 관리할 것이므로 디렉토리 단위로 미리 분리하였다.

oci/
├── main.tf         # backend, provider, data sources
├── variables.tf    # 입력 변수 정의 (12개)
├── network.tf      # VCN, subnet, security list
├── compute.tf      # VM 인스턴스 + user_data 주입
├── budget.tf       # 예산 알림
└── user_data.sh    # cloud-init 부트스트랩 스크립트

제로 인그레스(Zero Ingress) 구성

network.tfdefault security list는 다음과 같다.

resource "oci_core_default_security_list" "sl" {
  manage_default_resource_id = oci_core_vcn.homelab_vcn.default_security_list_id

  # Ingress: 없음. VPN 통해서만 접근.

  egress_security_rules {
    protocol    = "all"
    destination = "0.0.0.0/0"
  }
}

인바운드 규칙이 하나도 없다. SSH(22)도, HTTP(80/443)도, ICMP도 전부 닫혀 있다. 외부에서 보면 이 VM은 사실상 보이지 않는다. 포트 스캔을 해도 응답 자체가 없다.

"그러면 어떻게 접속하느냐"는 질문이 자연스럽게 따라붙는다. 일차적으로는 IaC 취지에 맞게 접속을 안 할 예정이고, 불가피할 경우엔 WireGuard VPN을 경유하여 접속할 예정이다.

OCI Security List는 stateful 하므로, VM이 먼저 시작한 아웃바운드 연결의 응답 트래픽은 자동으로 허용된다. 그리고 SSH 포트가 닫혀 있으니 브루트포스 공격이라는 말 자체가 성립하지 않는다. 이론상 완벽하나, 인생이란 게 꼭 계획대로는 되지 않더라. 후술하겠지만, 이러한 설정으로 인해 굉장히 고생했다.

Stateful 이란?

여기서 stateful 이 무슨 의미인지 짚고 넘어갈 필요가 있다.

방화벽은 동작 방식에 따라 statelessstateful 두 가지로 나뉜다.

  • Stateless :
    • 방화벽이 패킷을 하나하나 독립적으로만 본다. "이 패킷이 규칙에 부합하는가?"만 판단할 뿐, 그 패킷이 어떤 흐름의 일부인지는 신경 쓰지 않는다. 따라서 외부로 나간 요청에 대한 응답 패킷을 받으려면, 응답이 들어올 포트를 따로 인바운드 규칙으로 열어 줘야 한다.
  • Stateful :
    • 방화벽이 연결의 상태(state)를 추적한다. 내가 먼저 시작한 아웃바운드 연결의 응답이라면 자동으로 허용해 준다. 인바운드 규칙을 따로 열 필요가 없다.

OCI Security List는 stateful 이다. 일반적인 가정용 공유기와 같은 동작 방식이라고 보면 된다. 우리 집 공유기에 외부에서 들어오는 포트가 따로 열려 있지 않은데도 브라우저로 구글에 접속할 수 있는 이유가 바로 stateful 방화벽이기 때문이다 — 내가 먼저 보낸 요청의 응답이라는 걸 공유기가 기억하고 있어서, 그 응답 패킷만 골라서 통과시켜 준다.

OCI VM도 마찬가지다. 인바운드 규칙이 단 하나도 없어도, VM이 먼저 시작한 아웃바운드 연결의 응답 트래픽은 전부 자동으로 허용된다. 패키지 다운로드도, Docker 이미지 pull도, WireGuard 핸드셰이크 응답도 모두 정상 동작한다. 그리고 외부에서 누군가 먼저 말을 거는 시도(SSH 접속, 포트 스캔, 브루트포스)는 전부 차단된다.

이 성질 덕분에 이 아키텍처가 성립한다. WireGuard는 OCI VM에서 pfSense로 아웃바운드 UDP 51820 연결을 먼저 시작하고, PersistentKeepalive = 25로 25초마다 세션을 갱신해 stateful 테이블에 항상 살려 둔다. 단일 UDP 세션 위에 모든 VPN 트래픽이 캡슐화되므로, 별도의 인그레스 규칙 없이도 양방향 통신이 가능하다.

OCI VM  ──(outbound UDP 51820 시작)──▶  pfSense (홈서버)
        ◀──(stateful return: 자동 허용)──
        ◀──(이후 모든 VPN 트래픽: 위 세션 안에 캡슐화)──▶

두 번째 부트스트랩(Bootstrap)

앞서 첫 번째 부트스트랩으로 인프라를 관리하는 Semaphore가 관리 대상인 인프라 내부에 위치할 경우의 문제점에 대해 다루었다. 그 답으로 OCI를 통해 홈서버 외부에 위치에 두기로 하였다.

이어서 아래 질문이 자연스레 따라온다.

Semaphore는 Ansible 실행기인데, 정작 Semaphore 자기 자신은 누가 설정하는가?

다행히 같은 문제를 먼저 겪은 선발대들이 해결책을 마련해 두었다. 리눅스 기반의 VM이라면 cloud-init 오픈소스를 통하면 VM 배포 후 초기 설정을 자동화할 수 있다.

cloud-init - The standard for customising cloud instances
The standard for customising cloud instances

cloud-init은 거의 모든 클라우드 공식 이미지(Ubuntu, Amazon Linux, Oracle Linux 등)에 사전 설치되어 있는 부팅 시 자동 설정 데몬이다. VM이 처음 켜질 때 클라우드 공급자가 제공하는 메타데이터를 읽어, 거기 들어 있는 user_data 스크립트를 root 권한으로 실행한다. 즉 "VM이 처음 켜지자마자 root로 임의의 스크립트를 한 번 돌릴 수 있는 표준 훅"이다. SSH도 없고 사람의 손도 닿지 않은 빈 VM이 스스로를 부트스트랩 할 수 있는 이유가 여기 있다.

Terraform은 이 훅에 스크립트를 흘려보내는 역할만 하면 된다. compute.tf 에서 다음과 같이 작성한다.

metadata = {
  user_data = base64encode(templatefile("${path.module}/user_data.sh", {
    wg_private_key       = var.wg_private_key
    wg_address           = var.wg_address
    ...
    ubuntu_password      = var.ubuntu_password
  }))
}

templatefile()user_data.sh${변수명} 자리에 실제 값을 채우고, base64encode()가 이를 인코딩해 OCI 메타데이터로 등록한다. VM이 부팅되면 cloud-init이 그 값을 가져와 디코딩해 실행한다.

참고로 cloud-init은 첫 부팅에만 user_data를 실행한다. 때문에 OCI Terraform provider는 metadata 변경을 force replacement로 처리한다. 즉, user_data를 한 글자만 고쳐도 VM을 삭제 후 재생성한다.

이 말은즉슨, user_data에 오류가 있다면 매번 VM을 재생성하며 시도해야 한다.

스크립트는 아래와 같이 VM 생성 후 VPN 설치 및 연결, Docker 설치 및 실행과 같은 VM의 전반적인 초기 설정을 담당한다. 이는 앞서 언급한 Zero Ingress 와도 맞물린다.

#!/bin/bash
set -euo pipefail

# 1. WireGuard 설치 + 설정
apt-get install -y wireguard
cat > /etc/wireguard/wg0.conf <<EOF
...
EOF
systemctl enable --now wg-quick@wg0

# 2. Docker 설치 + Semaphore 컨테이너 기동
curl -fsSL https://get.docker.com | sh
docker compose up -d

트러블슈팅

Out of host capacity

이제 작성한 코드를 푸시하여 Github Actions을 통해 Terraform을 실행하면, 운이 정말 좋지 않은 이상 아래와 같은 에러를 만나게 된다.

Github Actions Log | 출처 : 내 Github Actions

OCI Free Tier ARM 인스턴스(VM.Standard.A1.Flex)는 무료인 만큼 경쟁이 치열하다. 빈자리가 없으면 못 만든다. 해결책은 단순하다. 빈자리가 날 때까지 계속 돌리면 된다. 실제로 검색해 보면 이 문제를 다룬 글이 워낙 많다 보니, OCI API를 직접 두드려서 인스턴스 생성을 무한 재시도하는 스크립트도 찾아볼 수 있다.

[Oracle Cloud] 인스턴스 생성 자동화 (Out of host capacity)
나도 이제 최대 4CORE, 24RAM 무료 인스턴스를 생성할 수 있다는 설레는 마음으로 생성을 시도했다. Out of capacity for shape VM.Standard.A1.Flex in availability domain AD-1. Create the instance in a different availability domain or try again later.If you specified a fault domain, try creating the instance without specifying a fault domain. If that doesn’t work, please try again later.Learn more about host capacity.헛된 꿈이었음을...싱가포르(가입…
오라클 클라우드 Out of host capacity 오류, 그리고 스크립트로 뚫기
결론 1. 프리 티어로 줄만한 용량이 남아있지 않아서 생기는 오류이다. 2. 계속 자원이 추가되기에 기다리면 언젠가 해결되긴 하겠지만 3. 스크립트화해서 API 가지고 계속 시도가 가능하다 스크립트화해서 계속 시도하는 방법을 보고싶다면 밑으로 내용 솔직히 애초에 오라클에서 이렇게 하라고 API를 제공하지만 직접 찾아보려하니 상당히 안으로 들어가야 합니다. 바로 시작합시다. 이 글에서는 REST api를 파이썬으로 접근하는 방법을 사용하도록 하겠습니다. 준비물 : oci 설치 python3 -m pip install oci https://pypi.org/project/oci/ 오라클에서 제공해주는 OCI의 파이썬 SDK입니다. 준비물 : API key 받기 오라클 클라우드에 로그인하고 우측 상단 버튼을 눌…

문제는 그런 스크립트로 만든 VM은 Terraform이 모른다는 점이다. tfstate에 기록되지 않은 자원은 IaC 입장에서 유령이고, 이후 변경·추적·재현이 모두 막힌다. 그럴 거면 IaC를 쓸 이유가 없다.

그래서 어떻게든 Terraform을 통해서, 그것도 빈자리가 날 때까지 끈질기게 재시도해야 한다. 처음에는 자연스럽게 GitHub Actions의 cron 스케줄을 떠올렸다. 6시간 동안 1분 간격으로 terraform apply를 반복하는 워크플로를 짰는데, 며칠 만에 GitHub에서 메일이 날아왔다.

Github Actions Usage Warning | 출처 : 내 이메일
Github Actions 실패 시신 | 출처 : 내 Github Actions

GitHub Pro의 사용량은 월 3,000분이다. 6시간짜리 워크플로를 몇 번 돌리니 금세 한도에 도달했다.

어쩔 수 없이 좀 귀찮지만, 초기 배포용으로 홈서버에 Docker Compose를 띄워 24/7 무한 재시도를 돌렸다. 핵심은 컨테이너 안에서도 여전히 Terraform으로 배포한다는 점이다. B2의 동일한 tfstate를 바라보고 있으므로, 어디서 실행하든 인프라의 단일 진실 공급원(single source of truth)은 유지된다.

FROM hashicorp/terraform:latest

WORKDIR /workspace
COPY oci/*.tf oci/user_data.sh ./
COPY scripts/docker-entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

#!/bin/sh
# docker-entrypoint.sh — 무한 재시도 루프
set -eu
RETRY_INTERVAL=${RETRY_INTERVAL:-60}

terraform init -no-color

ATTEMPT=0
while true; do
  ATTEMPT=$((ATTEMPT + 1))
  echo "[$(date)] ===== Attempt #${ATTEMPT} ====="

  terraform plan -out=tfplan -no-color
  if terraform apply -auto-approve tfplan -no-color; then
    echo "[$(date)] SUCCESS on attempt #${ATTEMPT}"
    exit 0
  fi

  sleep "$RETRY_INTERVAL"
done

이렇게 하니 홈서버로 옮긴 뒤 약 3일이 지나서야 마침내 VM이 생성되었다.

제로 인그레스(Zero Ingress)의 폐해

마침내 VM이 만들어졌으나, 뭔가 이상하다. pfSense의 Peer 정보에 연결 이력이 보이지 않는다.

Peer    Handshake    PublicKey      Endpoint   Allowed IPs       Transfer
OCI     never        BaBHZIaCsh..   (none)     10.80.80.105/32   0 B / 0 B

핸드셰이크가 never이고 endpoint가 (none)이다. WireGuard는 peer에서 패킷이 도착할 때 발신지를 endpoint로 학습하는데, (none)이라는 건 OCI VM으로부터 단 한 개의 패킷도 받지 못했다는 얘기다. 한 번 연결됐다 끊긴 게 아니라, 시도조차 안 된 것이다.

user_data가 어딘가에서 죽었다는 의미인데 문제는 이걸 들어가서 확인하거나 재시도할 방법이 없다.

다행히도 OCI는 인스턴스 시리얼 콘솔 출력을 스냅샷으로 캡처하는 Console History 기능을 제공한다. SSH 없이도 부팅 로그를 들여다볼 수 있다.

cloud-init[1691]: Reading package lists...
cloud-init[1691]: E: Could not get lock /var/lib/apt/lists/lock. It is held by process 1708 (apt)
cloud-init[1691]: E: Unable to lock directory /var/lib/apt/lists/
cc_scripts_user.py[WARNING]: Failed to run module scripts_user

원인이 보인다. unattended-upgrades(process 1708)가 백그라운드에서 apt lock을 잡고 있어서 user_dataapt-get update가 실패하고, set -e 때문에 이후 모든 단계(WireGuard, Docker, Semaphore)가 통째로 실행되지 않은 것이다.

처음에는 단순히 lock이 풀릴 때까지 기다리기만 했다. 그런데 lock이 풀린 직후의 짧은 틈에 unattended-upgrades가 다시 잡아버리는 race가 있어, lock 대기와 명령 실행을 한 묶음으로 묶어 retry 하도록 하였다.

wait_apt_lock() {
  while fuser /var/lib/apt/lists/lock /var/lib/dpkg/lock-frontend
/var/lib/dpkg/lock >/dev/null 2>&1; do
    sleep 5
  done
}

apt_retry() {
  local max_attempts=60
  local attempt=0
  until wait_apt_lock && "$@"; do
    attempt=$((attempt + 1))
    [ $attempt -ge $max_attempts ] && return 1
    sleep 10
  done
}

apt_retry apt-get update
apt_retry apt-get install -y wireguard

이렇게 수정하고 나니, 수정한 user_data 스크립트를 실행할 방법이 없다. 앞서 언급하였듯이, cloud-init 은 VM이 최초 생성될 때에만 실행되도록 구성되어 있기 때문이다.

즉, 이렇게 험난한 과정을 뚫고 간신히 생성한 VM을 삭제 후 재생성(Force Replacement)해야 한다는 뜻이 된다. 앞서 언급하였듯이, OCI Terraform Provider가 user_data 변경을 감지하여 자동으로 Force Replacement로 처리하여 VM을 삭제 후 재생성하였다.

배포 실패 후 삭제된 OCI VM 잔해 | 출처 : 내 OCI
재생성하는 와중에도 자리를 뺏겨서인지 다시 뚫을 때까지 하루 걸렸다.

수정하고 재배포하니 VPN이 정상적으로 연결되어 아래와 같은 Semaphore GUI를 조회할 수 있게 되었다.

Semaphore GUI | 출처 : 내 OCI


결론

이번 작업으로 세 가지를 얻었다.

  • 외부에 독립된 제어 평면
    • 홈서버가 죽어도 Semaphore는 살아 있다. 닭과 달걀의 모순을 적어도 한 단계 해소했다.
  • 코드로 정의된 인프라
    • OCI VM의 스펙, 네트워크, 보안 정책, 부트스트랩이 모두 Git에 남았다. "왜 이렇게 설정했지?"의 답을 git log로 찾을 수 있다.
  • 명료한 보안 경계
    • Zero-Ingress + WireGuard. SSH 포트는 영원히 닫혀 있다.

​사실 이번 글을 통해 다룬 건 본격적인 IaC 전환이 아닌, 그 준비의 준비에 가까웠는데 이렇게 험난할 줄은 몰랐다. 다음 글을 통해 본격적으로 홈서버를 IaC로 전환해보도록 하겠다.


참고자료

[terraform] 테라폼 backend, tfstate란
이번게시글에서는 테라폼의 backend, tfstate에 대해 정리합니다. 1. 테라폼의 동작순서와 상태 먼저 테라폼의 동작순서와 .tfstate​ 에 대해 정리합니다. (1). terraform init tf파일(provider가 지정되어 있어 어떤 인프라를 관리할지 정해져있는)이 있는곳에서 terraform init 명령어를 입력하면 provider, module, state 설정을 진행하며 테라폼을 시작하기 위한 준비를 진행합니다. 표면적으로는 .terraform 디렉토리가 생성되며 관련 테라폼 관련 라이브러리 모듈 등을 가져옵니다.(내부에 .tfstate에 정의된 내용도 포함) 인프라 관련 동시성처리를 안전하게 해줄 .terraform.lock.hcl 파일도 생성됩니다. (2). terraform…
Terraform 사용 가이드 [1]
Terraform 가이드 테라폼 사용을 위한 용어 설명 및 VPC 구성 하는 방법에 대한 설명 ◎ Terrafrom이란? 인프라스트럭처를 코드로 작성, 계획, 생성하는 도구. “Write, Plan and Create Infrastructure as Code” 환경 구성에 있어 필요한 리소스를 선언적인 코드로 작성하여 관리할 수 있다. 쉽게 말해 테라폼을 이용하면 개별로 VPC, EC2를 개별로 올리는게…
Terraform 사용 가이드 [2]
◎ Terraform Module 실제 운영환경은 dev, stg, prd로 구분되어 있으나 VPC 운영 환경은 동일하게 구성되어 있는 경우가 많은데, 이처럼 동일 환경을 구성하기위해 테라폼을 이용하는 경우 &#39;Terraform 사용 가이드 [1]&#39;을 참고하여 작성하게 되면 모든 tf파일을 복사, 수정해야하는 번거로움이 생긴다. 수정한다고 하더라도 내용 편집에 실수하게 되면 구성이 안되거나 환경이 달라질 수 있다. 이러한 불편함을 해결…
Navigating the IaC Bootstrap Problem
오라클 클라우드 인프라스트럭쳐(Oracle Cloud Infrastructure) 세팅하기
오라클 클라우드 인프라스트럭쳐(Oracle Cloud Infrastructure) 세팅하기 0. 서론정말 간만의 포스팅이긴 한데... 요 근래 AWS가 정책이 매우 괴랄하게 변했다..25년 7월 15일에 엄청난 개악(?)을 단행했는데...기간 반토막: 12개월 월 750시간 프리티어를 주던 정책을 6개월로 줄임대신 용돈 줌: 새로 가입하면 $200 크레딧(현금 포인트)을 주고 ”이걸로 6개월 동안 마음껏 써봐!”라는 식으로 바뀜거기다가 요새는 가장 기본 프리티어인 t2.micro 조차도 용량을 수정하기 매우 어렵게 완전히 꽁꽁 숨겨놨다.덕분에 나름 꼼꼼히 보면서 다시 만들었어도, 기본 8gb 용량이라 제대로 아나콘다 하나 설치를 못한다...그래서 인스턴스 뿌시고, 다시 만들어봤더니... 이야 한달만에 크…
cloud-init - The standard for customising cloud instances
The standard for customising cloud instances
[Oracle Cloud] 인스턴스 생성 자동화 (Out of host capacity)
나도 이제 최대 4CORE, 24RAM 무료 인스턴스를 생성할 수 있다는 설레는 마음으로 생성을 시도했다. Out of capacity for shape VM.Standard.A1.Flex in availability domain AD-1. Create the instance in a different availability domain or try again later.If you specified a fault domain, try creating the instance without specifying a fault domain. If that doesn’t work, please try again later.Learn more about host capacity.헛된 꿈이었음을...싱가포르(가입…
오라클 클라우드 Out of host capacity 오류, 그리고 스크립트로 뚫기
결론 1. 프리 티어로 줄만한 용량이 남아있지 않아서 생기는 오류이다. 2. 계속 자원이 추가되기에 기다리면 언젠가 해결되긴 하겠지만 3. 스크립트화해서 API 가지고 계속 시도가 가능하다 스크립트화해서 계속 시도하는 방법을 보고싶다면 밑으로 내용 솔직히 애초에 오라클에서 이렇게 하라고 API를 제공하지만 직접 찾아보려하니 상당히 안으로 들어가야 합니다. 바로 시작합시다. 이 글에서는 REST api를 파이썬으로 접근하는 방법을 사용하도록 하겠습니다. 준비물 : oci 설치 python3 -m pip install oci https://pypi.org/project/oci/ 오라클에서 제공해주는 OCI의 파이썬 SDK입니다. 준비물 : API key 받기 오라클 클라우드에 로그인하고 우측 상단 버튼을 눌…