수정 이력(7건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 기사 맨 앞에 「이 기사의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건)에 대응해 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참조하세요.
- 사내 리포지토리의 권한 설정을, `icacls /grant`로 추가하기만 하는 방식에서, 허용 목록으로 교체하는 형태로 고쳤습니다. `/grant`는 기존 ACE를 지우지 않으므로, 이전에 이 폴더가 다른 공유로 공개되어 있었거나 누군가에게 일시적으로 변경 권한을 달아 둔 이력이 있으면 그 쓰기 경로가 남습니다. 모든 단말이 `Trusted = $true`로 등록하는 전제의 리포지토리이므로, 남은 한 사람이 전사 PowerShell 실행 환경에 임의의 코드를 배포할 수 있게 됩니다. 상속 차단만으로는 부족하고(`SetAccessRuleProtection`이 다룰 수 있는 것은 상속 ACE뿐입니다), 직접 부여분도 제거한 뒤 허용 목록의 주체를 다시 넣고, 한 번의 `Set-Acl`로 적용하도록 했습니다.
- 개발부터 사내 리포지토리를 거쳐 이용 측으로 전달되기까지의 구성도를 1장 끝에 추가했습니다. 더불어 대상 독자와 전제 환경 표, 공유 폴더로 배포할 때의 권한 설정 예, AllUsers 스코프에 관리자 권한이 필요하다는 점을 추가했습니다.
- 본문의 관련 기사 링크 문구가 링크 대상의 현재 제목과 어긋나 있던 것을, 실제 제목에 맞췄습니다. 본문 내용은 바꾸지 않았습니다.
- 최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22175055)
아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.
Go Komura (2026). 「PowerShell 모듈의 사내 배포와 업데이트 ── PSResourceGet와 사내 리포지토리」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/powershell-module-distribution-psresourceget/
- DOI(등록된 아카이브)
- 10.5281/zenodo.22175055
- DOI(마지막 등록 버전)
- 10.5281/zenodo.22175056
「편리한 스크립트를 작성했으니 공유 폴더에 두었습니다」── 그 순간부터, 조용히 유지보수 부채가 쌓이기 시작합니다. 누군가 복사해 로컬에서 개조하고, 원본 파일이 고쳐져도 반영되지 않으며, 어느 버전이 어디에서 돌아가는지 아무도 파악하지 못합니다. 몇 년 뒤, 공유 폴더에는 集計.ps1, 集計_v2.ps1, 集計_修正版_最新.ps1이 늘어섭니다.
이 문제의 답은 PowerShell 세계에서는 분명합니다. 모듈로 만들고, 버전을 붙이고, 리포지토리에서 배포한다. 이것만으로 「어느 버전이 설치되어 있는지」「업데이트하면 모두에게 전달되는지」라는 물음에 답할 수 있게 됩니다. 그리고 PowerShell 7.4 이후에는 그를 위한 체계(PSResourceGet)가 처음부터 들어 있습니다.
이 기사에서는 사내에서 공유하는 스크립트를 모듈화하고, 사내 리포지토리를 세워 배포·업데이트하는 절차를, 전용 서버를 세우지 않는 현실적인 구성으로 설명합니다. 함수의 모듈화 자체는 「PowerShell의 인수 설계와 모듈화」를 먼저 읽어 두면 이해가 빠를 것입니다.
대상 독자·전제 환경
| 항목 | 내용 |
|---|---|
| 대상 독자 | 공유 폴더에 둔 .ps1을 배포하는 정보시스템·운영 담당 |
| 대상 버전 | Windows PowerShell 5.1과 PowerShell 7.x 양쪽. 다만 PSResourceGet가 포함되어 있는 것은 PowerShell 7.4 이후이며, 5.1에서는 배포 측·이용 측 모두 사전 설치가 필요합니다(제5장)1 |
| 기존 환경과의 병존 | PSResourceGet는 기존의 PowerShellGet 2.2.5와 병존할 수 있습니다. 기존 스크립트를 다시 쓰지 않고 도입할 수 있습니다1 |
| 샘플 검증 환경 | 기사 말미에서 배포하는 샘플 코드는 PowerShell 7.6에서 실행해 검증했습니다 |
| 필요한 권한 | 리포지토리용 공유 폴더 작성과 권한 설정, -Scope AllUsers로의 설치에는 관리자 권한이 필요합니다 |
1. 먼저 결론
- PSResourceGet(
Microsoft.PowerShell.PSResourceGet)가 PowerShell 7.4에 포함되어 있습니다. 기존의 PowerShellGet 2.2.5와 병존하므로, 기존 스크립트를 깨지 않고 사용할 수 있습니다. Windows PowerShell 5.1에는 포함되어 있지 않으므로, 이용 측·배포 측 양쪽에서 사전 도입이 필요합니다(다음 장).1 - 사내 리포지토리는 파일 공유로 시작할 수 있습니다.
Register-PSResourceRepository에 UNC 경로만 지정하면 됩니다. 전용 서버는 필요 없습니다.2 - 매니페스트(
.psd1)는 필수라고 생각하세요. 버전 번호가 없으면 업데이트도, 원인 분리도 할 수 없습니다.3 FunctionsToExport는 와일드카드로 두지 않고, 배열로 명시합니다. 명령 탐색이 빨라지고, 내부 함수의 의도하지 않은 공유도 막을 수 있습니다.3- 버전은 시맨틱 버저닝으로. 파괴적 변경을 메이저로 표현하지 못하면, 이용 측은 안심하고 업데이트할 수 없습니다.4
- 공개는
Publish-PSResource, 취득은Install-PSResource, 업데이트는Update-PSResource.56 - 모듈 탐색 경로는 5.1과 7에서 다릅니다.
$env:PSModulePath의 차이를 이해하지 않으면 「넣었는데 찾을 수 없다」가 일어납니다.7 - 실행 정책이
AllSigned이면, 각 스크립트 파일에 Authenticode 서명이 필수입니다. 카탈로그 서명은 패키지 무결성 검증용이며, 실행 정책은 충족하지 않습니다.8 - 무인 실행이 의존하는 모듈의 자동 업데이트는 신중하게. 검증한 뒤 계획적으로 올리는 운영이 안전합니다.
이 기사에서 만드는 체계는 전체적으로 다음 형태가 됩니다.
flowchart LR
DEV["개발 측<br/>정보시스템 담당이 모듈을 작성"]
TEST["테스트와 정적 분석<br/>Pester / PSScriptAnalyzer"]
REPO["사내 리포지토리<br/>파일 공유(UNC 경로) 또는<br/>NuGet 호환 피드"]
USER["이용 측 PC<br/>Find / Install / Update-PSResource"]
SRV["이용 측 서버<br/>야간 배치 등 무인 실행"]
DEV --> TEST
TEST -->|"Publish-PSResource"| REPO
REPO -->|"Install-PSResource"| USER
REPO -->|"Install-PSResource -Scope AllUsers"| SRV
화살표에 실린 명령이 이 기사에서 다루는 중심입니다. 리포지토리는 배포 측·이용 측 양쪽에서 처음에 한 번만 Register-PSResourceRepository로 등록하고(제5장), 이후에는 공개·취득·업데이트 명령만으로 돌아갑니다(제6장). 쓰기 권한을 가진 사람은 배포 담당자뿐이라는 점이, 이 그림의 유일한 보안 경계입니다.
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 22건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
2. 「공유 폴더의 ps1」이 안고 있는 문제
먼저, 무엇을 해결하려는지 분명히 합니다.
| 증상 | 근본 원인 | |
|---|---|---|
| 어느 버전이 돌아가고 있는지 모른다 | 버전 번호라는 개념이 없다 | |
| 고쳐도 모두에게 반영되지 않는다 | 각자 복사본을 가지고 있다 | |
| 누가 쓰는지 모른다 | 취득 기록이 남지 않는다(※후술대로, 이것만은 배포 방식의 선택이 필요) | |
| 일부 환경만 깨진다 | 의존 관계(필요한 모듈·PS 버전)가 선언되어 있지 않다 | |
| 고치고 싶지만 영향 범위를 가늠할 수 없다 | 공개하는 함수와 내부 함수의 구분이 없다 |
모듈화와 리포지토리 배포는 이 중 위의 4가지에는 직접 효과가 있습니다. 다만 「누가 쓰는지」만은 배포 방식에 따라 달라집니다. 다음 장부터 소개하는 파일 공유 리포지토리는 손쉬운 반면, 누가 언제 취득했는지의 기록은 남지 않습니다(Get-InstalledPSResource로 알 수 있는 것은, 그 명령을 실행한 단말의 상태뿐입니다). 이용 상황을 파악하고 싶다면, 다음 중 하나를 병용하세요.
- 공유 폴더의 읽기 감사를 켭니다(파일 액세스 감사. 「Get-WinEvent로 이벤트 로그를 실무적으로 조사한다」)
- 다운로드 통계를 낼 수 있는 NuGet 호환 피드(Azure Artifacts 등)를 사용합니다
- 각 단말에서
Get-InstalledPSResource를 실행해 결과를 집계합니다(「PowerShell Remoting(WinRM) 입문」)
3. 모듈의 최소 구성
배포 가능한 모듈의 최소 형태는 폴더·.psm1·.psd1의 3점입니다.
KsOps\
KsOps.psd1 ← 매니페스트(버전·공개 함수·의존 관계)
KsOps.psm1 ← 구현(또는 Public/Private 폴더에서 dot-source)
Public\
Get-KsShareUsage.ps1
Invoke-KsArchive.ps1
Private\
ConvertTo-KsSize.ps1
매니페스트는 New-ModuleManifest로 뼈대를 만들고, 필요한 항목을 채웁니다.3
$manifest = @{
Path = '.\KsOps\KsOps.psd1'
RootModule = 'KsOps.psm1'
ModuleVersion = '1.0.0'
GUID = [guid]::NewGuid().Guid
Author = '정보시스템부'
CompanyName = '주식회사 샘플'
Description = '사내 운영 스크립트 공통 모듈(파일 서버 재고 조사·아카이브)'
PowerShellVersion = '5.1'
CompatiblePSEditions = @('Desktop', 'Core') # 5.1과 7 양쪽에서 쓰는 경우
# 와일드카드로 두지 않는다. 공개할 것만 명시한다
FunctionsToExport = @('Get-KsShareUsage', 'Invoke-KsArchive')
CmdletsToExport = @()
VariablesToExport = @()
AliasesToExport = @()
RequiredModules = @() # 의존이 있으면 여기서 선언한다
Tags = @('internal', 'operations')
ProjectUri = 'https://git.example.co.jp/it/ksops'
}
New-ModuleManifest @manifest
.psm1은 Public/Private 스크립트를 읽어 들여 공개 함수만 내보내는 관용 패턴으로 작성할 수 있습니다.
# KsOps.psm1
$public = @(Get-ChildItem -Path "$PSScriptRoot\Public\*.ps1" -ErrorAction SilentlyContinue)
$private = @(Get-ChildItem -Path "$PSScriptRoot\Private\*.ps1" -ErrorAction SilentlyContinue)
foreach ($file in @($public + $private)) {
try { . $file.FullName }
catch { throw "모듈을 불러오지 못했습니다: $($file.FullName) ── $_" }
}
# 공개하는 것은 Public 아래의 함수만(매니페스트의 선언과 맞춘다)
Export-ModuleMember -Function $public.BaseName
FunctionsToExport를 와일드카드로 두지 않는 이유는 두 가지입니다. 하나는 명령 탐색 성능으로, 명시하면 모듈 본체를 분석하지 않고 「어떤 명령이 어디에 있는지」를 판단할 수 있습니다. 다른 하나는 설계상의 이유로, 내부 헬퍼가 밖에서 호출되면 그것이 사실상의 공개 API가 되어, 나중에 바꿀 수 없게 됩니다.3
위 예에서는 CompatiblePSEditions에 Desktop(Windows PowerShell 5.1)과 Core(PowerShell 7) 양쪽을 선언했지만, 선언만으로 양쪽에서 돌아가게 되는 것은 아닙니다. 5.1에서 PSResourceGet 자체를 쓰기 위한 준비는 제5장, 5.1과 7에서 모듈 탐색 경로가 달라서 생기는 「넣었는데 찾을 수 없다」는 제7장에 정리했습니다. 양쪽을 지원하도록 배포할 생각이면, 먼저 그 두 장을 읽어 두세요.
4. 버전 부여 방법
이용 측이 안심하고 업데이트할 수 있는지는 버전 번호 붙이는 방식으로 정해집니다. 시맨틱 버저닝(메이저.마이너.패치)을 채택하고, 파괴적 변경은 반드시 메이저로 표현하세요.4
| 변경 내용 | 올리는 곳 |
|---|---|
| 매개변수 이름 변경, 함수 삭제, 반환값 형태의 변경 | 메이저(1.2.3 → 2.0.0) |
| 함수나 매개변수 추가(기존은 그대로 동작) | 마이너(1.2.3 → 1.3.0) |
| 결함 수정만 | 패치(1.2.3 → 1.2.4) |
검증용 버전을 배포하고 싶을 때는 프리릴리스 버전을 쓸 수 있습니다. 매니페스트의 PrivateData.PSData.Prerelease에 beta1 같은 문자열을 설정하면(버전과의 구분 하이픈은 자동으로 붙어 1.3.0-beta1이 됩니다), 통상의 취득에서는 내려오지 않고, -Prerelease를 명시한 때만 설치됩니다. 문자열에 쓸 수 있는 것은 ASCII 영숫자와 하이픈뿐이며, 마침표나 +는 쓸 수 없습니다.4
파괴적 변경의 다루기는 인터페이스 설계의 사고방식이 참고가 됩니다(「DLL·COM 인터페이스의 하위 호환성」).
5. 사내 리포지토리를 만든다 ── 파일 공유로 충분하다
PSResourceGet는 파일 공유 상의 폴더를 리포지토리로 다룰 수 있습니다.2 이것이 도입 비용이 가장 낮은 구성입니다.
먼저 전제를 확인합니다. PowerShell 7.4 이후에는 포함되어 있지만, Windows PowerShell 5.1에는 들어 있지 않습니다. 5.1에서 이후의 명령(Register-PSResourceRepository 등)을 쓰려면, 미리 모듈을 도입하세요.1
# 【Windows PowerShell 5.1만】PSResourceGet를 도입한다.
# 5.1과 7에서는 읽어 들이는 모듈 경로가 다르므로, 쓰는 에디션에서 실행할 것
if (-not (Get-Module -ListAvailable -Name Microsoft.PowerShell.PSResourceGet)) {
Install-Module -Name Microsoft.PowerShell.PSResourceGet -Scope AllUsers -Force
}
# 【배포 측·이용 측 양쪽에서 한 번만 실행】사내 리포지토리를 등록한다
# Trusted: 사내 배포물이므로 신뢰됨으로 다룬다 / Priority: PSGallery보다 우선해 검색한다
$repo = @{
Name = 'KsInternal'
Uri = '\\fileserver\PSRepository'
Trusted = $true
Priority = 10
}
Register-PSResourceRepository @repo
Get-PSResourceRepository | Format-Table Name, Uri, Trusted, Priority
공유 폴더의 액세스 권한은 「배포 담당자만 쓰기 가능, 이용자는 읽기만」으로 합니다. 여기가 느슨하면, 누구든 임의의 코드를 전사에 배포할 수 있는 경로가 됩니다. 공유 액세스 허용과 NTFS 액세스 허용은 양쪽 설정하세요(실효 권한은 더 엄한 쪽이 됩니다).9
# 파일 서버 측에서 실행한다(관리자 권한 필요)
$path = 'D:\PSRepository'
$null = New-Item -Path $path -ItemType Directory -Force
# 공유: 이용자는 읽기만, 배포 담당자만 쓸 수 있다
New-SmbShare -Name 'PSRepository' -Path $path `
-ReadAccess 'EXAMPLE\Domain Users' -ChangeAccess 'EXAMPLE\モジュール配布担当'
# NTFS: 「추가」가 아니라 「허용 목록으로 교체」한다.
# New-Item -Force는 기존 폴더에서도 성공하므로, 이전에 이 폴더가
# 다른 공유로 공개되어 있었거나·누군가에게 일시적으로 변경 권한을 달아 둔 이력이 있으면,
# icacls /grant를 추가하는 것만으로는 그 쓰기 경로가 남는다. 모든 단말이 Trusted로
# 등록하는 리포지토리이므로, 남은 한 사람이 전사에 코드를 배포할 수 있게 된다
$acl = Get-Acl -Path $path
$acl.SetAccessRuleProtection($true, $false) # 상속을 끊고, 상속 ACE도 이어받지 않는다
foreach ($ace in @($acl.Access)) { # 직접 부여된 ACE도 제거한다
[void]$acl.RemoveAccessRuleSpecific($ace)
}
# 남기는 것은 여기에 적은 주체만
$allow = @(
@{ Id = 'EXAMPLE\モジュール配布担当'; Rights = 'Modify' } # 공개할 수 있다
@{ Id = 'EXAMPLE\Domain Users'; Rights = 'ReadAndExecute' } # 취득만
@{ Id = 'BUILTIN\Administrators'; Rights = 'FullControl' }
@{ Id = 'NT AUTHORITY\SYSTEM'; Rights = 'FullControl' }
)
foreach ($a in $allow) {
$acl.AddAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new(
$a.Id, $a.Rights, 'ContainerInherit, ObjectInherit', 'None', 'Allow'))
}
# 「비운다」와 「다시 넣는다」를 따로 Set-Acl하면, 아무도 손댈 수 없는 순간이 생긴다.
# 한 번에 적용한다
Set-Acl -Path $path -AclObject $acl
icacls $path # 설정 결과를 확인한다. 의도하지 않은 주체가 늘어서 있지 않은지 눈으로 본다
icacls /grant를 추가하는 것만으로 끝내지 마세요. /grant는 기존 ACE를 지우지 않습니다. 이 폴더에 쓸 수 있는 경로를 가진 사람이 따로 있어도, 그대로 남습니다.9 게다가 이 리포지토리는 모든 단말이 Trusted = $true로 등록하는 전제입니다. 신뢰된 리포지토리에 놓인 모듈은 확인 없이 설치됩니다. 남은 한 사람이, 전사의 PowerShell 실행 환경에 임의의 코드를 배포할 수 있게 되어, 「배포 담당자만 공개할 수 있다」는 전제가 그 시점에서 무너집니다.
상속 차단(SetAccessRuleProtection($true, $false))만으로도 부족합니다. 이 메서드가 다룰 수 있는 것은 상속 ACE뿐이며, 그 폴더에 직접 부여된 ACE는 대상 밖이기 때문입니다.9 위와 같이 직접 부여분도 제거한 뒤, 허용 목록의 주체만 다시 넣습니다. 새로 만든 폴더라면 결과는 같지만, 기존 폴더를 재사용했을 때만 깨지는 종류의 문제이므로, 절차에 넣어 두는 편이 확실합니다.
「배포 담당자」는 개인 계정이 아니라 그룹으로 하세요. 담당자 이동으로 모듈을 업데이트할 수 없게 되는, 그런 정지 방식이 실제로 일어납니다. 인증이 필요한 NuGet 호환 피드를 쓰는 경우에는, 이용 측에 배포하는 자격 증명을 읽기 전용 권한으로 발급하고, 공개용 자격 증명(API 키)과 나눕니다. 공유 폴더의 권한 설계 자체는 「PowerShell로 파일 서버를 재고 조사한다」도 참고하세요.
보다 본격적으로 운영한다면, Azure Artifacts나 GitHub Packages 같은 NuGet 호환 피드를 등록합니다. 인증이 필요한 리포지토리에서는 자격 증명을 SecretManagement 보관소에서 참조하는 구성으로 할 수 있습니다(「PowerShell에서 자격 증명의 안전한 다루기」).2
6. 공개·취득·업데이트
# 【배포 측】모듈을 공개한다
Publish-PSResource -Path .\KsOps -Repository 'KsInternal'
# 【이용 측】검색해서 넣는다
Find-PSResource -Name 'KsOps' -Repository 'KsInternal'
Install-PSResource -Name 'KsOps' -Repository 'KsInternal' -Scope CurrentUser
# 버전을 고정해서 넣는다(운영 서버는 이쪽을 권장)
Install-PSResource -Name 'KsOps' -Version '1.2.3' -Repository 'KsInternal' -Scope AllUsers
# 업데이트한다
Update-PSResource -Name 'KsOps' -Repository 'KsInternal'
# 무엇이 들어가 있는지 확인한다(어디까지나 「이 단말」의 상태)
Get-InstalledPSResource -Name 'KsOps' | Format-Table Name, Version, Repository, InstalledDate
# 전사의 도입 상황을 알고 싶다면, 각 단말에서 실행해 집계한다
Invoke-Command -ComputerName $servers -ScriptBlock {
Get-InstalledPSResource -Name 'KsOps' -ErrorAction SilentlyContinue |
Select-Object Name, Version
} | Sort-Object PSComputerName
-Scope의 구분은 중요합니다. 작업 스케줄러의 무인 실행에서 쓰는 모듈은 AllUsers(또는 서비스 계정 자신의 환경)에 넣어야 합니다. 「내 환경에서는 도는데 야간 배치만 cmdlet이 인식되지 않습니다로 떨어진다」의 전형적 원인이 CurrentUser 스코프로의 설치입니다.67
참고로 -Scope AllUsers는 모든 사용자 공통 장소(Program Files 아래)에 쓰므로, 관리자로 시작한 PowerShell이 아니면 실패합니다.7 최초 도입에서 「액세스가 거부되었습니다」가 되면, 먼저 관리자 권한으로 실행 중인지 확인하세요.
7. 모듈 탐색 경로와 5.1/7의 차이
PowerShell은 $env:PSModulePath에 나열된 폴더에서 모듈을 찾습니다. Windows PowerShell 5.1과 PowerShell 7에서는 기본 경로가 다릅니다.7
| 에디션 | 사용자 스코프의 기본 경로 |
|---|---|
| Windows PowerShell 5.1 | %USERPROFILE%\Documents\WindowsPowerShell\Modules |
| PowerShell 7 | %USERPROFILE%\Documents\PowerShell\Modules |
양쪽에서 쓰는 모듈은 CompatiblePSEditions에 Desktop과 Core 양쪽을 선언하고, 양쪽 환경에서 테스트한 뒤 배포합니다. 호환성 검증에는 PSScriptAnalyzer의 PSUseCompatibleSyntax가 도움이 됩니다(「PSScriptAnalyzer로 PowerShell 스크립트의 품질을 지킨다」).
문제 발생 시 확인은 다음 3점입니다.
$env:PSModulePath -split ';' # 탐색 경로
Get-Module -Name KsOps -ListAvailable # 찾아졌는지·어느 버전인지
(Get-Module KsOps -ListAvailable).ModuleBase # 실제로 읽히는 장소
8. 서명과 실행 정책
서명에는 목적이 다른 두 가지 체계가 있으며, 혼동하면 「서명했는데 실행할 수 없다」가 됩니다.8
| 체계 | 무엇을 보장하는가 | 실행 정책 AllSigned를 충족하는가 |
|
|---|---|---|---|
Authenticode 서명(Set-AuthenticodeSignature) |
개별 스크립트 파일의 발행자와 무결성 | 충족한다(각 파일에 서명이 필요) | |
카탈로그 서명(New-FileCatalog + 서명) |
모듈 세트(패키지)의 무결성 | 충족하지 않는다 |
실행 정책은 읽어 들이려는 .ps1 / .psm1 자체의 Authenticode 서명을 검증합니다. 카탈로그 파일(.cat)에 서명해도, 안의 스크립트 파일은 미서명인 채로이므로, AllSigned 환경에서는 실행이 차단됩니다. 따라서 AllSigned를 운영하고 있다면, 실행되는 각 파일에 서명하는 것이 필수입니다.
# (1) AllSigned 환경에서 필수: 실행되는 각 파일에 Authenticode 서명한다
Get-ChildItem .\KsOps -Recurse -Include *.ps1, *.psm1, *.psd1 | ForEach-Object {
Set-AuthenticodeSignature -FilePath $_.FullName -Certificate $cert `
-TimestampServer 'http://timestamp.digicert.com'
}
# (2) 더불어, 패키지 전체의 변조 감지에 카탈로그를 병용한다
New-FileCatalog -Path .\KsOps -CatalogFilePath .\KsOps\KsOps.cat -CatalogVersion 2
Set-AuthenticodeSignature -FilePath .\KsOps\KsOps.cat -Certificate $cert `
-TimestampServer 'http://timestamp.digicert.com'
# 이용 측에서의 검증(실행 정책과는 별도로, 배포물이 깨지지 않았는지 확인한다)
Test-FileCatalog -Path .\KsOps -CatalogFilePath .\KsOps\KsOps.cat -Detailed
카탈로그의 가치는 「취득한 모듈 세트가 배포 때와 동일한지」를 확인할 수 있다는 점에 있으며, PSResourceGet 측에도 서명·카탈로그를 검증하는 -AuthenticodeCheck가 있습니다.6 실행 가부는 Authenticode 서명, 배포물의 무결성은 카탈로그로 역할을 나누어 이해하세요.
타임스탬프를 붙여 두면, 서명 인증서의 유효 기간이 끝난 뒤에도 서명이 유효한 채로 남습니다. 실행 정책과 서명 운영의 전체 그림은 「PowerShell의 실행 정책과 스크립트 서명」에 정리했습니다.
9. 운영 규칙으로 정해 둘 것
기술보다 운영이 더 중요합니다. 최소한 다음을 정하세요.
- 누가 공개할 수 있는가. 공유 폴더의 쓰기 권한을 가진 사람=전사에 코드를 배포할 수 있는 사람입니다
- 변경 이력을 어디에 쓰는가.
ReleaseNotes(매니페스트의PrivateData.PSData)나 CHANGELOG에 파괴적 변경을 명시합니다 - 운영 서버는 버전 고정인가. 무인 실행이 의존하는 모듈은 고정하고 계획적으로 업데이트하는 것이 안전합니다
- 폐기 절차. 함수를 삭제할 때는 메이저 버전을 올리고, 사전에 비권장을 알립니다
- 테스트와 lint를 통과한 뒤 공개한다. 공개는 CI에서 수행하는 것이 이상입니다(「Pester로 PowerShell 테스트 정비」)
10. 실무의 정석(판단 표)
| 논점 | 선택지 | 판단의 기준 |
|---|---|---|
| 배포 형식 | 공유 폴더의 .ps1 / 모듈 + 리포지토리 |
버전과 업데이트를 관리할 수 있는지가 분기점 |
| 모듈 관리 | PowerShellGet 2.x / PSResourceGet | 7.4 이후는 포함. 신규는 PSResourceGet1 |
| 리포지토리 | 파일 공유 / NuGet 호환 피드 | 우선 파일 공유로 시작. 인증·감사가 필요하면 피드2 |
| 매니페스트 | 생략 / 필수 | 버전이 없으면 운영이 성립하지 않음3 |
| 공개 함수 | '*' / 배열로 명시 |
탐색 성능과, 내부 함수를 비공개로 두기 위해3 |
| 설치 위치 | CurrentUser / 무인 실행은 AllUsers | 서비스 계정에서 보이는지가 기준7 |
| 운영 업데이트 | 자동 업데이트 / 버전 고정 + 계획적 업데이트 | 야간 배치가 멋대로 새 버전으로 도는 것을 막음 |
| 서명 | 없음 / 각 파일의 Authenticode 서명(+카탈로그) | AllSigned 환경에서는 파일 단위 서명이 필수. 카탈로그는 무결성 검증용8 |
11. 정리
- 공유 폴더의
.ps1배포는 버전·업데이트·의존 관계·공개 범위 모두가 관리 불능이 됩니다. 모듈화와 리포지토리 배포로 해결할 수 있습니다. 다만 「누가 쓰는지」만은 별개입니다. 파일 공유 리포지토리에는 취득 기록이 남지 않으므로, 공유 폴더의 읽기 감사, 다운로드 통계를 낼 수 있는 피드, 각 단말에서의Get-InstalledPSResource집계 중 하나를 병용하세요. - 매니페스트는 필수입니다.
FunctionsToExport는 배열로 명시하고, 내부 함수는 공개하지 마세요. - 사내 리포지토리는 파일 공유를 UNC 경로로 등록하기만 하면 시작할 수 있습니다. 쓰기 권한 관리가 실질적인 보안 경계입니다.
- 공개는
Publish-PSResource, 취득은Install-PSResource, 업데이트는Update-PSResource. 무인 실행이 쓰는 모듈은AllUsers스코프에 넣습니다. - 5.1과 7에서는 탐색 경로가 다릅니다. 양쪽을 지원한다면
CompatiblePSEditions를 선언하고, 양쪽에서 테스트하세요. AllSigned환경에서는 각 스크립트 파일에 Authenticode 서명이 필요합니다. 카탈로그 서명은 배포물 무결성 검증용이며, 실행 가부와는 별개입니다. 타임스탬프는 반드시 붙이세요.
샘플 코드 다운로드
이 기사에서 다룬 코드는, 그대로 움직일 수 있는 형태로 묶어 배포합니다. 공개/비공개를 나눈 모듈 세트와, 사내 리포지토리로의 배포 절차가 들어 있습니다.
이 기사의 샘플은 PowerShell 7.6에서 실제로 실행해 검증했습니다(Pester 16건). zip에 포함된 Invoke-SampleTests.ps1을 실행하면, 여러분 환경에서도 같은 검증을 재현할 수 있습니다.
# 구문 분석 + 정적 분석 + Pester 테스트
./Invoke-SampleTests.ps1
설정값(경로, 서버 이름, 테넌트 ID 등)은 예입니다. 그대로 운영 환경에서 실행하지 말고, 자사 환경에 맞게 바꿔 읽으세요.
관련 기사
- PowerShell 스크립트의 인수 설계와 모듈화 ── 「동작하는 스크립트」에서 「사람에게 넘길 수 있는 스크립트」로
- PowerShell의 실행 정책과 스크립트 서명 ── 「Bypass로 덮어 두는」 운영에서 벗어난다
- PSScriptAnalyzer로 PowerShell 스크립트의 품질을 지킨다 ── 규칙 선정과 CI 도입
- Pester로 PowerShell 테스트 정비 ── 운영 스크립트를 깨지기 어렵게 하는 실무의 틀
- PowerShell에서 자격 증명의 안전한 다루기 ── 평문 비밀번호를 스크립트에서 몰아낸다
- 「만든 사람이 그만둬도 멈추지 않는」 특정인 의존 대책
관련 상담 영역
合同会社小村ソフト에서는 사내 스크립트 자산의 모듈화와 배포 기반 정비, 특정인에게 의존하는 운영의 표준화, 기존 스크립트의 유지보수성 개선을 다룹니다.
참고 링크
-
Microsoft Learn, Package management for PowerShell. Microsoft.PowerShell.PSResourceGet가 PowerShellGet 및 PackageManagement를 대체하는 모듈이라는 점, PowerShell 7.4에 포함되어 기존의 PowerShellGet 2.2.5와 병존한다는 점, Windows PowerShell 5.1에서도 PowerShell Gallery에서 도입할 수 있다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Register-PSResourceRepository. -Uri에 로컬 폴더나 파일 공유(UNC 경로), NuGet 호환 피드 URL을 지정해 리포지토리를 등록할 수 있다는 점, -Trusted에 의한 신뢰됨 설정, -Priority에 의한 검색 순서, 인증이 필요한 리포지토리에서의 자격 증명 지정에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, How to write a PowerShell module manifest. New-ModuleManifest에 의한 매니페스트 작성, RootModule·ModuleVersion·GUID·PowerShellVersion·CompatiblePSEditions·RequiredModules 등의 각 키, FunctionsToExport 등의 내보내기 지정에 와일드카드를 쓰지 않고 명시해야 하는 이유(명령 탐색 성능)에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Prerelease module versions. 시맨틱 버저닝에 기반한 버전 부여, PrivateData.PSData.Prerelease에 의한 프리릴리스 버전 지정, 프리릴리스 버전이 기본 취득 대상이 되지 않는다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Publish-PSResource. -Path로 지정한 모듈 폴더를 리포지토리에 공개한다는 점, -Repository에 의한 공개 대상 지정, -ApiKey에 의한 인증에 대해. ↩
-
Microsoft Learn, Install-PSResource. -Name / -Version / -Repository에 의한 설치, -Scope(CurrentUser / AllUsers)에 의한 배치 위치 지정, -TrustRepository에 의한 확인 생략에 대해. 더불어 Update-PSResource에 의한 업데이트에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, about_PSModulePath. PowerShell이 $env:PSModulePath에 나열된 폴더에서 모듈을 검색한다는 점, Windows PowerShell과 PowerShell 7에서 사용자 스코프·전체 사용자 스코프의 기본 경로가 다르다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, New-FileCatalog. 폴더 아래 파일의 해시를 포함한 카탈로그 파일(.cat)을 생성할 수 있다는 점, Set-AuthenticodeSignature로 카탈로그에 서명할 수 있다는 점, Test-FileCatalog로 카탈로그와 파일 군을 대조해 변조를 검출할 수 있다는 점에 대해. 실행 정책이 검증하는 것은 실행 대상 스크립트 파일 자체의 Authenticode 서명인 점은 about_Execution_Policies(AllSigned에서는 신뢰된 발행자가 서명한 스크립트만 실행할 수 있다는 점) 및 Set-AuthenticodeSignature(파일에 Authenticode 서명을 부여한다는 점, -TimestampServer에 의한 타임스탬프 부여)를 참조. ↩ ↩2 ↩3
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
winget + PowerShell로 PC 키팅을 자동화한다 ── 절차서를 실행 가능하게 만들기
신입 사원 PC 설정을 재현 가능하게 만드는 방법을 정리합니다. winget으로 앱을 도입하고 export/import하는 방법, WinGet Configuration의 선언적 구성, PowerShell로 보완하는 설정, 무인 실행 시 주의점까지...
Write-Host를 그만두기 ── PowerShell의 출력 스트림과 로그 설계
PowerShell 6가지 출력 스트림의 구분 사용, Write-Host가 가진 문제와 올바른 쓰임새, 함수 반환값이 오염되는 원인, -Verbose와 -InformationVariable로 호출 측에서 제어하는 방법, 구조화 로그를 남기는 방법...
Microsoft Graph PowerShell 입문 ── AzureAD·MSOnline 폐지 이후의 Microsoft 365 운영
AzureAD·MSOnline 모듈 폐지 이후의 Microsoft 365 운영을 Microsoft Graph PowerShell로 이전하는 실무 가이드입니다. 연결과 스코프 설계, 인증서를 이용한 무인 실행, 사용자 현황 조사와 라이선스 집계의 ...
PSScriptAnalyzer로 PowerShell 스크립트 품질을 지키는 방법 ── 규칙 선정과 CI 도입
PowerShell 정적 분석 도구 PSScriptAnalyzer를 사내 스크립트에 도입하는 실무 가이드입니다. 먼저 활성화해야 할 규칙, 설정 파일 작성법, 기존 자산에 대한 단계적 도입, GitHub Actions에서의 CI화까지 설명합니다.
PowerShell로 REST API와 연동하기 ── Invoke-RestMethod의 실무
PowerShell에서 사내 API나 SaaS REST API를 호출하는 실무를 정리합니다. 인증 헤더 전달 방법, 일본어 JSON 문자 깨짐 대책, 4xx/5xx 오류 처리, 429 재시도, 페이징, 프록시와 TLS의 함정까지 설명합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- PowerShellGet와 PSResourceGet는 무엇이 다른가요? 어느 쪽을 써야 하나요?
- PSResourceGet(Microsoft.PowerShell.PSResourceGet)는 기존의 PowerShellGet와 PackageManagement를 대체하는 새로운 모듈 관리 방식이며, PowerShell 7.4에는 처음부터 포함되어 있습니다. 기존의 PowerShellGet 2.2.5와 병존할 수 있으므로, 기존 스크립트를 깨지 않고 이전할 수 있습니다. 새로 작성한다면 cmdlet 이름이 -PSResource 계열(Install-PSResource, Publish-PSResource 등)인 PSResourceGet를 쓰는 것이 권장입니다. Windows PowerShell 5.1에서도 PowerShell Gallery에서 도입하면 사용할 수 있습니다.
- 사내 리포지토리를 세우는 데 전용 서버가 필요합니까?
- 필요하지 않습니다. 가장 간단한 방법은 파일 공유 상의 폴더를 리포지토리로 등록하는 것이며, Register-PSResourceRepository에 UNC 경로만 지정하면 동작합니다. 전용 서버도 데이터베이스도 필요 없습니다. 조직 차원에서 액세스 제어나 감사를 걸고 싶거나, 사외에서도 가져오고 싶은 경우에는 Azure Artifacts나 GitHub Packages 같은 NuGet 호환 피드를 사용합니다. 우선 파일 공유로 시작하고, 필요가 생긴 뒤에 이전하는 것이 현실적입니다.
- 모듈 매니페스트(.psd1)는 반드시 필요합니까?
- 실무에서는 필요하다고 생각하세요. .psm1만으로도 모듈로 읽어 들일 수 있지만, 매니페스트가 없으면 버전 번호를 가질 수 없어 「어느 버전이 설치되어 있는지」를 알 수 없게 됩니다. 버전이 없으면 업데이트 관리도, 장애 발생 시 원인을 분리하는 일도 할 수 없습니다. 더불어 매니페스트에서는 내보낼 함수를 명시하고, 의존 모듈을 선언하며, 대응하는 PowerShell 버전과 에디션을 지정할 수 있습니다. New-ModuleManifest로 뼈대를 만들 수 있으므로, 작성 비용도 얼마 되지 않습니다.
- FunctionsToExport에 '*'를 쓰면 무엇이 문제가 됩니까?
- 명령의 자동 탐색이 느려지고, 의도하지 않은 내부 함수까지 공개됩니다. PowerShell은 모듈을 읽어 들이기 전에 어떤 명령이 어느 모듈에 있는지를 알아야 하며, 와일드카드면 모듈 본체를 분석하지 않고는 알 수 없습니다. 공개할 함수를 배열로 명시하면 이 분석이 필요 없어집니다. 또한 내부용 헬퍼 함수가 밖에서 호출되면 그것이 사실상의 공개 API가 되어, 나중에 바꿀 수 없게 됩니다.
- 배포한 모듈을 이용 측에서 자동 업데이트하게 할 수 있습니까?
- Update-PSResource로 업데이트할 수 있지만, 업무 스크립트가 의존하는 모듈의 자동 업데이트는 신중하게 설계하세요. 무인 실행되는 야간 배치가 모르는 사이에 새 버전으로 돌아가게 되기 때문입니다. 실무에서는 검증 환경에서 새 버전을 확인한 뒤 운영 업데이트를 계획적으로 수행하는 운영이 안전합니다. 꼭 자동화한다면 메이저 버전을 고정해 업데이트하는(-Version '1.*'처럼 범위를 지정하는) 식의 제한을 두세요.