「편리한 스크립트를 작성해서 공유 폴더에 두었습니다」── 그 순간부터 조용히 유지보수 부채가 쌓이기 시작합니다. 누군가가 복사해서 로컬에서 개조하고, 원본 파일을 고쳐도 반영되지 않으며, 어떤 판이 어디서 동작하고 있는지 아무도 파악하지 못합니다. 몇 년 후, 공유 폴더에는 집계.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장). 쓰기 권한을 가진 사람은 배포 담당자뿐이라는 것이 이 그림의 유일한 보안 경계입니다.
2. ‘공유 폴더의 ps1’이 안고 있는 문제
먼저 무엇을 해결하려는 것인지 분명히 합니다.
| 증상 | 근본 원인 |
|---|---|
| 어떤 판이 동작하고 있는지 알 수 없다 | 버전 번호라는 개념이 없다 |
| 수정해도 전원에게 반영되지 않는다 | 각자가 사본을 가지고 있다 |
| 누가 사용하고 있는지 알 수 없다 | 취득 기록이 남지 않는다(※후술하는 대로, 이것만은 배포 방식의 선택이 필요) |
| 일부 환경에서만 깨진다 | 의존 관계(필요한 모듈·PS 버전)가 선언되어 있지 않다 |
| 고치고 싶지만 영향 범위를 알 수 없다 | 공개하는 함수와 내부 함수의 구별이 없다 |
모듈화와 리포지토리 배포는 이 중 위 4가지에는 직접적으로 효과가 있습니다. 다만 「누가 사용하고 있는가」만은 배포 체계에 따라 달라집니다. 다음 장 이후에서 소개하는 파일 공유 리포지토리는 손쉬운 반면, 누가 언제 취득했는지의 기록은 남지 않습니다(Get-InstalledPSResource로 알 수 있는 것은 그 명령을 실행한 단말의 상태뿐입니다). 이용 현황을 파악하고 싶다면 다음 중 하나를 병용하십시오.
- 공유 폴더의 읽기 감사를 활성화한다(파일 접근 감사. 「Get-WinEvent로 이벤트 로그를 실무적으로 조사하기」)
- 다운로드 통계를 얻을 수 있는 NuGet 호환 피드(Azure Artifacts 등)를 사용한다
- 각 단말에서
Get-InstalledPSResource를 실행해 결과를 취합한다(「PowerShell Remoting(WinRM) 입문」)
3. 모듈의 최소 구성
배포 가능한 모듈의 최소 형태는 폴더·.psm1·.psd1의 세 가지입니다.
KsOps\
KsOps.psd1 ← 매니페스트(버전·공개 함수·의존 관계)
KsOps.psm1 ← 구현(또는 Public/Private 폴더로부터의 점 소싱)
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
}
# 【배포 측·이용 측 모두에서 1회만 실행】사내 리포지토리를 등록한다
# 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(또는 서비스 계정 자신의 환경)에 설치해야 합니다. 「내 환경에서는 동작하는데 야간 배치만 명령을 인식할 수 없습니다로 실패한다」의 전형적인 원인이 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 스크립트의 품질 지키기」).
문제 발생 시 확인할 것은 다음 세 가지입니다.
$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에서 자격 증명을 안전하게 다루기 ── 평문 비밀번호를 스크립트에서 추방한다
- Power Automate의 속인화 대책 ── 만든 사람이 퇴사해도 플로우가 멈추지 않으려면
관련 상담 영역
합동회사 코무라소프트에서는 사내 스크립트 자산의 모듈화와 배포 기반 정비, 속인화된 운영의 표준화, 기존 스크립트의 유지보수성 개선을 다루고 있습니다.
참고 링크
-
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와 병존할 수 있어 기존 스크립트를 깨뜨리지 않고 이전할 수 있습니다. 새로 작성한다면 커맨드릿 이름이 -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.*'처럼 범위를 지정하는) 등의 안전장치를 마련하십시오.