레거시·상용 스키드 연결: Modbus, Siemens S7, PLC4X
📍 현재 위치: 제2부 · 공정 포착하기 — 제9장. 5–7장에서 다룬 깔끔한 OPC UA 바이오리액터(bioreactor)는 규칙이 아니라 예외입니다. 이 장은 현장에 놓인 다른 장비들 — 수확 원심분리기, TFF 스키드, 저울 — 에 손을 뻗습니다. 이들은 더 낡고 안전하지 않은 프로토콜로 말하며, 우리는 OT 분할(segmentation) 뒤에서 이들을 안전하게 읽어 동일한 태그 네임스페이스(tag namespace)로 들여옵니다.
OPC UA는 정중하고 스스로를 설명하는 프로토콜입니다. 서버에 "무엇을 가지고 있나요?"라고 물으면, 이름과 단위와 보안과 함께 답해 줍니다. Modbus는 그 반대입니다. Modbus 장치는 16비트짜리 비둘기집 칸들에 번호를 매긴 찬장입니다. 1번 칸에는 1850이 들어 있습니다. 그것이 장치가 알려 주는 전부입니다. 1850이 1.850 bar를 뜻하는지, 1850 밀리바를 뜻하는지, 아니면 무언가의 18.50을 뜻하는지는 전선 위 어디에도 적혀 있지 않습니다. 그것은 누군가의 책상 위 PDF 데이터시트(datasheet)에 살고 있습니다. 찬장에는 비밀번호가 없고 문에는 자물쇠가 없습니다. 그래서 우리는 두 가지를 합니다. 찬장을 경비가 지키는 방 안에 넣고(네트워크 분할), 우리만의 라벨이 붙은 열쇠고리(태그 사전)를 따로 지닙니다. 이 열쇠고리에는 "1번 칸, 0.001을 곱하고, 단위는 bar인 막간 압력(transmembrane pressure)이라 부른다"라고 적혀 있습니다. 이 장은 그 열쇠고리를 실제로 실행 가능한 코드로 만듭니다.
이 장에서 다루는 내용
이제 우리는 현대적인 바이오리액터를 깔끔하게 포착할 수 있습니다. 하지만 실제 바이오 제조 현장을 걸어 보면, 대부분의 장비는 OPC UA보다 더 오래되었습니다. 2009년에 설치된 원심분리기, "사이버보안(cybersecurity)"이 조달 항목이 되기도 전에 사양이 정해진 PLC가 들어간 접선 흐름 여과(tangential-flow-filtration, TFF) 스키드, 시리얼 포트가 달린 실험대용 저울 등이 그렇습니다. 이들은 Modbus와 Siemens S7으로 말합니다. 신뢰할 수 있는 전선을 전제로 설계된, 인증도 없고 암호화도 없는 프로토콜입니다.
우리는 다음을 하게 됩니다.
- PyModbus로 (모의) TFF 스키드를 Modbus TCP로 읽고, 그 원시(raw) 정수 레지스터를 플랫폼의 나머지 부분이 쓰는 것과 동일한 공학 단위(engineering-unit) 태그로 스케일링(scaling)합니다.
- Modbus와 S7이 보안을 전혀 담고 있지 않다는 냉혹한 진실, 그리고 Siemens S7-1200/1500의 PUT/GET 단서가 실제로 무엇을 의미하는지를 마주합니다.
- **OT 네트워크 분할(network segmentation)**을, 우리가 이 장치들에 손을 댈 수 있게 해 주는 보완 통제(compensating control)로 다루고, 그것을 위험 기반(risk-based) 결정으로 기록합니다.
- 그리고 python-snap7과 Apache PLC4X가 동일한 패턴을 Siemens PLC와 혼합 레거시 장비군으로 어떻게 확장하는지 살펴봅니다.
이 장의 실행 가능한 코드는 examples/chapters/09-legacy-skids-modbus-s7/modbus_reader.py입니다. S7과 PLC4X 자료는 현실적이고 명확히 라벨이 붙은 구성(configuration)으로 제시됩니다. 노트북 안에 Siemens PLC가 들어 있는 것은 아니며, 우리는 시뮬레이션이 정확히 어디에서 멈추는지에 대해 솔직할 것입니다.
레거시 프로토콜이 다른 종류의 동물인 이유
Modbus는 1979년에 발표되어, 단순한 요청/응답, 클라이언트/서버 메시징 프로토콜로 표준화되었습니다. 클라이언트가 함수 코드(function code, 홀딩 레지스터 읽기, 코일 쓰기)와 주소를 보내면, 서버가 16비트 레지스터 값이나 단일 비트 코일로 응답합니다 [1]. 프레임 안 어디에도 사용자 이름, 비밀번호, 서명, 세션 키를 담을 자리가 없습니다. 전혀 없습니다. 전선 위의 장치는 어떤 클라이언트가 요청하든 그대로 따릅니다. 이것은 패치할 수 있는 버그가 아니라 — 프로토콜 그 자체입니다.
Modbus 단체는 결국 이를 인정하고, 프로토콜을 X.509 클라이언트 인증서를 갖춘 TLS로 감싸는 Modbus/TCP Security 사양을 발표했습니다. 하지만 그것은 나중에 나온, 선택적인 변형이며, 이 장에서 우리가 손을 뻗는 스키드들은 그보다 앞서 만들어졌고 결코 그것을 지원하지 않을 것입니다 [2]. 그래서 정직한 엔지니어링 입장은 이렇습니다. 우리는 프로토콜을 안전하게 만들 수 없으므로, 그것을 둘러싼 네트워크를 안전하게 만들고, 장치 자체가 담지 못하는 의미를 소유하는 검증된 엣지(edge) 계층을 통해 장치를 읽습니다.
Siemens S7comm(그리고 그 최신판인 S7CommPlus)도 다른 방언으로 같은 이야기를 합니다. 그것은 TCP 위에서 TPKT/COTP/S7comm 스택을 타고 다니며, Modbus와 마찬가지로 신뢰할 수 있는 자동화 네트워크를 전제로 만들어졌습니다. 보안 연구자들은 S7CommPlus의 무결성(integrity) 메커니즘이 재전송(replay) 및 주입(injection) 공격으로 무력화될 수 있음을 입증했습니다. 그 "재전송 방지(anti-replay)"는 견고한 인증이 아닙니다 [9]. 데이터 엔지니어에게 실질적인 교훈은 Modbus와 동일합니다. 프로토콜이 스스로를 보호하리라 믿지 말고, 분할하고, 통제된 계층 아래에서 읽어라.
Modbus 스키드를 실제로 읽기
여기 레거시 PLC가 내놓는 그대로의 장치가 있습니다. examples/chapters/09-legacy-skids-modbus-s7/modbus_reader.py에서 가져왔습니다.
# raw holding registers the skid PLC exposes (scaled integers, as legacy PLCs do).
# Modbus holding registers are conventionally numbered 40001.., here at addresses 1-4.
TFF_RAW = [1850, 320, 1240, 78] # TMP, flux, conductivity, recovery
# position -> (tag, scale, unit) — the normalization the edge gateway applies
SCALING = [
("TFF01.TMP.PV", 0.001, "bar"), # 1850 -> 1.850 bar
("TFF01.Flux.PV", 0.1, "LMH"), # 320 -> 32.0 LMH
("TFF01.Cond.PV", 0.01, "mS/cm"), # 1240 -> 12.40 mS/cm
("TFF01.Recovery.PV", 1.0, "%"), # 78 -> 78 %
]
TFF_RAW를 잠깐 응시해 보세요. 이것이 네 개의 정수 안에 담긴 문제 전체이기 때문입니다. 장치에게 1850은 1.850 bar가 아닙니다. 그것은 그저 숫자 1850일 뿐입니다. 레거시 PLC는 부동소수점(floating-point) 공학 단위를 저장하는 일이 거의 없습니다. 메모리와 필드버스(fieldbus) 대역폭이 귀했기 때문에, 값은 *스케일링된 정수(scaled integer)*로 저장됩니다. 압력은 ×1000, 플럭스(flux)는 ×10, 전도도(conductivity)는 ×100입니다. 스케일 인수(scale factor)는 레지스터 맵(register map)에서 합의된 관례이지, 프로토콜이 알려 주는 무언가가 아닙니다. 만약 SCALING 표가 소수점 한 자리만큼 틀리면, 막간 압력은 1.85 대신 18.5 bar로 읽히고, 전선 위 그 무엇도 그 사실을 알려 주지 않습니다.
그 SCALING 표가 이 장의 조용한 영웅입니다. 그것은 원시 레지스터에서 4장에서 설계한 UNS 태그 네임스페이스로 이어지는 다리입니다. TFF01.TMP.PV는 접선 흐름 여과 스키드의 막간 압력 공정값(process value)을 bar 단위로 나타내는 표준(canonical) 이름입니다. 레거시 장치는 그중 무엇도 알지 못합니다. 엣지 계층이 그것을 공급합니다. 운영 환경에서 바로 이 매핑은 gov.tag_dictionary에 살아 있으며, 4장에서 생성되고 린트(lint)됩니다. 그래서 레지스터-태그 스케일링은 스크립트에 묻힌 마법의 숫자가 아니라 검토된 구성입니다.
스케일링 자체는 여섯 줄짜리 함수입니다. 의도적으로 우직하게, 테스트하기 쉽고, 검증하기 쉽게 만들었습니다.
def scale(registers: list[int]) -> dict[str, dict]:
out = {}
for value, (tag, factor, unit) in zip(registers, SCALING):
out[tag] = {"value": round(value * factor, 3), "unit": unit}
return out
그리고 여기, 실제 장치와 대화하는 부분이 있습니다. 엣지 수집기(collector)가 호출하는 실제 PyModbus 클라이언트 호출입니다. PyModbus는 TCP와 시리얼 RTU/ASCII 양쪽을 위한 완전한 오픈소스 Modbus 클라이언트/서버이며, 바로 그렇기 때문에 검증된 엣지 계층에서 이런 스키드에 손을 뻗을 때 선택되는 도구입니다 [3].
async def read_skid(host: str = "127.0.0.1", port: int = 502, unit: int = 1) -> dict:
"""Read a real TFF skid over Modbus TCP and normalize to engineering units.
This is the actual pymodbus client call an edge collector makes; point it at
a real skid (or the repo's mock) by host/port. Holding registers are read
starting at address 0 (40001) — confirm your device's base with its map.
"""
from pymodbus.client import AsyncModbusTcpClient
client = AsyncModbusTcpClient(host, port=port)
await client.connect()
rr = await client.read_holding_registers(address=0, count=len(TFF_RAW), device_id=unit)
client.close()
if rr.isError():
raise OSError(f"Modbus read failed: {rr}")
return scale(list(rr.registers))
그 짧은 함수 안의 세 가지 세부 사항은 주목할 가치가 있습니다. 각각이 고전적인 레거시 통합의 함정이기 때문입니다.
**address=0**은 Modbus 세계의 그 악명 높은 하나 차이(off-by-one) 문제입니다. 홀딩 레지스터는 40001, 40002, 40003…으로 문서화되어 있지만, 실제 전선 위 프로토콜 주소는 0부터 시작합니다. 그래서 "40001" 레지스터는 주소 0에서 읽힙니다. 데이터시트가 문서 번호를 뜻하는지 전선 번호를 뜻하는지에 대해 벤더마다 의견이 갈리므로, 코드 안의 주석 — confirm your device's base with its map — 은 형식적인 문구가 아닙니다. 첫 읽기가 쓰레기 값을 돌려주는 가장 흔한 단 하나의 이유입니다.
device_id=unit(Modbus "유닛 ID" 또는 슬레이브 주소)이 중요한 이유는, 하나의 Modbus/TCP 게이트웨이가 뒤에 데이지 체인(daisy-chain)으로 연결된 여러 시리얼 장치를 종종 앞단에서 대표하기 때문입니다. 유닛 ID가 어느 물리적 상자가 응답할지 고릅니다. 그리고 명시적인 rr.isError() 검사가 존재하는 이유는, Modbus에는 OPC UA처럼 품질 플래그(quality flag)가 없기 때문입니다(7장의 Good/Uncertain/Bad 상태 코드를 떠올려 보세요). Modbus 읽기는 숫자를 돌려주거나 예외 응답을 돌려줄 뿐이며, 그것을 히스토리안(historian)이 믿을 수 있는 무언가로 바꾸는 일은 우리 — 엣지 계층 — 의 몫입니다. 데이터 품질에 대한 정직함은 우리가 더해야 하는 것입니다. 프로토콜은 해 주지 않습니다.
하드웨어 없이 실행하기
노트북 안에 TFF 스키드가 없으므로, 이 파일은 동일한 스케일링을 알려진 레지스터 스냅샷에 적용하는 demo()를 함께 제공합니다. 그래서 이 장은 하드웨어 0, 네트워크 0으로 처음부터 끝까지 실행할 수 있습니다.
def demo() -> dict:
"""Apply the engineering-unit scaling to a known register snapshot (no network)."""
return scale(TFF_RAW)
if __name__ == "__main__":
for tag, v in demo().items():
print(f" {tag:20} = {v['value']} {v['unit']}")
실행해 봅시다.
$ python chapters/09-legacy-skids-modbus-s7/modbus_reader.py
TFF01.TMP.PV = 1.85 bar
TFF01.Flux.PV = 32.0 LMH
TFF01.Cond.PV = 12.4 mS/cm
TFF01.Recovery.PV = 78.0 %
그 네 줄이 이 장의 요점 전체를 구체적으로 보여 줍니다. 의미 없는 정수 네 개가 들어가서, 이름이 붙고, 스케일링되고, 단위가 찍힌 측정값 네 개가 나왔습니다. 그것도 플랫폼의 나머지 부분이 말하는 바로 그 ASSET.Measurement.PV 형태로 말입니다. 1.85 bar의 막간 압력과 32 LMH(제곱미터당 시간당 리터)의 플럭스는 mAb TFF 단계에 합당한 수치이며, 이제 그것들은 ts.sensor_reading으로 흘러들어 다른 어떤 OPC UA 태그와도 똑같이 배치(batch)와 페이즈(phase)에 맥락화(contextualize)될 수 있습니다. 이것이 무엇을 증명하는지에 대해서는 솔직해야 합니다. 이것은 특정 벤더의 Modbus 특이점이 아니라 통합 로직과 데이터 형태를 실행해 본 것입니다. read_skid() 경로가 실제 클라이언트 호출입니다. 호스트와 포트로 그것을 실제 스키드(또는 저장소의 Modbus 모의 장치)에 겨누면, 돌아오는 값에 대해 동일한 scale()이 실행됩니다.
번호 매겨진 비둘기집 칸에서 이름이 붙은 기록으로: 레거시 Modbus 스키드는 원시 스케일링 정수만을 실어 나르므로, 검증된 엣지 계층이 프로토콜이 빠뜨린 스케일 인수, 단위, 표준 태그 이름을 공급한다 — 그것도 모두 OT 분할 뒤에서. Original diagram by the authors, created with AI assistance.
Siemens S7과 PUT/GET 함정
많은 상용 스키드는 Modbus가 아니라 Siemens S7 PLC 위에 만들어져 있습니다. 여기서 오픈소스로 들어가는 문은 python-snap7입니다. TPKT/COTP/S7comm/S7CommPlus 스택을 구현하고 S7-300/400/1200/1500 제어기를 네이티브로 읽을 수 있는 순수 파이썬 S7 라이브러리입니다 [4]. 패턴은 read_skid()를 그대로 따라갑니다. 연결하고, 데이터 블록의 한 덩어리를 읽은 다음, 바이트로 디코딩하여 태그로 스케일링합니다. 다음은 예시적인 코드 조각입니다. 노트북에 Siemens PLC가 없으므로, 이것은 테스트된 실행이 아니라 호출의 형태입니다.
# Illustrative — requires a real/simulated S7 PLC; not run on a laptop.
import snap7
from snap7.util import get_int
client = snap7.client.Client()
client.connect("192.0.2.50", rack=0, slot=1) # S7-1500: rack 0, slot 1
db = client.db_read(db_number=10, start=0, size=8) # read 8 bytes of DB10
tmp_raw = get_int(db, 0) # offset 0 -> TMP scaled int
client.disconnect()
하지만 S7 PLC를 무언가에 연결하기 전에 반드시 알아야 할, 구체적이고 악명 높은 함정이 하나 있습니다. 현대의 S7-1200과 S7-1500 제어기에서 snap7의 "최적화된(optimized)" 데이터 블록 접근은 PLC의 PUT/GET 통신(communication) 설정과, 데이터 블록이 TIA Portal에서 "optimized block access"로 표시되지 않는 것에 달려 있습니다. PUT/GET이 비활성화되어 있으면(안전을 위해 이 제품군에서는 기본적으로 꺼져 있습니다) 읽기가 아예 실패합니다. 반대로 자동화 엔지니어가 여러분의 수집기가 읽을 수 있도록 그것을 켜면, 그들은 방금 문을 하나 열어젖힌 것이며 — S7comm의 약한 인증과 결합되어 — 어떤 클라이언트든 PLC를 읽고 쓸 수 있게 됩니다 [9]. 그 체크박스 하나는 편의를 위한 토글이 아니라 문서화된 위험 기반 결정입니다. 히스토리안에 데이터를 공급하기 위해 그것을 켜는 일은, 변경 기록(change record)과 네트워크 분할 정당화에 마땅히 들어가야 할 종류의 절충(trade-off)이지, 현장에서 조용히 넘겨선 안 되는 일입니다.
혼합 장비군을 위한 하나의 라이브러리: Apache PLC4X
실제 공장은 단 하나의 프로토콜인 경우가 드뭅니다. 같은 수확 스위트(suite) 안에서 Modbus 와 S7 과 Allen-Bradley를 동시에 만나게 되며, 각각을 위해 맞춤형 클라이언트를 작성하는 것이야말로 통합 프로젝트가 썩어 가는 방식입니다. Apache PLC4X는 프로토콜별 드라이버 뒤에 하나의 공유된 API를 제공하므로, 동일한 연결 문자열-읽기 코드가 TCP 위의 Siemens S7이나 TCP/RTU/ASCII 위의 Modbus 장치에 닿되, 애플리케이션은 어느 쪽인지 신경 쓰지 않아도 됩니다 [5]. Modbus 드라이버는 그 공통 API 아래에서 코일, 디스크리트 입력, 홀딩 레지스터, 입력 레지스터에 주소를 부여하고, S7 드라이버는 S7-300/400/1200/1500 계열과 대화합니다 [6].
실제로는 장비군을 구성(configuration)으로 표현하게 됩니다. 아래 블록은 우리의 두 레거시 자산을 위한 예시적인 PLC4X 스타일 연결 맵입니다. 엣지 서비스가 소비할 법한 edge/plc4x/plc4x-connect.yaml 종류이며, 이 장 디렉터리 안의 테스트된 산출물은 아닙니다.
# Illustrative PLC4X connection map (not a tested artifact in this chapter dir).
connections:
tff01:
url: "modbus-tcp://10.20.0.11:502?unit-identifier=1"
poll_ms: 1000
tags:
TFF01.TMP.PV: { address: "holding-register:1:INT", scale: 0.001, unit: "bar" }
TFF01.Flux.PV: { address: "holding-register:2:INT", scale: 0.1, unit: "LMH" }
centrifuge01:
url: "s7://10.20.0.21?remote-rack=0&remote-slot=1"
poll_ms: 2000
tags:
CFG01.Speed.PV: { address: "%DB10.DBW0:INT", scale: 1.0, unit: "rpm" }
scale과 unit 키가 이제 프로토콜마다 다시 등장하는 점에 주목하세요. 레거시의 의미 문제는 결코 사라지지 않습니다. PLC4X는 단지 열쇠고리를 보관할 일관된 장소 하나를 줄 뿐입니다. 정직한 절충은 이렇습니다. PLC4X는 강력하고 Apache 라이선스인 Java/Go 프로젝트이지만, 60줄짜리 PyModbus 스크립트보다 무겁고, 그 프로토콜 드라이버의 성숙도는 장치마다 다릅니다. Modbus 스키드 하나라면 PyModbus가 옳습니다. 하나의 엣지 서비스에 데이터를 공급하는 혼합 레거시 제어기 장비군이라면, PLC4X가 그 무게값을 합니다.
왜 중요한가
레거시 통합은 데이터 무결성에 대한 야심이 실제 현장과 만나는 지점입니다. 우리가 옹호해 온 모든 ALCOA+ 속성 — 귀속 가능(attributable), 정확(accurate), 동시 기록(contemporaneous) — 은 그중 무엇도 자발적으로 제공하지 않는 프로토콜에서 살아남아야 합니다. Modbus는 단위를 알려 주지 않고, 값을 출처에서 타임스탬프 찍지 않으며, 불량 데이터를 표시하지 않습니다. 엣지 계층이 1850을 잘못 스케일링하면, 히스토리안은 정확해 보이지만 잘못된 막간 압력을 충실하게 그리고 영구히 기록하며, 하류의 공정 검증(process-validation) 검토자는 그 오류를 볼 방법이 없습니다. 따라서 스케일링 표는 배관이 아닙니다. 그것은 데이터 무결성 통제이며, 다른 어떤 GMP 통제가 받는 검토, 버전 관리, 적격성 평가(qualification)를 똑같이 받아야 마땅합니다.
또한 중요한 이유는, 이 장비를 대개 통째로 뜯어내 교체할 수 없기 때문입니다. 적격성이 평가된 TFF 스키드나 원심분리기는 수년간의 검증을 대표합니다. "OPC UA 네이티브 장비를 사면 되지"는 기존 라인에 대해 좀처럼 현실적인 선택지가 아닙니다. 그래서 데이터 엔지니어의 일은 레거시 프로토콜이 사라지기를 바라는 것이 아니라 — 그것을 정직하게 읽고, 프로토콜이 결여한 통제로 감싸는 것입니다.
실제 현장에서는
냉정한 현실은, 안전하지 않은 레거시 프로토콜이 제약 업계 도처에 있다는 것, 그리고 규제 및 보안 프레임워크가 이미 여러분이 네트워크 계층에서 보완하기를 기대하고 있다는 것입니다. 권위 있는 OT 보안 지침인 NIST SP 800-82 Rev. 3은 바로 이것을 중심으로 세워져 있습니다. 존-앤-컨듀잇(zone-and-conduit) 아키텍처, 네트워크 분할, 그리고 스스로를 방어할 수 없는 프로토콜을 위한 보완 통제입니다 [7]. IEC 62443-3-3은 그것을 조언이 아니라 요구사항으로 만듭니다. 그 기본 요구사항 FR5(Restricted Data Flow, 제한된 데이터 흐름), 그중 SR 5.1 Network Segmentation은, 안전하지 않은 OT를 보안 수준별로 존으로 나누고 통제된 컨듀잇을 통해서만 연결하도록 의무화합니다 [8]. 그래서 우리의 read_skid()가 Modbus 장치에 닿을 때, 그것은 정의된 컨듀잇 안에서 그렇게 합니다. 엣지 수집기는 통제된 존에 자리하고, 스키드는 OT 존에 자리하며, 둘 사이의 유일한 트래픽은 우리가 문서화한 특정 포트의 특정 Modbus 읽기뿐입니다.
결정적으로, 답으로 분할을 선택하는 것 자체가 하나의 문서화된 위험 기반 결정입니다. FDA의 컴퓨터 소프트웨어 보증(Computer Software Assurance) 지침은 생산 및 품질 시스템 소프트웨어에 대한 보증을 의도된 용도와 위험을 중심으로 구성합니다. 여러분은 프로토콜이 안전하지 않음을 식별하고, 네트워크 통제에 검증된 엣지 계층을 더한 것이 비례하는 완화책이라고 결정하며, 그 논리를 적어 두기를 기대받습니다 [10]. "프로토콜에 인증이 없기 때문에 우리는 Modbus 스키드를 분할하고 적격성이 평가된 게이트웨이를 통해 읽었다"는 진술은, 검사관이 보고 싶어 하는 바로 그 종류의 위험 기반 진술이며 — 또한 이 장의 코드가 구체적으로 만들어 내는 바로 그것입니다.
이제 정직한 OSS 대 상용 경계선입니다. 읽기는 오픈소스에서 진정으로, 완전히 해결되어 있습니다. PyModbus, python-snap7, PLC4X는 거의 모든 레거시 제어기와 대화할 수 있으며, 라이선스 비용 없이, 여러분이 읽고 테스트할 수 있는 코드로 그렇게 합니다. 순수 OSS가 주지 않는 것은, 상용 히스토리안의 커넥터가 함께 제공하는 검증된 드라이버 책임성입니다(AVEVA PI의 인터페이스와 커넥터, Kepware/KEPServerEX 등은 벤더 적격성 패키지와, 책임을 물을 대상을 지목하는 지원 계약을 함께 제공합니다). 우리의 PyModbus 수집기로는, 스케일링이 정확하다는 것, 읽기가 신뢰할 수 있다는 것, 분할이 유지된다는 것을 입증하는 일을 여러분이 소유합니다 — 이는 이 책에서 거듭 등장하는 형태입니다. 오픈소스는 장치에 깔끔하게 닿고, 그것을 둘러싼 GxP 래퍼(wrapper)는 여러분이 만들거나 사야 합니다. 그리고 어느 쪽도 프로토콜을 바꾸지는 못합니다. Modbus와 S7은 안전하지 않은 채로 남고, 고칠 수 있는 유일한 곳은 네트워크뿐입니다.
핵심 용어
- Modbus — 함수 코드와 16비트 레지스터/코일을 사용하는 1979년의 요청/응답 클라이언트/서버 프로토콜로, 인증도 암호화도 없다. 레거시 스키드, 저울, 펌프에 흔하다.
- 홀딩 레지스터(holding register) — Modbus 장치 안의 16비트 읽기/쓰기 메모리 슬롯으로, 관례상 40001부터 번호가 매겨지지만 전선 위에서는 0부터 주소가 부여된다. 레거시 PLC는 여기에 공학 값을 스케일링된 정수로 저장한다.
- 스케일링된 정수(scaled integer) — 장치가 부동소수점을 저장할 수 없거나 저장하지 않기 때문에, 정수에 고정 인수를 곱한 값으로 저장된 값(예: 압력 ×1000). 엣지 계층이 공학 단위를 복원하려면 스케일을 적용해야 한다.
- 유닛 ID / device_id(unit ID / device_id) — 공유된 TCP 게이트웨이 뒤에서 어느 물리적 장치가 응답할지 선택하는 Modbus 슬레이브 주소.
- Siemens S7comm / S7CommPlus — Siemens 독자 PLC 프로토콜 스택(TPKT/COTP/TCP 위). 설계상 안전하지 않으며, 재전송/주입으로 무력화된 약한 인증을 갖는다.
- PUT/GET 통신(PUT/GET communication) — snap7 같은 외부 클라이언트가 데이터 블록을 읽으려면 활성화해야(그리고 최적화 블록 접근은 비활성화해야) 하는 Siemens S7-1200/1500 설정. 기본적으로 꺼져 있으며, 켜면 공격 표면이 넓어진다.
- OT 네트워크 분할(OT network segmentation) — 운영 기술(operational-technology) 장비를 통제된 컨듀잇으로만 연결된 존으로 격리하는 것. 안전하지 않은 레거시 프로토콜을 위한 IEC 62443 / NIST SP 800-82 보완 통제.
- 존과 컨듀잇(zones and conduits) — 자산을 보안 수준별로 묶고(존), 정의되고 통제된 경로(컨듀잇)를 통해서만 트래픽을 허용하는 IEC 62443 모델.
다음 이야기
우리는 업스트림 세계의 어수선한 가장자리 — 번호 매겨진 비둘기집 칸으로 말하는 레거시 스키드 — 에 도달했고, 분할 뒤에서, 여러분이 실행할 수 있는 코드로, 그것들을 다른 모든 것과 동일한 태그 네임스페이스로 끌어들였습니다. 다음으로 우리는 제품을 따라 하류로 내려갑니다. 제10장 — 다운스트림 수집: 크로마토그래피와 여과 스키드에서는 Protein A 포획(capture) 사이클과, 항체를 정제하고 농축하는 여과 트레인(train)을 포착하여, 크로마토그램(chromatogram)의 적재/세척/용출/제거(load/wash/elute/strip) 페이즈를 events.operation_event 행으로 바꿉니다. 바로 수확물이 원료 의약품(drug substance)이 되는 순간이자, 우리가 수집해 온 시계열이 정제 이야기를 들려주기 시작하는 순간입니다.