plink・PowerShell 5.1・SQL Server 2022에서 밟은 10가지 함정 ― AI 에이전트에게 Windows 실기 작업을 맡기고(SSH/문자 코드/T-SQL)

plink・PowerShell 5.1・SQL Server 2022에서 밟은 10가지 함정 ― AI 에이전트에게 Windows 실기 작업을 맡기고(SSH/문자 코드/T-SQL)

10 min read

BESTNET TECH BLOG

plink・PowerShell 5.1・SQL Server 2022에서 밟은 10가지 함정

AI 에이전트에게 Windows 실기 작업을 맡기고 ― SSH 비대화화·문자 코드·T-SQL의 “이음매”에서 밟은 실전기

저자: Hideyuki Chinda / BESTNET LLC2026-06-13시리즈 기술 번외편

시리즈 본편에서는, Oracle 19c에서 SQL Server 2022로의 마이그레이션 전처리를, AI 코딩 에이전트(Claude Code)에 실데이터를 넘기지 않고 자동화하여, 마이그레이션 대상 실기에서의 적용·기능 검증까지 완료한 것을 보고했습니다.

본고는 그 이면입니다. AI 에이전트에게 「실기에서의 작업」을 맡기면, 발이 걸리는 것은 대개 AI의 영리함이 아니라, AI와 셸과 OS의 문자 코드와 도구의 “이음매”였습니다. SSH의 비대화화, Windows의 문자 깨짐, cmd의 따옴표, 코드로부터의 T-SQL 투입 ―― 어느 것이나 단독으로는 수수하지만, 자동화를 멈추기에는 충분합니다. 실전에서 밟은 함정 중, 본고에서는 기술 쪽 10가지를, 증상·원인·대처의 형태로 남깁니다(나머지 2가지는 AI의 안전 기구에 관한 거버넌스 이야기이므로, 번외편 그 2 「AI 가드레일 편」으로). 같은 종류의 자동화를 검토하고 있는 분의 몇 시간을 절약할 수 있다면 다행입니다.

본고의 명령 예시는, 접속 대상·인증 정보를 모두 플레이스홀더(USER@HOST, -pw '****', C:\work)로 치환하고 있습니다.


1. 접속 편 ― 핸즈프리의 SSH를 성립시킨다 #

AI 에이전트에게 작업시키는 전제로서, 사람이 한 번도 비밀번호를 치지 않고 SSH 명령이 통할 필요가 있습니다. 여기서 곧바로 3가지.

① OpenSSH는 비밀번호를 비대화로 넘길 수 없다 #

  • 증상: ssh user@host "コマンド"이 비밀번호 입력 대기로 멈춘다. 스크립트에서 자동 실행할 수 없다.
  • 원인: OpenSSH 클라이언트는 보안상, 비밀번호를 단말(tty)에서만 읽는다. 표준 입력에 파이프해도 받지 않는다. Windows에는 sshpass도 표준으로는 없다.
  • 대처: PuTTY의 plink를 사용하여 -pw로 비밀번호를 넘긴다.
plink -ssh -pw '****' USER@HOST "whoami"

보안적으로는, 운영 환경에서는 공개 키 인증(키 페어+pageant-i)을 제일로 검토해 주십시오. -pw의 명령줄 넘김은 프로세스 일람이나 셸 이력에 남을 수 있으므로, 부득이하게 사용하는 경우에도 일회용+작업 후 로테이션이 전제입니다.

② plink의 초회 호스트 키 프롬프트에서 행한다 #

  • 증상: echo y | plink ...로 해도 진행되지 않는다. 타임아웃될 때까지 무반응.
  • 원인: plink는 초회 접속 시의 키 캐시 확인 「Store key in cache? (y/n)」을 단말(tty)에서 읽습니다. AI 에이전트 실행처럼 tty가 없는 환경(파이프/프로세스 기동 경유)에서는, 파이프한 y를 받지 못해 행합니다. 대화 콘솔상에서는 echo y | plink가 듣는 구성도 있지만, 자동 실행에서는 믿을 수 없습니다.
  • 대처: 먼저 ssh-keyscan으로 호스트 키를 취하고, SHA256 핑거프린트를 -hostkey로 핀 고정해 버린다. -batch로 프롬프트 자체를 무효화.
# 1) ホスト鍵のフィンガープリントを取得
ssh-keyscan -T 6 HOST 2>/dev/null | ssh-keygen -lf -
#   => 256 SHA256:<fingerprint> <host> (...)  のように、鍵種ごとに1行ずつ出る

# 2) 取得値を -hostkey で固定し、-batch で非対話接続
#    サーバが複数の鍵種(ED25519/RSA等)を返す環境では、ネゴシエートされる鍵に備え各 -hostkey を列挙する
plink -ssh -hostkey SHA256:<fingerprint> -batch -pw '****' USER@HOST "コマンド"

핀 고정은 보안상으로도 플러스입니다(중간자 공격에 대해, 상정한 키 이외를 튕긴다).

③ 장시간 명령은 「던지고 기다리는」 설계로 한다 #

  • 증상: ISO 다운로드나 제품 인스톨 같은 장시간 처리를 동기 실행하면, 도구 측의 타임아웃에 걸려 끊긴다.
  • 원인: SSH 세션을 쥔 채 10분 이상 블록하는 처리는, 에이전트의 실행 제한에 걸리기 쉽다.
  • 대처: 장시간 처리는 백그라운드에서 기동하고, 완료를 센티널 파일이나 종료 통지로 줍는다. SSH 세션을 장시간 점유하지 않는다.
:: 非同期起動し、完了時に終了コードをセンチネルへ(同一 cmd /c 内なので & は逐次評価される)
start "" /b cmd /c "installer.exe /quiet & echo DONE=%errorlevel%> C:\work\done.txt"
:: 別コマンドで完了確認:  if exist C:\work\done.txt type C:\work\done.txt

2. 문자 코드 편 ― CP932와 BOM의 2단 구성 #

일본어 Windows를 상대하면, 문자 깨짐은 「겉모습의 문제」가 아니라 처리를 망가뜨리는 버그가 됩니다.

④ 리모트 출력이 Shift-JIS로 깨진다 #

  • 증상: 명령 결과가 �w�肳�ꂽ�p�X... 같은 문자 깨짐으로 읽을 수 없다.
  • 원인: 일본어 Windows는 CP932(Shift-JIS)로 출력한다. UTF-8 전제의 단말에 넘어가면 깨진다.
  • 대처: 리모트 측 명령의 선두에서 chcp 65001(UTF-8로 전환). PowerShell은 출력 인코딩도 명시.
chcp 65001>nul & 後続コマンド
[Console]::OutputEncoding = [Text.UTF8Encoding]::new()

⑤ PowerShell 5.1은 BOM 없는 .ps1을 CP932로 읽는다【가장 시간을 녹인 함정】 #

  • 증상: 스크립트 중의 일본어 리터럴('すべて' 등)이 '鬟溷刀' 같은 다른 것으로 깨지고, '~' 付近に不適切な構文があります로 SQL이나 처리가 떨어진다.
  • 원인: PowerShell 5.1은, BOM이 없는 .ps1을 시스템 ANSI 코드 페이지(일본어 환경이라면 CP932)로 해석한다. UTF-8로 저장한 스크립트의 일본어가 망가진다. ASCII만의 스크립트는 무사하므로, 발각이 늦어지는 것이 골치 아프다.
  • 대처: 일본어를 포함하는 .ps1UTF-8(BOM 첨부)로 저장한다. PS5.1은 BOM을 보고 UTF-8로 판정한다.
# UTF-8 BOM付きで書き出す(既存のBOM無しUTF-8を変換する例)
$txt = [IO.File]::ReadAllText($src, [Text.UTF8Encoding]::new($false))
[IO.File]::WriteAllText($dst, $txt, [Text.UTF8Encoding]::new($true))  # $true = BOM付き

이 2가지(④⑤)는 다른 계층의 문제(단말 출력 vs 스크립트 읽기)이지만, 증상이 같은 「문자 깨짐」이므로 혼동하기 쉽습니다. 출력은 chcp, 입력(스크립트)은 BOM, 이라고 분리해서 외우면 빠릅니다.


3. 실행 패턴 편 ― cmd 따옴표 지옥으로부터의 탈출 #

⑥ cmd 경유의 인라인 PowerShell이 | " ;로 망가진다 #

  • 증상: powershell -Command "Get-ChildItem | Select-Object Name"을 흘리면 'Select-Object' は…認識されていません. "의 중첩에서도 무너진다.
  • 원인: SSH→cmd→PowerShell로 다단으로 넘어가기 때문에, |cmd가 파이프로서 먼저 해석해 버린다. 큰따옴표의 중첩도 cmd 단계에서 망가진다. 표준 입력에 흘려 넣는 방식(-Command -)도, 긴 스크립트라면 도중에 끊기는 경우가 있었다.
  • 대처: 인라인을 포기하고, 로컬에서 .ps1을 쓴다→pscp로 전송→powershell -NoProfile -File로 실행한다. 본 작업의 환경(Windows Server 2022 기본의 RemoteSigned)에서는, 로컬의 미서명 .ps1이 그대로 동작했습니다. 이것으로 따옴표 문제도 표준 입력 끊김도 대부분이 해소됩니다.

본 작업에서는 이 패턴이 안정되었습니다. 「복잡한 리모트 처리는 인라인으로 애쓰지 말고, 파일로 만들어 보내서 -File로 두드린다」. 실행 정책을 Bypass로 약화시킬 필요도 없습니다(그 조작은 오히려 안전 기구에 막혔습니다 ―― 번외편 그 2 참조).

다만 전제에 주의가 필요합니다. RemoteSigned로 전송 스크립트가 동작할지는, 조직의 그룹 정책이나 서명 정책(AllSigned/Constrained Language Mode/AppLocker・WDAC 등)에 따라 달라집니다. 또한 「pscp 전송 파일에 MOTW(Mark of the Web)가 붙지 않는다=서명 가드가 듣지 않는다」는 것은 이점이 아니라 트레이드오프이며, 제3자 유래의 스크립트에서는 오히려 리스크입니다. 사내에서 내용을 파악한 도구이므로 허용했다, 는 전제를 잊지 마십시오.

pscp -hostkey SHA256:xxxx -batch -pw '****' .\task.ps1 USER@HOST:C:/work/task.ps1
plink -ssh -hostkey SHA256:xxxx -batch -pw '****' USER@HOST "powershell -NoProfile -File C:\work\task.ps1"

4. SQL 투입 편 ― 코드에서 T-SQL을 흘릴 때의 함정 #

스키마 DDL을, sqlcmd를 거치지 않고 System.Data.SqlClient(.NET 표준의 SQL Server 접속 라이브러리. sqlcmd나 SSMS를 사용하지 않고 앱에서 직접 DDL/DML을 발행할 수 있는 반면, sqlcmd 전용 기능=GO 구분 등은 갖지 않는다)로, 통합 인증으로 직접 흘렸을 때의 3연발입니다.

GO 분할이, 코멘트 내의 GO로 갈라진다 #

  • 증상: DDL을 배치 실행하면 コメントの終了マーク '*/' がありません이나 '=' 付近に不適切な構文으로 일부 배치가 떨어진다.
  • 원인: System.Data.SqlClientGO를 이해하지 못합니다(GO는 sqlcmd/SSMS의 배치 구분이며 T-SQL이 아니다). 직접 분할할 필요가 있지만, 소박하게 ^\s*GO\s*$로 가르면, /* … */ 코멘트 안에 인덴트로 쓰여진 GO(예시 코드 등)까지 구분해 버려, 코멘트가 분단됩니다.
  • 대처: 가르는 것은 행두(열0)의 GO만으로 한다. 코멘트 내의 GO가 인덴트되어 있으면(이 코드베이스는 그랬습니다) 이것으로 제외할 수 있습니다. 보다 엄밀하게는 「코멘트나 문자열 리터럴을 제외한 다음에 GO 분할한다」가 본래적입니다.
# 列0のGOのみで分割(インデントされたコメント内GOは無視)
$batches = [regex]::Split($script, '(?im)^GO[ \t]*;?[ \t]*\r?$')

⑧ 인덱스 뷰의 작성이 ARITHABORT로 실패한다 #

  • 증상: CREATE UNIQUE CLUSTERED INDEX(인덱스 뷰화)가 …SET options have incorrect settings: 'ARITHABORT'로 실패.
  • 원인: System.Data.SqlClient는 기본으로 ARITHABORT OFF. 인덱스 뷰의 작성에는 복수의 SET 옵션이 ON일 필요가 있다.
  • 대처: 접속 직후에 필요한 SET를 투입하고 나서 DDL을 흘린다.
SET ANSI_NULLS ON; SET ANSI_PADDING ON; SET ANSI_WARNINGS ON;
SET ARITHABORT ON; SET CONCAT_NULL_YIELDS_NULL ON;
SET QUOTED_IDENTIFIER ON; SET NUMERIC_ROUNDABORT OFF;

⑨ 건수 취득에서 列名 'rows' が無効です #

  • 증상: 테이블별 건수를 SELECT SUM(p.rows) FROM sys.dm_db_partition_stats p …로 취하려고 하면 列名 'rows' が無効です(Invalid column name 'rows')로 떨어진다.
  • 원인: sys.dm_db_partition_stats(DMV=동적 관리 뷰. 서버 내부 상태를 반환하는 시스템 뷰)의 행 수 열은 row_count이며 rows가 아닙니다. rows 열을 갖는 것은 sys.partitions 쪽. 잘못 알고 있었습니다.
  • 대처: 건수 집계는 sys.partitions를 사용한다(이것이 정석).
SELECT t.name, ISNULL(SUM(p.rows), 0) AS rows
FROM sys.tables t
LEFT JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0,1)
WHERE t.is_ms_shipped = 0
GROUP BY t.name ORDER BY t.name;

5. 뒷정리 편 ― 수수하지만 자동화를 멈추는 함정 #

Get-ChildItem -Exclude가 아무것도 반환하지 않아, 뒷정리가 진행되지 않는다 #

  • 증상: Get-ChildItem 'C:\work' -Exclude 'keep.sql' | Remove-Item -Force를 실행해도 1파일도 지워지지 않는다(에러도 나지 않는다).
  • 원인: 디렉터리 지정의 -Exclude는, 경로 끝에 \*를 붙이거나 -Recurse를 병용하지 않으면 기대대로 열거되지 않고 빈 것을 반환하는 경우가 있다(PowerShell의 알려진 버릇). Remove-Item에 넘어가는 입력이 비어 있으므로, 아무 일도 일어나지 않는다.
  • 대처: 게으름 피우지 말고, 대상을 명시 열거하여 Remove-Item -LiteralPath -Force.
foreach ($f in 'a.exe','b.iso','c.ps1') {
  Remove-Item -LiteralPath (Join-Path 'C:\work' $f) -Force -ErrorAction SilentlyContinue
}

정리 ― 함정은 「영리함」이 아니라 「이음매」에 나온다 #

늘어놓고 보면, AI 에이전트 자체가 틀린 곳은 거의 없습니다. 막힌 것은 거의 전부가 이음매였습니다.

  • AI와 셸의 이음매(cmd의 따옴표 해석, 호스트 키 프롬프트의 입력 경로)
  • 셸과 OS의 이음매(CP932, BOM)
  • 코드와 도구의 이음매(SqlClient가 GO를 모른다, DMV의 열명, ARITHABORT의 기본값)

뒤집어 말하면, 이음매를 표준화할수록 자동화는 안정됩니다(물론 다른 이음매가 남는 경우는 있습니다). 본 작업에서의 실용해는 다음 3가지로 집약되었습니다.

  1. 접속은 plinkpscp + 호스트 키 핀 고정 + -batch로 핸즈프리화
  2. 복잡한 처리는 인라인으로 쓰지 말고, .ps1을 보내서 -File로 실행(본 작업의 RemoteSigned 환경에서는 성립. 조직의 정책에 따라 요확인)
  3. 문자 코드는 「출력=chcp 65001/입력(스크립트)=UTF-8 BOM」으로 분리

또한, 이것들을 잡는 과정에서, AI 에이전트 측의 안전 기구가 AI 자신의 조작을 2회 차단하는 한 장면도 있었습니다. 실행 정책을 약화시키려 한 건과, 인증 정보를 파일에 쓰려고 한 건입니다. 이것은 「함정」이라기보다 거버넌스 이야기이므로, 번외편 그 2 「AI 가드레일 편」에서 다룹니다.

베스트네트 합동회사에서는, AI 에이전트를 활용한 인프라·DB 마이그레이션 작업의 자동화 지원을 하고 있습니다. 「자사 환경의 이음매를 어떻게 표준화할 것인가」부터 상담해 주십시오.
Updated on 2026년 6월 27일

What are your feelings

  • Happy
  • Normal
  • Sad

©2020 BESTNET.LLC . All Rights Reserved.