· 5793 words · 28 min read

2. 설계 원칙#

한 네트워크 자동화 팀이 6개월을 들여 진심으로 자랑스러워하는 시스템을 구축했다. 구조화된 데이터 모델에서 인텐트를 가져와 템플릿 엔진으로 장비 설정을 생성하고, 정책 라이브러리에 대한 변경 사항을 검증하며, 완전한 롤백 지원과 함께 NETCONF를 통해 푸시했다. 아키텍처적으로 견고했다. 리더십에 대한 데모는 잘 됐다. 그런 다음 그들은 네트워크 운영 팀에 넘겼다.

채택은 일어나지 않았다. 운영자들은 계속 CLI를 사용했다. 이유를 물었을 때 답변은 일관됐다: “실행하기 전에 무엇을 할지 모르겠어요.” “무언가 잘못되면 무슨 일이 있었는지 알 수가 없어요.” “출력 결과를 어떻게 읽는지 모르겠어요.” 자동화 팀은 기술적으로 인상적이지만 운영상 불투명한 것을 구축했다. 드라이런 모드도 없고, 사람이 읽을 수 있는 변경 미리보기도 없고, 운영자가 인식하는 방식으로 작성된 감사 추적도 없었다. 시스템은 아직 신뢰를 얻지 못했으면서도 신뢰를 요구하는 블랙박스였다. 6개월의 엔지니어링 노력이 사용되지 않은 채 방치됐다.

그 결과는 대부분의 팀이 인정하는 것보다 더 흔하다. 자동화는 버그나 나쁜 아키텍처 때문만 실패하지 않는다. 의존해야 하는 사람들이 신뢰하지 않기 때문에 실패한다. 우리는 네트워크 자동화를 신뢰할 수 있고, 확장 가능하며, 안전하게 만드는 근본적인 설계 원칙을 탐구할 것이다. 이것들은 추상적인 이론이 아니다: 채택되어 실제 가치를 제공하는 자동화와 무시되고 결국 포기되는 자동화의 차이다.

이러한 원칙들 중 많은 것이 다른 소프트웨어 프로젝트에도 적용된다. 하지만 네트워크 자동화는 중요한 인프라를 지원하기 때문에 독특한 특성을 갖는다. 네트워크 엔지니어들은 수십 년 동안 신중함, 정밀함, 수동 검증을 기반으로 한 모델을 사용해 이러한 시스템을 구축하고 유지해왔다. 이제 우리는 그들에게 근본적으로 다른 모델을 채택하도록 요청하고 있다. 그것은 신뢰를 필요로 한다.

2.1 신뢰 구축#

특정 원칙들을 살펴보기 전에, 신뢰에 대해 이야기해보자. 신뢰는 성공적인 네트워크 자동화의 초석이다. 신뢰 없이는 채택이 거의 불가능해진다.

신뢰가 왜 그토록 중요한가? 신뢰 없이는 네트워크 엔지니어들이 자동화를 채택하지 않을 것이기 때문이다. 그들은 자동화가 그것이 대체하는 수동 프로세스와 적어도 동일한 수준의 신뢰를 제공한다고 믿어야 한다 (그리고 다른 이점도).

더 까다로운 점은: 자동화 시스템은 종종 깊은 네트워킹 경험이 없는 엔지니어들에 의해 구축된다는 것이다. 따라서 자동화는 네트워크 엔지니어링 팀이 사용하고 맞춤화하며 관리하기에 안전하고, 믿을 수 있으며, 쉽게 만드는 강력하고 명시적인 특성을 내포함으로써 보완해야 한다.

신뢰할 수 있는 자동화의 관련성은 Autocon3에서 Damien Garros의 발표 Building Trustworthy Network Automation에서 영감을 받았다.

간단히 말하면, 우리에게 필요한 네 가지 기본 특성이 있다:

  • Predictable: 일관적이고 결정론적인 결과. 엔지니어들은 “실행"을 누르기 전에 무슨 일이 일어날지 알아야 하고, 매번 동일한 동작을 얻어야 한다.
  • Reliable: 오류를 우아하게 처리한다. 실패에서 복구한다. 예상치 못한 상황과 규모에서도 작업이 안전하게 완료되거나 (또는 롤백되거나) 되도록 보장한다.
  • Usable: 과도한 복잡성 없이 엔지니어들이 동작을 검증하고, 추론하고, 제어할 수 있게 하는 인터페이스. 가드레일 포함.
  • Understandable: 블랙박스가 되어서는 안 된다. 인간의 신뢰를 구축하는 방식으로 인텐트, 단계, 결과, 결정을 표출해야 한다.
graph BT
  %% ===== Middle Layer =====
  subgraph L2[**품질**]
    direction LR
    B1[예측 가능]:::layer2
    B2[신뢰할 수 있음]:::layer2
    B3[사용 가능]:::layer2
    B4[이해 가능]:::layer2
  end

  %% ===== Top Layer =====
  subgraph L1[" "]
    direction TB
    A[신뢰]:::layer1
  end

  %% ===== Connections: Behavior → Outcome =====
  B1 --> A
  B2 --> A
  B3 --> A
  B4 --> A

  %% ===== Styling =====
  classDef layer1 fill:#ffcccc,stroke:#b8860b,stroke-width:2px,color:#000;
  classDef layer2 fill:#ffe6cc,stroke:#4682b4,stroke-width:1.5px,color:#000;

AI/ML 기술이 네트워크 자동화 영역에 진입함에 따라 예측 가능성 또는 결정론적 동작이 점점 더 중요해지고 있다. 이 기술들은 일정한 무작위성을 도입하기 때문이다.

이러한 특성들은 타협할 수 없으며, 나중에 추가하는 것이 아니다. 자동화를 사용하고 의존하게 될 네트워크 엔지니어들이 이를 인식해야 한다.

나는 많은 네트워크 자동화 솔루션을 구축해왔다. 네트워크 운영자들이 신뢰하지 않았기 때문에 잘 설계된 솔루션이 무시되거나 포기되는 것을 보는 것보다 더 좌절스러운 것은 없다. 이러한 원칙들은 그 결과를 피하는 데 도움이 된다.

그럼 이러한 특성들을 감안하여, 그것들을 지지하는 원칙들을 탐구해보자.

2.2 설계 원칙#

신뢰를 기반으로, 이제 신뢰할 수 있는 자동화에 필요한 품질을 지지하는 설계 원칙들을 탐구할 수 있다. 이것들이 자동화를 신뢰할 수 있고, 예측 가능하며, 확장 가능하게 만든다.

전체 목록은 훨씬 길 수 있다 (나중에 소프트웨어 엔지니어링의 다른 원칙들을 소개할 때 확장할 것이다). 하지만 모든 네트워크 자동화 솔루션이 포함해야 하는 여섯 가지 근본적인 원칙이 있다:

  • Idempotency: 같은 작업을 여러 번 수행 = 같은 최종 상태. 부작용을 줄이고, 모호성을 제거하며, 자동화를 더 이해할 수 있고 반복하기에 안전하게 만든다.
  • Transactional: 변경이 완전히 완료되거나 안전하게 실패한다. 부분적이거나 일관성 없거나 절반만 적용된 상태가 없다. 예측 가능성에 필수적이며 네트워크 전체 롤백을 가능하게 한다.
  • Intent-Driven: 다양한 조건에서 시스템의 원하는 상태를 정의한다. 예측 가능성을 향상시키고, 인지 부하를 줄이며, 시스템을 더 사용하기 쉽게 만든다.
  • Dry Run 친화적: 실행하기 전에 무엇을 할지 보여준다. 인간이 작업을 검증하고, 결과를 시각화하며, 네트워크에 도달하기 전에 문제를 감지할 수 있게 함으로써 자동화를 더 사용 가능하고 신뢰할 수 있게 만든다.
  • Versioning: 데이터와 로직 모두의 버전 관리를 지원한다. 시간을 앞뒤로 이동하고, 이전 상태를 복구하며, 여러 상태를 병렬로 유지한다. 예측 가능성을 강화하고 신뢰성을 높인다.
  • 테스트 가능: 프로덕션에서 변경을 실행하기 전에 적절한 테스트 환경이 필수적이다. 신뢰성을 개선하고 엔지니어들이 시스템 동작을 이해하는 데 도움이 된다.

이러한 원칙들은 네트워킹에만 적용되는 것이 아니라, 모든 IT 도메인의 자동화에 적용된다. 하지만 변경이 상당한 운영 위험을 수반하는 네트워킹에서는 특히 중요해진다.

이 여섯 가지 원칙은 안전하고, 예측 가능하며, 신뢰할 수 있는 네트워크 자동화의 기반을 형성한다. 나중에 소개되는 더 고급 개념들의 토대가 된다. 대규모의 복잡한 인프라에서 자동화를 확장할 때 더욱 중요해진다.

graph BT
  %% ===== Bottom Layer =====
  subgraph L3[**원칙**]
    direction LR
    C1[멱등적]:::layer3
    C2[버전 관리]:::layer3
    C3[트랜잭셔널]:::layer3
    C4[테스트 가능]:::layer3
    C5[드라이런]:::layer3
    C6[인텐트 기반]:::layer3
  end

  %% ===== Middle Layer =====
  subgraph L2[**품질**]
    direction LR
    B1[예측 가능]:::layer2
    B2[신뢰할 수 있음]:::layer2
    B3[사용 가능]:::layer2
    B4[이해 가능]:::layer2
  end

  %% ===== Top Layer =====
  subgraph L1[" "]
    direction TB
    A[신뢰]:::layer1
  end

  %% ===== Connections: Foundation → Behavior =====
  C1 --> B1
  C1 --> B4

  C2 --> B3
  C2 --> B2

  C3 --> B1
  C3 --> B2

  C4 --> B2
  C4 --> B4

  C5 --> B3
  C5 --> B4

  C6 --> B1
  C6 --> B3

  %% ===== Connections: Behavior → Outcome =====
  B1 --> A
  B2 --> A
  B3 --> A
  B4 --> A

  %% ===== Styling =====
  classDef layer1 fill:#ffcccc,stroke:#b8860b,stroke-width:2px,color:#000;
  classDef layer2 fill:#ffe6cc,stroke:#4682b4,stroke-width:1.5px,color:#000;
  classDef layer3 fill:#ccffcc,stroke:#228b22,stroke-width:1.5px,color:#000;

이러한 각 설계 원칙은 더 많은 맥락을 필요로 한다. 논리적 의존성을 반영하는 순서로 탐구할 것이다: 달성하고자 하는 것을 정의하는 것부터 시작하여, 안전하고 정확하게 실행되도록 보장하고, 마지막으로 작동하는지 검증한다.

2.2.1. 인텐트 기반#

왜 인텐트로 시작하는가? 자동화를 어떻게 실행할지 논의하기 전에, 무엇을 달성하려는지 이해해야 한다. 인텐트 기반 설계는 다른 모든 것이 구축되는 기반이다.

자동화를 시작하는 일반적인 유스케이스: 단순한 작업을 실행하는 스크립트나 플레이북. FQDN과 인터페이스 이름을 제공하여 인터페이스를 올리거나 내린다. 하지만 이것은 문제를 만든다. 그 인터페이스를 다음에 작업하는 사람은 그것의 Desired State가 무엇인지 알 수 없다. 올라가야 하는가 내려가야 하는가?

이 문제는 수동 및 자동화된 작업 모두에 영향을 미친다. 하지만 자동화에서는 문제가 더 심해진다. 특히 많은 임시방편적인 변경이 적용되었을 때, 현재 상태에서 네트워크 인텐트를 자신 있게 추론할 수 없다. 실제 네트워크 설정은 Intent-Driven 로직의 결과이지, 그것의 신뢰할 수 있는 기록이 아니다.

인텐트는 가변성과 사용자 정의 필요에 따라 다른 수준에서 나타난다. 단순한 환경은 더 적은 변수 데이터를 필요로 할 수 있고 템플릿에 의존할 수 있다. 다른 데이터는 팀 간 검토가 필요할 수 있는 반면, 일부는 규칙과 가드레일을 따라 업데이트될 수 있다.

누군가는 자신의 인텐트가 단순히 네트워크의 Actual State라고 주장할 수 있다: “설정을 확인하라, 그것이 내 인텐트다.” 이것은 기술적으로 인텐트이지만, 비교할 별도의 원천이 없다면 그것이 원래 또는 실제 인텐트와 일치하고 상황에 의해 변경되지 않았다는 것을 증명할 수 없다. 네트워크 설정은 인텐트의 출력이지, 인텐트 자체가 아니다.

따라서 Intent-Driven 데이터의 품질이 자동화의 품질을 직접 결정한다. 이 개념은 4장에서 자세히 다룰 Source of Truth (SoT) (SoT)와 밀접하게 관련되어 있다.

코드로서의 인프라 (IaC): 인텐트 기반 자동화는 Infrastructure as Code (IaC)의 기반이다: 네트워크 인프라 정의를 소프트웨어 코드처럼 취급하는 것. IaC를 통해 버전 제어, 코드 검토 프로세스, 테스트, 그리고 인프라 변경을 소프트웨어 변경과 동일한 방식으로 취급하는 능력을 갖는다. 이것은 네트워크 자동화를 소프트웨어 엔지니어링 모범 사례와 연결한다. IaC는 템플릿, 인벤토리, 인텐트를 버전 제어 하의 파일로 유지한다. 변경 사항은 검토되고, 테스트되고, 추적된다. 일회성 CLI 편집으로 적용되지 않는다.

인텐트를 기반으로, 다음 원칙은 원하는 것을 알게 된 후 안전하고 일관되게 실행할 수 있도록 보장한다.

2.2.2. 멱등성#

왜 멱등성이 중요한가: 멱등성은 반복된 실행이 동일한 결과를 생성하도록 보장한다. 자동화는 종종 실수로, 재시도 메커니즘에 의해, 또는 설계에 의해 여러 번 실행되기 때문에 필수적이다. 멱등성 없이는 각 실행이 예측할 수 없는 변경을 만들 수 있다.

Idempotency는 같은 작업을 여러 번 실행하면 의도하지 않은 부작용 없이 동일한 최종 상태가 된다는 것을 의미한다.

예를 들어, 자동화된 ACL 프로비저닝에서 규칙을 정의하면 다음을 기대한다:

  • 규칙이 존재하지 않으면, 특정 위치에 추가된다.
  • 규칙이 이미 존재하면, 중복을 추가하지 않는다.
  • 나중에 다른 규칙이 추가되면, 이전 규칙은 그대로 유지되고 재정렬되지 않는다.

상식처럼 들리지만 현실은 더 복잡하다. 시스템은 멱등성을 보장하는 로직을 구현해야 한다. 예를 들어, 다음을 확인해야 한다:

  • ACL의 현재 상태는 무엇인가?
  • 규칙이 이미 그것의 일부인가?
  • 그렇지 않다면, 어떤 차이를 적용해야 하는가?

멱등성 없이는 설정이 예측할 수 없는 순서로 적용되어 재현성에 영향을 줄 수 있다.

멱등성은 네트워크 자동화의 다양한 차원에 적용된다. 예를 들어, DHCP를 통해 또는 식별자 요청과 결과 사이의 매핑을 이해하는 IPAM 시스템에서 IP 주소를 요청할 때:

그림 1: 멱등성의 예 (Damien Garros의 Autocon3 발표에서)

나중에 읽게 되겠지만, 멱등성을 달성하기 위해서는 Imperative 모델 대신 Declarative 모델을 사용하는 것이 좋다. 단계가 아닌 최종 상태를 정의하라. 이것이 명령형 모드를 사용하는 것이 불가능하다는 의미는 아니지만, 더 복잡하다.

멱등성은 단일 실행 내에서의 일관성에 초점을 맞춘다. 트랜잭셔널 원칙은 변경이 여러 시스템에 걸쳐 원자적으로 적용되어 부분적이거나 일관성 없는 상태를 방지하도록 보장한다.

2.2.3. 트랜잭셔널#

왜 트랜잭셔널이 중요한가: 네트워크 변경은 종종 여러 장비에 동시에 영향을 미친다. 트랜잭셔널이 없으면 중간에 실패하면 네트워크가 일관성 없는 상태로 남는다: 일부 장비는 업데이트되고 다른 것은 그렇지 않다. 여러 행위자 간의 조율이 본질적으로 복잡한 분산 시스템에서 특히 문제가 된다.

Transactional은 데이터베이스에서 일반적인 개념으로, 여러 변경이 일관성을 유지하기 위해 원자적으로 적용된다. 네트워킹에서 (분산 시스템에서) 트랜잭셔널은 더 복잡하지만, 새로운 문제는 아니다. 2006년에 NETCONF (RFC 6241)는 변경을 여러 장비에 전역적으로 적용하거나 검증이 실패하면 완전히 롤백하는 네트워크 전체 커밋을 도입했다. 이 Atomic Operation은 일부 장비는 업데이트되고 다른 것은 그렇지 않은 부분적인 상태를 방지한다: 트러블슈팅의 악몽 같은 시나리오. 하지만 모든 플랫폼이 NETCONF를 지원하지는 않고, 통합 복잡성도 크게 다르다. 따라서 이론적으로는 문제가 해결됐지만, 실용적인 구현은 인프라에 따라 다르다.

트랜잭셔널 동작을 제공하면 부분적이거나 깨진 작업(트러블슈팅이 어렵고 시스템을 예측할 수 없는 상태로 남기는)을 방지하며, 자동화에 대한 신뢰를 훼손하지 않기 위해 필수적이다. 일부 장비는 변경을 적용하고 다른 것은 그렇지 않은 네트워크는 완전히 롤백된 네트워크보다 더 나쁘다.

따라서 일반적으로 네트워킹에 트랜잭셔널을 어떻게 적용할 수 있을까? 이것은 기술들의 조합이다:

  • NETCONF: 분산 관리 조율을 지원하는 네트워크 관리 기능. 하지만 지원이 매우 제한적이다.
  • 데이터베이스 기반 트랜잭션: ACID 속성의 데이터베이스에 설정 변경을 저장한 다음 장비 실패 시 Rollback 기능과 함께 조율된 배치로 적용
  • 변경 알림 큐: 모든 의도된 변경을 캡처하고 그룹으로 검증한 다음 장비가 실패하면 조율된 Rollback 메커니즘으로 실행
  • 2단계 커밋 패턴: 먼저 모든 장비에 변경을 준비하고 (단계 1), 동시에 커밋하며 (단계 2), 어느 장비에서든 단계 2가 실패하면 Rollback
  • 트랜잭셔널 로그: 부분적인 작업을 수동 또는 자동으로 취소하기에 충분한 세부 정보로 모든 변경 시도를 기록

분산 환경의 모든 메커니즘은 원활한 롤백을 제공하기 위해 롤백 기능이 필요하다는 점에 주목하라. 이전 상태로 이동하기 위해 (플랫폼 내부 또는 외부에서) 활성화할 수 있는 커밋 또는 스냅샷이 필요하다. 이것이 항상 가능하지는 않다. 진정한 원자적 커밋이 가능하지 않은 경우, 검증된 “준비” 단계를 구현하고 위험을 줄이기 위해 조율된 커밋 또는 단계적 롤아웃을 사용하라 (네이티브 원자성이 없을 때만 사용).

이 원칙은 자연스럽게 Versioning과 연결된다: 무언가 잘못되면, 알려진 좋은 버전으로 롤백할 수 있도록 정확히 어떤 상태인지 알아야 한다.

2.2.4. 버전 관리#

왜 버전 관리가 중요한가: 모든 변경은 위험을 수반한다. 버전 관리는 무엇이 변경됐는지, 언제 변경됐는지, 누가 변경했는지 정확히 알 수 있게 하고, 필요할 때 되돌릴 수 있는 능력을 제공한다. 규모에서 안전한 운영의 기반이다.

역사적으로 네트워크 엔지니어링은 롤백 목적으로 사용되는 스냅샷인 설정 백업을 통해 변경을 관리했다. 하지만 롤백 후 변경 이후 상태를 조정하는 것은 백업에 컨텍스트가 부족하기 때문에 극도로 복잡하다.

소프트웨어 엔지니어링에서 Git과 같은 Version Control System (VCS)는 표준 관행이다 (나는 어떤 형태의 VCS도 없는 환경에서 일한 적이 없다). 이러한 시스템들은 다음을 가능하게 한다:

  • 손쉬운 협업: 여러 개발자가 코드에 기여하고 다른 브랜치에서 진행할 수 있다
  • 시간 여행: 역사의 이전 지점으로 돌아가 무엇이 언제 변경됐는지 이해
  • 원자적 그룹화: 여러 파일 변경을 단일 단위로 묶을 수 있다

네트워크 자동화에 Versioning을 적용하면 상당한 이점이 있다:

  • 감사 가능성과 추적성: 누가 무엇을 언제 변경했는지 쉽게 추적하여 시스템을 투명하게 만든다
  • 팀 간 협업: 변경 검토를 용이하게 하고 팀 기반 자동화 개발을 가능하게 한다
  • Atomic Operation: 여러 파일에 걸친 관련 변경이 함께 적용될 수 있어 부분적이거나 불완전한 상태를 방지한다
  • CI/CD 통합: 코드 변경이 감지될 때마다 자동화된 파이프라인이 테스트와 검증을 트리거할 수 있다

설정 템플릿은 버전 관리의 이점을 받는 데이터의 대표적인 예다. 단일 템플릿 변경은 네트워크 전반의 모든 파생 설정에 영향을 미치므로 버전 제어가 필수적이 된다. 일반적으로 모든 데이터는 버전 관리된 방식으로 저장되어야 한다. 어떤 종류의 데이터나 더 복잡한 관계를 가진 데이터를 모델링하는 버전 관리된 YAML 또는 JavaScript Object Notation (JSON) 파일을 갖는 것이 일반적이다.

GitOps: 버전 관리를 더 발전시키는 새로운 패턴으로, Git 저장소가 단일 진실의 원천이 되고, 컨트롤러가 Git의 Desired State와 네트워크의 Actual State를 지속적으로 비교하여 드리프트를 자동으로 수정한다. 특히 클라우드 네이티브 및 Kubernetes 통합 환경에서 네트워크 자동화에서 채택이 증가하고 있다.

버전 관리는 이력과 감사 추적을 제공한다. 다음 원칙은 배포 전에 변경이 올바르게 작동하는지 검증하도록 보장한다.

2.2.5. 테스트 가능#

왜 테스트 가능성이 중요한가: 규모에서의 자동화는 오류를 증폭시킨다. 하나의 장비에 영향을 미치는 플레이북의 버그는 관리할 수 있다. 10,000개의 장비에 적용된 같은 버그는 재앙이다. 테스트는 프로덕션에 배포하기 전의 안전망이다.

테스트는 네트워킹에서 특히 도전적이다. 종종 전체 네트워크를 소유하지 않거나 (인터넷 BGP 피어링처럼 반대편을 소유하지 않는 경우를 생각해보라), 또는 규모가 엄청나다 (수천 개의 스위치가 있는 데이터 센터 패브릭 테스트를 상상해보라). 실제 테스트 시나리오를 재현하는 것이 거의 불가능하거나 엄청난 비용이 들 수 있다.

하지만 선택지가 없는 것은 아니다. 다양한 상황에서 자동화 동작과 네트워크 상태를 검증하는 여러 수준의 테스트를 구현할 수 있다.

테스팅 피라미드는 유용한 프레임워크를 제공한다:

graph BT
  %% Unit tests (base)
  U1["단위 테스트<br/>(데이터 품질, 로직)"]:::unit

  %% Integration tests
  I1["통합 테스트<br/>(시뮬레이션 환경)"]:::integration

  %% End-to-end tests (top)
  E1["엔드-투-엔드 테스트<br/>(랩, 검증)"]:::enduser

  %% Links
  U1 --> I1
  I1 --> E1

  %% Styling
  classDef unit fill:#a6e3a1,stroke:#3b7a57,stroke-width:2px,color:#000;
  classDef integration fill:#89b4fa,stroke:#1e3a8a,stroke-width:2px,color:#000;
  classDef enduser fill:#f9e2af,stroke:#b8860b,stroke-width:2px,color:#000;
  • Unit Test는 모의 객체나 가짜 시스템을 사용해 격리 상태에서 데이터 품질, 기본 로직, 특정 동작을 검증한다. 빠르게 실행되고 기본적인 오류를 조기에 발견한다.
  • Integration Test는 시뮬레이션된 (단순화된) 환경에서 서드파티 시스템을 도입한다. 다양한 자동화 구성 요소들이 서로 상호작용하고 시뮬레이션된 네트워크 유형과 상호작용하여 통합 지점을 검증할 수 있다 (예: 네트워크 장비의 Application Programming Interface (API) 또는 Secure Shell (SSH) Command Line Interface (CLI) 인터페이스를 가짜로 만드는 것).
  • End-to-End Test는 가상화된 네트워크 환경이나 프로덕션 네트워크의 일부를 시뮬레이션하는 랩을 사용해 실제에 가까운 시나리오에서 동작을 검증한다 (9장에서 이 주제를 더 깊이 다룰 것이다).

AI 코드 어시스턴트는 테스트 생성에 점점 더 유용해지고 있다. 무엇을 테스트해야 하는지 명확히 표현할 수 있다면, 이러한 도구들이 포괄적인 테스트 스위트를 빠르게 만드는 데 도움이 될 수 있다 (그리고 처음에 놓쳤을 수 있는 로직 결함을 발견할 수도 있다).

핵심적인 통찰: 자동화는 시스템이 성장함에 따라 아무것도 깨지지 않도록 지속적인 테스트가 필요하다. 개발 후반에 도입된 회귀는 단위 테스트 중에 발견된 것보다 훨씬 더 많은 비용이 든다. 자동화가 확장될수록, 이것은 협상할 수 없게 된다.

고급 테스팅 전략:

  • 카오스 엔지니어링: 의도적으로 실패(장비 중단, 네트워크 지연)를 주입하여 자동화와 모니터링이 우아하게 처리하는지 검증 (Netflix는 그들의 Chaos Monkey로 이 접근 방식을 대중화했다).
  • 속성 기반 테스팅: 항상 참이어야 하는 속성(예: “BGP는 항상 30초 내에 수렴해야 한다”)을 정의하고 테스트 프레임워크가 이를 검증하는 시나리오를 생성하게 한다

마지막으로, 어떤 변경을 배포하기 전에 운영자들은 무슨 일이 일어날지 가시성이 필요하다.

2.2.6. 드라이런 친화적#

왜 드라이런 기능이 중요한가: 이전의 모든 보호 장치에도 불구하고, 인간들은 자동화가 실행하기 전에 무엇을 할지 이해해야 한다. 드라이런 기능은 신뢰와 행동 사이의 다리 역할을 한다: 어떤 변경이 이루어지기 전에 운영자에게 정확히 무엇이 변경될지 보여준다.

중요한 결정에서 실행 전에 행동 계획을 이해하는 것은 중요하다. 집을 지을 때, 공사를 승인하기 전에 어떻게 보일지 보고 싶다. 마찬가지로 네트워크 엔지니어링에서 우리는 배포 전에 실행 계획을 검토하는 변경 관리 프로세스에 익숙하다.

네트워크 자동화에서 변경의 빈도와 범위가 극적으로 증가할 수 있다. 운영자에게 실행 전에 무엇이 실행될지 명확한 가시성을 제공하는 것은 신뢰를 구축하고 적절한 검토를 가능하게 하는 데 필수적이다.

Dry Run 기능을 제공하는 도구들이 있지만, 필요하다면 직접 만들 수도 있다:

  • Ansible: --check--diff 플래그는 무엇이 변경될지 보여준다
  • Terraform: terraform plan 명령어는 현재와 Desired State 간의 차이를 표시한다
  • 커스텀 네트워크 APIs: 일부는 “드라이런” 또는 미리보기 모드를 노출할 수 있다

이러한 가시성이 어떻게 보이는지 예시:

Ansible Diff 출력

- description: Uplink to CoreSwitch1
+ description: Uplink to CoreSwitch2
  mtu: 9216

Terraform Plan 출력

~ description = "Uplink to CoreSwitch1" -> "Uplink to CoreSwitch2"
  mtu          = 9216

커스텀 네트워크 API 미리보기 응답

{
  "operation": "PATCH",
  "path": "/interfaces/Gi0-1",
  "changes": [
    {
      "field": "description",
      "current_value": "Uplink to CoreSwitch1",
      "proposed_value": "Uplink to CoreSwitch2",
    }
  ],
  "mtu": 9216
}

드라이런은 자동화를 “최선을 바라는” 접근 방식에서 의도적이고 검토 가능한 프로세스로 변환한다.

고급 드라이런 개념:

  • 영향 분석: 무엇이 변경되는지 보여주는 것 이상으로, 비즈니스 영향을 분석하고 전달한다 (예: “이것은 일시적으로 트래픽의 10%에 영향을 줄 것입니다”)
  • 단계적 롤아웃: 먼저 장비의 부분 집합에 변경을 점진적으로 롤아웃하고 전체 배포 전에 영향을 검증함으로써 규모에서 드라이런을 구현
  • 네트워크 시뮬레이션: 네트워크 테스팅 도구와 결합하여 프로덕션 네트워크의 복제본에 대해 드라이런을 실행

2.3. 아키텍처 결정 패턴#

기반 원칙들 외에도, 이론과 실용적인 구현 사이를 연결하는 핵심 개념과 고려 사항들이 있다. 이러한 패턴들은 자동화 아키텍처에 대한 전략적 결정을 내리는 데 도움이 된다.

2.3.1. 선언적 vs. 명령적#

선언적과 명령적 접근 방식 중의 선택은 근본적이다. 각각은 유스케이스에 따라 강점과 트레이드오프를 갖는다. 인프라 설정 관리는 두 가지 접근 방식을 취할 수 있다:

  • Declarative 자동화는 원하는 최종 상태를 정의하고, 시스템이 어떻게 달성할지 알아낸다. 예: “인터페이스 Gi0/1의 MTU를 9000으로 하고 싶다.” 자동화 엔진이 현재 상태를 결정하고 필요한 변경만 적용한다. 무엇을, 원하는 결과에 초점을 맞춘다.
  • Imperative 자동화는 실행할 정확한 단계를 지정한다. 예: “인터페이스 설정을 표시하고, 파싱하고, 차이를 계산하고, 이 순서로 이러한 정확한 CLI 명령을 보내라.” 어떻게, 특정 작업에 초점을 맞춘다.

선언적 접근 방식은 자연스럽게 Idempotency, Dry Run 기능, Transactional 동작을 지원하지만, 그것을 지원하는 인프라와 시스템이 필요하다.

명령적 접근 방식은 정확한 제어를 제공하지만 멱등적으로 만들기 어렵고 의도하지 않은 부작용이 더 많이 발생한다. 하지만 대상 시스템에 선언적 기능이 없을 때는 유일한 선택일 수 있다.

핵심 트레이드오프: 선언적 접근 방식이 더 잘 확장된다. 가능할 때, 복잡한 워크플로를 구현하는 복잡성은 일정하게 유지되는 반면, 명령적 접근 방식은 워크플로가 더 복잡해질수록 복잡성이 기하급수적으로 증가한다.

대부분의 현대 자동화 프레임워크 (Terraform, Ansible, Kubernetes, 또는 Yet Another Next Generation (YANG) 기반 도구)는 필요할 때 Imperative 폴백을 허용하면서 (예: Ansible raw 모듈, 직접 Command Line Interface (CLI), Netmiko) Declarative 모델로 향한다.

Ansible과 같은 일부 도구는 모듈 유형에 따라 두 가지 방식으로 기능할 수 있다. 네트워크 특정 모듈 (예: cisco.ios.ios_interfaces)은 선언적이지만, ansible.netcommon.cli_command는 명령적 명령을 실행한다. 아래 예에서 두 가지 접근 방식을 볼 수 있다:

- name: Ansible에서의 명령적 vs. 선언적 접근 방식
  hosts: switches
  gather_facts: no
  vars:
    interface_name: GigabitEthernet0/1
    new_description: Uplink to CoreSwitch2
  tasks:
    - name: 명령적 - 정확한 명령 실행
      ansible.netcommon.cli_command:
        command: |
          configure terminal
          interface {{ interface_name }}
          description {{ new_description }}
          no shutdown
          end
      # 이 작업을 반복하면 중복이 추가되고 멱등성을 위반할 수 있다

    - name: 선언적 - 원하는 상태 정의
      cisco.ios.ios_interfaces:
        config:
          - name: "{{ interface_name }}"
            description: "{{ new_description }}"
            enabled: true
        state: merged
      # 이 작업을 반복하는 것은 안전하며, 원하는 상태로 수렴한다

선언적 접근 방식에서는 로직을 외부 시스템에 위임하고 있으며, 일부 시나리오에서는 가능한 옵션이 아닐 수 있다는 점을 명심하라. 이전 예에서 선언적 특성을 제공하는 데 필요한 로직을 구현하는 Ansible 모듈 cisco.ios.ios_interfaces를 어떻게 활용하는지 주목하라.

선언적 접근 방식이 더 안전하고 예측 가능하지만, 특정 장비에 대한 모듈 지원이 필요하다.

2.3.2. 가변 vs. 불변 인프라#

개념: 인프라(네트워크)를 인플레이스로 수정하는 것을 허용하는가, 아니면 변경이 필요할 때 인프라를 완전히 교체하는가?

  • 가변 인프라: 장비에 Secure Shell (SSH)로 접속하여 설정을 직접 수정하는 전통적인 접근 방식 (또는 NETCONF, gNMI, 또는 어떤 Application Programming Interface (API)를 통해 수정). 변경이 인플레이스로 적용된다.
    • 장점: 덜 파괴적이고, 오버헤드가 낮다.
    • 단점: 상태 추적이 어렵고, Configuration Drift 위험이 증가한다.
  • 불변 인프라: 실행 중인 인프라를 절대 수정하지 않는다. 대신, 원하는 변경으로 새 인프라를 생성하고 트래픽/연결을 전환한다. 클라우드(컨테이너, VM)에서 많이 사용되지만 네트워킹에서는 덜 일반적이다.
    • 장점: 예측 가능한 상태, 검증이 쉽고, 드리프트를 제거한다.
    • 단점: 오케스트레이션이 필요하고, 복구가 더 복잡하며, 리소스 오버헤드가 높다.

네트워크 자동화에서 우리는 일반적으로 하이브리드 상태에 있다: 설정은 가변적이지만 (인플레이스로 변경), 불변성의 원칙이 설계를 안내해야 한다: 모든 것을 버전 관리하고, 변경을 추적하며, 필요할 때 처음부터 재구축할 수 있어야 한다.

2.3.3. 그린필드 vs. 브라운필드#

기술적인 용어는 아니지만, 네트워크 자동화 솔루션을 개발할 때 두 가지 다른 시나리오를 이해하는 데 유용하다:

  • Brownfield 환경은 레거시 시스템, 수동 프로세스, 일관성 없는 설정, 부족 지식으로 존재한다. 자동화하기 어렵다. 자동화되도록 의도되지 않은 복잡성을 자동화하는 것이기 때문이다: 일관성 없는 설계, 누락된 데이터, 레거시 기술, 인간의 습관, 그리고 라이브 트래픽 제약. 하지만 가장 일반적인 환경이다.
  • Greenfield 환경은 자동화 우선 설계로 처음부터 구축된다. 모든 원칙을 깔끔하게 구현할 수 있다: 첫날부터 버전 제어, 선언적 인텐트, 깔끔한 데이터 모델, 포괄적인 테스팅. 이상적이지만 드물다.

왜 브라운필드 환경이 그토록 복잡한지 더 깊이 들여다보자…

  • 일관성 없는 네트워크 설계 (또는 일관성이 있었더라도 더 이상 완전히 구현되지 않는다) 그리고 그것은 단지 예외들의 묶음이다.
  • 깔끔하고 신뢰할 수 있는 데이터의 부족. 네트워크 자체나 아마도 오래된 스프레드시트가 레퍼런스다.
  • 다양한 벤더와 세대의 일부 네트워크 장비는 선언적 접근 방식의 구현을 제한하는 현대적인 네트워크 관리 인터페이스 (CLI 이상)를 지원하지 않는다.
  • 운영 문화와 변화에 대한 두려움. 사람이 먼저라는 것을 잊지 말라, 그리고 채택 전에 신뢰를 얻어야 한다.
  • 자동화는 깔끔한 컷오버 없이 라이브 트래픽과 공존해야 한다. 오류의 여지가 매우 작다.

하지만 이러한 경우에도 희망이 있다. 다음 세 가지 접근 방식이 이러한 시나리오를 시작하는 데 도움이 된다:

  1. 부분적 그린필드 접근 방식: 새로운 인프라를 자동화하면서 레거시 시스템을 점진적으로 리팩토링한다. 최대 복잡성으로 시작하지 않고 진전을 보여준다.
  2. 점진적 대상 선택: 자동화하기 쉽고 빠른 성과를 제공하는 네트워크 부분에 집중한다:
    • 몇 가지 관리 기능 (AAA, NTP, SNMP)에 대한 설정 드리프트 및 수정 추가
    • 단일 장비 유형에 대한 인터페이스 프로비저닝 자동화
    • 확장하기 전에 단일 서브시스템 표준화
  3. 모멘텀 구축: 각각의 작은 성공은 자동화의 가치를 입증하고 자금 조달 및 확장에 대한 신뢰를 구축한다

2.3.4. 장비 다양성과 서비스 추상화#

모든 네트워크 장비나 서비스가 동일하게 작동하지는 않는다. 다른 벤더, 모델, 심지어 소프트웨어 버전도 고유한 인터페이스, 기능, 제한이 있다. 자동화는 이 이기종성을 전략적으로 해결해야 한다.

두 가지 기본 접근 방식:

  • 벤더 특정 자동화 수용: 재현성을 손상시키지 않고 각 벤더의 고유한 기능에 맞춰진 자동화를 작성한다. 장점: 초기에는 단순하고, 장비의 강점을 활용한다. 단점: 사일로를 만들고, 요구 사항이 변경되면 마이그레이션이 어렵다.
  • 추상화: 공통 작업을 표준화하는 벤더 중립적인 추상화 레이어를 만든다. 장점: 이식성, 통합 인터페이스. 단점: 추가 복잡성, 잠재적인 장비 특정 기능 손실.

모범 사례: 계층적 접근 방식 대부분의 성숙한 운영은 두 가지를 모두 사용한다: 하단에 벤더 특정 드라이버(장비 유형당 하나의 레이어), 그 위에 공통 추상화 레이어. 이것은 두 전략의 이점을 모두 제공한다.

실제 예시: DMVPN 대안 구축 네트워크 벤더들은 허브-스포크 VPN 확장성을 위해 DMVPN (Dynamic Multipoint VPN)을 제공한다. 대안은 대신 많은 단순한 포인트-투-포인트 VPN 터널을 설정하는 것이다. 하지만 자동화 없이는 번거롭다 (그리고 프로토콜이 존재하는 이유다). 자동화로 수천 개의 터널을 관리하는 것은 가능하다 (그리고 극도로 복잡하지 않다), 그리고 사실상 모든 플랫폼이 기본 VPN 터널을 지원하기 때문에 프로토콜 정교함 대신 오케스트레이션을 통해 유사한 확장성을 제공한다. 프로토콜 의존성을 자동화 추상화로 대체했다: 종종 더 나은 트레이드오프다.

핵심 원칙: 구현 세부 사항(특정 장비 작동 방식)을 달성하고자 하는 최종 목표와 분리하라. 이것은 벤더 독립성을 가능하게 하고 시스템에 대한 추론을 단순화한다.

2.3.5. 설계 대신 도구의 오류#

도구는 필수적이지만, 좋은 설계의 대체물이 아니다. 일반적인 실수: 도구를 구매하고 자동화 문제를 해결해줄 것을 기대하는 것. 도구는 중요하지만, 기존 아키텍처와 설계를 증폭시킨다. 평범한 도구로 잘 설계된 자동화 전략은 최고의 도구로 잘못 설계된 전략을 이긴다.

설계와 아키텍처는 전략이며 먼저 와야 한다. 도구는 구현 세부 사항이다. 도구를 선택하기 전에 요구 사항을 이해하고, 아키텍처를 설계하며, 원칙을 정의하는 데 시간을 투자하라. 때로는 분산 아키텍처가 적절하다. 다른 때는 더 단순하고 더 유능한 솔루션이 결과 대 복잡성 측면에서 필요에 더 잘 맞는다. 단 하나의 공식은 없지만, 선택에 대해 인식하고 의도적이어야 한다.

또한 도구는 마법 상자가 아님을 기억하라. 항상 자신만의 로직을 가져와야 한다. 사용자 정의와 도메인 특정 설정은 피할 수 없다.

3장에서 도구를 평가할 때 고려해야 할 주요 구성 요소를 강조하는 참조 아키텍처를 탐구할 것이다.

2.3.6. 구매 vs. 구축#

일반적인 전략적 질문: 기성품 솔루션을 구매해야 하는가, 커스텀 자동화를 구축해야 하는가, 아니면 하이브리드 접근 방식을 사용해야 하는가?

단순한 규칙: 가능하면 구매하고, 반드시 필요할 때만 구축하라. 직관에 반하지만, 구축은 종종 구매보다 더 비싸다. 하지만 필요한 것을 구매하는 것이 항상 가능하지는 않다.

결정을 평가할 때:

  • 구매: 제품이 요구 사항과 밀접하게 일치하고, 아키텍처를 지원하며, 비용/편익 분석이 유리할 때 사용
  • 구축: 요구 사항이 독특하고, 전문성이 있거나, 기성품 솔루션이 원칙에 맞지 않을 때 사용

이것은 근본적으로 조달 결정이 아닌 설계 결정이다. 얼마나 많은 사용자 정의와 제어가 진정으로 필요한지에 관한 것이다.

실제로 대부분의 조직은 하이브리드 접근 방식을 사용한다: 전략적 구성 요소 (오케스트레이션 플랫폼, CI/CD 시스템, 데이터 스토리지)를 구매하지만 도메인 특정 자동화 (템플릿, 워크플로, 검증 로직)를 구축한다. 도메인 특정 레이어를 소유해야 한다: 일반적인 레시피는 필요한 결과를 거의 제공하지 않는다.

오픈소스 솔루션을 평가할 때, 확장성을 고려하라. 종종 프레임워크를 재사용하고 그 위에 자신만의 커스텀 레이어를 구축할 수 있다: 예를 들어, 커스텀 데이터 모델로 코어 도구를 확장하는 네트워크 인텐트를 저장하는 데이터 소스. 또한 오픈소스의 경우에도 필요할 때 안전망을 추가하기 위해 지원과 엔터프라이즈 에디션을 여전히 받을 수 있다는 것을 잊지 말라.

2.3.7. 자동화 거버넌스#

무엇을 자동화할지 누가 결정하는가? 자동화 로직의 변경을 누가 승인하는가? 프로덕션에서 수천 개의 장비에 대해 잡 템플릿을 실행하도록 누가 승인할 수 있는가? 이러한 질문들은 깔끔한 기술적 답변이 거의 없지만, 자동화가 어떤 중요한 규모에 도달하기 전에 조직적으로 답변되어야 한다.

거버넌스 없는 자동화는 수동 운영과 다른 종류의 위험을 도입한다. 수동 오류를 범하는 단일 엔지니어는 하나의 장비나 하나의 세션에 영향을 미친다. 플레이북 템플릿에서 오류를 범하는 자동화 엔지니어는 범위 내의 모든 장비에 동시에 영향을 줄 수 있다. 폭발 반경은 자동화의 기능과 함께 확장된다.

이 맥락에서 거버넌스는 네 가지를 정의하는 것을 의미한다:

  • 범위 경계: 어떤 작업이 자동화에 적합하고 자동화 성숙도와 관계없이 인간의 실행이 필요한지. BGP 정책 변경, 라우팅 프로토콜 수정, 또는 보안 정책 업데이트는 자동화가 할 수 없기 때문이 아니라 오류의 결과가 루프에 인간을 정당화할 만큼 크기 때문에 나머지 스택이 자동화되더라도 인간이 게이트를 유지할 수 있다.

  • 로직 승인 프로세스: 자동화 로직의 변경 (새 플레이북, 수정된 템플릿, SoT의 업데이트된 데이터 모델)은 소프트웨어 코드 검토와 동등한 검토 프로세스를 거쳐야 한다. 여기서의 규율은 소프트웨어 엔지니어링을 반영한다: 프로덕션에서 실행되는 코드의 변경은 프로덕션에 도달하기 전에 검토가 필요하다. 자동화 코드도 다르지 않다.

  • 실행 권한: 누가 어떤 조건에서 어떤 범위에 대해 어떤 잡을 트리거할 수 있는가. 이것은 실행 레이어 (5장)와 프레젠테이션 레이어 (8장)의 역할 기반 접근 제어에 직접 매핑된다. 거버넌스 모델은 정책 문서뿐만 아니라 도구의 접근 제어에 표현되어야 한다.

  • 감사 및 책임: 자동화는 수동 변경 관리가 생성하는 동일한 감사 추적을 생성해야 한다. 실행 이벤트는 변경 티켓, 승인자, 실행된 자동화 로직의 특정 버전으로 추적 가능해야 한다. 이것은 규제된 환경에서 선택적이 아니다.

거버넌스는 그 자체를 위한 관료주의가 아니다. 조직이 자동화에 대한 신뢰를 점진적으로 확장하는 메커니즘이다. 위험이 낮고, 가역적이며, 잘 이해된 작업으로 시작하여 자동화가 실적을 쌓으면 범위를 확장하는 것이, 자동화를 완전히 막거나 가드레일 없이 배포하는 것보다 더 효과적이다.

2.3.8. 이러한 원칙들이 왜 중요한가: 실패에서 배우기#

자동화는 무엇을 넣든 증폭시킨다. 깔끔하고 잘 설계된 프로세스는 더 효율적이고 신뢰할 수 있게 된다. 하지만 나쁜 설계, 잘못된 데이터, 또는 결함 있는 로직도 확장되어 더 빠르게 더 큰 문제를 만든다.

메타 서비스 중단 사례 연구

2021년 10월, 메타 (구 Facebook)는 몇 시간 동안 시스템이 오프라인 상태가 된 글로벌 네트워크 서비스 중단을 경험했다. 원인은 무엇인가? 자동화가 잘못된 설정을 증폭시켰다. 일상적인 트래픽 이동 중에 자동화된 시스템이 불완전한 정책 검증을 기반으로 전역 변경을 수행했다. 설정이 네트워크 전체로 전파되어 전역적으로 전파된 단일 실패 지점을 만들었다.

근본 원인은 백본 라우터의 잘못된 설정 변경으로, 데이터 센터 간 통신을 방해했다. 이 연쇄 실패는 전역적으로 서비스를 중단시키고 내부 도구에도 영향을 미쳐 진단과 해결을 복잡하게 만들었다. 메타는 이 문제가 악의적인 활동에 의한 것이 아니며 사용자 데이터가 손상되지 않았다고 명확히 했다. 하지만 서비스 중단은 자동화 전략의 추정되는 심각한 공백을 강조했다:

  • 멱등성 없음: 현재 상태를 확인하지 않고 변경이 적용됨
  • 트랜잭셔널 없음: 일부 장비는 변경을 받고 다른 것은 받지 못함 (부분적 실패)
  • 불충분한 테스팅: 시나리오가 프리프로덕션에서 발견되지 않음
  • 드라이런 기능 없음: 미리보기나 검증 없이 변경이 적용됨
  • 나쁜 버전 관리: 문제가 있는 변경을 빠르게 식별하고 되돌릴 수 없음
  • 불충분한 옵저버빌리티: 충분히 빠르게 실패를 감지할 수 없음
  • 우아한 저하 없음: 폭발 반경 억제 없이 전역적으로 변경이 전파됨
  • 불명확한 책임: 자동화에 대한 명확한 의사결정 계층 없음

원칙에 매핑: 우리의 여섯 가지 핵심 설계 원칙은 각각 이러한 실패 중 하나를 직접 해결한다. 이것은 선택적인 좋은 것이 아님을 보여준다: 필수적인 안전 장치다.

교훈: 나중에 고치기를 바라며 혼돈을 자동화하지 말라. 대신:

  1. 이해하고 검증할 수 있는 프로세스로 시작하라
  2. 각 단계에서 배우며 점진적으로 자동화하라
  3. 모든 자동화 실패를 배움의 기회로 취급하라
  4. 안전 장치를 내장하라: 속도 제한, 롤백 메커니즘, 옵저버빌리티, 폭발 반경 억제
  5. 한 영역의 실패가 전역적으로 전파되지 않도록 관심사를 분리하라

이것이 1장에서 소개한 사람, 프로세스, 기술 접근 방식과 어떻게 연결되는지 주목하라.

우리가 탐구한 원칙들 (Intent-Driven, Idempotency, Transactional, Versioning, 테스트 가능성, Dry Run 기능)은 이러한 실패 모드를 직접 해결한다. 선택적인 기능이 아니라 처음부터 내장되어야 하는 필수적인 안전 장치다.

2.4. 소프트웨어 엔지니어링 원칙#

네트워크 특정 설계 원칙 외에도, 더 광범위한 소프트웨어 엔지니어링 원칙이 유지 보수 가능하고 확장 가능한 자동화 시스템을 구축하는 데 중요한 역할을 한다. 소프트웨어 엔지니어링 배경에서 온 경우, 이것들 대부분을 인식할 것이다; 여기서 가치는 그것들이 특별히 네트워크 자동화에 어떻게 적용되는지를 보는 것이다.

이 도메인에서 모든 열두 개가 동일한 비중을 갖지 않는다. 네 가지는 중요 인프라 자동화에 특정한 실패 모드를 해결한다: 최소 놀라움의 원칙 (예상치 못하게 동작하는 자동화는 자리를 잡기 전에 2.1절에서 확립된 운영 신뢰를 파괴한다), 방어적이고 견고한 프로그래밍 (네트워크는 설계에 의해 자동화가 예측해야 하는 부분적이고 비결정론적인 방식으로 실패하며, 예외로 처리하지 않는다), 빠르게 실패, 가시적으로 실패 (조기 실패 감지는 장비 전반에 걸쳐 확장되기 전에 폭발 반경을 억제한다), 관심사 분리 (3장의 NAF 프레임워크가 직접 인스턴스화하는 구조적 원칙 - 인텐트, 실행 로직, 프레젠테이션을 분리하는 것은 여기서 일반적인 좋은 관행이 아니라, 블록을 독립적으로 발전할 수 있게 유지하는 특정 구조다). 나머지 여덟 가지 원칙은 표준 소프트웨어 위생이다: 정확하고, 중요하며, 알 만한 가치가 있지만, 네트워크 자동화가 특별히 실패하거나 성공하는 이유가 아니다.

Robert C. Martin의 “Clean Code"와 “Clean Architecture"에서 영감을 받아 두 가지 범주로 정리한다:

  • 클린 코드 원칙: 읽기 가능하고, 유지 보수 가능하며, 정확한 자동화 로직을 작성하는 방법.
  • 클린 아키텍처 원칙: 구성 요소가 독립적이고, 테스트 가능하며, 발전 가능하게 시스템을 구조화하는 방법.

네트워크 자동화에 적용하는 것에 집중하면서, Ken Celenza의 이 Network to Code 블로그 시리즈에서 이러한 원칙들의 좋은 예를 찾을 수 있다.

2.4.1. 클린 코드 원칙#

이 섹션에서는 소프트웨어 구성 요소가 어떻게 구축되는지에 초점을 맞춘다.

독자를 위한 코드 작성

당신이 작성하는 코드는 미래에 여러 번 읽힐 것이다: 당신 자신에 의해 (디버깅할 때) 또는 다른 사람들에 의해. 그리고 그들은 아마도 당신의 원래 맥락을 갖지 않을 것이다. 의미 있는 이름, 주석, 구조를 통해 코드에서 의도를 명확하게 표현하라 (주석을 과도하게 사용하지 말라; 신중하게 사용하라). 자동화 코드는 단순히 기계에 대한 지침이 아니라 인간을 위한 문서의 한 형태다.

DRY - 반복하지 말라

자동화 코드베이스 전반에 걸쳐 로직을 중복하지 말라. 대신, 공통 패턴을 재사용 가능한 템플릿, 함수, 또는 워크플로로 추출하라.

열 가지 다른 플레이북에 장비 특정 설정 로직을 작성하는 대신, 차이에 대한 변수가 있는 공유 템플릿을 만들라. 이것은 버그를 줄이고 업데이트를 더 쉽게 만든다. 이 원칙은 데이터에도 적용된다: 상속과 다형성을 활용하는 적절한 데이터 구조를 사용하여 더 확장 가능하고 재사용 가능한 데이터 모델을 만들라.

DRY를 위반하면, 한 곳의 수정이 다른 다섯 곳에서 수정이 필요하고, 불가피하게 하나를 놓치게 된다.

단일 책임 원칙 (SRP)

각 모듈, 함수, 또는 워크플로는 변경할 하나의 이유만 가져야 한다. 네트워크 자동화에서 이것은:

  • 설정 템플릿 렌더러가 장비 발견도 처리해서는 안 된다
  • 검증 워크플로가 변경도 실행해서는 안 된다

각 구성 요소가 단일 책임을 가질 때, 실패가 격리되고, 테스팅이 단순해지며, 변경이 덜 위험해진다. 물론, 이러한 모든 함수들을 함께 연결하는 합성 함수 (또는 오케스트레이션)가 있을 것이다.

빠르게 실패, 가시적으로 실패

가능한 한 일찍 문제를 감지하고 명확하게 노출하라. 자동화에서:

  • 입력 즉시 데이터를 검증하라 (배포까지 기다리지 말라)
  • Dry Run 출력을 명시적이고 명확하게 만들어라
  • 막연한 오류 코드가 아닌 전체 맥락과 함께 실패를 기록하라
  • 무언가 잘못됐을 때 운영자에게 즉시 경보를 발하라

조기에 문제를 발견하면 폭발 반경과 응답 시간이 줄어든다.

보안

암호화, 인증, 최소 권한, 감사 추적은 처음부터 자동화 시스템에 내장되어야 하며, 나중에 추가되어서는 안 된다.

네트워크 자동화에서: 모든 변경은 감사 가능해야 하고, 자격 증명은 절대 하드코딩되어서는 안 되며, 접근 제어는 최소 권한의 원칙을 따라야 한다. 전체 네트워크 접근 권한을 가진 단일 자동화 시스템은 재앙을 기다리는 보안 위험이다.

12장에서 보안 및 컴플라이언스 고려 사항에 대해 깊이 다룰 것이다.

최소 놀라움의 원칙

자동화는 사용자가 기대하는 방식으로 동작해야 한다. 놀랍거나 직관에 반하는 동작은 신뢰를 훼손한다.

예를 들어, 자동화 작업의 이름이 “deploy_interface"라면, 운영자들은 인터페이스를 생성할 것을 기대하지, 삭제할 것을 기대하지 않는다. 예상치 못한 동작은 사용자를 좌절시키고 실수를 유발한다.

방어적이고 견고한 프로그래밍

재시도, Circuit Breaker 패턴, Compensation Logic, 폴백 메커니즘을 내장하라. 분산 시스템은 실패한다. 그것에 반하는 것이 아니라 그것을 위해 설계하라. 장비가 일시적으로 도달 불가능하면, 즉시 실패하는 대신 지수 백오프로 재시도하라. 변경이 중간에 실패하면, 롤백 계획을 가져라.

이러한 패턴은 7장 (오케스트레이션)에서 구체적으로 나타난다. 여기서 Saga 보상 패턴은 부분적인 워크플로 실패를 처리하고, 재시도/백오프 로직이 복원력 기능에 내장된다.
“보내는 것에는 보수적이고, 받는 것에는 관대하라.” Postel의 법칙 (RFC 761)

네트워크 자동화에서:

  • 보수적인 전송: API나 장비에 보내는 데이터가 엄격한 스키마와 계약을 준수하도록 보장
  • 관대한 수용: 다른 시스템 버전과의 상호 운용성을 극대화하기 위해 변형을 처리할 준비 (예: 정수나 문자열로서의 속성을 변환과 함께)

이 원칙은 클린 코드와 아키텍처를 연결한다. 통합 로직을 작성하는 방법과 시스템 인터페이스를 구조화하는 방법 모두에 영향을 미친다.

2.4.2. 클린 아키텍처 원칙#

다음으로, 네트워크 자동화 솔루션의 구성 요소가 어떻게 함께 구성되는지를 규율하는 원칙들을 탐구한다.

KISS - 단순하게 유지하라

단순할수록 이해하고, 테스트하고, 유지 보수하기 쉽다. 설계, 구현, 아키텍처에서 과도한 엔지니어링을 피하라. 단순성은 버그를 줄이고, 유지 보수성을 향상시키고, 가독성을 개선하며, 시스템을 확장하거나 디버깅하기 쉽게 만든다.

단순하다는 것이 단순화했다는 의미는 아니다. 과도한 엔지니어링이나 이른 추상화 없이 요구 사항을 충족하는 가장 직관적인 접근 방식을 선택한다는 의미다.

네트워크 자동화에서 이것은 (가능할 때) 복잡한 명령적 스크립트보다 직관적인 Declarative 접근 방식을 선호하고, 명확성, 작은 합성 가능한 구성 요소, 예측 가능한 동작, 그리고 다른 사람들이 (미래의 자신을 포함하여) 쉽게 이해할 수 있는 솔루션을 우선시한다는 의미다.

관심사 분리

데이터 (설정), 로직 (워크플로), 프레젠테이션 (APIs/UIs)을 명확히 분리하라. 이것은 긴밀한 결합을 방지하고 각 레이어의 독립적인 발전을 가능하게 한다.

네트워크 자동화에서:

  • 데이터 레이어: 구조화된 형태로 저장된 네트워크 인텐트
  • 로직 레이어: 자동화 엔진과 검증 규칙
  • 프레젠테이션 레이어: 운영자를 위한 APIs, CLIs, 대시보드

이 분리는 기본 로직에 영향을 미치지 않고 운영자가 자동화와 상호 작용하는 방식을 변경할 수 있게 한다. 이 분리는 NAF 구성 요소에 직접 매핑된다: 데이터 레이어는 진실의 원천 (4장), 로직 레이어는 실행 (5장), 옵저버빌리티 (6장), 오케스트레이션 (7장)에 걸쳐 있으며, 프레젠테이션 레이어는 8장이다. 3장에서 전체 NAF 프레임워크를 소개한다.

옵저버빌리티

자동화는 자체 동작을 측정하고, 실패를 감지하며, 수정 행동을 트리거하도록 계측되어야 한다. 측정할 수 없는 것은 최적화할 수 없다. 기술 메트릭과 비즈니스 지향 메트릭 (예: 자동화 이니셔티브의 ROI) 모두를 추적하라.

6장에서 네트워킹에서 관심을 갖는 다양한 유형의 옵저버빌리티 데이터를 다룰 것이다: 메트릭, 로그, 트레이스, 네트워크 플로우, 경보 (그리고 훨씬 더!), 그리고 모든 수준에서 유용한 정보를 제공하기 위해 그것들을 어떻게 활용하는지.

옵저버빌리티 없이는, 눈을 감고 비행하는 것이다. 자동화가 올바르게 작동하는지 또는 단순히 작동하는 것처럼 보이는지 알 수 없다. 기억하라: 자동화 시스템 자체도 실패하고 모니터링이 필요하다. 네트워크를 계측하는 것만큼 철저하게 자동화 도구를 계측하라.

확장성

미래를 염두에 두고 설계하라. 새로운 벤더, 새로운 기술, 새로운 요구 사항이 올 것이다. 아키텍처는 완전한 재작성을 요구하지 않고 이것들을 허용해야 한다.

실제로: 벤더 특정 드라이버에 플러그인 아키텍처를 사용하고, 네트워크 토폴로지에 대한 하드코딩된 가정을 피하며, 구현이 발전하는 동안 인터페이스를 안정적으로 유지하라.

최소 결합, 최대 응집

시스템이 어떻게 통신하는지에 대한 명확한 계약을 정의하라: 스키마, 검증 규칙, 하위 호환성 정책. 이러한 계약은 구성 요소의 독립적인 발전을 가능하게 한다.

네트워크 자동화에서, 오케스트레이션 시스템이 잘 정의된 REST API를 통해 장비 드라이버와 통신한다면, API 계약이 유지되는 한 어느 레이어든 독립적으로 발전할 수 있다.

항상 API 우선 설계로 모든 시스템에 접근하라: API를 먼저 설계하라 (구현이 아니라). 이것은 시스템이 독립적으로 개발되고 다른 구성 요소를 깨지 않고 교체될 수 있게 한다.


이러한 고급 원칙들은 나중 장에서 대규모 (그리고 더 소규모) 조직에 걸쳐 자동화를 확장하는 것을 논의하면서 더 깊이 탐구될 것이다. 지금은, 이러한 원칙들이 앞서 탐구한 설계 원칙들을 보완한다는 것을 이해하라: 함께 그것들은 규모에서 신뢰할 수 있고 유지 보수 가능한 네트워크 자동화의 기반을 형성한다.

2.5. 요약#

이 장은 신뢰가 성공적인 네트워크 자동화의 기반이라는 것을 확립했다. 신뢰는 네 가지 핵심 품질에서 나온다: Predictable, Reliable, Usable, Understandable.

이러한 품질은 여섯 가지 근본적인 설계 원칙에 의해 지지된다:

  1. 인텐트 기반: 어떻게 달성할지보다 무엇을 달성하고 싶은지를 정의하라
  2. 멱등적: 반복된 실행이 일관된 결과를 생성한다
  3. 트랜잭셔널: 변경이 완전히 완료되거나 안전하게 실패하며, 절대 부분적으로 실패하지 않는다
  4. 버전 관리: 전체 이력과 감사 추적으로 모든 변경을 추적한다
  5. 테스트 가능: 프로덕션에 배포하기 전에 동작을 검증한다
  6. 드라이런 친화적: 실행 전에 변경을 미리 본다

이러한 핵심 원칙들 외에도, 실제 시스템에서 이러한 패턴을 운영화하는 아키텍처 결정 패턴 (선언적 vs. 명령적, 그린필드 vs. 브라운필드, 장비 추상화)과 소프트웨어 엔지니어링 원칙 (클린 코드와 클린 아키텍처)을 탐구했다.

이러한 원칙들은 추상적인 이론이 아니다: 당신이 사용할 도구와 프레임워크에 구체적인 구현이 있다. 이 책의 나머지 부분에서 아키텍처 사고 (3장)가 이러한 원칙들을 더 큰 시스템에 어떻게 적용하는지, 그리고 구성 요소 (4-9장)가 그것들을 어떻게 운영화하는지 볼 것이다.

핵심 시사점:

  • 도구가 아닌 원칙으로 시작하라
  • 복잡성이 아닌 Predictable 결과를 위해 설계하라
  • 지속적으로 측정하고 개선하라.

이것을 일관되게 하면, 신뢰가 자연스럽게 따르며, 신뢰와 함께 전체 조직에 걸쳐 자동화를 확장하는 능력이 온다.

이제 자동화를 신뢰할 수 있게 만드는 원칙들을 이해했다. 3장 (아키텍처 사고)에서 이러한 원칙들을 확장 가능한 시스템으로 구조화하는 방법을 볼 것이다. 관리 불가능하게 되지 않고 조직과 함께 성장하는 자동화를 어떻게 설계할지 배울 것이다: 여기서 배운 원칙들을 체계적으로 적용하는 방법에 대한 실용적이고 아키텍처적인 관점.

💬 Found something to improve? Send feedback for this chapter